| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android开发艺术探索 第三章 -> 正文阅读 |
|
[移动开发]Android开发艺术探索 第三章 |
Android开发艺术探索 第三章 VIew的事件体系 3.1基础 View是界面层的控件的一种抽象,可以使一组或者多组控件构成的一组控件。 View的位置由四个顶点决定,top、left、right、buttom,这个坐标都是相对View的父容器来说的。 需要留意的是新的参数,x、y、translationX、translationY,x和y对应view左上角的坐标,translationX和translationY是View的左上角相对父容器的偏移量。当View在平移过程中,top和left不会改变,改变的是x、y、translationX、translationY这四个参数。 MotionEvent? 触摸事件 主要: ACTION_DOWN :触摸到屏幕 ACTION_MOVE :在屏幕上移动 ACTION_UP? ? :从屏幕上离开的一瞬间 在触摸事件中我们可以得到事件的x、y坐标,还有点对手机屏幕的getRawX和getRawY坐标 TouchSlop 系统能识别的滑动最小距离,低于这个不认为是滑动。ViewCOnfiguration.get(mContent).getScaledTouchSlop(); <dimen name = "config_vieConfigurationTOuchSlop">8dp</dimen> VelocityTracker? 速度追踪,包括水平和竖直方向的速度,在onTounchEvent方法中调用 VelocityTracker?velocityTracker =VelocityTracker.obtain(); velocityTracker.addMovement(event) 获取速度:速度=(终点位置-起点位置)/时间段 velocityTracker.computeCurrentVelocity(1000); float x=velocityTracker.getXVelocity(); float y=velocityTracker.getXVelocity(); 结束时候记得是释放资源 velocityTracker.clear(); velocityTracker.recycle(); GestureDetector 手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。参考如下。 GestureDetector? gestureDetector=new GestureDetector(this, new GestureDetector.SimpleOnGestureListener(){ ????????@Override ????????public boolean onDown(MotionEvent e) { ????????????//点了一下屏幕,ACTION_DOWN触发 ????????????return false; ????????} ????????@Override ????????public void onShowPress(MotionEvent e) { ????????????//点击屏幕中,没有松开和移动,ACTION_DOWN触发 ????????} ????????@Override ????????public boolean onSingleTapUp(MotionEvent e) { ????????????//单击事件,轻轻点击屏幕后松开,ACTION_DOWN触发和ACTION_UP触发 ????????????return false; ????????} ????????@Override ????????public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { ????????????//拖动事件,点击屏幕后拖动,ACTION_DOWN触发和多个ACTION_MOVE触发 ????????????return false; ????????} ????????@Override ????????public void onLongPress(MotionEvent e) { ????????????//长按事件,点击屏幕中,没有松开和移动,ACTION_DOWN触发 ????????} ????????@Override ????????public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { ????????????//滑动事件,点击屏幕快速滑动松开,ACTION_DOWN触发和多个ACTION_MOVE、一个ACTION_UP触发 ????????????return false; ????????} ????????@Override ????????public boolean onDoubleTap(MotionEvent e) { ????????????//双击事件,快速点击了两下屏幕 ????????????return super.onDoubleTap(e); ????????} ????????@Override ????????public boolean onDoubleTapEvent(MotionEvent e) { ????????????//双击事件中,ACTION_DOWN、ACTION_UP和ACTION_MOVE都会触发这个回调 ????????????return super.onDoubleTapEvent(e); ????????} ????????@Override ????????public boolean onSingleTapConfirmed(MotionEvent e) { ????????????//严格的单击事件,不可能是双击中的一次 ????????????return super.onSingleTapConfirmed(e); ????????} ????????@Override ????????public boolean onContextClick(MotionEvent e) { ????????????//发生上下文单击时候都会触发 ????????????return super.onContextClick(e); ????????} ????}); ????return super.onTouchEvent(event); } 解决长按屏幕无法拖动问题 gestureDetector.setIsLongpressEnabled(false); Scroller 弹性滑动对象,用于实现View的弹性滑动。 public class ViewDemo extends View { ????private final Scroller mScroller; ????public ViewDemo(Context context) { ????????super(context); ????????mScroller = new Scroller(context); ????} ????public void smoothScrollerTo(int destX, int destY) { ????????int scrollX=mScroller.getCurrX(); ????????int delta=destX-scrollX; ????????//1000ms慢慢滑动向delta ????????mScroller.startScroll(scrollX,0,delta,0,1000); ????} ????@Override ????public void computeScroll() { ????????if (mScroller.computeScrollOffset()){ ????????????smoothScrollerTo(mScroller.getCurrX(),mScroller.getCurrY()); ????????????postInvalidate(); ????????} ????????super.computeScroll(); ????} } 3.2 View?的滑动 主要有三种方式:View本身的srollTo/srollBy,通过动画,通过改变View的LayoutParams重新布局。 1.使用srollTo/srollBy /** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { ????if (mScrollX != x || mScrollY != y) { ????????int oldX = mScrollX; ????????int oldY = mScrollY; ????????mScrollX = x; ????????mScrollY = y; ????????invalidateParentCaches(); ????????onScrollChanged(mScrollX, mScrollY, oldX, oldY); ????????if (!awakenScrollBars()) { ????????????postInvalidateOnAnimation(); ????????} ????} } /** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ public void scrollBy(int x, int y) { ????scrollTo(mScrollX + x, mScrollY + y); } 源码上srollBy也是调用scrollTo方法,scrollTo实现基于参数的绝对滑动,需要注意scrollX的值总是等于View的边缘和View的内容的水平距离,scrollY也是同理,区分正负值,需要理解View的坐标体系,屏幕左上角是(0,0)。 ? 3.2 使用动画 View动画主要是改变view的translationX和translatioY属性, <?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" ????android:duration="1000" ????android:fromYDelta="0%p" ????android:interpolator="@android:anim/accelerate_interpolator" ????android:toYDelta="-100%p" /> android:interpolator: 加速器,非常有用的属性,可以简单理解为动画的速度,可以是越来越快,也可以是越来越慢,或者是先快后忙,或者是均匀的速度等等,对于值如下: @android:anim/accelerate_interpolator: 越来越快 @android:anim/decelerate_interpolator:越来越慢 @android:anim/accelerate_decelerate_interpolator:先快后慢 @android:anim/anticipate_interpolator: 先后退一小步然后向前加速 @android:anim/overshoot_interpolator:快速到达终点超出一小步然后回到终点 @android:anim/anticipate_overshoot_interpolator:到达终点超出一小步然后回到终点 @android:anim/bounce_interpolator:到达终点产生弹球效果,弹几下回到终点 @android:anim/linear_interpolator:均匀速度。 属性动画更简单(布局,属性,开始值,结束值),持续时间,开始 ObjectAnimator.ofFloat(getRootView(),"translationX",0,100).setDuration(100).start(); 例二: ValueAnimator animator = ValueAnimator.ofFloat(m.progress, progress); animator.addUpdateListener(animation -> { ????float animatedValue = (float) animation.getAnimatedValue(); ????setProgress(animatedValue); }); animator.setInterpolator(new OvershootInterpolator()); animator.setDuration(animTime); animator.start(); View动画的主要问题是不会改变控件真正的位置,点击事件响应还在原来位置。 3.2.3?改变布局参数 改变LayoutParams参数,思路上就是增大间距,或者增加空的VIEW在中间,改变这个view的大小打到平移的效果 public void setWidth(int width) { ????view.getLayoutParams().width = width; ????view.requestLayout(); } 这个是比较灵活的方法,在系统WindowsManager里面,提供就是改变布局参数实现布局动画 3.2.4三种方式对比 srollTo/srollBy:操作简单,适合对View内容的滑动 动画:操作简单,主要适合没有交互的View和实现复杂的动画效果 改变布局参数:操作稍微复杂,适用于有交互的View 3.3 弹性滑动 弹性滑动的共同思想:将一次打的滑动分为若干次小的滑动并在一个时间段内完成。 3.3.1使用Scroller 前面讲过用法了,Scroller本身不能实现view滑动,关键在于startScroll后的重新绘制,会在draw里面去调用computeScroll,再次执行获取x\y去执行滑动重绘。设计很巧妙,没有对view的依赖,也没有定时器的使用。 private final Scroller mScroller; public ViewDemo(Context context) { ????super(context); ????mScroller = new Scroller(context); } public void smoothScrollerTo(int destX, int destY) { ????int scrollX=mScroller.getCurrX(); ????int delta=destX-scrollX; ????//1000ms慢慢滑动向delta ????mScroller.startScroll(scrollX,0,delta,0,1000); ????//绘制 ????invalidate(); } @Override public void computeScroll() { ????if (mScroller.computeScrollOffset()){ ????????smoothScrollerTo(mScroller.getCurrX(),mScroller.getCurrY()); ????????postInvalidate(); ????} ????super.computeScroll(); } 3.3.2通过动画 动画本身就是一种渐进的过程 ObjectAnimator.ofFloat(getRootView(),"translationX",0,100).setDuration(100).start(); 当然也可以模仿Scroller来实现滑动 ValueAnimator animator = ValueAnimator.ofFloat(m.progress, progress); animator.addUpdateListener(animation -> { ????float animatedValue = (float) animation.getAnimatedValue(); ? ? mButton.scrollTo(startX+animatedValue,0); }); animator.setInterpolator(new OvershootInterpolator()); animator.setDuration(animTime); animator.start(); 3.3.3使用延迟策略 定时刷新View,具体可以是Handler或View的POSTDelay方法,也可以使用线程的sleep,或者定时器之类延迟。 private static int count=0; private static Handler handler=new Handler(){ ????@Override ????public void handleMessage(@NonNull Message msg) { ????????switch (msg.what){ ????????????case 0: ????????????????count++; ????????????????mButton.scrollTo(count*100,0); ????????????????handler.sendEmptyMessageDelayed(0,30); ????????????????break; ????????} ????} }; 3.4View的事件分发机制 首先分析的对象是MotionEvent,点击事件。最关键的三个方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent 翻译成中文就是,分发事件、拦截事件、处理事件,先看三者的关系,然后我们在细分讲解 public boolean dispatchTouchEvent(MotionEvent ev) { ????boolean consume=false; ????if (onInterceptTouchEvent(ev)){ ????????consume=onTouchEvent(ev); ????}else { ????????consume=child.dispatchTouchEvent(ev); ????} ????return consume; } public boolean dispatchTouchEvent(MotionEvent ev) 用于进行事件分发,如果当前事件能够传递到当前View,这个方法一定会被调用,返回值收当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消耗当前事件 public boolean onInterceptTouchEvent(MotionEvent ev) 内部调用,用来判断当前View拦截事件,如果当前view拦截了事件,那么同个事件序列中,此方法不会再次被调用 public boolean onTouchEvent(MotionEvent ev) 用来处理点击事件,返回结果表示是否消耗当前事情,如果不消耗,在同一个事件序列中,当前View无法再次接收到事件。 当一个View需要处理事件时,View如果设置onTouchListenter,那么onTouchListenter的onTouch方法会被回调,这时候如果返回FALSE,那么当前View的onTouchEvent会被调用。由此可见,onTouch优先级高于onTouchEvent,在onTouchEvent中,如果设置了onClickListener,那么onClick方法会被调用,因此 onClickListener优先级最低,处于事件传递的尾端。 点击事件传递过程:Activity ->?WIndow?-> View? 责任链模式,如果最顶级的View都没处理这个事件,那么父容器的onTouchEvent将会被调用,一直到Activity的onTouchEvent处理。 总结: 1.事件从down事件开始,不定数move事件,up事件结束,很好理解,手指从触摸-移动-抬起完成事件流程。 2.一个事件只能由一个view处理,除非通过其他强制手段将事件在转给其他View. 3.某个View一旦决定拦截,那么同个事件序列中,onInterceptTouchEvent此方法不会再次被调用。 4.某个view一旦开始处理事件,不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一序列的其他事件都不会再交给它处理,而是交个它的父元素onTouchEvent。 5.某个view消耗ACTION_DOWN事件,没消耗其他事件,这个点击事件会消失,父元素onTouchEvent也不会调用,而是传递给Activity处理。 6.ViewGroup默认不拦截任何事件。 7.View没有onInterceptTouchEvent方法,默认直接调用onTouchEvent。(因为它没有子布局) 8.View默认消耗事件,onTouchEvent返回true,除非它不可以点击(clickable和longClicable同时为false)。 9.View的enable属性不影响onTouchEvent返回true。 10.onClick会发生的前提是View可以点击的。 11.事件传递由外向内,总是先传递给父元素,再由父元素分发给子View,子View可以通过requestDisallowInterceptTouchEvent方法可以干预父元素分发过程,ACTION_DOWN事件除外。 源码分析 1.Activity对点击事件的分发过程 public boolean dispatchTouchEvent(MotionEvent ev) { ????if (ev.getAction() == MotionEvent.ACTION_DOWN) { ????????onUserInteraction(); ????} ????if (getWindow().superDispatchTouchEvent(ev)) { ????????return true; ????} ????return onTouchEvent(ev); } 首先事件开始交给 这里的逻辑十分简单,先判断down事件并触发onUserInteraction(),之后就是把事件传递给所依附的window进行分发,根据处理结果决定是否再交给activity的onTouchEvent()处理。onUserInteraction是一个空方法,对事件分发无影响。 然后看一下window中的事件分发源码,window是如何分发给ViewGroup的, 这里看到是一个抽象方法,所以我们需要找到实现window的子类 public abstract class Window { ????public abstract boolean superDispatchTouchEvent(MotionEvent event);} 而PhoneWindow就是window的唯一实现子类,那继续到phoneWindow中找: public class PhoneWindow extends Window implements MenuBuilder.Callback { ????@Override ????public boolean superDispatchTouchEvent(MotionEvent event) { ????????return mDecor.superDispatchTouchEvent(event); ????}} PhoneWindow将事件直接传递给了DecorView,DecorView就是我们布局里面的父布局。DecorView继承FrameLayout而且是父View,最终事件传递给View. ? 2.顶级View对事件的分发过程 事件到了ViewGroup?后,调用dispatchTouchEvent方法,进入之前我们说的父到子View的一个分发流程。 public abstract class ViewGroup extends View implements ViewParent, ViewManager { ????@Override ????public boolean dispatchTouchEvent(MotionEvent ev) { ????????????/**省略部分代码**/ ????????????// Handle an initial down. ????????????if (actionMasked == MotionEvent.ACTION_DOWN) { ?? ??? ??? ??? ?//开始新的触摸手势时,放弃所有以前的状态。 ?? ??? ??? ??? ?//framework可能已放弃上一个手势的up或cancel事件 ?? ??? ??? ??? ?//由于应用程序切换、ANR或某些其他状态更改。 ????????????????cancelAndClearTouchTargets(ev); ????????????????resetTouchState(); ????????????} ????????????// Check for interception. ????????????final boolean intercepted; ????????????if (actionMasked == MotionEvent.ACTION_DOWN ????????????????????|| mFirstTouchTarget != null) {//按下去的时候mFirstTouchTarget如果没有赋值,则后面事件就被拦截 ????????????????final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; ????????????????if (!disallowIntercept) { ????????????????????intercepted = onInterceptTouchEvent(ev); ????????????????????ev.setAction(action); // restore action in case it was changed ????????????????} else { ????????????????????intercepted = false; ????????????????} ????????????} else { ? ? ? ? ? ? ? ??//没有接触目标,此动作不是初始下降 ?? ??? ??? ??? ?//因此,该视图组继续拦截触摸。 ????????????????intercepted = true; ????????????} ????????????/**省略部分代码**/ ??????}} 从上面代码看,ViewGroup两种情况是否需要拦截:ACTION_DOWN或者mFirstTouchTarget != null,ACTION_DOWN代表事件开始,mFirstTouchTarget != null代表ViewGroup的子元素处理成功,mFirstTouchTarget会被赋值并指向子元素。反之,如果ViewGroup拦截时,mFirstTouchTarget将会为空,同一序列将都会交给它处理。 在往下看,可以看到FLAG_DISALLOW_INTERCEPT标记位,这个子View可以通过requestDisallowInterceptTouchEvent方法设置,为何ACTION_DOWN改变不了,看代码开始的时候就已经处理了actionMasked == MotionEvent.ACTION_DOWN事件,重置了FLAG_DISALLOW_INTERCEPT标记位,导致设置无效。所以当ACTION_DOWN事件来临时候,ViewGroup总是会调用自己的onInterceptTouchEvent来询问自己是否需要拦截事件。 代码接着往下走,接下来是子View处理的流程 ??????final int childrenCount = mChildrenCount; ????????????????????if (newTouchTarget == null && childrenCount != 0) { ????????????????????????final float x = ev.getX(actionIndex); ????????????????????????final float y = ev.getY(actionIndex); ? ? ? ? ? ? ? ? ? ? ? ?//查找可以接收事件的子级。 ????????????????????????final ArrayList<View> preorderedList = buildTouchDispatchChildList(); ????????????????????????final boolean customOrder = preorderedList == null ????????????????????????????????&& isChildrenDrawingOrderEnabled(); ????????????????????????final View[] children = mChildren; ????????????????????????//遍历子View ????????????????????????for (int i = childrenCount - 1; i >= 0; i--) { ????????????????????????????final int childIndex = getAndVerifyPreorderedIndex( ????????????????????????????????????childrenCount, i, customOrder); ????????????????????????????final View child = getAndVerifyPreorderedView( ????????????????????????????????????preorderedList, children, childIndex); ? ? ? ? ? ? ? ? ? ? ? ? ? ? //如果有一个视图具有可访问性焦点,我们需要它要先获取事件, ?? ??? ??? ??? ??? ??? ??? ?//如果未处理,我们将执行正常调度。 ?? ??? ??? ??? ??? ??? ??? ?//我们可以进行双重迭代,但这是考虑到时间框架,更安全。 ????????????????????????????if (childWithAccessibilityFocus != null) { ????????????????????????????????if (childWithAccessibilityFocus != child) { ????????????????????????????????????continue; ????????????????????????????????} ????????????????????????????????childWithAccessibilityFocus = null; ????????????????????????????????i = childrenCount - 1; ????????????????????????????} ????????????????????????????//判断view是否能接受点击事件 ????????????????????????????//判断点击区域是不是在view的范围内 ????????????????????????????//其中有一项不符合,就标记不合适并继续找下一个 ????????????????????????????if (!child.canReceivePointerEvents() ????????????????????????????????????|| !isTransformedTouchPointInView(x, y, child, null)) { ????????????????????????????????ev.setTargetAccessibilityFocus(false); ????????????????????????????????continue; ????????????????????????????} ????????????????????????????newTouchTarget = getTouchTarget(child); ????????????????????????????if (newTouchTarget != null) { ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??//孩子已经在自己的范围内受到了触碰。 ?? ??? ??? ??? ??? ??? ??? ??? ?//除了它正在处理的指针之外,还要给它一个新指针。 ????????????????????????????????newTouchTarget.pointerIdBits |= idBitsToAssign; ????????????????????????????????break; ????????????????????????????} ????????????????????????????resetCancelNextUpFlag(child); ????????????????????????????if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ????????????????????????????????//子view希望在自己的范围内得到触摸。 ????????????????????????????????mLastTouchDownTime = ev.getDownTime(); ????????????????????????????????if (preorderedList != null) { ????????????????????????????????????//childIndex指向预排序列表,查找原始索引 ????????????????????????????????????for (int j = 0; j < childrenCount; j++) { ????????????????????????????????????????if (children[childIndex] == mChildren[j]) { ????????????????????????????????????????????mLastTouchDownIndex = j; ????????????????????????????????????????????break; ????????????????????????????????????????} ????????????????????????????????????} ????????????????????????????????} else { ????????????????????????????????????mLastTouchDownIndex = childIndex; ????????????????????????????????} ????????????????????????????????mLastTouchDownX = ev.getX(); ????????????????????????????????mLastTouchDownY = ev.getY(); ????????????????????????????????newTouchTarget = addTouchTarget(child, idBitsToAssign); ????????????????????????????????//设置已经处理了Down事件 ????????????????????????????????alreadyDispatchedToNewTouchTarget = true; ????????????????????????????????break; ????????????????????????????} ????????????????????????????//?可访问性焦点没有处理事件,非常清楚 ?? ??? ??? ??? ??? ??? ??? ?//标记并对所有子项执行正常调度。 ????????????????????????????ev.setTargetAccessibilityFocus(false); ????????????????????????} ????????????????????????if (preorderedList != null) preorderedList.clear(); ????????????????????} 首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。 dispatchTransformedTouchEvent其实就是调用了子元素的dispatchTouchEvent方法,如果子元素返回了ture,那么会跳出for循环 if (child == null) { ????handled = super.dispatchTouchEvent(event); } else { ????handled = child.dispatchTouchEvent(event); } 如果子元素返回了true,那么mFirstTouchTarget会被赋值同时跳出循环 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; mFirstTouchTarget真正赋值在addTouchTarget内部完成的,其实是一种单链表结构,mFirstTouchTarget是否为null,直接影响到ViewGroup对事件的拦截策略,为空时ViewGroup就默认拦截接下来同一序列的所有点击事件。意味分发过程,只有点击下去的时候会判断一遍,后面同一序列的移动和抬起,一起处理 /** *将指定子对象的触摸目标添加到列表的开头。 *假设目标子项尚未存在。 */ private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { ????final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); ????target.next = mFirstTouchTarget; ????mFirstTouchTarget = target; ????return target; } 最后事件没有被处理,两种情况:1.ViewGroup没有子元素;2.子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这是因为子元素onTouchEvent返回了false。这两种情况下,ViewGroup自己处理点击事件: if (mFirstTouchTarget == null) { ????//?无接触目标,因此将其视为普通视图。 ????handled = dispatchTransformedTouchEvent(ev, canceled, null, ????????????TouchTarget.ALL_POINTER_IDS); } 第三个元素child为空,这就导致调用了super.dispatchTouchEvent(event)方法,也就是ViewGroup的父类View的dispatchTouchEvent方法处理,接下来我们看view的处理。 3.view对事件的处理过程 这里的View不包括ViewGroup,首先看dispatchTouchEvent分发过程 public boolean dispatchTouchEvent(MotionEvent event) { ????// 事件应首先由辅助功能焦点处理。 ????if (event.isTargetAccessibilityFocus()) { ????????//?我们没有焦点或者虚拟后代没有焦点,请不要处理事件。 ????????if (!isAccessibilityFocusedViewOrHost()) { ????????????return false; ????????} ????????//我们聚焦并获取事件,然后使用正常事件调度。 ????????event.setTargetAccessibilityFocus(false); ????} ????boolean result = false; ????if (mInputEventConsistencyVerifier != null) {//输入事件一致性验证器 ????????mInputEventConsistencyVerifier.onTouchEvent(event, 0); ????} ????final int actionMasked = event.getActionMasked(); ????if (actionMasked == MotionEvent.ACTION_DOWN) { ????????//?新姿态的防御性清理,停止滚动 ????????stopNestedScroll(); ????} ????if (onFilterTouchEventForSecurity(event)) {//过滤触摸事件,窗口外或者被遮挡的事件 ????????if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {//通过鼠标输入处理滚动条拖动。 ????????????result = true; ????????} ????????//noinspection SimplifiableIfStatement ????????ListenerInfo li = mListenerInfo;//监听回调对象 ????????if (li != null && li.mOnTouchListener != null ????????????????&& (mViewFlags & ENABLED_MASK) == ENABLED ????????????????&& li.mOnTouchListener.onTouch(this, event)) {//是否注册了OnTouchListener监听,返回true才会消耗事件 ????????????result = true; ????????} ????????if (!result && onTouchEvent(event)) {//没有OnTouchListener监听才会调用onTouchEvent事件,返回true才会消耗事件 ????????????result = true; ????????} ????} ????if (!result && mInputEventConsistencyVerifier != null) {//通知验证器给定的事件未处理,其余的应忽略事件的跟踪。 ????????mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); ????} ?? ?//如果这是一个手势的结束,在嵌套的滚动之后清理; ?? ?//如果我们尝试了一个动作,但我们不想要其他动作,也可以取消它手势 ????if (actionMasked == MotionEvent.ACTION_UP || ????????????actionMasked == MotionEvent.ACTION_CANCEL || ????????????(actionMasked == MotionEvent.ACTION_DOWN && !result)) { ????????stopNestedScroll(); ????} ????return result; } View是一个单独的元素,因此只能自己处理事件,上面看出处理过程,首先判断有没有设置mOnTouchListener,如果mOnTouchListener里面的onTouch方法返回了true,那么onTouchEvent将不被调用,这样的好处是方便在外界处理点击事件。 ?? ?? 接下来在分析onTouchEvent的实现,首先看View处于不可用的情况下依然会消耗掉点击事件,还有就是是CLICKABLE和LONG_CLICKABLE有一个为true,就会消耗事件,onTouchEvent返回true; final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE ????????|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ????????|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { ????if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { ????????setPressed(false); ????} ????mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; ? ? //可单击的禁用视图仍然会使用触摸事件,它只是不响应这些事件 ????return clickable; } 如果view设置有代理,还会执行mTouchDelegate的onTouchEvent方法,类似OnTouchListener if (mTouchDelegate != null) { ????if (mTouchDelegate.onTouchEvent(event)) { ????????return true; ????} } 接下来是onTouchEvent中对点击事件的具体处理,比较多代码。 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { ????switch (action) { ????????case MotionEvent.ACTION_UP: ????????????mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; ????????????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) { ????????????????//检查焦点,没有获取需要获取焦点 ????????????????boolean focusTaken = false; ????????????????if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { ????????????????????focusTaken = requestFocus(); ????????????????} ????????????????if (prepressed) { ????????????????????//按钮在我们实际显示为按下之前就被释放了。现在(在安排单击之前)使其显示按下的状态,以确保用户看到它 ????????????????????setPressed(true, x, y); ????????????????} ????????????????if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { ????????????????????//?移除长按回调 ????????????????????removeLongPressCallback(); ????????????????????//仅当我们处于按下状态时才执行“采取单击”操作 ????????????????????if (!focusTaken) { ????????????????????????//?使用Runnable并发布此消息,而不是直接调用performClick。这使视图的其他可视状态可以在单击操作开始之前更新。 ????????????????????????if (mPerformClick == null) { ????????????????????????????mPerformClick = new PerformClick(); ????????????????????????} ????????????????????????if (!post(mPerformClick)) {//通过POST将runnable将在用户界面线程上运行 ????????????????????????????performClickInternal();//失败通常是因为正在退出处理消息队列的循环器,在当前线程执行 ????????????????????????} ????????????????????} ????????????????} ????????????????if (mUnsetPressedState == null) { ????????????????????mUnsetPressedState = new UnsetPressedState(); ????????????????} ????????????????if (prepressed) {//延迟取消前面按下的状态 ????????????????????postDelayed(mUnsetPressedState, ????????????????????????????ViewConfiguration.getPressedStateDuration()); ????????????????} else if (!post(mUnsetPressedState)) { ????????????????????//?如果post失败,请立即取消 ????????????????????mUnsetPressedState.run(); ????????????????} ????????????????removeTapCallback(); ????????????} ????????????mIgnoreNextUpEvent = false; ????????????break; ????????case MotionEvent.ACTION_DOWN: ????????????if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { ????????????????mPrivateFlags3 |= PFLAG3_FINGER_DOWN; ????????????} ????????????mHasPerformedLongPress = false; ????????????if (!clickable) {//如果已经点击,那么将长按事件放入延迟线程,结束流程 ????????????????checkForLongClick(ViewConfiguration.getLongPressTimeout(),x, y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); ????????????????break; ????????????} ????????????if (performButtonActionOnTouchDown(event)) {//(事件来源鼠标)在事件期间执行与按钮相关的操作。 ????????????????break; ????????????} ????????????//沿着层次结构向上走,确定我们是否在滚动容器中。 ????????????boolean isInScrollingContainer = isInScrollingContainer(); ????????????//对于滚动容器内的视图,如果这是滚动,则将按下的反馈延迟一小段时间。 ????????????if (isInScrollingContainer) { ????????????????mPrivateFlags |= PFLAG_PREPRESSED; ????????????????if (mPendingCheckForTap == null) { ????????????????????mPendingCheckForTap = new CheckForTap(); ????????????????} ????????????????mPendingCheckForTap.x = event.getX(); ????????????????mPendingCheckForTap.y = event.getY(); ????????????????postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); ????????????} else { ????????????????//?不在滚动容器中,因此立即显示反馈 ????????????????setPressed(true, x, y); ????????????????checkForLongClick(ViewConfiguration.getLongPressTimeout(),x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); ????????????} ????????????break; ????????case MotionEvent.ACTION_CANCEL://触摸取消时处理 ????????????if (clickable) { ????????????????setPressed(false); ????????????} ????????????removeTapCallback(); ????????????removeLongPressCallback(); ????????????mInContextButtonPress = false; ????????????mHasPerformedLongPress = false; ????????????mIgnoreNextUpEvent = false; ????????????mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; ????????????break; ????????case MotionEvent.ACTION_MOVE: ????????????if (clickable) { ????????????????drawableHotspotChanged(x, y);//每当视图热点发生更改并且需要传播到由视图管理的可绘制视图或子视图时,都会调用此函数。 ????????????} ????????????final int motionClassification = event.getClassification(); ????????????final boolean ambiguousGesture = motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; ????????????int touchSlop = mTouchSlop; ????????????if (ambiguousGesture && hasPendingLongPressCallback()) { ????????????????if (!pointInView(x, y, touchSlop)) { ????????????????????//这里的默认操作是取消长按。但是,我们只是在这里延长超时时间,以防分类不明确。 ????????????????????removeLongPressCallback(); ????????????????????long delay = (long) (ViewConfiguration.getLongPressTimeout() ????????????????????????????* mAmbiguousGestureMultiplier); ????????????????????//减去已经花费的时间 ????????????????????delay -= event.getEventTime() - event.getDownTime(); ????????????????????checkForLongClick(delay,x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); ????????????????} ????????????????touchSlop *= mAmbiguousGestureMultiplier; ????????????} ????????????//?在按钮外移动时要小心 ????????????if (!pointInView(x, y, touchSlop)) { ????????????????//?外部按钮,删除任何未来长按/点击检查 ????????????????removeTapCallback(); ????????????????removeLongPressCallback(); ????????????????if ((mPrivateFlags & PFLAG_PRESSED) != 0) { ????????????????????setPressed(false); ????????????????} ????????????????mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; ????????????} ????????????final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS; ????????????if (deepPress && hasPendingLongPressCallback()) { ????????????????//?立即处理长单击操作 ????????????????removeLongPressCallback(); ????????????????checkForLongClick(0 /* send immediately */,x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); ????????????} ????????????break; ????} ????return true; 可以看到ACTION_UP事件发生时,会触发performClickInternal里面的performClick()方法,如果View设置了OnClickListener,那么onClick方法会被调用。 //注意:视图上的其他方法不应直接调用此方法,而应调用performClickInternal(),以确保在必要时通知自动填充管理器(因为子类可以在不调用super.performClick()的情况下扩展此方法)。 public boolean performClick() { ????//我们仍然需要调用此方法来处理外部调用performClick()而不是通过performClickInternal()调用performClick()的情况 ????notifyAutofillManagerOnClick(); ????final boolean result; ????final ListenerInfo li = mListenerInfo; ????if (li != null && li.mOnClickListener != null) { ????????playSoundEffect(SoundEffectConstants.CLICK); ????????li.mOnClickListener.onClick(this);//调用onClick事件 ????????result = true; ????} else { ????????result = false; ????} ????sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); ????notifyEnterOrExitForAutoFillIfNeeded(true); ????return result; } 长按事件流程,延迟执行CheckForLongPress对象run,在CheckForLongPress对象里面run方法执行performLongClick方法,performLongClick最终检查ListenerInfo里面是否有OnLongClickListener监听,有监听就调用onLongClick方法。 private void checkForLongClick(long delay, float x, float y, int classification) { ???? ???... ????????if (mPendingCheckForLongPress == null) { ????????????mPendingCheckForLongPress = new CheckForLongPress();//初始化长按事件对象 ????????} ? ? ? ? ... ????????postDelayed(mPendingCheckForLongPress, delay); ????} } private final class CheckForLongPress implements Runnable { ??? ... ????@Override ????public void run() { ? ? ? ? .. ? ? ? ? ?if (performLongClick(mX, mY)) {//长按事件 ? ? ? ? ? ? mHasPerformedLongPress = true; ? ? ? ? ?} ????????.. ????} ?? ?... } public boolean performLongClick(float x, float y) { ????.. ????final boolean handled = performLongClick(); ????.. ????return handled; } private boolean performLongClickInternal(float x, float y) { ??? ... ????final ListenerInfo li = mListenerInfo; ????if (li != null && li.mOnLongClickListener != null) {//长按事件监听回调 ????????handled = li.mOnLongClickListener.onLongClick(View.this); ????} ? ? ... ????return handled; } 长按事件LongClickable默认是False,而点击事件Clickable默认和具体View有关系,例如Button和TextView,这两个属性都可以通过设置监听而改变。 public void setOnClickListener(@Nullable OnClickListener l) { ????if (!isClickable()) { ????????setClickable(true); ????} ????getListenerInfo().mOnClickListener = l; } public void setOnLongClickListener(@Nullable OnLongClickListener l) { ????if (!isLongClickable()) { ????????setLongClickable(true); ????} ????getListenerInfo().mOnLongClickListener = l; } 到这里流程基本分析结束。 3.5 View?的滑动冲突 1.冲突场景 ? 2.处理规则 场景1处理规则:左右滑动时候需要让外部的View拦截事件,上下滑动时需要让内部View拦截。具体可以根据水平滑动和垂直滑动来判断,速度、距离、夹角都可以做判断。 场景2处理规则:无法通过速度、距离、夹角都做判断,因此只能从业务上找突破点。没有规定就是内部滑动完滑外部顺序好了。 场景3处理规则:具体方法和2一样,只能从业务上找突破点。 3.解决方式 ?? ?1.外部拦截法 ? ? 外部拦截指的是事件都经过父容器的拦截处理,这种比较符合点击事件的分发机制。需要重写父容器的方法,在内部做拦截,下面参考,在滑动的时候做出判断,是否需要拦截事件。 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ?? ?boolean?intercept=false; ? ??switch (event.getAction()) { ????case MotionEvent.ACTION_UP: ?? ??? ?intercept=false; ????????break; ????case MotionEvent.ACTION_DOWN: ? ??? ??intercept=false; ?? ?? ? if(!mScroller.isFinished()){//防止在滑动中止前用户改变了滑动方向,导致滑动没有滑动到终点。 ?? ??? ??? ?mScroller.abortAnimation();//停止动画 ?? ??? ??? ?intercept=true; ?? ?? ? } ????????break; ????case MotionEvent.ACTION_MOVE: ? ??? ? if(需要拦截的条件){ ?? ??? ??? ?intercept=true; ?? ?? ? }else{ ?? ??? ??? ?intercept=false; ?? ?? ? } ????????break; ?? ??? } ????return intercept; } 需要注意ACTION_DOWN不能拦截,不然同序列后面的事件全部被拦截,ACTION_UP也不能拦截,不然子元素的onClick事件就无法触发了。 ? ? 2.内部拦截法 ? ? 内部拦截指的是事件都由子元素的处理,父容器不做任何拦截,如果子元素需要这个事件就直接消耗,否则就交给父容器处理。这个就用到了我们前面说的requestDisallowInterceptTouchEvent方法,这样父容器就不会启用拦截。 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ????switch (event.getAction()) { ????case MotionEvent.ACTION_UP: ? ? ? ?? ????????break; ????case MotionEvent.ACTION_DOWN: ? ? ? ??getParent().requestDisallowInterceptTouchEvent(true); ????????break; ????case MotionEvent.ACTION_MOVE: ????????if(父容器需要拦截的条件){ ? ? ? ? ? ??getParent().requestDisallowInterceptTouchEvent(false); ????????} ????????break; ???????} ????return super.dispatchTouchEvent(ev); } 父容器还是得修改拦截器,不过这次比较简单,比较容易理解,父容器除了DOWN事件其他都拦截,但是因为子元素设置requestDisallowInterceptTouchEvent(true)的时候并不会走拦截方法,因为当子元素没有消耗事件的时候,就有父容器处理。 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ? ?if (ev.getAction()==MotionEvent.ACTION_DOWN){ ? ? ?return false; ?? ?}else { ? ? ?return true; ?? ?} } 两者相比较,内部拦截稍微复杂一点,因此推荐外部拦截法解决常见的滑动冲突。对于场景2和3,解决的基本逻辑是一样的,只不过具体实现细节不太一样,具体情况需要根据具体的业务做判断,这里的代码不具备通用性就不讲解了。 |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 1:03:09- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |