赞
踩
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) {
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 {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
这些题目是今年群友去腾讯、百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。并且大多数都整理了答案,熟悉这些知识点会大大增加通过前两轮技术面试的几率
有Android开发3-5年基础,希望突破瓶颈,成为架构师的小伙伴,可以关注我
]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-hsKRzfIH-1711632832675)]
[外链图片转存中…(img-v0PFmMbC-1711632832675)]
这些题目是今年群友去腾讯、百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。并且大多数都整理了答案,熟悉这些知识点会大大增加通过前两轮技术面试的几率
[外链图片转存中…(img-yzlvHjSb-1711632832676)]
有Android开发3-5年基础,希望突破瓶颈,成为架构师的小伙伴,可以关注我
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。