第3章-View的事件体系读书笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/willway_wang/article/details/83959952

目录

1 View 基础知识

1.1 对于 View 的理解

View 是 Android 中所有控件的基类;ViewGroup 继承了 View,这样 View 本身就可以是单个控件也可以多个控件组成的一组控件,通过这种关系就形成了 View 树的结构。

1.2 View 的位置参数有哪些?

View 的位置由四个顶点来确定,对应 View 的四个属性:left、top、right、bottom。注意,这些坐标都是相对于 View 的父容器的。

从 Android 3.0 开始,新增的几个参数:x、y、translationX 和 translationY。x 是左上角的横坐标,y 是左上角的纵坐标, translationX 是左上角相对父容器横向的偏移量,translationY 是左上角相对父容器纵向的偏移量。

 public float getX() {
        return mLeft + getTranslationX();
 }
public float getY() {
        return mTop + getTranslationY();
}

View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值不会发生改变,发生改变的是 x、y、translationX 和 translationY。

1.3 MotionEvent 类中的 getX()/getY() 和 getRawX()/getRawY() 这两组方法的区别是什么?

getX()/getY() 返回的是点击事件距离当前 View 左边/顶边的距离,对应于视图坐标系,是视图坐标;而 getRawX()/getRawY() 返回的是点击事件距离整个屏幕左边/顶边的距离,对应的是 Android 坐标系,是绝对坐标。可以看一下下边的图:

需要注意的是 getLeft()、getTop()、getRight() 和 getBottom() 是 View 类中的方法。

1.4 如何获取滑动的最小距离?

ViewConfiguration.get(getContext()).getScaledTouchSlop();

当处理滑动时,可以利用这个常量来做一些过滤,用来判断是不是滑动,可以有更好的用户体验。在不同的设备上,这个值可能是不同的。

1.5 VelocityTracker、GestureDetector 和 Scroller 怎么使用?

VelocityTracker 用于速度追踪,方便根据获取到的速度来作进一步的操作。注意的地方有,获取速度前必须先计算速度,速度指的是一段时间内手指滑过的像素数,不使用的时候需要调用 recycle 方法来重置并回收内存;
GestureDectector 用于手势检测,其中监听双击行为是 onTouchEvent() 方法没有的,自己使用过用于手势切换 Activity;
Scroller 用于实现 View 的弹性滑动。当使用 View 的 scrollTo/scrollBy 方法进行滑动时,是瞬间完成的,没有过渡效果。而使用 Scroller 可以实现有过渡效果的滑动。但是,Scroller 本身无法让 View 弹性滑动,它必须和 View 的 computeScroll 方法配合使用才能完成这个功能。

2 View 的滑动

2.1 实现 View 的滑动的方法有哪些?

1,使用 scrollTo/scrollBy 方法;
2,使用动画,注意 View 动画只是对 View 的影像做操作,不能真正改变 View 的位置参数,而属性动画可以;
3,改变布局参数,需要使用 MarginLayoutParams 的 leftMargin,topMargin 属性。

2.2 View 的 scrollTo、scrollBy 方法的区别是什么?

scrollBy 内部调用了 scrollTo 方法,

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

scrollBy 实现的是基于当前位置的相对滑动,当传入都为负值时会向右下角移动,而 scrollTo 实现的是基于所传递参数的绝对滑动。
scrollTo 和 scrollBy 都是只能改变 View 内容的位置而不能改变 View 在布局中的位置。

2.3 View 内部的两个属性 mScrollX 和 mScrollY 的改变规则

mScrollX 指的是 View 的内容在横向滑动的距离,即 View 左边缘和 View 内容左边缘在水平方向的距离;
mScrollY 指的是 View 的内容在纵向滑动的距离,即 View 上边缘和 View 内容上边缘在竖直方向的距离;
mScrollX 和 mScrollY 的单位是像素,可以分别通过 getScrollX 和 getScrollY 来获取;
当 View 左边缘在 View 内容左边缘的右边时,mScrollX 的值为正,反之,为负;
当 View 上边缘在 View 内容上边缘的下边时,mScrollY 的值为正,反之,为负。

3 弹性滑动

3.1 实现弹性滑动的共同思想是什么?

将一次大的滑动分成若干次小的滑动并在一定时间内完成,实现渐进式滑动,或者说有过渡效果的滑动。

3.2 实现弹性滑动的方法有哪些?

1,使用 Scroller
2,通过动画
3,使用延时策略

3.3 Scroller 的工作原理是什么?

这里写一个使用 Scroller 实现弹性滑动的例子:

public class ScrollerLayout extends LinearLayout {
    private Scroller mScroller;
    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        mScroller = new Scroller(context);
    }

    public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int deltaX = destX - scrollX;
        // 1000 ms 内滑向 destX, 效果就是慢慢滑动
        mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);

        invalidate();

    }

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

第一步,先构造一个 Scroller 对象;
第二步,调用 Scroller 的 startScroll 方法,传入的参数是滑动的起点,要滑动的距离,滑动的时间。但是仅调用 startScroll 方法并不能实现滑动,可以看 startScroll 方法的内部只是保存了传入的参数而已。
第三步,在 startScroll 后面,调用 invalidate 方法,这样会导致 View 重绘,在 View 的 draw 方法中又会去调用 computeScroll 方法,computeScroll 方法在 View 里是空实现的。
第四步,重写 computeScroll 方法,在里面调用 Scroller 的 computeScrollOffset 方法,这个方法的作用是判断滑动是否结束了,根据经过的时间计算出要滑动到的位置。如果这个方法返回 false,表示弹性滑动结束了。
第五步,调用 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 滑动到要滑动的位置。
第六步,调用 postInvalidate 方法,再次导致 View 重绘,会继续走第四步。
总之,Scroller 正是将一次大的滑动分成若干次小的滑动并在一定时间内完成,实现渐进式滑动这一思想的代码实现。

4 View 的事件分发机制

4.1 什么是事件分发?

首先要知道,Android 的视图是由一个个 View 构成的层级视图,也就是说一个 View 里可以包含多个子 View,而每个子 View 又可以包含更多的子 View;当用户触摸屏幕产生一系列事件时,事件会由高到低,由外向内依次传递,最终把事件传递给一个具体的 View,这个传递的过程就叫做事件分发。

4.2 当一个点击事件产生后,它的传递过程遵循什么顺序?

Activity -> Window -> View,即事件总是先传给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶级 View。顶级 View 接收到事件后,就会按照事件分发机制去分发事件。如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件最终会传递给 Activity 处理,即 Activity 的 onTouchEvent 方法会被调用。

4.3 当 View 需要处理事件时,它的 OnTouchListener,OnTouchEvent 和 OnClickListener 的优先级是怎样的?

可以阅读 View 类的 dispatchTouchEvent(MotionEvent event) 方法得到答案:如果这个 View 设置了 OnTouchListener,

    public interface OnTouchListener {
        boolean onTouch(View v, MotionEvent event);
    }

那么 OnTouchListener 的 onTouch 方法就会被回调。这时如果 onTouch 方法返回 true,那么 onTouchEvent 方法就不会被调用;如果 onTouch 方法返回 false,那么 onTouchEvent 方法会被调用。所以,View 设置的 OnTouchListener,其优先级比 onTouchEvent 方法要高。这样做的好处是方便在外界处理 View 的点击事件。具体可以看这段源码:

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}
if (!result && onTouchEvent(event)) {
    result = true;
}

在 View 的 onTouchEvent 方法中,如果当前设置了 OnClickListener,那么它的 onClick 方法会被调用。所以,onTouchEvent 方法的优先级比 OnCLickListener 要高。具体可以看下面的源码:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

4.4 事件分发的三个重要方法是什么,以及它们在 Activity,ViewGroup 和 View 中的存在状态是怎样的?

三个重要方法是

public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev) ;
public boolean onTouchEvent(MotionEvent event);

dispatchTouchEvent 方法的作用是分发点击事件,当点击事件能够传递给当前 View时就会被调用;
onInterceptTouchEvent 方法的作用是用于判断是否拦截点击事件,在 ViewGroup 的 dispatchTouchEvent 方法内部调用;
onTouchEvent 方法的作用是处理点击事件,在 dispatchTouchEvent 方法内部调用。
对应的存在状态如下:

方法 Activity ViewGroup View
dispatchTouchEvent
onInterceptTouchEvent × ×
onTouchEvent

可以看到,只有 ViewGroup 具有 onInterceptTouchEvent 方法,而在 Activity 和 View 中是没有这个方法的。

4.5 Activity 对点击事件的分发过程是什么?

看一下 Activity 的 dispatchTouchEvent 方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

a. 当点击事件传递给 Activity 时,会通过这个方法进行分发。
b. getWindow().superDispatchTouchEvent(ev) 中的 getWindow() 的真正实现是 PhoneWindow;
在 PhoneWindow 内部,会调用 DecorView 的 superDispatchTouchEvent 方法(DecorView 是 PhoneWindow 的内部类,继承于 FrameLayout,到这里就实现了事件从 Activity 到 ViewGroup 的传递。);
c. 若 mDecor.superDispatchTouchEvent(event) 返回 true,则 getWindow().superDispatchTouchEvent(ev) 也返回 true,则事件分发结束;
d. 若 mDecor.superDispatchTouchEvent(event) 返回 false,则 getWindow().superDispatchTouchEvent(ev) 也返回 false,会继续调用 Activity 的 onTouchEvent 方法,然后事件分发结束。

4.6 顶级 View 对点击事件的分发过程是什么?

a, 当点击事件传递给顶级 View 时,就会调用顶级 View 的 dispatchTouchEvent 方法;
b, 若顶级 View 拦截事件,即它的 onInterceptTouchEvent 方法返回 true,那么点击事件就由顶级 View 自己处理,和 View 对点击事件的处理过程是一样的;
c, 若顶级 View 不拦截事件,那么点击事件会传递给点击事件链上的子 View,这时子 View 的 dispatchTouchEvent 方法会被调用。这样,事件就从顶级 View 传递到了下一级 View。

4.7 View 对点击事件的处理过程是什么?

a, 当点击事件传递给 View 时,就会调用 View 的 dispatchTouchEvent 方法;
b, 若 View 设置了 OnTouchListener 监听事件并且 OnTouchListener 的 onTouch 方法返回 true,那么就不会调用 View 的 onTouchEvent 方法,若没有设置 OnTouchListener 或者设置了 OnTouchListener 但 onTouch 方法返回 false,则会调用 View 的 onTouchEvent 方法;
c, 若设置了 OnClickListener 事件,在 onTouchEvent 方法中,会调用 onClick 方法。

4.8 当当前 View 是 DISABLED 状态时,还可以消耗点击事件吗?

查看 View 类的 onTouchEvent 方法:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

可以得出结论:当 View 处于 DISABLED 状态时,只要它可点击(CLICKABLE 或者 LONG_CLICKABLE),那么它仍然可以消耗点击事件。

4.9 View 的 setOnLongClickListener 和 setOnClickListener 是否只能执行一个?

可以同时执行两个。当 OnLongClickListener 的 onLongClick 返回 false时,进行长按会执行两个方法;当返回 true 时,则只会执行长按方法。

4.10 ViewGroup 的 dispatchTouchEvent 方法的返回值如何确定?

返回值受当前 View 的 onTouchEvent 方法和下级 View 的 dispatchTouchEvent 方法的影响。

4.11 ViewGroup 的 onInterceptTouchEvent 方法如何调用?

a, 当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件,从源码中可以看出:面对 ACTION_DOWN 事件时,会清除 mFirstTouchTarget 的值并且重置 FLAG_DISALLOW_INTERCEPT 标记。
b, 当面对其余事件时,若 onInterceptTouchEvent 方法返回 true,那么在同一个事件序列中,将导致 ViewGroup 的 onInterceptTouchEvent 方法不再调用;若返回 false,则仍然会每次都调用该方法。

4.12 当 ViewGroup 不拦截点击事件时,事件会向下分发交给它的子 View进行处理,那么如何判断子元素能够接收点击事件呢?

查看 dispatchTouchEvent 方法:

if (!canViewReceivePointerEvents(child)
        || !isTransformedTouchPointInView(x, y, child, null)) {
    continue;
}

条件一:子元素是否可见或者正在执行动画;
条件二:点击事件的坐标是否落在子元素的区域内。
这两个条件需要同时满足,子元素才可以接收点击事件。

相关面试题

1. 一个 LinearLayout 里放置两个 Button,说一下在这种情况下,事件是如何分发的?

5 View 的滑动冲突

5.1 滑动冲突的场景

外部滑动方向和内部滑动方向不一致;
外部滑动方法和内部滑动方法一致;
上面两种情况的嵌套。

5.1 解决滑动冲突的方式有哪些?

外部拦截法和内部拦截法。

5.2 什么是外部拦截法?

外部拦截法是指点击事件都会先经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可。外部拦截法的典型逻辑如下:

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (父容器需要当前点击事件) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
        	break;
    }
    mLastInterceptX = x;
    mLastInterceptY = y;
    mLastX = x;
    mLastY = y;
    return intercepted;
}

5.3 什么是内部拦截法?

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这种方法和 Android 中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来比外部拦截法稍显复杂。所以,推荐使用外部拦截法来解决常见的滑动冲突。伪代码如下,
需要重写子元素的 dispatchTouchEvent 方法:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器需要此类点击事件) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(event);
}

父元素需要默认拦截除了 ACTION_DOWN 以外的其他事件,在父元素中修改的代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    } 
}

参考

1.Android事件分发机制详解:史上最全面、最易懂
2.Android View 事件分发机制 源码解析 (上)
3.Android ViewGroup事件分发机制

猜你喜欢

转载自blog.csdn.net/willway_wang/article/details/83959952
今日推荐