悬浮窗口主要分为两类:一类是应用内悬浮窗口,一类是系统类的悬浮窗口(类似微信视频弹窗,由于会覆盖在其他应用上,需要申请额外的系统权限)。
其本质上都是一样,创建某个window,只是创建的window的type不一样,可以参考官方对不同type的描述文档。
本文主要介绍的是应用内的悬浮球如何开发
根据文档描述,我们可以知道TYPE_APPLICATION_PANEL适合用于应用内悬浮球的开发。
由于应用的悬浮球是依附在某Activity上的,这就需要在切换Activity的时候,不断切换悬浮球的token。所以我们选择在Activity的生命周期监听做处理:
class FloatWindowLifecycle : Application.ActivityLifecycleCallbacks {var weakCurrentActivity: WeakReference<Activity?>? = nullvar weakGlobalListener: WeakReference<ViewTreeObserver.OnGlobalLayoutListener>? = nulloverride 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的封装。
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_PANELflags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE orWindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH orWindowManager.LayoutParams.FLAG_NOT_TOUCH_MODALformat = PixelFormat.RGBA_8888gravity = Gravity.CENTER_VERTICAL or Gravity.STARTwidth = WindowManager.LayoutParams.WRAP_CONTENTheight = WindowManager.LayoutParams.WRAP_CONTENTtoken = pToken}}fun initDebugPanelFloatWindow(context: Context,clickAction: (context: Context) -> Unit) {FloatWindowManager.getInstance(context.applicationContext).addWindowLayout(object : FloatWindowLayout(context.applicationContext) {override fun stickySide(): Boolean = trueoverride 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。
internal class FloatWindowManager private constructor(context: Context) {companion object {@Volatileprivate var instance: FloatWindowManager? = nullfun 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 WindowManagerprivate 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? = nullwindowViewList.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.LayoutParamsif (!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在其中。
abstract class FloatWindowLayout : FrameLayout {private var lastX = 0fprivate var lastY = 0fprivate var downX = 0fprivate var downY = 0fprivate var startMove = falseprivate var animator: ValueAnimator? = nullprivate var isAddToWindowManager = falseconstructor(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.rawXval rawY = event.rawYval offsetX = (rawX - lastX).toInt()val offsetY = (rawY - lastY).toInt()lastX = rawXlastY = rawYval params = layoutParams as WindowManager.LayoutParamsparams.x += offsetXparams.y += offsetYval windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManagerwindowManager.updateViewLayout(this, params)}private fun touchCancel() {if (stickySide()) { //自动吸边val params = layoutParams as WindowManager.LayoutParamsval screenWidth = FloatWindowUtils.getScreenWidth(context)val currentX = params.xval destX = if (currentX + width / 2 > screenWidth / 2) {//向右screenWidth - width} else {//向左0}animator = ValueAnimator.ofInt(currentX, destX).apply {duration = 200interpolator = AccelerateInterpolator()addUpdateListener { animation ->animation?.run {val value = animation.animatedValue as Intparams.x = valueif (isAttachedToWindow) {val windowManager =context.getSystemService(Context.WINDOW_SERVICE) as WindowManagerwindowManager.updateViewLayout(this@FloatWindowLayout, params)} else {animation.cancel()}}}start()}}}override fun onInterceptTouchEvent(event: MotionEvent): Boolean {var intercept = falsewhen (event.action) {MotionEvent.ACTION_DOWN -> {animator?.also {it.cancel()}startMove = falsedownX = event.rawXdownY = event.rawYlastX = event.rawXlastY = event.rawY}MotionEvent.ACTION_MOVE -> {val offsetX = abs(event.rawX - downX)val offsetY = abs(event.rawY - downY)val minTouchSlop = ViewConfiguration.get(context).scaledTouchSlopif (startMove || (offsetX > minTouchSlop || offsetY > minTouchSlop)) {intercept = true}}MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {startMove = false}}return intercept}abstract fun uniqueStr(): Stringabstract fun stickySide(): Booleanfun isAddToWindowManager(): Boolean = isAddToWindowManagerfun setAddToWindowManager(addToWindowManager: Boolean) {isAddToWindowManager = addToWindowManager}
}
至此,应用内的悬浮球开发完成。