Android 应用内悬浮球开发

悬浮窗口主要分为两类:一类是应用内悬浮窗口,一类是系统类的悬浮窗口(类似微信视频弹窗,由于会覆盖在其他应用上,需要申请额外的系统权限)。

其本质上都是一样,创建某个window,只是创建的window的type不一样,可以参考官方对不同type的描述文档。

本文主要介绍的是应用内的悬浮球如何开发

根据文档描述,我们可以知道TYPE_APPLICATION_PANEL适合用于应用内悬浮球的开发。
由于应用的悬浮球是依附在某Activity上的,这就需要在切换Activity的时候,不断切换悬浮球的token。所以我们选择在Activity的生命周期监听做处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class FloatWindowLifecycle : Application.ActivityLifecycleCallbacks {
    var weakCurrentActivity: WeakReference<Activity?>? = null
    var weakGlobalListener: WeakReference<ViewTreeObserver.OnGlobalLayoutListener>? = null
    override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {}
    override fun onActivityStarted(activity: Activity?) {}
   
    override fun onActivityResumed(activity: Activity?) {
        weakCurrentActivity = WeakReference(activity)
        activity?.window?.decorView?.let { decorView ->
            decorView.viewTreeObserver?.let { viewTree ->
                if (decorView.windowToken != null) {
                    FloatWindowUtils.bindDebugPanelFloatWindow(activity, decorView.windowToken)
                    weakGlobalListener?.get()?.let { globalListener ->
                        decorView.viewTreeObserver.removeOnGlobalLayoutListener(globalListener)
                    }
                } else {
                    val globalListener = object : ViewTreeObserver.OnGlobalLayoutListener {
                        override fun onGlobalLayout() {
                            activity.window?.decorView?.windowToken?.let {
                                FloatWindowUtils.bindDebugPanelFloatWindow(activity, it)
                            }
                            decorView.viewTreeObserver.removeOnGlobalLayoutListener(this)
                        }
                    }
                    viewTree.addOnGlobalLayoutListener(globalListener)
                    weakGlobalListener = WeakReference(globalListener)
                }
            }
        }
    }

    override fun onActivityPaused(activity: Activity?) {
        activity?.let {
            FloatWindowUtils.unbindDebugPanelFloatWindow(activity)
        }
    }
    override fun onActivityStopped(activity: Activity?) {}
    override fun onActivityDestroyed(activity: Activity?) {}
    override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {}
}

工具类封装:
工具类主要提供了 WindowManager.LayoutParams的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
internal object FloatWindowUtils {
    fun updateLayoutParams(
        params: WindowManager.LayoutParams?,
        pToken: IBinder
    ): WindowManager.LayoutParams {
        return params?.apply {
            token = pToken
        }
            ?: WindowManager.LayoutParams().apply {
                type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
                flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                        WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
                        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                format = PixelFormat.RGBA_8888
                gravity = Gravity.CENTER_VERTICAL or Gravity.START
                width = WindowManager.LayoutParams.WRAP_CONTENT
                height = WindowManager.LayoutParams.WRAP_CONTENT
                token = pToken
            }
    }

    fun initDebugPanelFloatWindow(
        context: Context,
        clickAction: (context: Context) -> Unit
    ) {
        FloatWindowManager.getInstance(context.applicationContext)
            .addWindowLayout(object : FloatWindowLayout(context.applicationContext) {
                override fun stickySide(): Boolean = true

                override fun uniqueStr(): String = FloatWindowConst.UNIQUE_STR_DEBUG

            }.apply {
                addView(ImageView(context.applicationContext).apply {
                    setImageDrawable(
                        ContextCompat.getDrawable(
                            context,
                            R.drawable.house
                        )
                    )
                    setOnClickListener {
                        clickAction(it.context)
                    }
                })
            })
    }
    fun bindDebugPanelFloatWindow(context: Context, token: IBinder) {
        FloatWindowManager.getInstance(context)
            .bindWindowLayout(FloatWindowConst.UNIQUE_STR_DEBUG, token)
    }
    fun unbindDebugPanelFloatWindow(context: Context) {
        FloatWindowManager.getInstance(context)
            .unbindWindowLayout(FloatWindowConst.UNIQUE_STR_DEBUG)
    }
    fun getScreenWidth(context: Context): Int {
        return context.resources.displayMetrics.widthPixels
    }

}

封装FloatWindowManager 管理类:
主要用于WindowLayout管理和向WindowManager中添加以及移除某个View。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
internal class FloatWindowManager private constructor(context: Context) {
    companion object {
        @Volatile
        private var instance: FloatWindowManager? = null

        fun getInstance(c: Context): FloatWindowManager {
            if (instance == null) {
                synchronized(FloatWindowManager::class) {
                    if (instance == null) {
                        instance = FloatWindowManager(c.applicationContext)
                    }
                }
            }
            return instance!!
        }
    }


    private var windowViewList = mutableListOf<FloatWindowLayout>()

    private var windowManager =
        context.getSystemService(Context.WINDOW_SERVICE) as WindowManager


    private fun hasWindowLayout(key: String): Boolean {
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                return true
            }
        }
        return false
    }


    fun addWindowLayout(view: FloatWindowLayout) {
        if (hasWindowLayout(view.uniqueStr())) {
            return
        }
        windowViewList.add(view)
    }

    fun removeWindowLayout(key: String) {
        var target: FloatWindowLayout? = null
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                target = it
            }
        }
        target?.let {
            windowViewList.remove(it)
        }
    }

    fun bindWindowLayout(key: String, token: IBinder) {
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                val params = it.layoutParams as? WindowManager.LayoutParams
                if (!it.isAddToWindowManager()) {
                    windowManager.addView(it, FloatWindowUtils.updateLayoutParams(params, token))
                    it.setAddToWindowManager(true)
                } else {
                    windowManager.removeView(it)
                    windowManager.addView(it, FloatWindowUtils.updateLayoutParams(params, token))
                    it.setAddToWindowManager(true)
                }
            }
        }
    }

    fun unbindWindowLayout(key: String) {
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                if (it.isAddToWindowManager()) {
                    windowManager.removeView(it)
                    it.setAddToWindowManager(false)
                }
            }
        }
    }
}

抽象类:FloatWindowLayout

添加到windowManager中的的ViewGroup,继承至FeameLayout,你可以添加各种View在其中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
abstract class FloatWindowLayout : FrameLayout {
    private var lastX = 0f
    private var lastY = 0f

    private var downX = 0f
    private var downY = 0f
    private var startMove = false

    private var animator: ValueAnimator? = null

    private var isAddToWindowManager = false

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> touchMove(event)
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> touchCancel()
        }
        return true
    }


    private fun touchMove(event: MotionEvent) {
        val rawX = event.rawX
        val rawY = event.rawY
        val offsetX = (rawX - lastX).toInt()
        val offsetY = (rawY - lastY).toInt()
        lastX = rawX
        lastY = rawY
        val params = layoutParams as WindowManager.LayoutParams
        params.x += offsetX
        params.y += offsetY
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        windowManager.updateViewLayout(this, params)

    }

    private fun touchCancel() {
        if (stickySide()) {  //自动吸边
            val params = layoutParams as WindowManager.LayoutParams
            val screenWidth = FloatWindowUtils.getScreenWidth(context)
            val currentX = params.x
            val destX = if (currentX + width / 2 > screenWidth / 2) {
                //向右
                screenWidth - width
            } else {
                //向左
                0
            }

            animator = ValueAnimator.ofInt(currentX, destX).apply {
                duration = 200
                interpolator = AccelerateInterpolator()
                addUpdateListener { animation ->
                    animation?.run {
                        val value = animation.animatedValue as Int
                        params.x = value
                        if (isAttachedToWindow) {
                            val windowManager =
                                context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
                            windowManager.updateViewLayout(this@FloatWindowLayout, params)
                        } else {
                            animation.cancel()
                        }
                    }
                }
                start()
            }
        }
    }


    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        var intercept = false
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                animator?.also {
                    it.cancel()
                }
                startMove = false
                downX = event.rawX
                downY = event.rawY
                lastX = event.rawX
                lastY = event.rawY
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = abs(event.rawX - downX)
                val offsetY = abs(event.rawY - downY)
                val minTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
                if (startMove || (offsetX > minTouchSlop || offsetY > minTouchSlop)) {
                    intercept = true
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                startMove = false
            }
        }
        return intercept
    }

    abstract fun uniqueStr(): String

    abstract fun stickySide(): Boolean

    fun isAddToWindowManager(): Boolean = isAddToWindowManager

    fun setAddToWindowManager(addToWindowManager: Boolean) {
        isAddToWindowManager = addToWindowManager
    }
}

至此,应用内的悬浮球开发完成。