ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Android悬浮窗的一种实现,kotlin系统入门到进阶

2022-01-31 14:03:17  阅读:177  来源: 互联网

标签:MotionEvent 进阶 layoutParams kotlin private event windowInfo Android 浮窗


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_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? = 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事件被GestureDetector解析成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) {

screenWidth - width

} else {

0

}

//‘构建贴边动画’

if (weltAnimator == null) {

weltAnimator = ValueAnimator.ofInt(windowInfo?.layoutParams!!.x, endX).apply {

interpolator = LinearInterpolator()

duration = 300

addUpdateListener { animation ->

val x = animation.animatedValue as Int

if (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] = windowInfo

windowInfo.view?.setOnTouchListener(this)

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)

}

}

}

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

监听浮窗界外点击事件


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

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

public class PopupWindow {

/**

  • 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 |

WindowManager.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.x = x;

p.y = y;

invokePopup§;

}

}

想在源码中继续搜索,但到FLAG_WATCH_OUTSIDE_TOUCH,线索就断了。现在只知道为了让界外点击事件传递给 window,必须为布局参数设置FLAG_WATCH_OUTSIDE_TOUCH。但事件响应逻辑应该写在哪里?

当调用PopupWindow.setOutsideTouchable(true),在窗口界外点击后,窗口会消失。这必然是调用了dismiss(),沿着dismiss()的调用链往上找一定能找到界外点击事件的响应逻辑:

public class PopupWindow {

//‘窗口根视图’

private class PopupDecorView extends FrameLayout {

//‘窗口根视图触摸事件’

@Override

public boolean onTouchEvent(MotionEvent event) {

final int x = (int) event.getX();

final int y = (int) event.getY();

if ((event.getAction() == MotionEvent.ACTION_DOWN)
参数’

final WindowManager.LayoutParams p = createPopupLayoutParams(token);

preparePopup§;

p.x = x;

p.y = y;

invokePopup§;

}

}

想在源码中继续搜索,但到FLAG_WATCH_OUTSIDE_TOUCH,线索就断了。现在只知道为了让界外点击事件传递给 window,必须为布局参数设置FLAG_WATCH_OUTSIDE_TOUCH。但事件响应逻辑应该写在哪里?

当调用PopupWindow.setOutsideTouchable(true),在窗口界外点击后,窗口会消失。这必然是调用了dismiss(),沿着dismiss()的调用链往上找一定能找到界外点击事件的响应逻辑:

public class PopupWindow {

//‘窗口根视图’

private class PopupDecorView extends FrameLayout {

//‘窗口根视图触摸事件’

@Override

public boolean onTouchEvent(MotionEvent event) {

final int x = (int) event.getX();

final int y = (int) event.getY();

if ((event.getAction() == MotionEvent.ACTION_DOWN)

标签:MotionEvent,进阶,layoutParams,kotlin,private,event,windowInfo,Android,浮窗
来源: https://blog.csdn.net/m0_66264169/article/details/122759518

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有