Android floating window is enough to read this

Before I wanted to achieve the effect of a global floating ball, I searched the blogs of the big guys on the Internet and stepped on a lot of pits, but there are still some problems that have not been solved, such as some secondary interfaces of the setting interface of individual mobile phones cannot be displayed (for example: MIUI settings - about the phone [dog head life] )

Simply summarize here a detailed blog on the use and adaptation of floating windows (Kotlin code)

The old rules first go to the source code link gitee.com/AndroidLMY/…

renderings

The basic principle of floating window

First of all, let's talk about the basic principle of the floating window

Dynamically add View

We all know that we want to dynamically add View to the interface is nothing more than

Instantiate a View and add it to a layout. For example:

val view = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)
ll_all.addView(view)
复制代码

Then at this time we want to add View when the current Activity does not depend on any layout, we can get WindowManager to add our View

E.g:

 val view = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)
 var layoutParam = WindowManager.LayoutParams().apply {
     //设置大小 自适应
     width = WRAP_CONTENT
     height = WRAP_CONTENT
 }
 windowManager.addView(view,layoutParam)
复制代码

Floating window principle

  • getWindowManager

  • Create View

  • Add to WindowManager

In-app floating window

In-app floating window implementation process

  • getWindowManager

  • Create a floating view

  • Set the drag event of the floating View

  • Add View to WindowManager

code show as below:

var layoutParam = WindowManager.LayoutParams().apply {
    //设置大小 自适应
    width = WRAP_CONTENT
    height = WRAP_CONTENT
    flags =
        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
}
// 新建悬浮窗控件
floatRootView = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)
//设置拖动事件
floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))
// 将悬浮窗控件添加到WindowManager
windowManager.addView(floatRootView, layoutParam)
复制代码

The drag and drop listener ItemViewTouchListener code is as follows:

class ItemViewTouchListener(val wl: WindowManager.LayoutParams, val windowManager: WindowManager) :
    View.OnTouchListener {
    private var x = 0
    private var y = 0
    override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
        when (motionEvent.action) {
            MotionEvent.ACTION_DOWN -> {
                x = motionEvent.rawX.toInt()
                y = motionEvent.rawY.toInt()

            }
            MotionEvent.ACTION_MOVE -> {
                val nowX = motionEvent.rawX.toInt()
                val nowY = motionEvent.rawY.toInt()
                val movedX = nowX - x
                val movedY = nowY - y
                x = nowX
                y = nowY
                wl.apply {
                    x += movedX
                    y += movedY
                }
                //更新悬浮球控件位置
                windowManager?.updateViewLayout(view, wl)
            }
            else -> {

            }
        }
        return false
    }
}
复制代码

Effect

Apply external floating window (with limitations)

The implementation process of the floating window outside the application Here I use LivaData to communicate with the Service

  • Apply for Floating Window Permission

  • Create Service

  • getWindowManager

  • Create a floating view

  • Set the drag event of the floating View

  • Add View to WindowManager

Add permission to manifest file

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
复制代码

Above code:

Open the floating window

startService(Intent(this, SuspendwindowService::class.java))
Utils.checkSuspendedWindowPermission(this) {
    isReceptionShow = false
    ViewModleMain.isShowSuspendWindow.postValue(true)
}
复制代码

SuspendwindowService code is as follows

package com.lmy.suspendedwindow.service

import android.annotation.SuppressLint
import android.graphics.PixelFormat
import android.os.Build
import android.util.DisplayMetrics
import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.lifecycle.LifecycleService
import com.lmy.suspendedwindow.R
import com.lmy.suspendedwindow.utils.Utils
import com.lmy.suspendedwindow.utils.ViewModleMain
import com.lmy.suspendedwindow.utils.ItemViewTouchListener

/**
 * @功能:应用外打开Service 有局限性 特殊界面无法显示
 * @User Lmy
 * @Creat 4/15/21 5:28 PM
 * @Compony 永远相信美好的事情即将发生
 */
class SuspendwindowService : LifecycleService() {
    private lateinit var windowManager: WindowManager
    private var floatRootView: View? = null//悬浮窗View


    override fun onCreate() {
        super.onCreate()
        initObserve()
    }

    private fun initObserve() {
        ViewModleMain.apply {
            isVisible.observe(this@SuspendwindowService, {
                floatRootView?.visibility = if (it) View.VISIBLE else View.GONE
            })
            isShowSuspendWindow.observe(this@SuspendwindowService, {
                if (it) {
                    showWindow()
                } else {
                    if (!Utils.isNull(floatRootView)) {
                        if (!Utils.isNull(floatRootView?.windowToken)) {
                            if (!Utils.isNull(windowManager)) {
                                windowManager?.removeView(floatRootView)
                            }
                        }
                    }
                }
            })
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun showWindow() {
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        val outMetrics = DisplayMetrics()
        windowManager.defaultDisplay.getMetrics(outMetrics)
        var layoutParam = WindowManager.LayoutParams().apply {
            type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            } else {
                WindowManager.LayoutParams.TYPE_PHONE
            }
            format = PixelFormat.RGBA_8888
            flags =
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            //位置大小设置
            width = WRAP_CONTENT
            height = WRAP_CONTENT
            gravity = Gravity.LEFT or Gravity.TOP
            //设置剧中屏幕显示
            x = outMetrics.widthPixels / 2 - width / 2
            y = outMetrics.heightPixels / 2 - height / 2
        }
        // 新建悬浮窗控件
        floatRootView = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)
        floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))
        // 将悬浮窗控件添加到WindowManager
        windowManager.addView(floatRootView, layoutParam)
    }
}
复制代码

ViewModleMain code is as follows

package com.lmy.suspendedwindow.utils

import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

/**
 * @功能: 用于和Service通信
 * @User Lmy
 * @Creat 4/16/21 8:37 AM
 * @Compony 永远相信美好的事情即将发生
 */
object ViewModleMain : ViewModel() {
    //悬浮窗口创建 移除  基于无障碍服务
    var isShowWindow = MutableLiveData<Boolean>()
    //悬浮窗口创建 移除

    var isShowSuspendWindow = MutableLiveData<Boolean>()

    //悬浮窗口显示 隐藏
    var isVisible = MutableLiveData<Boolean>()

}
复制代码

Utils code is as follows:

package com.lmy.suspendedwindow.utils

import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import com.lmy.suspendedwindow.service.WorkAccessibilityService
import java.util.*

/**
 * @功能: 工具类
 * @User Lmy
 * @Creat 4/16/21 8:33 AM
 * @Compony 永远相信美好的事情即将发生
 */
object Utils {
    const val REQUEST_FLOAT_CODE=1001
    /**
     * 跳转到设置页面申请打开无障碍辅助功能
     */
   private fun accessibilityToSettingPage(context: Context) {
        //开启辅助功能页面
        try {
            val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            context.startActivity(intent)
        } catch (e: Exception) {
            val intent = Intent(Settings.ACTION_SETTINGS)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            context.startActivity(intent)
            e.printStackTrace()
        }
    }

    /**
     * 判断Service是否开启
     *
     */
    fun isServiceRunning(context: Context, ServiceName: String): Boolean {
        if (TextUtils.isEmpty(ServiceName)) {
            return false
        }
        val myManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val runningService =
            myManager.getRunningServices(1000) as ArrayList<ActivityManager.RunningServiceInfo>
        for (i in runningService.indices) {
            if (runningService[i].service.className == ServiceName) {
                return true
            }
        }
        return false
    }

    /**
     * 判断悬浮窗权限权限
     */
   private fun commonROMPermissionCheck(context: Context?): Boolean {
        var result = true
        if (Build.VERSION.SDK_INT >= 23) {
            try {
                val clazz: Class<*> = Settings::class.java
                val canDrawOverlays =
                    clazz.getDeclaredMethod("canDrawOverlays", Context::class.java)
                result = canDrawOverlays.invoke(null, context) as Boolean
            } catch (e: Exception) {
                Log.e("ServiceUtils", Log.getStackTraceString(e))
            }
        }
        return result
    }

    /**
     * 检查悬浮窗权限是否开启
     */
    fun checkSuspendedWindowPermission(context: Activity, block: () -> Unit) {
        if (commonROMPermissionCheck(cont ext)) {
            block()
        } else {
            Toast.makeText(context, "请开启悬浮窗权限", Toast.LENGTH_SHORT).show()
            context.startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
                data = Uri.parse("package:${context.packageName}")
            }, REQUEST_FLOAT_CODE)
        }
    }

    /**
     * 检查无障碍服务权限是否开启
     */
    fun checkAccessibilityPermission(context: Activity, block: () -> Unit) {
        if (isServiceRunning(context, WorkAccessibilityService::class.java.canonicalName)) {
            block()
        } else {
            accessibilityToSettingPage(context)
        }
    }

    fun isNull(any: Any?): Boolean = any == null

}
复制代码

Effect:

Adaptation of Floating Window Permissions

Permission configuration and request

There's nothing wrong with this one

When Android7.0 or above, you need to declare the SYSTEM_ALERT_WINDOW permission in the AndroidManefest.xml file

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
复制代码

The pit of LayoutParam! ! ! !

The addView method of WindowManager has two parameters, one is the control object to be added, and the other parameter is the WindowManager.LayoutParam object.

The type variable in LayoutParam. There is a big hole! ! ! ! ! ! , this variable is used to specify the window type. When setting this variable, it is necessary to adapt to different versions of the Android system.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
复制代码

Before Android 8.0, the floating window setting could be TYPE_PHONE, which is a non-application window used to provide user interaction.

However, if you continue to use the floating window of type TYPE_PHONE in Android 8.0 and above, the following abnormal information will appear:

android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002
复制代码

After Android 8.0, the following window types are not allowed to display reminder windows above other applications and windows. These types include:

TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
复制代码

If you need to display the reminder window above other applications and windows, it must be of the type TYPE_APPLICATION_OVERLAY.

But this TYPE_APPLICATION_OVERLAY type cannot be displayed on all interfaces

like this

Some students will ask what the hell is this operation? how to solve this

Don't panic, just be patient and look for Baidu, which will always find the answer, if it doesn't work, then Google

After tireless efforts finally found a solution

This problem can be solved by using another type TYPE_ACCESSIBILITY_OVERLAY

But when you use it happily you will find the following errors

After checking some information, it is found that this type must be used in conjunction with the accessibility AccessibilityService

Barrier-free floating window

To configure the accessibility process, see my other blog for automatic skipping of APP startup page advertisements based on accessibility services

Barrier-free floating window implementation process

  • Configure Accessibility Services

  • Get WindowManager in AccessibilityService

  • Create a floating view

  • Set the drag event of the floating View

  • Add View to WindowManager

Start the floating window:

Utils.checkAccessibilityPermission(this) {
    ViewModleMain.isShowWindow.postValue(true)
}
复制代码

WorkAccessibilityService code is as follows

package com.lmy.suspendedwindow.service

import android.accessibilityservice.AccessibilityService
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.util.DisplayMetrics
import android.view.*
import android.view.accessibility.AccessibilityEvent
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.lmy.suspendedwindow.R
import com.lmy.suspendedwindow.utils.ItemViewTouchListener
import com.lmy.suspendedwindow.utils.Utils.isNull
import com.lmy.suspendedwindow.utils.ViewModleMain

/**
 * @功能:利用无障碍打开悬浮窗口 无局限性 任何界面可以显示
 * @User Lmy
 * @Creat 4/15/21 5:57 PM
 * @Compony 永远相信美好的事情即将发生
 */
class WorkAccessibilityService : AccessibilityService(), LifecycleOwner {
    private lateinit var windowManager: WindowManager
    private var floatRootView: View? = null//悬浮窗View
    private val mLifecycleRegistry = LifecycleRegistry(this)
    override fun onCreate() {
        super.onCreate()
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
        initObserve()
    }

    /**
     * 打开关闭的订阅
     */
    private fun initObserve() {
        ViewModleMain.isShowWindow.observe(this, {
            if (it) {
                showWindow()
            } else {
                if (!isNull(floatRootView)) {
                    if (!isNull(floatRootView?.windowToken)) {
                        if (!isNull(windowManager)) {
                            windowManager?.removeView(floatRootView)
                        }
                    }
                }
            }
        })
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun showWindow() {
        // 设置LayoutParam
        // 获取WindowManager服务
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        val outMetrics = DisplayMetrics()
        windowManager.defaultDisplay.getMetrics(outMetrics)
        var layoutParam = WindowManager.LayoutParams()
        layoutParam.apply {
            //显示的位置
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
                //刘海屏延伸到刘海里面
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    layoutInDisplayCutoutMode =
                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
                }
            } else {
                type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
            }
            flags =
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            width = WindowManager.LayoutParams.WRAP_CONTENT
            height = WindowManager.LayoutParams.WRAP_CONTENT
            format = PixelFormat.TRANSPARENT
        }
        floatRootView = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)
        floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))
        windowManager.addView(floatRootView, layoutParam)
    }


    override fun onServiceConnected() {
        super.onServiceConnected()
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
    }

    override fun getLifecycle(): Lifecycle = mLifecycleRegistry
    override fun onStart(intent: Intent?, startId: Int) {
        super.onStart(intent, startId)
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
    }

    override fun onUnbind(intent: Intent?): Boolean {
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
        return super.onUnbind(intent)
    }

    override fun onDestroy() {
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        super.onDestroy()
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
    }

    override fun onInterrupt() {
    }
}
复制代码

Summarize

Using ordinary Service to create a floating window cannot display any interface

Any interface can be suspended using accessibility services

Guess you like

Origin juejin.im/post/6951608145537925128