admin 管理员组

文章数量: 887007

Android高级UI之京东淘宝首页二级联动怎么实现

本文的主题是了解View的事件分发整体流程,然后重点实现京东淘宝首页二级联动,解决滑动冲突。

1、事件的种类和手势

1.1 单点触摸

根据面向对象思想,事件被封装成 MotionEvent 对象

1.2 多点触摸

多点触控 ( Multitouch,也称 Multi-touch ),即同时接受屏幕上多个点的人机交互操作,多点触 控是从 Android 2.0 开始引入的功能

1.3 手势

1.4 多点手势手指操作流程

2、View的体系结构和事件分发的框架

2.1 View和ViewGroup的关系

2.2 Android页面View的体系结构

2.3 事件的处理函数

2.4 事件的处理函数的关系

2.5.1 事件分发大流程


上图的注意点:
1.事件返回时 dispatchTouchEvent 直接指向了父View的 onTouchEvent 这一部分是不合理的,实际上它仅仅是给了父View的 dispatchTouchEvent 一个 false 返回值, 父View根据返回值来调用自身的 onTouchEvent。
2.ViewGroup的dispatchTouchEvent是根据 onInterceptTouchEvent 的返回值来确定是调用子View的 dispatchTouchEvent 还是调用自身的 onTouchEvent, 并没有将调用交给onInterceptTouchEvent

ViewGroup的dispatchTouchEvent事件分发简化流程:

public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = false; // 默认状态为没有消费过 if (!onInterceptTouchEvent(ev)) { // 如果没有拦截,则交给子View 		result = child.dispatchTouchEvent(ev); }if (!result) { // 如果事件没有被消费,则询问自身onTouchEvent 		result = onTouchEvent(ev); }return result; 
}

2.5.2 事件分发大流程(View消费了事件)

2.5.3 事件分发大流程(ViewGroup消费了事件)

3、View和ViewGroup的分发流程

3.1 事件分发相关概念

3.2.1 事件分发极简流程

public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = false; // 默认状态为没有消费过 if (!onInterceptTouchEvent(ev)) { // 如果没有拦截,则交给子View 		result = child.dispatchTouchEvent(ev); }if (!result) { // 如果事件没有被消费,则询问自身onTouchEvent 		result = onTouchEvent(ev); }return result; 
}

3.2.2 事件分发进阶流程

public boolean dispatchTouchEvent(MotionEvent ev) { // 默认状态为没有消费过 boolean result = false; //决定是否拦截 final boolean intercepted = false; if (!requestDisallowInterceptTouchEvent()) { intercepted = onInterceptTouchEvent(ev); }//找出最适合接收的孩子 if (!intercepted && (DOWN || POINTER_DOWN || HOVER_MOVE)) { // 如果没有拦截交给子View for (int i = childrenCount - 1; i >= 0; i--) { mFirstTouchTarget = child.dispatchTouchEvent(ev); } }//分发事件 if (mFirstTouchTarget == null) { // 如果事件没有被消费,询问自身onTouchEvent result = onTouchEvent(ev); } else { for(TouchTarget touchTarget : mFirstTouchTarget) { result = touchTarget.child.dispatchTouchEvent(ev); }}return result; 
}

3.2.3 事件分发简单源码分析

	/**整体流程就是分3步:步骤1:判断事件是否拦截步骤2:遍历所有的子View,寻找targets步骤3: 将事件分发给targets*/@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {...boolean handled = false;if (onFilterTouchEventForSecurity(ev)) {final int action = ev.getAction();final int actionMasked = action & MotionEvent.ACTION_MASK;// ACTION_DOWN事件是一个手势的开始,所以这里会清空之前的手势的所有状态// 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();}//步骤1:判断事件是否拦截// 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;}...//步骤2:遍历所有的子View,寻找targetsif (!canceled && !intercepted) {...//如果是一个手势的开始事件,MotionEvent.ACTION_DOWN、 MotionEvent.ACTION_POINTER_DOWN、MotionEvent.ACTION_HOVER_MOVE都是一个手势的开始事件if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {...for (int i = childrenCount - 1; i >= 0; i--) {//遍历所有的子View,找到TouchTargetif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...//找到TouchTarget后,加入mFirstTouchTarget链表newTouchTarget = addTouchTarget(child, idBitsToAssign);...}...}...}// 步骤3: 将事件分发给targets// 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构成一个链表,mFirstTouchTarget指向的是链表的头节点:

    // First touch target in the linked list of touch targets.@UnsupportedAppUsageprivate TouchTarget mFirstTouchTarget;
/* Describes a touched view and the ids of the pointers that it has captured.** This code assumes that pointer ids are always in the range 0..31 such that* it can use a bitfield to track which pointer ids are present.* As it happens, the lower layers of the input dispatch pipeline also use the* same trick so the assumption should be safe here...*/private static final class TouchTarget {private static final int MAX_RECYCLED = 32;private static final Object sRecycleLock = new Object[0];private static TouchTarget sRecycleBin;private static int sRecycledCount;public static final int ALL_POINTER_IDS = -1; // all ones// The touched child view.@UnsupportedAppUsagepublic View child;// The combined bit mask of pointer ids for all pointers captured by the target.public int pointerIdBits;// The next target in the target list.public TouchTarget next;@UnsupportedAppUsageprivate TouchTarget() {}public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {if (child == null) {throw new IllegalArgumentException("child must be non-null");}final TouchTarget target;synchronized (sRecycleLock) {if (sRecycleBin == null) {target = new TouchTarget();} else {target = sRecycleBin;sRecycleBin = target.next;sRecycledCount--;target.next = null;}}target.child = child;target.pointerIdBits = pointerIdBits;return target;}public void recycle() {if (child == null) {throw new IllegalStateException("already recycled once");}synchronized (sRecycleLock) {if (sRecycledCount < MAX_RECYCLED) {next = sRecycleBin;sRecycleBin = this;sRecycledCount += 1;} else {next = null;}child = null;}}}

记录着每个手指触摸屏幕时的接收down事件的那些View,所以用链表。
如果多个手指跨越了多个View,则mFirstTouchTarget指向的链表就有多个节点;如果多个手指都在一个View上,则mFirstTouchTarget指向的链表只有一个结点。

3.3.1 View dispatchTouchEvent

1: 为什么 View 会有 dispatchTouchEvent ?

我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent() 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent(),所以 View 也会有事件分发。

2: 与 View 事件相关的各个方法的调用顺序是怎样的?

•单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)

•长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)

•触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)

•View自身处理(onTouchEvent()方法)
View提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent())

所以调用顺序是:onTouchListener > onTouchEvent() > onLongClickListener > onClickListener

3.3.2 View和ViewGroup的onTouchEvent

3.4.1 ViewGroup的onInterceptTouchEvent

	/* *	....*	* @param ev The motion event being dispatched down the hierarchy.* @return Return true to steal motion events from the children and have* them dispatched to this ViewGroup through onTouchEvent().* The current target will receive an ACTION_CANCEL event, and no further* messages will be delivered here.*/public boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.isFromSource(InputDevice.SOURCE_MOUSE)&& ev.getAction() == MotionEvent.ACTION_DOWN&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)&& isOnScrollbarThumb(ev.getX(), ev.getY())) {return true;}return false;}

3.5.1 ViewGroup的dispatchTouchEvent的成员变量

TouchTarget和mFirstTouchTarget, 这个变量是给手势设置的,跨越事件保留的。

 	/* Describes a touched view and the ids of the pointers that it has captured.** This code assumes that pointer ids are always in the range 0..31 such that* it can use a bitfield to track which pointer ids are present.* As it happens, the lower layers of the input dispatch pipeline also use the* same trick so the assumption should be safe here...*/private static final class TouchTarget {private static final int MAX_RECYCLED = 32;private static final Object sRecycleLock = new Object[0];private static TouchTarget sRecycleBin;private static int sRecycledCount;public static final int ALL_POINTER_IDS = -1; // all ones// The touched child view.@UnsupportedAppUsagepublic View child;// The combined bit mask of pointer ids for all pointers captured by the target.public int pointerIdBits;// The next target in the target list.public TouchTarget next;@UnsupportedAppUsageprivate TouchTarget() {}public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {if (child == null) {throw new IllegalArgumentException("child must be non-null");}final TouchTarget target;synchronized (sRecycleLock) {if (sRecycleBin == null) {target = new TouchTarget();} else {target = sRecycleBin;sRecycleBin = target.next;sRecycledCount--;target.next = null;}}target.child = child;target.pointerIdBits = pointerIdBits;return target;}public void recycle() {if (child == null) {throw new IllegalStateException("already recycled once");}synchronized (sRecycleLock) {if (sRecycledCount < MAX_RECYCLED) {next = sRecycleBin;sRecycleBin = this;sRecycledCount += 1;} else {next = null;}child = null;}}}

3.5.2局部变量: newTouchTarget & alreadyDispatchedToNewTouchTarget

3.6.1 辅助功能view的分发逻辑

1、如果现在分发的这个事件就是分发给我这个辅助功能获焦的view的,那么我们立即进行正常的分发,同时清除这个MotionEvent事件FLAG_TARGET_ACCESSIBILITY_FOCUS标志, 请注意这个标志是事件的,而不是view的

2、如果这个事件被该ViewGroup拦截,或者已经有子view正在处理这个手势,我们清除这个事件的FLAG_TARGET_ACCESSIBILITY_FOCUS标志,进行正常的分发

4、滑动冲突解决方案

4.1 滑动冲突解决方案

4.2 外部拦截


•当ViewPager接收到DOWN事件,ViewPager默认不拦截DOWN事件,DOWN事件交由ListView处理,由于ListView可以滚动,即可以消费事件,则ViewPager的 mFirstTouchTarget会被赋值,即找到处理事件的子View。然后ViewPager接收到MOVE事件,
•若此事件是ViewPager不需要,则同样会将事件交由ListView去处理,然后ListView处理事件; •若此事件ViewGroup需要,因为DOWN事件被ListView处理,mFirstTouchEventTarget会被赋值,也就会调用onInterceptedTouchEvent,此时由于ViewPager对此事件感兴趣 ,则onInterceptedTouchEvent方法会返回true,表示ViewPager会拦截事件,此时当前的MOVE事件会消失,变为CANCEL事件,往下传递或者自己处理,同时 mFirstTouchTarget被重置为null。
•当MOVE事件再次来到时,由于mFristTouchTarget为null,所以接下来的事件都交给了ViewPager。

4.3 内部拦截

4.4 嵌套滑动

页面布局:


嵌套滑动有两个角色,一个是父亲,一个是孩子,这里的父亲是ScrollView,孩子是RecyclerView。

在滑动ViewPager里面的RecyclerView时,需要先判断ScrollView需不需要滑动。

Android事件分发之ACTION_CANCEL机制及作用
深入理解事件分发 ViewGroup.mFirstTouchTarget的设计
Android事件分发mFirstTouchTarget的思考

本文标签: Android高级UI之京东淘宝首页二级联动怎么实现