Android中级——事件拦截机制和滑动冲突

事件拦截机制

定义MyViewGroupA,打印Log

public class MyViewGroupA extends RelativeLayout {

    private static final String TAG = MyViewGroupA.class.getSimpleName();

    public MyViewGroupA(Context context) {
        this(context, null);
    }

    public MyViewGroupA(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent: ");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }
}

定义MyViewGroupB同上

public class MyViewGroupB extends RelativeLayout {

    private static final String TAG = MyViewGroupB.class.getSimpleName();

    public MyViewGroupB(Context context) {
        this(context, null);
    }

    public MyViewGroupB(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyViewGroupB(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyViewGroupB(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent: ");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }
}

定义MyView,打印Log

public class MyView extends View {

    private static final String TAG = MyView.class.getSimpleName();

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(event);
    }
}

布局如下,MyViewGroupA包裹MyViewGroupB再包裹MyView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <com.demo.demo0.MyViewGroupA
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#FF0000">

        <com.demo.demo0.MyViewGroupB
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="#00FF00">

            <com.demo.demo0.MyView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:background="#000FFF" />
        </com.demo.demo0.MyViewGroupB>
    </com.demo.demo0.MyViewGroupA>
</LinearLayout>

点击最内层的MyView,打印如下

MyViewGroupA: dispatchTouchEvent: 
MyViewGroupA: onInterceptTouchEvent: 
MyViewGroupB: dispatchTouchEvent: 
MyViewGroupB: onInterceptTouchEvent: 
MyView: dispatchTouchEvent: 
MyView: onTouchEvent: 
MyViewGroupB: onTouchEvent: 
MyViewGroupA: onTouchEvent: 

可知

  • 事件传递顺序是由外向内,先dispatchTouchEvent再onInterceptTouchEvent
  • 事件处理顺序由内向外,即onTouchEvent
  • 要拦截事件,只需要让返回值为True

可用如下伪代码表示三个方法的关系

在这里插入图片描述

若MyViewGroupA的onInterceptTouchEvent()方法返回True,打印如下

MyViewGroupA: dispatchTouchEvent: 
MyViewGroupA: onInterceptTouchEvent: 
MyViewGroupA: onTouchEvent: 

若MyViewGroupB的onInterceptTouchEvent()方法返回True,打印如下

MyViewGroupA: dispatchTouchEvent: 
MyViewGroupA: onInterceptTouchEvent: 
MyViewGroupB: dispatchTouchEvent: 
MyViewGroupB: onInterceptTouchEvent: 
MyViewGroupB: onTouchEvent: 
MyViewGroupA: onTouchEvent: 

若MyView的onTouchEvent()方法返回True,打印如下

MyViewGroupA: dispatchTouchEvent: 
MyViewGroupA: onInterceptTouchEvent: 
MyViewGroupB: dispatchTouchEvent: 
MyViewGroupB: onInterceptTouchEvent: 
MyView: dispatchTouchEvent: 
MyView: onTouchEvent: 

若MyViewGroupB的onTouchEvent()方法返回True,打印如下

MyViewGroupA: dispatchTouchEvent: 
MyViewGroupA: onInterceptTouchEvent: 
MyViewGroupB: dispatchTouchEvent: 
MyViewGroupB: onInterceptTouchEvent: 
MyView: dispatchTouchEvent: 
MyView: onTouchEvent: 
MyViewGroupB: onTouchEvent: 

事件拦截机制源码分析

上层事件按照如下顺序进行传递:

  • Activity
  • Phone Window
  • DecorView
  • ViewGroup
  • View

Acitivity

onUserInteraction()是一个空方法,事件会传递给Window

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

Phone Window

Window的唯一实现类为Phone Window,其将事件传递给DecorView

public boolean superDispatchTouchEvent(MotionEvent event) {
     return mDecor.superDispatchTouchEvent(event);
}

DecorView

调用父类的dispatchTouchEvent,其继承FrameLayout(但未重写方法),故事件将会被传递给ViewGroup

public boolean superDispatchTouchEvent(MotionEvent event) {
   return super.dispatchTouchEvent(event);
}

ViewGroup

一个按键事件序列以ACTION_DOWN开始,以ACTION_UP结束,接下来逐一介绍各个内容

TouchTarget

按键传递用链表存储,即TouchTarget

  • mFirstTouchTarget是头节点
  • 相比于普通的链表,其采用了内部复用sRecycleBin

下面代码将TouchTarget相关方法复制出来,并重写了toString()

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private TouchTarget mFirstTouchTarget;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        View view1 = new View(this);
        View view2 = new View(this);
        View view3 = new View(this);
        TouchTarget touchTarget1 = addTouchTarget(view1, 1);
        TouchTarget touchTarget2 = addTouchTarget(view2, 2);
        TouchTarget touchTarget3 = addTouchTarget(view3, 3);
        Log.d(TAG, "print touchTarget ------------------------------------------------");
        Log.d(TAG, "touchTarget1 = " + touchTarget1);
        Log.d(TAG, "touchTarget2 = " + touchTarget2);
        Log.d(TAG, "touchTarget3 = " + touchTarget3);


        clearTouchTargets();
        Log.d(TAG, "clearTouchTargets ------------------------------------------------");
        Log.d(TAG, "touchTarget1 = " + touchTarget1);
        Log.d(TAG, "touchTarget2 = " + touchTarget2);
        Log.d(TAG, "touchTarget3 = " + touchTarget3);

        Log.d(TAG, "print touchTarget------------------------------------------------");
        TouchTarget touchTarget4 = addTouchTarget(view1, 1);
        TouchTarget touchTarget5 = addTouchTarget(view2, 2);
        TouchTarget touchTarget6 = addTouchTarget(view3, 3);
        Log.d(TAG, "touchTarget4 = " + touchTarget4);
        Log.d(TAG, "touchTarget5 = " + touchTarget5);
        Log.d(TAG, "touchTarget6 = " + touchTarget6);
    }

    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.
        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;

        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;
            }
        }

        @Override
        public String toString() {
            return "TouchTarget+" + Integer.toHexString(hashCode()) + "{" +
                    "child=" + child +
                    ", pointerIdBits=" + pointerIdBits +
                    ", next=" + next +
                    '}';
        }
    }

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
}

当第一次调用addTouchTarget(),通过移动mFirstTouchTarget链接链表,因为链表是反向链接的,所以遍历子View的时候要从后往前
在这里插入图片描述

touchTarget1 = TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}
touchTarget2 = TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}
touchTarget3 = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}
mFirstTouchTarget = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}

当调用clearTouchTargets()时,通过移动sRecycleBin反向链表

  • 数量小于32时,只清空child
  • 大于32时,将会断开节点,交由gc回收

在这里插入图片描述

touchTarget1 = TouchTarget+5787a73{child=null, pointerIdBits=1, next=TouchTarget+eb57da9{child=null, pointerIdBits=2, next=TouchTarget+d64acf{child=null, pointerIdBits=3, next=null}}}
touchTarget2 = TouchTarget+eb57da9{child=null, pointerIdBits=2, next=TouchTarget+d64acf{child=null, pointerIdBits=3, next=null}}
touchTarget3 = TouchTarget+d64acf{child=null, pointerIdBits=3, next=null}
mFirstTouchTarget = null

当再次调用addTouchTarget()时,将会复用sRecycleBin,如下,打印的TouchTarget456和之前创建的TouchTarget123地址一样

在这里插入图片描述

touchTarget4 = TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}
touchTarget5 = TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}
touchTarget6 = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}
mFirstTouchTarget = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}

dispatchTouchEvent

如下是基于API 28的源码,省略部分无关代码

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    .....
    
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        
        //每个down到来时都会初始化为最开始的状态,因为按键可能会因为某些原因未通过up或cancel终止
        
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);		//对所有child发送cancel事件
            resetTouchState();		//清除TouchTargets、FLAG_DISALLOW_INTERCEPT(故其不能在down事件中设置)
        }
        
       	//1.当ViewGroup接收到dwon时一定会调用onInterceptTouchEvent(默认返回false),若不拦截,则down会传给child
       	//1.1 若child处理down,则mFirstTouchTarget != null
       	//1.1.1 接下来的事件序列到来时,若child设置了FLAG_DISALLOW_INTERCEPT,则不再询问ViewGroup的onInterceptTouchEvent,即全部交给child处理
       	//1.1.2 若child未设置了FLAG_DISALLOW_INTERCEPT,则每次都询问ViewGroup的onInterceptTouchEvent
       	//1.2 若child未处理down,mFirstTouchTarget==null,则ViewGroup自身处理接下来的事件序列,且不用再询问onInterceptTouchEvent()
       	
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {	
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {			//对应情况1.1.2
                intercepted = onInterceptTouchEvent(ev);	
                ev.setAction(action); 
            } else {		//对应情况1.1.1
                intercepted = false;
            }
        } else {			//对应情况1.2
            intercepted = true;
        }
      	
      	......
      	
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;
        ......
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
        	
        	......
        	
        	//当ViewGroup的onInterceptTouchEvent()不拦截down,则传递给child,询问child是否拦截
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || ......) {
                ......
                final int actionIndex = ev.getActionIndex();
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    
                    //下面是构建ChildList和获取Child
                    
                    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);
                        
                        //下面跳过 无法接收事件 与 不在触摸位置 的子view
                        
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        
                        //下面判断是否已经在TouchTarget链表中
                        
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        resetCancelNextUpFlag(child);
						
						//dispatchTransformedTouchEvent将坐标转换成child中的坐标
						//调用child的dispatchTouchEvent()判断是否处理down
						
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
                            ......
                            //若child处理,将其添加到TouchTarget链表中,设置标志位
                            
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                        ......
                    }
                   ......
                }
               ......
            }
        }
        
        if (mFirstTouchTarget == null) {
        
        	//走到这里说明
        	//1.当前ViewGroup的onInterceptTouchEvent()拦截所有或接下来的事件序列
        	//2.ViewGoup没有child
        	//3.child的onTouchEvent()处理down时返回false,接下来的事件也不会传递给child
        	//此时,ViewGroup将作为View调用onTouchEvent(),若仍不处理,则往上调用父容器的onTouchEvent()
        	
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
           
            //下面分发接下来的事件序列
            //对应情况1.1.1,down被child处理,且接下来的事件都由child处理
            //对应情况1.1.2,down(或+move)被child处理,但接下来的事件序列被ViewGroup的onInterceptTouchEvent()拦截
            
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                
               		//走到这,说明child处理了down,不再重复处理
               		
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                            
                    //若intercepted=true,cancelChild=true,对应情况1.1.2,遍历child发送cancel,接下来的事件序由ViewGroup处理
                	//若intercepted=false,cancelChild=false,对应上面情况1.1.1,接下来的事件都由child处理,若child成功处理,则当前dispatchTouchEvent()返回true,若返回false,往上调用父容器的onTouchEvent()
                	
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {	//1.1.2,遍历清空TouchTarget和mFirstTouchTarget,接下来的事件将会走mFirstTouchTarget == null的判断
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        
        //下面收尾,处理up/cancel,恢复初始状态
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || ......) {
            resetTouchState();
        } ......
    }
 	......
    return handled;
}

View

dispatchTouchEvent

如下是基于API 28的源码,省略部分无关代码

public boolean dispatchTouchEvent(MotionEvent event) {
   
    ......
    
    boolean result = false;
    
    ......
    
    if (onFilterTouchEventForSecurity(event)) {
       	
       	......
       	//若有设置OnTouchListener,则优先调用onTouch()
       	
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        
        //否则调用onTouchEvent()
        
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    
    ......
    
    return result;
}

onTouchEvent

如下是基于API 28的源码,省略部分无关代码

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
    
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
            
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        ......
        
        //当View disable但clickable时,也会消耗事件
        
        return clickable;
    }
   
    //下面处理事件
    
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
               	
               	......
               	
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    boolean focusTaken = false;
                    
                    ......
                    
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();

						//下面处理点击事件
						
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                    ......
                break;
            case MotionEvent.ACTION_DOWN:
              	......
                break;
            case MotionEvent.ACTION_CANCEL:
                .....
                break;
        }
        return true;
    }
    return false;
}

PerformClick会调用performClick(),在这里判断和调用onClick()方法

public boolean performClick() {
    ......
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        ......
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
   	......
    return result;
}

滑动冲突

主要分为

  • 外部滑动和内部滑动方向不一致,根据滑动方向判断谁来拦截事件
  • 外部滑动和内部滑动方向一致,根据业务逻辑判断谁来拦截事件
  • 上面两种情况的嵌套

外部拦截法

事件都先判断父容器是否拦截,然后再判断child,具体做法为

  • down返回false,因为返回true会导致接下来的事件都由父容器处理
  • move根据情况判断是否拦截
  • up返回false,因为返回true,child接收不到up,会导致child的onClick()失效

如下重写父容器的onInterceptTouchEvent()

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (isParentIntercept()) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    return intercepted;
}

private boolean isParentIntercept() {
    return false;
}

内部拦截法

父容器不拦截任何事件,如果child处理就消耗事件,否则交给父容器处理,具体做法为

  • 父容器拦截除down以外的所有事件,因为拦截down会导致接下来的事件都由父容器处理
  • child在down中设置FLAG_DISALLOW_INTERCEPT,在接下来的事件中不再询问父容器的onInterceptTouchEvent()
  • 当需要父容器处理时,child在move中取消FLAG_DISALLOW_INTERCEPT,此时事件会被父容器拦截处理

如下重写父容器的onInterceptTouchEvent()

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            return false;
    }
    return true;
}

如下重写child的dispatchTouchEvent()

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    ViewParent parent = getParent();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if (isParentIntercept()) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.dispatchTouchEvent(event);
}
private boolean isParentIntercept() {
    return false;
}

实例

外部拦截法

自定义横向滑动组件HorizontalScrollViewEx,往里面放两个ListView,滑动方向不一致导致冲突

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.demo.demo0.HorizontalScrollViewEx
        android:id="@+id/listContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/list1"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <ListView
            android:id="@+id/list2"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </com.demo.demo0.HorizontalScrollViewEx>

</LinearLayout>

MainActivity 代码如下

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private String[] data1 = {"A", "B", "C", "D", "E", "F", "G", "H",
            "A", "B", "C", "D", "E", "F", "G", "H"};
    private String[] data2 = {"1", "2", "3", "4", "5", "6", "7", "8",
            "1", "2", "3", "4", "5", "6", "7", "8"};


    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        ArrayAdapter<String> adapter1 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data1);
        ArrayAdapter<String> adapter2 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data2);
        ((ListView) findViewById(R.id.list1)).setAdapter(adapter1);
        ((ListView) findViewById(R.id.list2)).setAdapter(adapter2);
    }
}

具体实现如下,判断当横向移动距离大于纵向时,父容器才拦截处理

public class HorizontalScrollViewEx extends ViewGroup {

    private static final String TAG = "HorizontalScrollViewEx";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;
    private int mLastX;
    private int mLastY;
    private int mLastXIntercept;
    private int mLastYIntercept;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;


    public HorizontalScrollViewEx(Context context) {
        this(context, null);
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @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;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = x - mLastYIntercept;
                intercepted = Math.abs(deltaX) > Math.abs(deltaY);
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        Log.d(TAG, "onInterceptTouchEvent: intercepted = " + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = x - mLastY;
                scrollBy(-deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            View child = getChildAt(0);
            measureWidth = child.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureWidth, child.getMeasuredHeight());
        } else if (heightMode == MeasureSpec.AT_MOST) {
            View child = getChildAt(0);
            setMeasuredDimension(widthMeasureSpec, child.getMeasuredHeight());
        } else if (widthMode == MeasureSpec.AT_MOST) {
            View child = getChildAt(0);
            measureWidth = child.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureWidth, heightSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        int childCount = getChildCount();
        mChildrenSize = childCount;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                int childMeasuredWidth = child.getMeasuredWidth();
                mChildWidth = childMeasuredWidth;
                child.layout(childLeft, 0, childLeft + childMeasuredWidth, child.getMeasuredHeight());
                childLeft += childMeasuredWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mVelocityTracker.recycle();
    }
}

内部拦截法

修改HorizontalScrollViewEx,拦截除down外的事件

public class HorizontalScrollViewEx extends ViewGroup {

    private static final String TAG = "HorizontalScrollViewEx";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;
    private int mLastX;
    private int mLastY;
    private int mLastXIntercept;
    private int mLastYIntercept;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;


    public HorizontalScrollViewEx(Context context) {
        this(context, null);
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    return true;
                }
                return false;
        }
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = x - mLastY;
                scrollBy(-deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            View child = getChildAt(0);
            measureWidth = child.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureWidth, child.getMeasuredHeight());
        } else if (heightMode == MeasureSpec.AT_MOST) {
            View child = getChildAt(0);
            setMeasuredDimension(widthMeasureSpec, child.getMeasuredHeight());
        } else if (widthMode == MeasureSpec.AT_MOST) {
            View child = getChildAt(0);
            measureWidth = child.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureWidth, heightSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        int childCount = getChildCount();
        mChildrenSize = childCount;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                int childMeasuredWidth = child.getMeasuredWidth();
                mChildWidth = childMeasuredWidth;
                child.layout(childLeft, 0, childLeft + childMeasuredWidth, child.getMeasuredHeight());
                childLeft += childMeasuredWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mVelocityTracker.recycle();
    }
}

创建ListViewEx,重写dispatchTouchEvent()

public class ListViewEx extends ListView {

    private int mLastX = 0;
    private int mLastY = 0;

    public ListViewEx(Context context) {
        this(context, null);
    }

    public ListViewEx(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        ViewParent parent = getParent();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }
}

布局如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.demo.demo0.HorizontalScrollViewEx
        android:id="@+id/listContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.demo.demo0.ListViewEx
            android:id="@+id/list1"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <com.demo.demo0.ListViewEx
            android:id="@+id/list2"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </com.demo.demo0.HorizontalScrollViewEx>

</LinearLayout>

MainActivity如下

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private String[] data1 = {"A", "B", "C", "D", "E", "F", "G", "H",
            "A", "B", "C", "D", "E", "F", "G", "H"};
    private String[] data2 = {"1", "2", "3", "4", "5", "6", "7", "8",
            "1", "2", "3", "4", "5", "6", "7", "8"};


    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        ArrayAdapter<String> adapter1 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data1);
        ArrayAdapter<String> adapter2 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data2);
        ((ListView) findViewById(R.id.list1)).setAdapter(adapter1);
        ((ListView) findViewById(R.id.list2)).setAdapter(adapter2);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值