View
是 Android 中所有控件的基类;ViewGroup
继承了 View
,这样 View
本身就可以是单个控件也可以是多个控件组成的一组控件,通过这种关系就形成了 View
树的结构。
View
的位置由四个顶点来确定,对应 View
的四个属性:left、top、right、bottom。注意,这些坐标都是相对于 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。
getX()/getY()
返回的是点击事件距离当前 View 左边/顶边的距离,对应于视图坐标系,是视图坐标;而 getRawX()/getRawY()
返回的是点击事件距离整个屏幕左边/顶边的距离,对应的是 Android 坐标系,是绝对坐标。可以看一下下边的图:
需要注意的是 getLeft()
、getTop()
、getRight()
和 getBottom()
是 View
类中的方法。
ViewConfiguration.get(getContext()).getScaledTouchSlop();
当处理滑动时,可以利用这个常量来做一些过滤,用来判断是不是滑动(大于等于这个值,认为是滑动;否则,不认为是滑动),可以有更好的用户体验。在不同的设备上,这个值可能是不同的。
VelocityTracker
用于速度追踪,方便根据获取到的速度来作进一步的操作。注意的地方有,获取速度前必须先计算速度,速度指的是一段时间内手指滑过的像素数,不使用的时候需要调用 recycle
方法来重置并回收内存;
GestureDectector
用于手势检测,其中监听双击行为是 onTouchEvent()
方法没有的,自己使用过用于手势切换 Activity
;
Scroller
用于实现 View
的弹性滑动。当使用 View
的 scrollTo/scrollBy
方法进行滑动时,是瞬间完成的,没有过渡效果。而使用 Scroller
可以实现有过渡效果的滑动。但是,Scroller
本身无法让 View 弹性滑动,它必须和 View 的 computeScroll
方法配合使用才能完成这个功能。
1,通过 View
本身提供的 scrollTo/scrollBy
方法;
2,使用动画,注意 View 动画只是对 View 的影像做操作,不能真正改变 View 的位置参数,而属性动画可以;
3,改变布局参数,需要使用 MarginLayoutParams 的 leftMargin,topMargin 属性。
scrollBy 内部调用了 scrollTo 方法,
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy 实现的是基于当前位置的相对滑动,当传入都为负值时会向右下角移动,而 scrollTo 实现的是基于所传递参数的绝对滑动。
scrollTo 和 scrollBy 都是只能改变 View 内容的位置而不能改变 View 在布局中的位置。
mScrollX 指的是 View 的内容在横向滑动的距离,即 View 左边缘和 View 内容左边缘在水平方向的距离;
mScrollY 指的是 View 的内容在纵向滑动的距离,即 View 上边缘和 View 内容上边缘在竖直方向的距离;
mScrollX 和 mScrollY 的单位是像素,可以分别通过 getScrollX 和 getScrollY 来获取;
当 View 左边缘在 View 内容左边缘的右边时,mScrollX 的值为正,反之,为负;
当 View 上边缘在 View 内容上边缘的下边时,mScrollY 的值为正,反之,为负。
将一次大的滑动分成若干次小的滑动并在一定时间内完成,实现渐进式滑动,或者说有过渡效果的滑动。
1,使用 Scroller
2,通过动画
3,使用延时策略
这里写一个使用 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
正是将一次大的滑动分成若干次小的滑动并在一定时间内完成,实现渐进式滑动这一思想的代码实现。
首先要知道,Android 的视图是由一个个 View 构成的层级视图,也就是说一个 View 里可以包含多个子 View,而每个子 View 又可以包含更多的子 View;当用户触摸屏幕产生一系列事件时,事件会由高到低,由外向内依次传递,最终把事件传递给一个具体的 View,这个传递的过程就叫做事件分发。
Activity -> Window -> View,即事件总是先传给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶级 View。顶级 View 接收到事件后,就会按照事件分发机制去分发事件。如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件最终会传递给 Activity 处理,即 Activity 的 onTouchEvent 方法会被调用。
可以阅读 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;
}
三个重要方法是
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 中是没有这个方法的。
看一下 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 方法,然后事件分发结束。
a, 当点击事件传递给顶级 View 时,就会调用顶级 View 的 dispatchTouchEvent 方法;
b, 若顶级 View 拦截事件,即它的 onInterceptTouchEvent 方法返回 true,那么点击事件就由顶级 View 自己处理,和 View 对点击事件的处理过程是一样的;
c, 若顶级 View 不拦截事件,那么点击事件会传递给点击事件链上的子 View,这时子 View 的 dispatchTouchEvent 方法会被调用。这样,事件就从顶级 View 传递到了下一级 View。
a, 当点击事件传递给 View 时,就会调用 View 的 dispatchTouchEvent 方法;
b, 若 View 设置了 OnTouchListener 监听事件并且 OnTouchListener 的 onTouch 方法返回 true,那么就不会调用 View 的 onTouchEvent 方法,若没有设置 OnTouchListener 或者设置了 OnTouchListener 但 onTouch 方法返回 false,则会调用 View 的 onTouchEvent 方法;
c, 若设置了 OnClickListener 事件,在 onTouchEvent 方法中,会调用 onClick 方法。
查看 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),那么它仍然可以消耗点击事件。
可以同时执行两个。当 OnLongClickListener 的 onLongClick 返回 false时,进行长按会执行两个方法;当返回 true 时,则只会执行长按方法。
返回值受当前 View 的 onTouchEvent 方法和下级 View 的 dispatchTouchEvent 方法的影响。
a, 当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件,从源码中可以看出:面对 ACTION_DOWN 事件时,会清除 mFirstTouchTarget 的值并且重置 FLAG_DISALLOW_INTERCEPT 标记。
b, 当面对其余事件时,若 onInterceptTouchEvent 方法返回 true,那么在同一个事件序列中,将导致 ViewGroup 的 onInterceptTouchEvent 方法不再调用;若返回 false,则仍然会每次都调用该方法。
查看 dispatchTouchEvent 方法:
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
条件一:子元素是否可见或者正在执行动画;
条件二:点击事件的坐标是否落在子元素的区域内。
这两个条件需要同时满足,子元素才可以接收点击事件。
外部滑动方向和内部滑动方向不一致;
外部滑动方法和内部滑动方法一致;
上面两种情况的嵌套。
外部拦截法和内部拦截法。
外部拦截法是指点击事件都会先经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的 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;
}
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这种方法和 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事件分发机制