悬浮窗口主要分为两类:一类是应用内悬浮窗口,一类是系统类的悬浮窗口(类似微信视频弹窗,由于会覆盖在其他应用上,需要申请额外的系统权限)。
其本质上都是一样,创建某个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 } } |
至此,应用内的悬浮球开发完成。