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

193 篇文章 9 订阅
107 篇文章 0 订阅

本文的主题是了解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
	*/
    @Override
    public 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,寻找targets
			 if (!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,找到TouchTarget
                            if (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.
    @UnsupportedAppUsage
    private 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.
        @UnsupportedAppUsage
        public 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;

        @UnsupportedAppUsage
        private 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.
        @UnsupportedAppUsage
        public 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;

        @UnsupportedAppUsage
        private 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的思考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值