Android悬浮窗的一种实现,实现原理分析
作者:互联网
效果如下:
原生ViewManager
接口提供了向窗口添加并操纵View
的方法:
public interface ViewManager{
//‘向窗口添加视图’
public void addView(View view, ViewGroup.LayoutParams params);
//‘更新窗口中视图’
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
//‘移除窗口中视图’
public void removeView(View view);
}
使用这个接口显示窗口的模版代码如下:
//‘解析布局文件为视图’
val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)
//‘获取WindowManager系统服务’
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
//‘构建窗口布局参数’
WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
gravity = Gravity.START or Gravity.TOP
x = 0
y = 0
}.let { layoutParams->
//‘将视图添加到窗口’
windowManager.addView(windowView, layoutParams)
}
上述代码在当前界面的左上角显示R.id.window_view.xml
中定义的布局。
为避免重复,将这段代码抽象成一个函数,其中窗口视图内容和展示位置会随着需求而变,遂将其参数化:
object FloatWindow{
private var context: Context? = n
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
ull
//‘当前窗口参数’
var windowInfo: WindowInfo? = null
//‘把和Window布局有关的参数打包成一个内部类’
class WindowInfo(var view: View?) {
var layoutParams: WindowManager.LayoutParams? = null
//‘窗口宽’
var width: Int = 0
//‘窗口高’
var height: Int = 0
//‘窗口中是否有视图’
fun hasView() = view != null && layoutParams != null
//‘窗口中视图是否有父亲’
fun hasParent() = hasView() && view?.parent != null
}
//‘显示窗口’
fun show(
context: Context,
windowInfo: WindowInfo?,
x: Int = windowInfo?.layoutParams?.x.value(),
y: Int = windowInfo?.layoutParams?.y.value(),
) {
if (windowInfo == null) { return }
if (windowInfo.view == null) { return }
this.windowInfo = windowInfo
this.context = context
//‘创建窗口布局参数’
windowInfo.layoutParams = createLayoutParam(x, y)
//‘显示窗口’
if (!windowInfo.hasParent().value()) {
val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.addView(windowInfo.view, windowInfo.layoutParams)
}
}
//‘创建窗口布局参数’
private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {
if (context == null) { return WindowManager.LayoutParams() }
return WindowManager.LayoutParams().apply {
//‘该类型不需要申请权限’
type = WindowManager.LayoutParams.TYPE_APPLICATION
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
gravity = Gravity.START or Gravity.TOP
width = windowInfo?.width.value()
height = windowInfo?.height.value()
this.x = x
this.y = y
}
}
//‘为空Int提供默认值’
fun Int?.value() = this ?: 0
}
将FloatWindow
声明成了单例,目的是在 app 整个生命周期,任何界面都可以方便地显示浮窗。
为了方便统一管理窗口的参数,抽象了内部类WindowInfo
现在就可以像这样在屏幕左上角显示一个浮窗了:
val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)
WindowInfo(windowView).apply{
width = 100
height = 100
}.let{ windowInfo ->
FloatWindow.show(context, windowInfo, 0, 0)
}
产品要求当浮窗显示时,屏幕变暗。设置WindowManager.LayoutParams.FLAG_DIM_BEHIND
标签配合dimAmount
就能轻松实现:
object FloatWindow{
//当前窗口参数
var windowInfo: WindowInfo? = null
private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {
if (context == null) { return WindowManager.LayoutParams() }
return WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION
format = PixelFormat.TRANSLUCENT
flags =
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
//‘设置浮窗背景变暗’
WindowManager.LayoutParams.FLAG_DIM_BEHIND
//‘设置默认变暗程度为0,即不变暗,1表示全黑’
dimAmount = 0f
gravity = Gravity.START or Gravity.TOP
width = windowInfo?.width.value()
height = windowInfo?.height.value()
this.x = x
this.y = y
}
}
//‘供业务界面在需要的时候调整浮窗背景亮暗’
fun setDimAmount(amount:Float){
windowInfo?.layoutParams?.let { it.dimAmount = amount }
}
}
为浮窗设置点击事件等价于为浮窗视图设置点击事件,但如果直接对浮窗视图使用setOnClickListener()
的话,浮窗的触摸事件就不会被响应,那拖拽就无法实现。所以只能从更底层的触摸事件着手:
object FloatWindow : View.OnTouchListener{
//‘显示窗口’
fun show(
context: Context,
windowInfo: WindowInfo?,
x: Int = windowInfo?.layoutParams?.x.value(),
y: Int = windowInfo?.layoutParams?.y.value(),
) {
if (windowInfo == null) { return }
if (windowInfo.view == null) { return }
this.windowInfo = windowInfo
this.context = context
//‘为浮窗视图设置触摸监听器’
windowInfo.view?.setOnTouchListener(this)
windowInfo.layoutParams = createLayoutParam(x, y)
if (!windowInfo.hasParent().value()) {
val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.addView(windowInfo.view, windowInfo.layoutParams)
}
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
return false
}
}
在onTouch(v: View, event: MotionEvent)
中可以拿到更详细的触摸事件,比如ACTION_DOWN
,ACTION_MOVE
、ACTION_UP
。这方便了拖拽的实现,但点击事件的捕获变得复杂,因为需要定义上述三个 ACTION
以怎样的序列出现时才判定为点击事件。幸好GestureDetector
为我们做了这件事:
public class GestureDetector {
public interface OnGestureListener {
//‘ACTION_DOWN事件’
boolean onDown(MotionEvent e);
//‘单击事件’
boolean onSingleTapUp(MotionEvent e);
//‘拖拽事件’
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
…
}
}
构建GestureDetector
实例并将MotionEvent
传递给它就能将触摸事件解析成感兴趣的上层事件:
object FloatWindow : View.OnTouchListener{
private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
private var clickListener: WindowClickListener? = null
private var lastTouchX: Int = 0
private var lastTouchY: Int = 0
//‘为浮窗设置点击监听器’
fun setClickListener(listener: WindowClickListener) {
clickListener = listener
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
//‘将触摸事件传递给 GestureDetector 解析’
gestureDetector.onTouchEvent(event)
return true
}
//‘记忆起始触摸点坐标’
private fun onActionDown(event: MotionEvent) {
lastTouchX = event.rawX.toInt()
lastTouchY = event.rawY.toInt()
}
private class GestureListener : GestureDetector.OnGestureListener {
//‘记忆起始触摸点坐标’
override fun onDown(e: MotionEvent): Boolean {
onActionDown(e)
return false
}
override fun onSingleTapUp(e: MotionEvent): Boolean {
//‘点击事件发生时,调用监听器’
return clickListener?.onWindowClick(windowInfo) ?: false
}
…
}
//‘浮窗点击监听器’
interface WindowClickListener {
fun onWindowClick(windowInfo: WindowInfo?): Boolean
}
}
ViewManager
提供了updateViewLayout(View view, ViewGroup.LayoutParams params)
用于更新浮窗位置,所以只需监听ACTION_MOVE
事件并实时更新浮窗视图位置就可实现拖拽。ACTION_MOVE
事件被GestureDetecto
r解析成OnGestureListener.onScroll()
回调:
object FloatWindow : View.OnTouchListener{
private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
private var lastTouchX: Int = 0
private var lastTouchY: Int = 0
override fun onTouch(v: View, event: MotionEvent): Boolean {
//‘将触摸事件传递给GestureDetector解析’
gestureDetector.onTouchEvent(event)
return true
}
private class GestureListener : GestureDetector.OnGestureListener {
override fun onDown(e: MotionEvent): Boolean {
onActionDown(e)
return false
}
override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY:Float): Boolean {
//‘响应手指滚动事件’
onActionMove(e2)
return true
}
}
private fun onActionMove(event: MotionEvent) {
//‘获取当前手指坐标’
val currentX = event.rawX.toInt()
val currentY = event.rawY.toInt()
//‘获取手指移动增量’
val dx = currentX - lastTouchX
val dy = currentY - lastTouchY
//‘将移动增量应用到窗口布局参数上’
windowInfo?.layoutParams!!.x += dx
windowInfo?.layoutParams!!.y += dy
val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
var rightMost = screenWidth - windowInfo?.layoutParams!!.width
var leftMost = 0
val topMost = 0
val bottomMost = screenHeight - windowInfo?.layoutParams!!.height - getNavigationBarHeight(context)
//‘将浮窗移动区域限制在屏幕内’
if (windowInfo?.layoutParams!!.x < leftMost) {
windowInfo?.layoutParams!!.x = leftMost
}
if (windowInfo?.layoutParams!!.x > rightMost) {
windowInfo?.layoutParams!!.x = rightMost
}
if (windowInfo?.layoutParams!!.y < topMost) {
windowInfo?.layoutParams!!.y = topMost
}
if (windowInfo?.layoutParams!!.y > bottomMost) {
windowInfo?.layoutParams!!.y = bottomMost
}
//‘更新浮窗位置’
windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)
lastTouchX = currentX
lastTouchY = currentY
}
}
新的需求来了,拖拽浮窗松手后,需要自动贴边。
把贴边理解成一个水平位移动画。在松手时求出动画起点和终点横坐标,利用动画值不断更新浮窗位置:
object FloatWindow : View.OnTouchListener{
private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
private var lastTouchX: Int = 0
private var lastTouchY: Int = 0
//‘贴边动画’
private var weltAnimator: ValueAnimator? = null
override fun onTouch(v: View, event: MotionEvent): Boolean {
//‘将触摸事件传递给GestureDetector解析’
gestureDetector.onTouchEvent(event)
//‘处理ACTION_UP事件’
val action = event.action
when (action) {
MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0)
else -> {
}
}
return true
}
private fun onActionUp(event: MotionEvent, screenWidth: Int, width: Int) {
if (!windowInfo?.hasView().value()) { return }
//‘记录抬手横坐标’
val upX = event.rawX.toInt()
//‘贴边动画终点横坐标’
val endX = if (upX > screenWidth / 2) {
标签:实现,WindowManager,layoutParams,悬浮,LayoutParams,fun,Android,windowInfo,浮窗 来源: https://blog.csdn.net/flunsna/article/details/122169575