懸浮窗口主要分爲兩類:一類是應用內懸浮窗口,一類是系統類的懸浮窗口(類似微信視頻彈窗,由於會覆蓋在其他應用上,需要申請額外的系統權限)。
其本質上都是一樣,創建某個window,只是創建的window的type不一樣,可以參考官方對不同type的描述文檔。
本文主要介紹的是應用內的懸浮球如何開發
根據文檔描述,我們可以知道TYPE_APPLICATION_PANEL適合用於應用內懸浮球的開發。
由於應用的懸浮球是依附在某Activity上的,這就需要在切換Activity的時候,不斷切換懸浮球的token。所以我們選擇在Activity的生命週期監聽做處理:
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的封裝。
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。
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在其中。
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
}
}
至此,應用內的懸浮球開發完成。