LoginSignup
9
4

More than 5 years have passed since last update.

FacebookのMessengerみたいなアプリを作る

Last updated at Posted at 2018-12-02

はじめに

この記事はロボP Advent Calendar 2018の2日目の記事です.

FBのMessengerって?

メッセージが来ると横にプヨって出てくるやつ
ChatHeadって言うらしい。
chathead.jpg

作る

画面に残るViewを作る

WindowManagerをServiceで動かして実装します。
通常のアプリケーションより高いレイヤーに描画するイメージ。

Permission

画面を重ねて描画するためのSYSTEM_ALERT_WINDOW

AndroidManifest.xml
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

MainActivity

無くてもいけますが、テストしやすいため今回は使います。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //API23から権限が必要
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            checkPermission()
        }

        findViewById<TextView>(R.id.text).setOnClickListener {
            val intent = Intent(application, OverlayService::class.java)
            // Serviceの開始
            // API26以上はNotificationが入るため処理が別
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(intent)
            } else {
                startService(intent)
            }
        }
    }

    @RequiresApi(Build.VERSION_CODES.M)
    fun checkPermission() {
        if (!Settings.canDrawOverlays(this)) {
            val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:$packageName"))
            startActivityForResult(intent, 1000)
        }
    }
}

Android8.0(Oreo|API26)からは画面上に重ねて描画すると
Notificationが出るようになったため、startForegroundService()でServiceを開始します。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

レイアウトは特に変えてません。

OverlayService

こいつが今回の肝です。

OverlayService.kt
class OverlayService : Service() {
    private var view: View? = null
    private var windowManager: WindowManager? = null
    private var dpScale: Int = 0

    override fun onCreate() {
        super.onCreate()

        dpScale = resources.displayMetrics.density.toInt()
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

        val context = applicationContext
        val channelId = "default"
        val title = context.getString(R.string.app_name)

        val pendingIntent = PendingIntent.getActivity(context, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT)

        //8.0(Oreo)以上用のNotification設定
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            val channel = NotificationChannel(
                    channelId, title, NotificationManager.IMPORTANCE_DEFAULT)

            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel)

                val notification = Notification.Builder(context, channelId)
                        .setContentTitle(title)
                        .setSmallIcon(android.R.drawable.btn_star)
                        .setContentText("APPLICATION_OVERLAY")
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build()

                startForeground(1, notification)
            }
        }

        // inflaterの生成
        val layoutInflater = LayoutInflater.from(this)

        //8.0(Oreo)以上のみレイヤータイプを分ける
        val typeLayer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        else
            WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

        windowManager = applicationContext
                .getSystemService(Context.WINDOW_SERVICE) as WindowManager

        val params = WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                typeLayer,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT)

        val nullParent: ViewGroup? = null
        view = layoutInflater.inflate(R.layout.layout_overlay, nullParent)
        //クリックで解除できるように
        view?.findViewById<Button>(R.id.btn_overlay)
                ?.setOnClickListener {
                    // Viewを削除
                    windowManager!!.removeView(view)
                }

        // ViewにTouchListenerを設定する
        view!!.setOnTouchListener { v, event ->
            Log.d("debug", "onTouch")
            if (event.action == MotionEvent.ACTION_DOWN) {
                Log.d("debug", "ACTION_DOWN")
            }
            if (event.action == MotionEvent.ACTION_UP) {
                Log.d("debug", "ACTION_UP")

                view!!.performClick()

                stopSelf()
            }
            false
        }

        // Viewを画面上に追加
        windowManager!!.addView(view, params)

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("debug", "onDestroy")
        // Viewを削除
        windowManager!!.removeView(view)
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

レイヤータイプが8.0以上とそれ以外で異なるので、注意してください。


//8.0(Oreo)以上のみレイヤータイプを分ける
val typeLayer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
    WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

ここで描画するViewはここでinflateしてます。

view = layoutInflater.inflate(R.layout.layout_overlay, nullParent)
layout_overlay.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <Button
        android:id="@+id/btn_overlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ボタン"/>

</android.support.constraint.ConstraintLayout>

実行!

どこに行ってもボタンが出るようになったかと思います。

UIをそれっぽくする

今回はMessengerの代わりにWebを出すようにします。

Permission

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>

webViewを使うのでINTERNETを追加します。

OverlayService

OverlayService.kt
class OverlayService : Service() {
    private var view: View? = null
    private var windowManager: WindowManager? = null
    private var dpScale: Int = 0
    private var typeLayer: Int = 0

    private var isBigView: Boolean = false

    var overlayImgV: ImageView? = null
    var webV: WebView? = null

    override fun onCreate() {
        super.onCreate()

        dpScale = resources.displayMetrics.density.toInt()
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

        val context = applicationContext
        val channelId = "default"
        val title = context.getString(R.string.app_name)

        val pendingIntent = PendingIntent.getActivity(context, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT)

        //8.0(Oreo)以上用のNotification設定
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            val channel = NotificationChannel(
                    channelId, title, NotificationManager.IMPORTANCE_DEFAULT)

            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel)

                val notification = Notification.Builder(context, channelId)
                        .setContentTitle(title)
                        .setSmallIcon(android.R.drawable.btn_star)
                        .setContentText("APPLICATION_OVERLAY")
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build()

                startForeground(1, notification)
            }
        }

        // inflaterの生成
        val layoutInflater = LayoutInflater.from(this)

        //8.0(Oreo)以上のみレイヤータイプを分ける
        typeLayer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        else
            WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

        windowManager = applicationContext
                .getSystemService(Context.WINDOW_SERVICE) as WindowManager

        val params = WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                typeLayer,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT)

        params.gravity = Gravity.TOP or Gravity.LEFT

        val nullParent: ViewGroup? = null
        view = layoutInflater.inflate(R.layout.layout_overlay, nullParent)
        //クリックで解除できるように
        view?.findViewById<Button>(R.id.btn_overlay)
                ?.setOnClickListener {
                    // Viewを削除
                    windowManager!!.removeView(view)
                }
        overlayImgV = view?.findViewById<ImageView>(R.id.ic_overlay)
        overlayImgV?.setOnClickListener {
            if (!isBigView) {
                openBigView()
            } else {
                closeBigView()
            }
        }
        webV=view?.findViewById<WebView>(R.id.web_overlay)
        webV?.visibility=View.GONE
        webV?.webViewClient = WebViewClient()
        webV?.loadUrl("https://www.google.com/")

        // ViewにTouchListenerを設定する
        view!!.setOnTouchListener { v, event ->
            Log.d("debug", "onTouch")
            if (event.action == MotionEvent.ACTION_DOWN) {
                Log.d("debug", "ACTION_DOWN")
            }
            if (event.action == MotionEvent.ACTION_UP) {
                Log.d("debug", "ACTION_UP")

                view!!.performClick()

                stopSelf()
            }
            false
        }

        // Viewを画面上に追加
        windowManager!!.addView(view, params)

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("debug", "onDestroy")
        // Viewを削除
        windowManager!!.removeView(view)
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    private fun openBigView() {
        val params = WindowManager.LayoutParams(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.MATCH_PARENT,
                typeLayer,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT)
        params.gravity = Gravity.TOP or Gravity.LEFT
        windowManager?.updateViewLayout(view, params)

        val cx = overlayImgV?.left!! + overlayImgV?.width!! / 2
        val cy = overlayImgV?.top!! + overlayImgV?.height!! / 2
        val radius = Math.max(webV?.width!!, webV?.height!!)

        val anim = android.view.ViewAnimationUtils.createCircularReveal(webV, cx, cy, 0f, radius.toFloat())

        webV?.visibility = View.VISIBLE
        TransitionManager.beginDelayedTransition(webV?.rootView as ViewGroup)
        anim.start()

        isBigView = true
    }

    private fun closeBigView() {

        val cx = overlayImgV?.left!! + overlayImgV?.width!! / 2
        val cy = overlayImgV?.top!! + overlayImgV?.height!! / 2
        val radius = Math.max(webV?.width!!, webV?.height!!)

        val anim = android.view.ViewAnimationUtils.createCircularReveal(webV, cx, cy, radius.toFloat(), 0f)
        anim.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                super.onAnimationEnd(animation)
                webV?.visibility = View.GONE
                TransitionManager.beginDelayedTransition(webV?.rootView as ViewGroup)

                val params = WindowManager.LayoutParams(
                        WindowManager.LayoutParams.WRAP_CONTENT,
                        WindowManager.LayoutParams.WRAP_CONTENT,
                        typeLayer,
                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                        PixelFormat.TRANSLUCENT)
                params.gravity = Gravity.TOP or Gravity.LEFT
                windowManager?.updateViewLayout(view, params)
            }
        })
        anim.start()

        isBigView = false
    }
}

アニメーションを追加しました。

layout_overlay.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <WebView
        android:id="@+id/web_overlay"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="WebViewLayout" />

    <ImageView
        android:id="@+id/ic_overlay"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/btn_overlay"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="@drawable/bg_oval"
        android:text="×"
        android:textColor="#ffffff"
        android:textSize="20dp"
        app:layout_constraintEnd_toEndOf="@id/ic_overlay"
        app:layout_constraintTop_toTopOf="@id/ic_overlay" />

</android.support.constraint.ConstraintLayout>

ButtonをImageViewに買えて×ボタンとWebViewを追加しました。

実行!

参考

nyanのアプリ開発 | WindowManagerを使ってServiceから画像を表示させ続ける

AndroidでViewを他のアプリの上に表示する (APIバージョン別 対応方法まとめ)

めっちゃ参考にしました。ありがとうございます。

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4