天天看点

第3章-View的事件体系读书笔记1 View 基础知识2 View 的滑动3 弹性滑动4 View 的事件分发机制5 View 的滑动冲突参考

目录

  • 1 View 基础知识
    • 1.1 对于 View 的理解
    • 1.2 View 的位置参数有哪些?
    • 1.3 MotionEvent 类中的 getX()/getY() 和 getRawX()/getRawY() 这两组方法的区别是什么?
    • 1.4 如何获取滑动的最小距离?
    • 1.5 VelocityTracker、GestureDetector 和 Scroller 怎么使用?
  • 2 View 的滑动
    • 2.1 实现 View 的滑动的方法有哪些?
    • 2.2 View 的 scrollTo、scrollBy 方法的区别是什么?
    • 2.3 View 内部的两个属性 mScrollX 和 mScrollY 的改变规则
  • 3 弹性滑动
    • 3.1 实现弹性滑动的共同思想是什么?
    • 3.2 实现弹性滑动的方法有哪些?
    • 3.3 Scroller 的工作原理是什么?
  • 4 View 的事件分发机制
    • 4.1 什么是事件分发?
    • 4.2 当一个点击事件产生后,它的传递过程遵循什么顺序?
    • 4.3 当 View 需要处理事件时,它的 OnTouchListener,OnTouchEvent 和 OnClickListener 的优先级是怎样的?
    • 4.4 事件分发的三个重要方法是什么,以及它们在 Activity,ViewGroup 和 View 中的存在状态是怎样的?
    • 4.5 Activity 对点击事件的分发过程是什么?
    • 4.6 顶级 View 对点击事件的分发过程是什么?
    • 4.7 View 对点击事件的处理过程是什么?
    • 4.8 当当前 View 是 DISABLED 状态时,还可以消耗点击事件吗?
    • 4.9 View 的 setOnLongClickListener 和 setOnClickListener 是否只能执行一个?
    • 4.10 ViewGroup 的 dispatchTouchEvent 方法的返回值如何确定?
    • 4.11 ViewGroup 的 onInterceptTouchEvent 方法如何调用?
    • 4.12 当 ViewGroup 不拦截点击事件时,事件会向下分发交给它的子 View进行处理,那么如何判断子元素能够接收点击事件呢?
    • 相关面试题
      • 1. 一个 LinearLayout 里放置两个 Button,说一下在这种情况下,事件是如何分发的?
  • 5 View 的滑动冲突
    • 5.1 滑动冲突的场景
    • 5.1 解决滑动冲突的方式有哪些?
    • 5.2 什么是外部拦截法?
    • 5.3 什么是内部拦截法?
  • 参考

1 View 基础知识

1.1 对于 View 的理解

View

是 Android 中所有控件的基类;

ViewGroup

继承了

View

,这样

View

本身就可以是单个控件也可以是多个控件组成的一组控件,通过这种关系就形成了

View

树的结构。

1.2 View 的位置参数有哪些?

View

的位置由四个顶点来确定,对应

View

的四个属性:left、top、right、bottom。注意,这些坐标都是相对于

View

的父容器的。

第3章-View的事件体系读书笔记1 View 基础知识2 View 的滑动3 弹性滑动4 View 的事件分发机制5 View 的滑动冲突参考

从 Android 3.0 开始,新增的几个参数:x、y、translationX 和 translationY。x 是

View

左上角的横坐标,y 是

View

左上角的纵坐标, translationX 是

View

左上角相对父容器横向的偏移量,translationY 是

View

左上角相对父容器纵向的偏移量。

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 坐标系,是绝对坐标。可以看一下下边的图:

第3章-View的事件体系读书笔记1 View 基础知识2 View 的滑动3 弹性滑动4 View 的事件分发机制5 View 的滑动冲突参考

需要注意的是

getLeft()

getTop()

getRight()

getBottom()

View

类中的方法。

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

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

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,通过

View

本身提供的

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事件分发机制