Android进阶学习(三)View的事件体系
文本是阅读《Android开发艺术探索》的学习笔记记录,详细内容可以自行阅读书本。
View的事件体系
1 View基础知识
1.1 什么是View
View是Android中所有控件的基类。不管是简单的Button和TextView还是复杂的RelativeLayout和ListView,它们的共同基类都是View。还有ViewGroup也继承了View,它内部可以包含多个View。
1.2 View的位置参数
View的位置主要由它四个顶点确定,分别对应View的四个属性:top、left、right、bottom。在Android中,它的坐标系与我们所看到的有区别,如图所示。
1.3 MotionEvent和TouchSlop
1)MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的事件类型如下几种:
ACTION_DOWN ——手指刚接触屏幕
ACTION_MOVE ——手指在屏幕上移动
ACTION_UP ——手指从屏幕上松开
2)TouchSlop
TouchSlop是系统所能够识别出的被认为是滑动的最小距离。可以通过ViewConfiguration.get(this).getScaledTouchSlop()获取。
1.4 Scroller
弹性滑动对象,用于实现View的弹性滑动。当使用View的scrollTo/scrollBy时,其过程是瞬间完成的。这个时候就可以用Scroller来实现过渡滑动效果。它需要和View的computeScroll方法配合使用,如下:
scroller = new Scroller(context);
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
2 View的滑动
通过三种方式可以实现View的滑动:第一种是通过View本身提供的scrollTo/scrollBy方法;第二种是通过动画实现滑动;第三种是通过修改View的LayoutParams使得View重新布局从而实现滑动。
2.1使用scrollTo/scrollBy
先来看看两个方法的源码
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
首先scrollBy方法也是调用了scrollTo方法,scrollBy方法是相对当前位置移动,而scrollTo是指定位置的绝对移动。通过先判断传进来的(x, y)值是否和View的X, Y偏移量相等,如果不相等,就调用onScrollChanged()方法来通知界面发生改变,然后重绘界面。
举个例子,调用两次scrollTo(-10, 0),View第一次会向左滑动10,第二次不变化。调用两次scrollBy(-10, 0),View第一次向左滑动10,第二次再向左滑动10。
我们要先理解View 里面的两个成员变量mScrollX, mScrollY,X轴方向的偏移量和Y轴方向的偏移量,这个是一个相对距离,相对的不是屏幕的原点,而是View的左边缘和上边缘。
变化规律如图所示:
2.2 使用动画
上一篇介绍过了,直接使用属性动画。
ObjectAnimator.ofFloat(stopWebBt, "translationY", -startWebBt.getHeight()).start();
2.3 改变布局参数
修改LayoutParams参数
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams)
button.getLayoutParams();
marginLayoutParams.leftMargin += 100;
button.requestLayout();
3 弹性滑动
弹性滑动的方式很多,比如通过Scroller、动画、延时策略等等
3.1 使用Scroller
//弹性滑动到指定位置
public void smoothScrollto(int destX, int destY) {
int x = getScrollX();
int deltaX = destX - x;
//1000ms内
scroller.startScroll(x, 0, deltaX, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
下面探索一下Scroller的内部工作原理,我们调用Scroller的startScroll方法,其实Scroller内部什么也没有做,这是保存了传递进去的参数,源码如下。
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
真正推动弹性滑动的是我们的invalidate()方法,它导致View重绘,View重绘时又调用我们实现的computeScroll()方法,其中又调用postInvalidate()方法进行二次重绘,直到滑动结束。源码如下:
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
该方法主要两个作用,一个是判断滑动是否结束,一个是如果未结束,根据时间流失的百分比来给CurrX和CurrY赋值,帮助滑动。
3.2 通过动画
动画上篇结束了,就不多说了。动画实现弹性滑动非常轻松,也提供了许多丰富的api。
3.3 延时策略
它的核心思想结束通过发送延时消息来达到渐进式的效果,可以使用Handler的postDelayed方法;线程的sleep方法等等。
4 View的事件分发机制
4.1 点击事件的传递
点击事件的分发由三个很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。
dispatchTouchEvent,进行事件分发,如果事件传递给当前View,此方法一定被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent影响,表示是否消耗当前事件。
onInterceptTouchEvent,内部调用,用来拦截某个事件,如果当前View拦截了事件,那么同一个事件序列中,此方法不再调用,返回结果表示是否拦截当前事件。
onTouchEvent,在dispatchTouchEvent方法中调用,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。
当一个点击事件产生后,它的传递顺序:Activity -> Window -> View。
4.2 事件分发的源码分析
1)Activity对点击事件的分发过程
当一个点击操作发生时,事件最先传递给当前Activity,由activity的dispatchTouchEvent进行事件分发,具体工作由activity内部的Window来完成。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器)。
先从activity的dispatchTouchEvent开始分析
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//Window进行分发
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//未有view消耗事件,则调用activity的onTouchEvent
return onTouchEvent(ev);
}
首先事件交给activity所附属的Window进行分发,如果返回true,整个事件结束。否则调用activity的onTouchEvent。
2)Window点击事件的分发过程
由于Window唯一的实现是PhoneWindow,从源码上看:
@Override
public boolean superDispatchGenericMotionEvent(MotionEvent event) {
return mDecor.superDispatchGenericMotionEvent(event);
}
PhoneWindow将事件直接传递给了mDecor ,而mDecor = (DecorView) preservedWindow.getDecorView();也就是事件一定会传递到view。
3)顶层View点击事件的分发过程
顶层ViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由顶层ViewGroup处理,这时如果顶层ViewGroup的OnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。如果顶层ViewGroup不拦截事件,则事件会传递给它所在的点击事件链的子View。
先看ViewGroup的拦截源码
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
代码中可以看出,ViewGroup两种情况下会判断是否拦截事件:ACTION_DOWN和mFirstTouchTarget。从后面代码可知子View如果成功处理事件,会将mFirstTouchTarget赋值。即当ViewGroup不拦截事件交给子View处理时,mFirstTouchTarget != null。反之ViewGroup自己处理了该事件后,mFirstTouchTarget == null,onInterceptTouchEvent不再被调用,其余点击事件由ViewGroup自己处理。
特殊情况,FLAG_DISALLOW_INTERCEPT标记位,这个是通过子View调用设置。原来控制ViewGroup不拦截除了ACTION_DOWN以外的其他点击事件。因为当ACTION_DOWN事件到来时,ViewGroup会重置FLAG_DISALLOW_INTERCEPT标记位。源码如下:
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
从上面源码分析,得知当ViewGroup决定拦截事件后,后续点击事件不再调用onInterceptTouchEvent,直接交给它处理。
再看ViewGroup不拦截事件时,事件会向下分发给它的子View处理。
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
从上段代码可以看出,遍历ViewGroup的子View,判断子View是否能够接收到点击事件。两点判断:是否再播放动画和点击事件坐标是否落在子View区域内。满足则调用dispatchTransformedTouchEvent方法,部分源码如下:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
该方法内如果传入参数child 不为空,则调用子View的dispatchTouchEvent方法。该方法返回true,则跳出for循环,并在addTouchTarget方法内对mFirstTouchTarget赋值。
4)View对点击事件的处理过程
View是单独元素,没有子View进行传递,部分dispatchTouchEvent源码:
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;
}
首先,判断是否设置mOnTouchListener,有则返回true,并且不调用onTouchEvent。
5 View的滑动冲突
5.1 常见滑动冲突场景
场景一:外部滑动和内部滑动方向不一致
场景二:外部滑动和内部滑动方向一致
场景三:上面两种情况的嵌套
5.2 滑动冲突的处理规则
场景一:根据滑动是水平还是竖直来判断谁来拦截事件
场景二:需要根据业务来判断,由谁来拦截事件
场景一:需要根据业务来判断,由谁来拦截事件
5.3 滑动冲突的解决方法
1.外部拦截法
是指经过父类容器的拦截处理,父类容器需要则拦截此事件,反之传递给子View处理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.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;
}
return intercepted;
}