一、View基础知识
1)、初识View
Android体系中,View是承载了界面视图,它代表了一个矩形区域里面的内容。界面具体可分为两种类型View和ViewGroup,顾名思义ViewGroup中可以存放多个View。View称之为控件,分为系统提供的默认的控件和自定义的控件。
2)、View位置
借用网上的View坐标图,说明View中关于位置的属性和方法。因为View存在着相对位置,绝对位置以及动画时布局位置和绘画位置不一致导致的位置属性。
MotionEvent位置参数
- getX() : 鼠标点击的位置,距离该View的左边距距离
- getY() : 鼠标点击的位置,距离该View的上边距距离
- getRawX() : 鼠标点击的位置,距离该屏幕的左边距距离
- getRawY() : 鼠标点击的位置,距离该屏幕的上边距距离
- translationX : Animation动画在X轴移动的距离
- translationY : Animation动画在Y轴移动的距离
View位置参数
- getLeft() : 该控件相对于父控件左边距
- getTop() : 该控件相对于父控件上边距
- getRight() : 该控件相对于父控件右边距
- getBottom() : 该控件相对于父控件下边距
- getX() : 控件相对父布局的左边距 + translationX
- getY() : 控件相对父布局的上边距 + translationY
3)、MotionEvent事件类
MotionEvent类是保存了鼠标、键盘、触摸的一些属性类。
对于触摸事件,他会产生一些具体的事件如Down、Move、Up、Cancel事件。其中Down,n个Move,Up算作一组事件。
对于MotionEvent中的一些参数和方法,上面已经介绍过了。
4)、设计模式之组合设计模式
View体系中,ViewGroup是一个View组,继承自View,并且可以包含多个View。与文件夹和文件的含义一致。
常见的组合模式就是一方持有另一个类的对象,View的这种是个特殊,持有的和被持有的基类都是一致的。下面用File和Folder举例说明组合模式。
File类,基类,也是子节点。
定义了两个属性文件名和文件大小。和该文件的父文件对象parent,这个是为了保证增删改文件的时候,对相应的文件夹更新更新,一般情况父文件夹的大小会变化。其中invalidate()类似于View中的刷新功能。
public class File {
// 文件名
protected String name;
// 文件大小
protected int size;
// 父节点
protected File parent;
// 打印的前缀
protected final static String defaultPrefix = "----";
public File(String name) {
this.name = name;
}
public File(String name, int size) {
this.name = name;
this.size = size;
}
// 刷新大小
protected void invalidate() {
}
@Override
public String toString() {
return "File [name=" + name + ", size=" + size + "]";
}
}
Folder类,File类的派生类,文件夹可以包含多个文件或者文件夹。
其中注意的地方就是folder中重写invalidate()。先计算自己的大小后,再通知父节点刷新,从下往上刷新。对应的View刷新是从下往上再从上往下。
public class Folder extends File {
public final List<File> fileList = new ArrayList<>();
public Folder(String name) {
super(name);
}
protected void invalidate() {
// 1、先算自己的
this.size = 0;
for (File f : fileList) {
this.size += f.size;
}
// 2、通知父节点计算
if (this.parent != null) {
this.parent.invalidate();
}
}
// 增加一个文件夹
public void addFile(File file) {
if (file == null)
return;
file.parent =this;
fileList.add(file);
// 刷新大小
invalidate();
}
// 遍历文件夹
public void printFolder() {
System.out.println(this.toString());
print(fileList,defaultPrefix);
}
// 打印方法
private void print(List<File> fileList,String prefix) {
for (File f : fileList) {
if (f instanceof Folder) {
Folder folder = ((Folder) f);
System.out.println();
System.out.println(prefix+"Folder [name=" + folder.name + ", size=" + folder.size + "]");
if (!folder.fileList.isEmpty()) {
print(folder.fileList,prefix+defaultPrefix);
}
} else {
System.out.println(prefix+f.toString());
}
}
}
@Override
public String toString() {
return "Folder [name=" + name + ", size=" + size + "]";
}
}
测试类Test
再root目录下增加相应的节点,并打印输出。
public static void main(String[] args) {
Folder root = new Folder("root");
Folder texts = new Folder("texts");
texts.addFile(new File("text1",10));
texts.addFile(new File("text2",10));
Folder images = new Folder("images");
images.addFile(new File("image1",20));
images.addFile(new File("image2",30));
Folder vedios = new Folder("vedios");
vedios.addFile(new File("vedio1",20));
vedios.addFile(new File("vedio2",30));
root.addFile(texts);
root.addFile(vedios);
vedios.addFile(images);
root.printFolder();
}
结果:
二、MotionEvent分发流程
MotionEvent的分发流程:Activity --> PhoneWindow --> DecorView --->ViewGroup --->View
1)、Activity
Android中,Activity是MotionEvent事件分发的唯一来源,当有触摸事件发生是,Linux通过判断当前是在哪个Activity,最终调用该Activity中的dispatchTouchEvent(MotionEvent event)方法,Event是从Activity的dispatchTouchEvent开始分发的。
首先看下Activity中的Event分发方法:
Activity作为Event分发的起点,首先是给子View处理,如果有View能处理,View处理完成后到此结束,如果没有任何View消费该事件,最后由Activity自己尝试消费处理
public boolean dispatchTouchEvent(MotionEvent ev) {
// 1、MotionEvent.ACTION_DOWN事件会通知用户,可通过重写onUserInteraction()接收
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 如果Window消费了该Event事件,就返回true,该事件的分发到此结束
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 如果没有任何View消费该事件,最后又Activity自己尝试消费处理
return onTouchEvent(ev);
}
2)、Window
Window是个抽象类,它的唯一一个派生类就是PhoneWindow。一个Activity会有一个PhoneWindow载体,在Activity的attach方法中会初始化一个PhoneWindow,PhoneWindow中会有一个DecorView(见PhoneWindow的构造方法)。本质Window中没做任何处理,全权由DecorView处理。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
3)、DecorView
DecorView实际上就是一个ViewGroup,从类的声明来看,他继承了WindowCallbacks,这个是Window一些事件响应的回调。
/** @hide */
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
DecorView何许人也?和我们的布局有何关联?
正常情况下,Activity加载的是一个类似于下面的XML。外面的LinearLayout就是所说的DecorView。里面的id为content的则是我们在Activity中设置的XML。这样DecorView就和我们Activity的View联系起来了。
这样Event事件则传到了ViewGroup的dispatchTouchEvent中
三、View中MotionEvent分发
《Android开发艺术探索》中有段事件分发的伪代码,很精辟。
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
这段伪代码就描述了,View和ViewGroup中事件分发的伪代码。
consume代表了是否消费的标志;最开始调用 onInterceptTouchEvent方法判断该ViewGroup是否需要拦截该Event,View中没有onInterceptTouchEvent,所以默认返回为true。如果拦截后(false),该Event就不会往下执行了,会调用自己的onTouchEvent(event),直接返回该消费的结果。如果没有拦截,那么则代表自己不会处理,需要交给自己的子View处理。子View再按照此结构分发。
1)、dispatchTouchEvent(MotionEvent event)方法
dispatchTouchEvent(MotionEvent event)是分发的开始。官方解释是,该方法是将MotionEvent事件向下传递到目标视图,或者如果它是目标,则将此视图传递给目标视图。
View中的该方法比较精简,直接贴代码。
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
- result 代表了消费情况的标志,最终会被当做结果返回回去。
- Event消费之前和消费之后,可能会调用stopNestedScroll();停止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;
}
ListenerInfo类,保存View的一些接口对象,如View中的OnClickListener、OnLongClickListener等等
如果 li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event),mOnTouchListener是View.setOnTouchListener()设置的触摸监听器,它的执行方法是onTouch,如果存在该接口对象以及该方法返回为true。还有个是ENABLED标志,可通过setEnable来设置标志位,但一般情况如果设置了点击和长点击的监听就会设置Enable为true,对于Button等View本身就是为true。
如果onTouch返回的为true,代表这个event提前消费了,就不会走onTouchEvent方法了,否则就会走到onTouchEvent方法,并onTouchEvent的结果进行返回。
也就是说提前设置mOnTouchListener监听,并在onTouch中返回true,就直接消费了。这样的话就不会走到onTouchEvent方法中。
2)、onTouchEvent(MotionEvent event)
如果dispatchTouchEvent方法中正常走到了onTouchEvent方法,代表该View可以在onTouchEvent中进行消费逻辑的判断了。
下面给出了删减版的源码:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
XXXXXXXX.........
break;
case MotionEvent.ACTION_CANCEL:
XXXXXXXX.........
break;
case MotionEvent.ACTION_MOVE:
XXXXXXXX.........
break;
}
return true;
}
return false;
}
- 第一步,如果该View是Disable的,即(viewFlags & ENABLED_MASK) == DISABLED,这样也会代表该View消费了该事件。就会直接返回结果,不会再走之后的流程了。
- 第二步,分别判断每个Event,进行不同的处理。Down、Move和 Cancel没有多少说的,重点看下Up事件。这里面最关键的几行就在:
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
// performClick方法
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;
}
// 点击处理类
private final class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}
通过post()向主线程插入一个Message。相当于切换到主线程后,再执行performClick()方法。这个方法的意思就是判断有没有设置点击事件的监听,如果有通过li.mOnClickListener.onClick(this);通知给使用者。即在Up的事件中,执行了点击事件。
四、ViewGroup中MotionEvent分发
1)、ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
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();
}
// 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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
接下来再分析下,ViewGroup中的事件分发。其中handled是代表当前View及其子View的事件消费情况。
第一步:清除标志位
如果Down事件,代表一系列的事件的开始,先重置标志位。其中cancelAndClearTouchTargets(ev);主要将mFirstTouchTarget这个单链表。mFirstTouchTarget代表具体处理Event的系列View。resetTouchState();是重置一些状态标示。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
第二步:检测拦截器
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;
}
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null),ACTION_DOWN事件或者mFirstTouchTarget 不为null会到第一个逻辑里面。否则intercepted = true;就拦截,让自己的
ACTION_DOWN事件好理解,那么mFirstTouchTarget什么时候不为null了?当ViewGroup没有拦截View,给子View消费,而且子View也消费了Down事件。反之mFirstTouchTarget == null的情况则是该ViewGroup拦截了事件或者子View并没有消费Down事件。
disallowIntercept是允许拦截的标志。View中调用getParent().requestDisallowInterceptTouchEvent(true),可以控制FLAG_DISALLOW_INTERCEPT标志位,从而达到让父View拦截的作用。通过调用requestDisallowInterceptTouchEvent方法,可以解决滑动冲突。
如果disallowIntercept == false,那么intercepted = onInterceptTouchEvent(ev);否则intercepted = false
3)、寻找可以处理的子View
if(!canceled && !intercepted),也就是该事件没有取消以及没有拦截的情况下。会去通过坐标寻找那些View能够处理该MotionEvent,并将该View放在mFirstTouchTarget链中。
for (int i = childrenCount - 1; i >= 0; i--)遍历的方式中,可以看到这些遍历是从最后添加的先遍历。因为后加的子View是显示在界面的最上面。
遍历的缩减缩减代码如下:
通过getTouchTarget判断该子View有没有添加到mFirstTouchTarget链中,如果添加了就break,这是避免多点出触碰的问题。接下来通过dispatchTransformedTouchEvent判断子View会不会消费掉Down事件,如果能消费就添加到mFirstTouchTarget链中,不能消费就跳过。
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
break;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
4)、Event事件消费
在遍历了子View之后,找到了可以消费的View,就让其做之后的消费处理。
下面为消费的精简逻辑:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
predecessor = target;
target = next;
}
}
首先分两种情况:mFirstTouchTarget == null 和 mFirstTouchTarget != null
先看第一种mFirstTouchTarget == null,这种比较简单,为null代表没有子View能消费的,直接调用给该View的父View处理dispatchTouchEvent方法即可。
第二种,针对Down事件:如果alreadyDispatchedToNewTouchTarget && target == newTouchTarget,则代表之前寻找的时候已经子View已经处理了该Down事件,就不需要重新处理了;第二种:Mowe和Up事件,通过调用dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)让其子View处理事件,并直接将结果返回。
最后整个系列的事件结束了,执行了Up事件。或者该事件被取消了,就会重新的重置状态resetTouchState();
ViewGroup#dispatchTransformedTouchEvent逻辑
会分别对Cancel事件、多点触摸事件、正常MotionEvent事件分发处理。
删减代码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 取消事件处理
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
// 手指多点触发处理
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// 正常的事件分发
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
无论是Cancel事件、多点触摸事件还是正常事件。他都是通过判断child此View是否为空判断的,如果child为空,则让父View去处理dispatchTouchEvent。反之则让该child处理dispatchTouchEvent。
这样就可以在子View没处理的情况下,让其父View处理,达成了一个闭环。
MotionEvent之cancel事件
- 手指移出该View的范围
前面分析了正常的事件分发,但是MotionEvent还有一个特殊的事件cancel。cancel代表者该一系列事件的取消。
当手指点击了View,该View处理了Down事件(返回为true),手指继续滑动,直到滑动脱离了该View的区域,这个时候该View就会收到cancel。
- 事件拦截
首先子View处理了Down事件(返回为true),Move时候父View开始没拦截让子View处理,但中途时候拦截了该Move事件,这时候就会让子View收到该Cancel取消事件。
五、滑动冲突解决方法
1)、外部拦截法
外部拦截法:即拦截逻辑放在父View,由父View决定要不要拦截,哪些事件会拦截。
外部拦截法需要通过重写父View的onInterceptTouchEvent实现,伪代码如下(Android开发艺术中):
private int mLastX = 0;
private int mLastY = 0;
@Override
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:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) < Math.abs(deltaY))
intercepted = true;
else
intercepted= false;
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
Log.e(TAG, "onInterceptTouchEvent: intercepted = "+intercepted );
mLastY = y;
mLastX = x;
return intercepted;
}
上文大致意思是:通过两个变量记录上一次的手指的位置(X,Y)坐标,Down的时候记录最开始的位置,对于Down事件不能够拦截,必须为false,否则接下来的事件都会被拦截了。手指Move的时候,通过本次滑动位置,x轴和y轴移动位置的偏移量大小决定,由业务决定拦截或者不拦截。Up事件,也比较特殊,返回的是否拦截,会导致最终会不会让子View执行onTouch或者onClick回调方法。如果父View之前没有拦截,Up时候也没有拦截就会处理该回调方法,否则一概不会触发。
2)、内部拦截法
内部拦截法:内部拦截法,父View不需要处理是不是要拦截,一概由子View进行判断。当时分析的时候, ViewGroup#dispatchTouchEvent()#中final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;会判断会不会走ViewGroup中的onInterceptTouchEvent()方法。该标志位可以通过view.getParent().requestDisallowInterceptTouchEvent(boolean)来赋值。注意此方法不能只Down事件的时候调用,因为父View会重新的重置标志,导致标志为无效。
伪代码:
private int mLastX = 0;
private int mLastY = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)){
Log.e(TAG, "dispatchTouchEvent: " );
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastY = y;
mLastX = x;
return super.dispatchTouchEvent(event);
}
此代码同外部拦截法原理一致。