写点什么

悬浮窗的一种实现 _ Android 悬浮窗 Window 应用,移动互联网开发技术专业

用户头像
Android架构
关注
发布于: 刚刚

设置浮窗点击事件

为浮窗设置点击事件等价于为浮窗视图设置点击事件,但如果直接对浮窗视图使用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 = windowInfothis.context = context//'为浮窗视图设置触摸监听器'windowInfo.view?.setOnTouchListener(this)windowInfo.layoutParams = createLayoutParam(x, y)if (!windowInfo.hasParent().value()) {val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManagerwindowManager.addView(windowInfo.view, windowInfo.layoutParams)}}


override fun onTouch(v: View, event: MotionEvent): Boolean {return false}}


  • onTouch(v: View, event: MotionEvent)中可以拿到更详细的触摸事件,比如ACTION_DOWNACTION_MOVEACTION_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? = nullprivate var lastTouchX: Int = 0private 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事件被GestureDetector解析成OnGestureListener.onScroll()回调:


object FloatWindow : View.OnTouchListener{private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())private var lastTouchX: Int = 0private 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 - lastTouchXval dy = currentY - lastTouchY//'将移动增量应用到窗口布局参数上'windowInfo?.layoutParams!!.x += dxwindowInfo?.layoutParams!!.y += dyval windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManagervar rightMost = screenWidth - windowInfo?.layoutParams!!.widthvar leftMost = 0val topMost = 0val 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 = currentXlastTouchY = currentY}}

浮窗自动贴边

新的需求来了,拖拽浮窗松手后,需要自动贴边。


把贴边理解成一个水平位移动画。在松手时求出动画起点和终点横坐标,利用动画值不断更新浮窗位置::


object FloatWindow : View.OnTouchListener{private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())private var lastTouchX: Int = 0private 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.actionwhen (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) {screenWidth - width} else {0}


//'构建贴边动画'if (weltAnimator == null) {weltAnimator = ValueAnimator.ofInt(windowInfo?.layoutParams!!.x, endX).apply {interpolator = LinearInterpolator()duration = 300addUpdateListener { animation ->val x = animation.animatedValue as Intif (windowInfo?.layoutParams != null) {windowInfo?.layoutParams!!.x = x}val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager//'更新窗口位置'if (windowInfo?.hasParent().value()) {windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)}}}}weltAnimator?.setIntValues(windowInfo?.layoutParams!!.x, endX)weltAnimator?.start()}


//为空 Boolean 提供默认值 fun Boolean?.value() = this ?: false}


  • GestureDetector解析后ACTION_UP事件被吞掉了,所以只能在onTouch()中截获它。

  • 根据抬手横坐标和屏幕中点横坐标的大小关系,来决定浮窗贴向左边还是右边。

管理多个浮窗

若 app 的不同业务界面同时需要显示浮窗:进入 界面 A 时显示 浮窗 A,然后它被拖拽到右下角,退出 界面 A 进入 界面 B,显示浮窗 B,当再次进入 界面 A 时,期望还原上次离开时的浮窗 A 的位置。


当前FloatWindow中用windowInfo成员存储单个浮窗参数,为了同时管理多个浮窗,需要将所有浮窗参数保存在Map结构中用 tag 区分:


object FloatWindow : View.OnTouchListener {//'浮窗参数容器'private var windowInfoMap: HashMap<String, WindowInfo?> = HashMap()//'当前浮窗参数'var windowInfo: WindowInfo? = null


//'显示浮窗'fun show(context: Context,//'浮窗标签'tag: String,//'若不提供浮窗参数则从参数容器中获取该 tag 上次保存的参数'windowInfo: WindowInfo? = windowInfoMap[tag],x: Int = windowInfo?.layoutParams?.x.value(),y: Int = windowInfo?.layoutParams?.y.value()) {if (windowInfo == null) { return }if (windowInfo.view == null) { return }//'更新当前浮窗参数'this.windowInfo = windowInfo//'将浮窗参数存入容器'windowInfoMap[tag] = windowInfowindowInfo.view?.setOnTouchListener(this)this.context = contextwindowInfo.layoutParams = createLayoutParam(x, y)if (!windowInfo.hasParent().value()) {val windowManager =this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManagerwindowManager.addView(windowInfo.view, windowInfo.layoutParams)}}}


在显示浮窗时,增加tag标签参数用以唯一标识浮窗,并且为windowInfo提供默认参数,当恢复原有浮窗时,可以不提供windowInfo参数,FloatWindow就会去windowInfoMap中根据给定tag寻找对应windowInfo

监听浮窗界外点击事件

新的需求来了,点击浮窗时,贴边的浮窗像抽屉一样展示,点击浮窗以外区域时,抽屉收起。


刚开始接到这个新需求时,没什么思路。转念一想PopupWindow有一个setOutsideTouchable()


public class PopupWindow {/**


  • <p>Controls whether the pop-up will be informed of touch events outside

  • of its window.

  • @param touchable true if the popup should receive outside

  • touch events, false otherwise*/public void setOutsideTouchable(boolean touchable) {mOutsideTouchable = touchable;}}


该函数用于设置是否允许 window 边界外的触摸事件传递给 window。跟踪mOutsideTouchable变量应该就能找到更多线索:


public class PopupWindow {private int computeFlags(int curFlags) {curFlags &= ~(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |WindowMa


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


nager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);...//'如果界外可触摸,则将 FLAG_WATCH_OUTSIDE_TOUCH 赋值给 flag'if (mOutsideTouchable) {curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;}...}}


继续往上跟踪computeFlags()调用的地方:


public class PopupWindow {protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {final WindowManager.LayoutParams p = new WindowManager.LayoutParams();


p.gravity = computeGravity();//'计算窗口布局参数 flag 属性并赋值'p.flags = computeFlags(p.flags);p.type = mWindowLayoutType;p.token = token;...}}


createPopupLayoutParams()会在窗口显示的时候被调用:


public class PopupWindow {public void showAtLocation(IBinder token, int gravity, int x, int y) {if (isShowing() || mContentView == null) { return; }TransitionManager.endTransitions(mDecorView);detachFromAnchor();mIsShowing = true;mIsDropdown = false;mGravity = gravity;


//'构建窗口布局参数'final WindowManager.LayoutParams p = createPopupLayoutParams(token);preparePopup(p);p.x = x;p.y = y;invokePopup(p);}}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
悬浮窗的一种实现 _ Android悬浮窗Window应用,移动互联网开发技术专业