Android
Kotlin

爆速でfloat menuを作ってみた

爆速でこんな感じなものを作った
Untitled.gif
Serviceから動けるアイコンを作ってみた

とりあえずしておきたいこと

androidで表示しているレイヤー

android 8.0以下ならTYPE_PHONEを使っているけど、android 8.0以上ならTYPE_PHONETYPE_PRIORITY_PHONEが非推奨になったのでTYPE_APPLICATION_OVERLAYを使う

Permissionをちゃんと取る必要がある

他のアプリの上で表示するにはACTION_MANAGE_OVERLAY_PERMISSIONでユーザーの許可を取る必要がある。
今ユーザーが許可しているかどうかはSettings.canDrawOverlays(context)で確認するとが出来る。

android 8.0以上でbackground serviceを始まる時要注意

android 8.0でstartForegroundService(Intent)サービス開始する時onStartCommandでやる時にstartForeground(Int, Notification)をやらないと、serviceが強制終了させるので、要注意。
公式のドキュメントはこちら
https://developer.android.com/about/versions/oreo/background?hl=ja#services

主なコード

Fragment

MainFragment.kt
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        super.onCreateView(inflater, container, savedInstanceState)
        //Permissionチェック
        if (!hasDrawOverlaysPermission()) {
            requestPermission()
        } 
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        var count = 0
        when (requestCode) {
            OVERLAY_PERMISSION_REQUEST_CODE -> {
                // 「他アプリの上に表示する」設定の取得が画面復帰直後には取得できない為、1秒間(100ms x 10回)設定の変更を監視する
                handler.post(object: Runnable {
                    override fun run() {
                        if (count > 10 || hasDrawOverlaysPermission()) {
                            startService()
                        } else {
                            count++
                            handler.postDelayed(this, 100)
                        }
                    }
                })
            }
        }
    }

    private fun hasDrawOverlaysPermission(): Boolean {
        return Settings.canDrawOverlays(context)
    }

    private fun requestPermission() {
        val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                Uri.parse("package:${context?.packageName}"))
        startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE)
    }

    private fun startService() {

        val intent = Intent(context, FloatService::class.java)
        // Serviceの開始
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context?.startForegroundService(intent)
        } else {
            context?.startService(intent)
        }
    }

Service

FloatMenuService.kt
    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        createNotification(intent)
        createFloatMenuView()
        return super.onStartCommand(intent, flags, startId)
    }

    private fun createNotification(intent: Intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channelId = "好きなchannelId"
            val notificationTitle = "タイトル"

            var notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            var pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

            notificationManager.createNotificationChannel(NotificationChannel(channelId, title, NotificationManager.IMPORTANCE_DEFAULT))
            var notification = Notification.Builder(applicationContext, channelId)
                    .setContentTitle(notificationTitle)
                    .setSmallIcon(R.drawable.ic_stat_name)//image assetで生成出来る、Adaptive iconを使うとSystem UIがクラッシュする
                    .setContentText("notificationの本文")
                    .setAutoCancel(true)
                    .setContentIntent(pendingIntent)
                    .setWhen(System.currentTimeMillis())
                    .build()

            startForeground(ONGOING_NOTIFICATION_ID, notification)//ONGOING_NOTIFICATION_IDは0が使えない
        }
    }

    private fun createFloatMenuView() {
        windowManager = applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        windowManagerParams = WindowManager.LayoutParams()
        layoutInflater = LayoutInflater.from(this)

        windowManagerParams.let {
            //レイヤー設定
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                it.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            } else {
                it.type = WindowManager.LayoutParams.TYPE_PHONE
            }
            it.format = PixelFormat.RGBA_8888
            it.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            it.gravity = Gravity.LEFT or Gravity.TOP
            it.x = 0
            it.y = 0
            it.width = WindowManager.LayoutParams.WRAP_CONTENT
            it.height = WindowManager.LayoutParams.WRAP_CONTENT
        }

        floatMenuLayout = layoutInflater.inflate(R.layout.float_menu_layout, null)
        windowManager?.addView(floatMenuLayout, windowManagerParams)
        floatMenuView = floatMenuLayout.findViewById(R.id.float_menu_button) as ImageButton

        floatMenuLayout.measure(View.MeasureSpec.makeMeasureSpec(0,
                View.MeasureSpec.UNSPECIFIED), View.MeasureSpec
                .makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
        //floatMenuの座標移動
        floatMenuView.setOnTouchListener { _, event ->
            windowManagerParams.x = event.rawX.toInt() - floatMenuView.measuredWidth / 2
            windowManagerParams.y = event.rawY.toInt() - floatMenuView.measuredHeight / 2
            mWindowManager?.updateViewLayout(floatMenuLayout, windowManagerParams)
            false
        }

        floatMenuView.setOnClickListener {
            //タップした時の処理
        }
    }

float menuのXML

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

    <ImageButton
        android:id="@+id/float_menu_button"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:gravity="center_horizontal"
        android:src="@mipmap/ic_launcher" />

</android.support.constraint.ConstraintLayout>