第三章 View 的事件体系

3.1 View 基础知识

View 是 Android 中所有空间的基类。
3.1 View 的位置参数
在 Android 中,x 轴和 y 轴的正方向分别为右和下。这些坐标相对于父容器来说的:top 是 view 左上角的纵坐标,left 是 view 的横坐标,right 是右下角横坐标,bottom 是右下角纵坐标;x 和 y 分别是 View 左上角的坐标;translationX 和 translationY 是 view 左上角相对于父容器的偏移量。

x = left + translationX
y = top + translationY

注意:View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是 translationX 和 translationY、x 和 y。
调用 View.requestLayout(),直接改变了 View 的位置,并非平移,left、top会变,而 translationX、translationY 不会变。
而用属性动画的时候,left、top不会变化,translationX、x会发生变化。
2 MotionEvent 和 TouchSlop
MotionEvent:正常情况下,一次手指触摸屏幕的行为会触发一系列事件,
1:点击屏幕后离开:事件序列:DOWN –>UP
2:点击屏幕滑动一会在离开:DOWN –> MOVE –> … –> MOVE –> UP
通过MotionEvent 对象可以得到:
getX / getY:返回的是相对于当前 view 的左上角的 x 和 y 坐标
getRawX / getRawY:返回的是相对于手机屏幕左上角的 x 和 y 坐标。
TouchSlop:系统所能识别出的被认为是滑动的最小距离。这是一个常量,和设备有关,在不同的设备上这个值可能是不同的。通过代码获得该值:ViewConfiguration.get(getContext()).getScaledTouchSlop()
3 VelocityTracker、GestureDetector 和 Scroller
VelocityTracker: 用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度
注意:1. 获取速度之前必须要先计算速度。2. 这里的速度是指一段时间内手指所滑过的像素数。
GestureDetector :手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
建议:如果只是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使用 GestureDetector。
Scroller:实现 View 的弹性滑动。

3.2 View 的滑动

常见的滑动方式有三种:
1:通过 View 本身提供的 scrollTo / scrollBy 方法来实现滑动;
2:通过动画给 View 施加平移效果来实现滑动;
3:通过改变 View 的 LayoutParams 使得 View 重新布局从而实现滑动。
1. 使用scrollTo / scrollBy
在滑动过程中,mScrollX 的值总是等于 View 的左边界和 View 内容的左边界在水平方向上的距离,而mScrollY 的值总是等于 View 的上边界和 View 内容的上边界在竖直方向上的距离。
scrollTo / scrollBy 只能改变 View 内容的位置而不能改变 View 控件在布局中的位置。
mScrollX 和 mScrollY 的单位是像素,当 View 左边缘的位置在 View 内容左边缘的右边的时候,mScrollX 为正值,反之为负;当 View 上边缘的位置在 View 内容上边缘的下边的时候,mScrollX 为正值,反之为负。
2. 使用动画
通过动画能够让一个 View 进行平移,而平移是一种滑动,使用动画来移动 View,主要是操作 View 的 translationX 和 translationY。既可以采用传统的 View 动画,也可以采用属性动画。属性动画为了兼容3.0以下的版本,需要采用开源动画库 nineoldandroids。
View 动画是对 View 的影像做操作,不能真正改变 View 的位置参数,包括宽和高。动画后的动画结果会消失,除非设置 fillAfter 属性为 true。注意:当一个按钮通过 View 动画后,单击新位置无法响应点击事件,点击原来的位置却会响应,为了解决这个问题:1. 使用属性动画; 2. 在新位置预先创建一个相同的 button,和目标 button 的点击事件也要相同,当目标button 完成动画后,就把目标 button 隐藏,同时预先创建的 button 显示出来,这只是一个参考。
3. 改变布局参数
即:改变 LayoutParams。
1. 通过改变(MarginLayoutParams) LayoutParams 的 leftMargin、topMargin等,重新设置 LayoutParams。(view.setLayoutParams(params); 或 view.requestLayout();)
2. 为了改变一个 Button 的位置,可以在 Button 的左边放一个空的 View,且空 View 的默认宽度为0,需要向右移动 button 的时候,重新设置空 View 的宽度即可。
4. 各种滑动方式的对比
scrollTo / scrollBy:操作简单,适合对 View 内容滑动
动画:操作简单,适合对没有交互的 View 和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于有交互的 View

3.3 弹性滑动

如何实现弹性滑动:将一次大的滑动分成若干次小的滑动并在一个时间段内完成。
使用 Scroller
下面是典型使用方法:

    Scroller mScroller = new Scroller(mContext);

    private void smoothScrollTo(int destX, int destY){
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        mScroller.startScroll(scrollX, 0, delta, 0, 1000);
        invalidate();

    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

运行过程:当我们构造一个 Scroller 对象并调用它的 startScroll 方法时,Scroller 内部什么都没有做,只是保存了我们传递的几个参数。invalidate();会导致 View 重绘,在 View 的 draw 方法中又会调用computeScroll(),该方法在 View 中是一个空实现。因为这个方法才使得弹性滑动,原因:当 View 重绘后会在 draw 方法调用 computeScroll(),而 computeScroll()又会去向 Scroller 获取当前的 scrollX 和 scrollerY;然后通过 scrollTo 实现滑动;接着调用 postInvalidate();进行第二次重绘,重绘过程与上次相同,还是会导致computeScroll()被调用;然后继续向 Scroller 获取当前的 scrollX 和 scrollY,并通过 scrollTo 滑动到新的位置,如此反复,直至滑动过程结束。
Scroller 的工作原理:Scroller 本身并不能实现 View滑动,通过结合 View 的computeScroll()才能完成弹性滑动的效果,它不断让 View 进行重绘,每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得到 View 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 来完成 view 的滑动,就这样,View 的每一次重绘都会导致 View 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是 Scroller 的工作机制。
使用动画
1. 使用属性动画:ObjectAnimator
2. 使用 ValueAnimator,原理在动画到来的每一帧前获取动画完成的比例,然后再根据这个比例计算出当前 View 所要滑动的距离。思想和Scoller比较类似。

                ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(3000);
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float fraction = animation.getAnimatedFraction();
                        Log.i(TAG, "onAnimationUpdate: " + fraction);
                        ((View)view.getParent()).scrollTo(startX+(int)(deltaX * fraction), 300);
                    }
                });
                animator.start();

使用延时策略
核心思想就是通过发送一系列延时消息从而达到一种渐进式的效果。可以把一段距离分成100次进行滑动,算出每一次应该什么时候滑动和滑动的距离。

3.4 View 的事件分发机制

public boolean dispatchTouchEvent(MotionEvent ev)

进行事件分发,如果事件能够传到当前 View,该方法一定会被调用,返回值受当前 view 的 onInterceptTouchEvent 影响和子 view 的 dispatchTouchEvent 的影响。

public boolean onInterceptTouchEvent(MotionEvent ev)

在上述方法内部调用。如果当前 View 拦截了某个事件,那么在同一个事件序列中,该方法不会被调用,返回结果表示是否拦截当前事件。

public boolean onTouchEvent(MotionEvent event)

在 dispatchTouchEvent 内部调用,用来处理点击事件,返回结果表示是否消耗该事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
伪代码表示三者关系:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        if (onInterceptTouchEvent(ev)){
            consume = onInterceptTouchEvent(ev);
        } else {
            consume = child.dispatchTouchEvent(ev);
        }
        return consume;
    }

当一个点击事件产生后,传递顺序:Activity -> Window ( PhoneWindow ) -> View,如果一个 View 的 onTouchEvent 返回 false,则会调用父容器的 onTouchEvent,若还是返回false,则会调用Activity 的 onTouchEvent。
1. 某个 View 一旦决定拦截,那么这一事件序列就只能由它来处理,并且它的 onInterceptTouchEvent(ev)不会再被调用,但是父容器的onInterceptTouchEvent(ev)还是会调用。
2. 某个 View 一旦开始处理事件,如果它不消耗 ACTion_DOWN 事件,那么同一事件序列中的其它事件都不会交给它处理,并且交给它的父容器处理,即父容器的 onTouchEvent 会被调用。
3. 如果 View 不消耗除了 ACTION_DOWN 以外的事件,那么这个点击事件会消失,父元素的 onTouchEvent 也不会被调用,并且当前 View 会持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
4. View 没有onInterceptTouchEvent(ev)方法,一旦有点击事件传递给它,那么它的 onTouchEvent 就会被调用,除非在 dispatchTouchEvent 中直接返回了。
5. View 的 enable 属性不影响 onTouchEvent 的默认返回值,而clickable 和 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
事件分发源码解析地址

https://blog.csdn.net/ghdsq/article/details/79676915

3.5 View 的滑动冲突

常见的滑动冲突:
场景1:外部滑动方向和内部方向不一致 (ViewPager + ListView):
场景2:外部方向和内部方向一致 (SlideMenu + ViewPager)
场景3:上面两种情况的嵌套 (SlideMenu + ViewPage + ListView)
解决方法:
1. 外部拦截法 点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截。
注意:
1>. ACTION_DOWN 事件必须返回false,否则后面的一系列事件不会到达子View。
2>. ACTION_MOVE 事件可以根据条件拦截,父容器需要就拦截,不需要不拦截
3>. ACTION_UP 事件必须返回 false,因为该事件对于当前 ViewGroup 本身也没意义。前提是 Down 事件和 Move 事件都是子 View 消耗,如果此时当前 ViewGroup 的 up 事件返回true的话,子 View 无法接收到该事件,子元素的 onClick 事件无法触发。

    private int startX;
    private int startY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int lastX = 0;
        int lastY = 0;
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                startX = (int) ev.getX();
                startY = (int) ev.getY();
                return false;
            case MotionEvent.ACTION_MOVE:
                lastX = (int) ev.getX();
                lastY = (int) ev.getY();
                if (Math.abs(startX-lastX) > Math.abs(startY-lastY)){
                    return true;
                }else {
                    return false;
                }
            case MotionEvent.ACTION_UP:
                return false;
            default:
                break;
        }
        startX = lastX;
        startY = lastY;
        return false;
    }

2. 内部拦截法: 父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理。
重写子元素的 dispatchTouchEvent:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        int xNow = 0;
        int yNow = 0;
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                /**
                 * 不让父容器的 onInterceptTouchEvent 方法在接下来的事件被调用
                 */
                getParent().requestDisallowInterceptTouchEvent(true);
                x = (int) ev.getX();
                y = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                xNow = (int) ev.getX();
                yNow = (int) ev.getY();
                if (Math.abs(x - xNow) > Math.abs(y - yNow)){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        x = xNow;
        y = yNow;
        return super.dispatchTouchEvent(ev);
    }

父 View 的处理:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        super.onInterceptTouchEvent(ev);
        if (ev.getAction() == MotionEvent.ACTION_DOWN){
            return false;
        }else {
            return true;
        }
    }

注意:调用getParent().requestDisallowInterceptTouchEvent(false);方法的时候,仅仅是修改了 FLAG_DISALLOW_INTERCEPT标记位,让事件来临的时候走onInterceptTouchEvent(MotionEvent ev),并不一定真正会被拦截,因此,父元素的ACTION_UP 事件要返回true才能确保事件被父容器拦截。

            /**
             * 当事件由ViewGroup的子元素处理的时候,mFirstTouchTarget会被赋值指向子元素
             * 当ViewGroup拦截事件,mFirstTouchTarget!=null不成立
             *      一旦当前ViewGroup拦截事件,actionMasked == ACTION_MOVE和actionMasked == ACTION_UP
             *    并且 mFirstTouchTarget == null,不再走当前view的onInterceptTouchEvent(ev)方法,并且其他事件都交给它处理
             *    但是父容器的onInterceptTouchEvent(ev)还是会被调用
             */
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                /**
                 * FLAG_DISALLOW_INTERCEPT一般是在子view中通过requestDisallowInterceptTouchEvent(true)设置的
                 * FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN之外的事件,因为ACTION_DOWN会重置该标记位
                 */
                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;
            }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值