Android(安卓)view触摸反馈原理和源码分析
- 重写onTouchEvent(),在方法内部定制触摸反馈算法
-
是否消费事件取决于ACTION_DOWN事件或POINTER_DOWN事件是否返回true
-
MotionEvent
- getActionMasked()和getAction()有什么区别
- POINTER_DOWN/POINTER_UP和getActionIndex()的联系
-
首先了解下getAction(),getActionMasked()和getActionIndex()区别
getAction() 返回的是响应的事件,考虑使用getActionMasked或者getActionIndex来得到分隔开的动作和手指的下标。返回的一般是ACTION_DOWN,合并起来的已经转移的手指下标的ACTION_POINTER_DOWN。
getActionMasked()返回的是具体的响应事件,没有手指下标的信息。可以使用getActionIndex来返回和手指动作相关的下标,返回的值:比如:ACTION_DOWN,ACTION_POINTER_DOWN。
getActionIndex()返回的是如果调用getActionMasked返回ACTION_POINTER_DOWN或者ACTION_POINTER_UP,这个方法返回的是ACTION_POINTER_DOWN,ACTION_POINTER_UP相关的下标,这个下标可能会在getPointerId,getX,getY,getPressed,getSize中用到来获取手指的关于按下或者抬起的信息。
event.getAction()是早期代码中使用的,在多点触控时期才有的event.getActionMasked()方法,由于需要区分是第一个手指按下或者非第一个手指按下,最后一个手指抬起还是非最后一个手指抬起,所以有了ACTION_DOWN和ACTION_POINTER_DOWN,ACTIN_UP和ACTION_POINTER_UP,如果是普通的DOEN,MOVE,UP,CANCLE事件这两个方法都可以用,如果支持多点触控,那就只能用getActionMasked(),所以直接用getActionMasked()即可
源码分析
查看view的的源码onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) { //获取基本信息 final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //判断是否是可点击,CONTEXT_CLICKABLE上下文菜单类似于右键 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 如果是不可用状态,返回clickable,问题来了,问什么不可用状态还要返回clickable呢,打个比方, 个人中页面上面的头像区域和头像背景区域都能点击,当头像的imageview设置了DISABLED,只是头像不能点击了, 但是头像的背景区域仍能点击,相当于头像只把事件吃掉,但不处理事件。 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // 可单击的禁用视图仍然消耗触摸事件,只是不响应它们 return clickable; } //增大点击范围,一般不需要考虑 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //关键代码 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { ... ... ... }
在看onTouchEvent()关键代码 之前,什么叫TOOLTIP
Android API 28的时候加入的属性,看个例子
在view任意位置长按,显示文本
它的作用是解释这个view是什么,干什么用等,进入上述话题,关键代码:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ... case MotionEvent.ACTION_DOWN: ... case MotionEvent.ACTION_CANCEL: ... case MotionEvent.ACTION_MOVE: ... } return true; }
MotionEvent.ACTION_DOWN
case MotionEvent.ACTION_DOWN: //是否触摸到屏幕 if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { //1.如果不可点击,设置长按等待器, //checkForLongClick的源码见 1. checkForLongClick(0, x, y); break; } //2.检测鼠标和右键点击 //performButtonActionOnTouchDown源码见 2. if (performButtonActionOnTouchDown(event)) { break; } // 是否在滑动控件内. boolean isInScrollingContainer = isInScrollingContainer(); //在滑动控件内 if (isInScrollingContainer) { //状态置为预按下 mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { //点击等待期,是一个runnable //CheckForTap 源码见 3. mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { //如果不在滑动控件内,设置为按下状态和长按等待器(设置长按事件但不触发) setPressed(true, x, y); checkForLongClick(0, x, y); } break;
1.checkForLongClick(delayOffset,x, y)
private void checkForLongClick(int delayOffset, float x, float y) {//在滑动控件内,预按下等待时间减去100等待期 if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }
2.performButtonActionOnTouchDown(event)
protected boolean performButtonActionOnTouchDown(MotionEvent event) { //如果是鼠标右键 if (event.isFromSource(InputDevice.SOURCE_MOUSE) && (event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { //显示上下文菜单 showContextMenu(event.getX(), event.getY()); mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT; return true; } return false; }
3.CheckForTap
private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run() { //把预按下状态置空 mPrivateFlags &= ~PFLAG_PREPRESSED; setPressed(true, x, y);//设置按下状态 // 设置长按的等待期 //checkForLongClick源码见 4. checkForLongClick(ViewConfiguration.getTapTimeout(), x, y); } }
4.checkForLongClick(delayOffset, x, y)
//delayOffset 预按下的延时时间,最终会被减掉,比如上面的3.CheckForTap,在滑动控件中的延时时间是ViewConfiguration.getTapTimeout(),非滑动控件内的延时就是0private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); //减掉预按下状态的延时时间 postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }
MotionEvent.ACTION_MOVE
case MotionEvent.ACTION_MOVE: if (clickable) { //改变水波纹中心位置 drawableHotspotChanged(x, y); } // 如果没有在view范围,移出状态 if (!pointInView(x, y, mTouchSlop)) { // mTouchSlop:溢出距离,默认为8dp // 删除后续所有的长按/点击检查 removeTapCallback();//从预按下到按下 removeLongPressCallback();//长按Callback if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break;
MotionEvent.ACTION_UP
case MotionEvent.ACTION_UP: //TOOLTIP是否额外移动给置空 mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; //松手之后TOOLTIP消失 if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } //如果不可点击,初始化状态 if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; //按下或预按下 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { //isFocusableInTouchMode实体按键,比如电视 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } //处理点击事件 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); //仅在我们处于按下状态时才执行点击操作 if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { //预按下状态,手动加4帧延迟,稍后触发UP事件点击事件 postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break;
MotionEvent.ACTION_CANCEL
case MotionEvent.ACTION_CANCEL: //恢复状态 if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break;
更多相关文章
- activity基本模式
- Android(安卓)滑动效果入门篇(一)ViewFlipper
- android监听Key事件
- Android(安卓)xUtils框架(二) ViewUtils
- Android(安卓)-- 程序判断手机ROOT状态,获取ROOT权限
- Android(安卓)触摸事件传递机制
- Android连续点击多次(类似开发者模式)
- Android开发搭建环境
- 【Android】MVVM架构 RecyclerView加载数据滑动到后面,数据错乱,点