Android开发艺术探索 第三章 view的事件体系

第三章view的事件体系

view基础知识

  • view是所有控件的基类,除了view还有viewGroup,其也继承自view。
  • view的位置和view内容的位置
  1. view的位置对应的四个顶点的位置,分别是top,left,right,bottom。他们可以通过get方法获取。
    view的位置坐标和 父容器的关系
  2. view内容的位置为xy,偏移值为translationX(默认为0)translationY(默认为0)
    x= left+ translationX
    y= top+ translationY
  3. 以上的各值都是相对于父容器来说的。
  • MotionEvent
  1. 手指在接触屏幕后会产生一系列的点击事件,成为事件序列。
  2. 典型的基本事件类型有ACTION_DOWN,ACTION_MOVE,ACTION_UP。
  3. 事件序列有两种DOWN-> UPDOWN-> MOVE-> ...-> MOVE-> UP
  4. 通过MotionEvent对象我们可以得到点击事件发生的x和y坐标。即getX/getY, 此处返回的是相对于当前view左上角的x和y坐标。同时还有getRawXgetRawY,区别在于,其是相对于手机屏幕左上角的坐标。
  • TouchSlop
  1. 系统设定的,能被视为滑动的最小距离。
  2. 可以通过ViewConfiguration.get(getContext()).getScaledTouchSlop()来获取这个值。
  • VelocityTracker
  1. 获取VelocityTracker对象VelocityTracker velocityTraker = VelocityTracker.obtain();
  2. 在View的onTouchEvent中追踪当前单击事件的速度velocityTracker.addMovement(event)
  3. 获取当前速度,这里的速度是指一段时间内手指滑过的像素数。可以为负值,为从右往左。
	velocityTracker.computerCurrentVelocity(1000);//必须先计算速度,这里为1s滑过的像素
	int xVelocity = (int) velocityTraker.getXVelocity();
	int yVelocity = (int) velocityTraker.getYVelocity();
  1. 不需要的时候要调用clear方法来重置并回收内存。
	velocityTraker.clear();
	velocityTraker.recycle();
  • GestureDetector
  1. 手势检测,用于辅助用户的单击,滑动,长按,双击等行为。
  2. 如果只是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击等这种行为,可以使用GestureDetector。
  3. 创建一个GestureDetector对象并实现相应想监听的接口,并重写里面的方法(方法有很多,可以查看相关文档)。
	GestureDetector mGestureDetector = new GestureDetector(this);
	//解决长按屏幕后无法拖动的现象
	mGestureDetector.setIsLongpressEnbled(false);
  1. 接管目标view的onTouchEvent方法
	boolean consume = mGestureDetector.onTouchEvent(event);
	return consume;
  • Scroller
  1. 弹性滑动对象,用来实现有过渡效果的滑动。典型代码固定,可以看下面的弹性滑动。

view的滑动

  • view的滑动可以通过三种方法实现。
  1. 通过view本身提供的scrollTo/scrollBy方法。
  2. 通过动画给view施加平移效果来实现滑动
  3. 改变view的LayoutParams使得view重新布局从而实现滑动。
  • scrollTo/scrollBy
  1. scrollBy实际上使用的还是scrollTo方法
  2. 这两者只能改变view内容的位置,而并不能改变view的位置。
  3. mScrollX和mScrollY,是在相应方向上view与view内容之间的距离。负值view内容在右/下,正值在左/上。
  • 使用动画
  1. 既可以采用传统view动画,又可以采用属性动画。
  2. 采用传统view动画可以利用android:duration, android:fromXDelta, android:toXDelta等属性在xml文件中设置。由于传统view动画只能对view的影像做操作,并不能真正改变view的位置和宽高(即改变后的影像你不能点击),所以不是很推荐。并且如果希望保留变化后的状态,还必须将fillAfter属性设为true。属性动画可以很好地解决这个问题。
  • 改变布局参数。在第四章做分析,这里了解就行。
  • 三种方法的对比
  1. scrollTo/scrollBy: 适合对view内容的滑动
  2. 动画:适用于没有交互的view和实现复杂的动画
  3. 改变布局参数:适用于有交互的view

弹性滑动

  • 使用Scroller
  1. 典型实现如下所示
  2. Scroller.startScroll方法只是保存了传递的参数
  3. invalidate()方法会让view重绘,调用draw方法。draw方法又会去调用computeScroll方法,完成第一次重绘,然后postInvalidate会再次进行第二次重绘。过程与第一次相同。一直反复这个过程。
  4. scroller.computeScrollOffset方法会根据流逝的时间去计算当前scrollX和scrollY的值(根据过去的时间占设定时间的百分比)。返回true代表滑动还未结束。
  5. 知道了滑动位置就可以通过scrollTo方法进行移动。
  6. 多次小幅度的移动就实现了弹性滑动。scroller.computeScrollOffset返回false,滑动结束。

弹性滑动的典型实现

  • 动画本身就是渐进的,并且可以设置是均匀滑动还是变式滑动。
  • 使用延时策略
  1. Handler或view的postDelayed方法。通过延时发送消息,然后在消息中进行view的滑动。
  2. 使用线程中的sleep方法。通过在while循环中不断的滑动view和sleep就可以达到目的。
  3. 核心的思想就是通过发送一系列延时消息从而达到渐进的效果

view的事件分发机制

  • 事件分发
  1. 分析的对象是MotionEvent,点击事件的分发过程就是对MotionEvent事件的分发过程。
  2. 当一个MotionEvent产生后,系统需要把这个事件传递给一个具体的view。
  1. 一个事件序列 (DOWM, MOVE,UP) 视为一个整体
  2. onIntercepterTouchEvent如果返回true, 则一个事件序列中的后续动作到来时不会再调用 onIntercepterTouchEvent, 自己直接调用onTouchEvent处理
  3. onIntercepterTouchEvent中判断是否拦截该事件序列并返回boolean, onTouchEvent中处理和是否消耗该事件序列并返回boolean. 例如: onIntercepterTouchEvent确定拦截时, 该ViewGroup会调用onTouchEvent,若在DOWN阶段什么都没返回, 而在MOVE的时候返回true, 那么UP事件中的代码就不会再处理. 该次事件序列直接被消耗
  4. 如果存在一个ViewGroup没有子view, 或者所有的子view在onTouchEvent中返回false, 则自己直接调用onTouchEvent处理
  5. ViewGroup的onIntercepterTouchEvent默认返回false, 所以 根view会层层向下, 直接将事件序列整体传给最底层的子view, 而可点击状态下的View会消耗点击事件(返回true), 如果最底层view不处理(返回false), 则会层层向上, 交给父ViewGroup的onTouchEvent处理。 如果都不处理, 最终返回false, 交给Activity自己处理
  6. 如果向下过程中有view拦截(onInterceptTouchEvent中判断并返回true), 那么事件序列归该view处理(在onTouchEvent中处理并消耗)
  7. 如果向上过程中(子view都返回了false), 父ViewGroup.onTouchEvent返回了true, 那么事件序列归该ViewGroup消耗.
  • 具体的分发过程
  1. 当一个点击操作发生时,事件最先传递给当前Activity,Activity的dispatchTouchEvent会进行事件派发。派发时,Activity内部的window会将事件传递给decor view(是底层容器,即顶级view、根view。实质上是一个framelayout。分为菜单栏和内容栏content(setContentView设置的view就是这个)),至此到达了根view。
  2. 对点击事件的分发过程分为两种:viewGroup对点击事件的分发过程 和 view对点击事件的处理过程。
  3. viewGroup的分发:点击事件到达顶级view后会调用dispatchTouchEvent方法。此时会判断是自己拦截事件还是分发给子view。在源码中可以分析出,viewGroup默认不拦截点击事件,这样就可以分发给子view(在处理滑动冲突时可以根据需要重写此方法进行拦截)。
  4. 分发给子view时会遍历所有的子view,然后进行判断
    canViewReceivePointerEvents(这个方法里进行了是否播放动画的判定)和
    isTransformedTouchPointInView(点击事件的坐标是否落在view里面)
    只有当view在播放动画点击坐标不在它里面时,此时不接收点击事件
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;//判定动画是否为空
    }
  1. 当子view接收点击事件时,如果子view的dispatchTouchEvent返回true,mFirstTouchEvent(其为单链表结构,如果最终为null,那么viewGroup就默认拦截接下来同一序列中的所有点击事件)就会被赋值同时跳出遍历view的for循环。此时点击事件就传递给子view去处理。
  2. 子view可能还是个viewGroup,那么还是按上述方法进行。但也可能是view,那么就按view的分发来。
  3. view的处理过程:需要再次申明,这里的view不包含viewGroup。
    1.同样有dispatchTouchEvent方法。
    2.没有onInterceptTouchEvent方法
    2.由于没有 子view,只能自己处理事件。
    3.源码分析得出:OnTouchListener的优先级高于onTouchEvent(源码实现为首先判断有没有设置OnTouchListener,有,然后如果OnTouchListener.onTouch方法返回ture,那么onTouchEvent就不会被调用。)
    4.不管view是不是不可用状态,只要CLICKABLELONG_CLICKABLE有一个为true,那么他就会消耗这个点击事件。
    5.一般可点击的view其CLICKABLE为true(比如 button为true,而 textview为false),而所有view的LONG_CLICKABLE默认为false。这两个可以通过相应的set方法进行更改
    6.setOnClickListenersetOnLongClickListener会分别将view相应的CLICKABLE和LONG_CLICKABLE设置为true。
  • ViewGroup 对动作分发的部分源码
 if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                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 {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
  • 以上是对源码的分析,而我们在实际运用中只要针对不同的逻辑重写相应的方法,然后返回相应的值,就能达到拦截以解决滑动冲突的效果。

view的滑动冲突

  • 常见的滑动冲突场景
  1. 外部滑动方向与内部滑动方向不一致
  2. 外部滑动方向与内部滑动方向一致
  3. 前面两种的嵌套
  • 解决方法
    将相应的滑动规则改变去应对不同场景,而滑动规则需要根据实际需求来分析。
    处理方法可以分为外部拦截法和内部拦截法
  1. 外部拦截法
    1.在父容器中拦截需要的事件
    2.重写onInterceptTouchEvent进行条件判断并拦截(返回true)。
    3.在ACTION_DOWN事件中,必须返回false。
    4.重写onTouchEvent之类的方法进行功能上处理。
  2. 内部拦截法
    1.核心是在子view中通过requestDisallowInterceptTouchEvent来对父元素是否拦截做相应的处理。
    2.首先设置父元素默认拦截除了ACTION_DOWN以外的其他事件。
    3.由于父元素自己不拦截ACTION_DOWN元素,对MOVE和UP做拦截。当事件到来时,最先事件就分发给了子元素,子元素对所有动作进行判断,当符合父元素拦截的条件时,将父view.requestDisallowInterceptTouchEvent设置为false,即无影响,事件序列的后续动作到来父元素就拦截。不符合时设置为true,即请求父元素不拦截,此时交由子元素处理。
    4.在3中的处理是重写子元素(这里指viewGroup,view没有onInterceptTouchEvent方法)的dispatchTouchEvent,原因是要对事件序列中的所有动作进行判断,为什么不在onInterceptTouchEvent里判断呢?因为他只要判定为true,事件序列后续的动作都交由他处理而不会再调用onInterceptTouchEvent。想提前处理所有的动作就不能在onInterceptTouchEvent进行判断。所以在子元素,不管是view,还是viewGroup都是重写dispatchTouchEvent。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值