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