读书笔记|艺术探索第三章-View事件体系

这两天的面试,我发现很多东西我并没有了解其机制,只是停留在表面。

1. View位置参数

参数含义获取方式
(mLeft , mTop) (mRight , mBottom)左上角点和右下角点相对于父容器的坐标getXXX()
x,y 可视View左上角的位置getX/getY
translationX,translationY可视View相对于视图本体的偏移量getTranslationX/Y
mScrollX/Y内容的相对于原始的偏移量getScrollX/Y
  1. Left,Top,Right,Buttom 很好理解。

  2. 实际上View的类中并没有X,Y变量(我找了好久(=)。getX和getY实际上是:

    《读书笔记|艺术探索第三章-View事件体系》
    可见源码对x的解释是视图的可视x位置,以像素为单位。

  3. 那啥是translationX呢?这个属性和动画有关。在使用setTranslationX改变了View的位置但是没有改变LayoutParams里的margin属性。可以理解为translateX/Y和margin是同一级别的。

参考对TranslationX和动画关系的理解

  1. 这个用Button举例子,默认scrollX/Y为0,文字是居中。当设置scrollX为正数的时候,文字会在Button中向左边移动。文字就是Button中的内容。

2. View的滑动

2.1 使用scrollTo/scrollBy。

《读书笔记|艺术探索第三章-View事件体系》

根据源码可见:(1)scrollBy实际上是调用scrollTo的。(2)scrollTo实际上是修改了
mScrollX和mScrollY。而这两个参数表示内容的偏移量。不管怎么移动,文字都不会溢出Button。

所以可以得出结论这种移动只能改变View的内容。并且mScrollX>0 内容向左滑动。mScrollY>0 内容向上滑动。

2.2 使用动画

动画实际上是操作translationXY,Scale等。View动画不能改变监听器的位置。Android 3.0 以下无法使用属性动画。属性动画解决了监听器不随视图变化的问题。
具体在第七章读书笔记再学习。

 ObjectAnimator.ofFloat(button1, "translationX", 0, 500).setDuration(10000).start();

2.3 改变布局参数

可以通过修改布局参数中的Margin来实现滑动。

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)button2.getLayoutParams();
params.leftMargin += 1;
button2.requestLayout();

3.弹性滑动

3.1 使用Scroll

    public void smoothScrollTo(int dx, int dy) {
        scroller.startScroll(getScrollX(), 0, dx, dy, 9000); // 记录开始时间,设置标记为滑动
        invalidate(); // 重绘之后调用draw,
    }
    
    @Override
    public void computeScroll() {
        // 计算是否滑动到目的地,如果没有,修改mScrollX 和 mScrollY
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY()); // 真正的滑动
            postInvalidate(); //重绘
        }
    }

滑动的入口是方法smoothScrollTo,这是自己再View内部定义的方法。可以看到首先调用了scroller.startScroll(….)。查看源码。

《读书笔记|艺术探索第三章-View事件体系》

可见startScroll方法主要是初始化一些值,并没有做关于滑动的操作。注意其设置了mMode为滑动,记录了当前时间,计算出了结束时间。然后调用invalidate()。看了一下介绍,这个方法是导致当前View无效,然后会重新绘制,也就是调用Draw。

《读书笔记|艺术探索第三章-View事件体系》

draw方法比较长,确实调用了computerScroll()方法。

《读书笔记|艺术探索第三章-View事件体系》

View的computerScroll()是一个空实现,我们重写一下。

    @Override
    public void computeScroll() {
        // 计算是否滑动到目的地,如果没有,修改mScrollX 和 mScrollY
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY()); // 真正的滑动
            postInvalidate(); //重绘
        }
    }

首先看看scroller.computeScrollOffset()

《读书笔记|艺术探索第三章-View事件体系》

第一步检查标志位,这个我们再startScroll中设置为了false,表示没有滑动结束。

然后计算timePassed,mStartTime我们再startScroll中也初始化了,从开始准备滑动到现在过了多少毫秒,如果time<mDuration,显然滑动还没有结束。那么要滑动到哪儿呢?可见mStartX+Math.round(x*mDeltaX),没错是通过时间过去的百分比计算出路程,然后加上初始值,计算出目的地。最后返回true。

再后来就是调用
scrollTo了完成正真的滑动。可见修改了
mScrollX和mScrollY,这两个值在一开始就说了,是内容相对于最开始的偏移量。然后调用
postInvalidate();进行重绘,然后又是draw,再次计算时间流逝比,计算路程滑动….

《读书笔记|艺术探索第三章-View事件体系》

总结一下:

(1)mScroll.startScroll,计算开始时间,设置标志位,计算目的地

(2)invalidate,导致视图重绘,从而调用draw

(3)draw重绘的时候,调用了computeScroll方法

(4)computeScroll由调用computerScrollOffset方法,这个方法返回boolean,返回false,表示没有滑动结束,其中计算了
时间流逝比,再通过时间流逝比,计算出等价的路程。

(5) 最后通过
scrollTo完成滑动。如此循环。

还有一些细节没有展开。

3.2 通过动画

属性动画,移动View本身。

ObjectAnimator.ofFloat(button1, "translationX", 0, 500).setDuration(10000).start();

我们可以通过动画的性质来移动内容。原理很简短,就是利用ValueAnimator得到一个类似于时间流逝比的比值,再用scrollTo来更新视图。从而达到动画的效果。

        final int startX = 0;
        final int deltaX = 100;
        ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                button1.scrollTo((int) (startX + deltaX * fraction), 0); 
            }
        });

3.3 使用延迟策略

其实也很简单,如果对消息机制比较熟悉的话。

  handler.sendEmptyMessageDelayed(1, 100);
    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            params.leftMargin += 1;
            button2.requestLayout();
            count++;

            if (count > 200) return;
            handler.sendEmptyMessageDelayed(1, 10);
        }
    };

4. 事件分发

  • public boolean dispatchTouchEvent(MotionEvent ev):是否将事件传递给下一级,返回的结果受下一级的dispatchTouchEvent()和当前的onTouchEvent()
  • public boolean onInterceptTouchEvent(MotionEvent ev):在上面函数内部调用,当前View是否拦截事件,返回true表示拦截,返回false表示不拦截。
  • public boolean onTouchEvent(MotionEvent ev):用来处理事件,返回true表示消耗,返回false表示不消耗。

用《进阶之光》上的例子就是:

敌人来挑战武当山,武当山上现在有掌门张三丰,武当七侠宋远桥,武当弟子宋青书。张三丰肯定不会直接出动(onInterceptTouchEvent == false),而是让宋远桥去(child.dispathcTouchEvent(ev)),宋远桥(ViewGroup)也威名远扬,也不会轻易应战(onInterceptTouchEvent == false),派遣宋青书(child.dispathchTouchEvent(ev),宋青书(底层的View) 没有徒弟了(没有child了),只好自己去迎战… 这就是事件的从上向下传递。
结果挑战的是成昆,宋青书处理不掉他(onTouchEvent = false),于是叫宋远桥来,结果宋远桥也不是对手(onTouchEvent = false),于是张三丰只好亲自出马(调用 onTouchEvent())

5. 滑动冲突

  1. 左右嵌套上下滑动冲突,根据滑动的左右分量和上下分量的大小来解决。
  2. 左右嵌套左右滑动冲突,根据具体的业务来解决。在实际开发中可以判断内存View是否滑动到了尽头,如果滑动到了尽头再滑动外层View,否则外层View不动。

View的事件分发机制和滑动冲突解决方案

    原文作者:北国雪WRG
    原文地址: https://www.jianshu.com/p/99282d704ff4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞