上
事件分发的流程:
被分发的对象是哪些?被分发的对象是用户触摸屏幕而产生的点击事件,事件主要包括:按下、滑动、抬起和取消。这些事件被封装成MotionEvent对象。该对象中的主要事件如下:
事件传递的顺序为:Activity -> Window ->DecorView(当前界面的底层容器)。一个点击操作要是没有被Activity下的任何View处理,即顶层DecorView的dispatchTouchEvent()方法返回false的话,则Activity的onTouchEvent()方法会被调用。
我们下面在源码中追踪下。
当我们点击手机屏幕的时候,硬件会通知软件,软件底层程序(C/C++)会调用java层Activity的dispatchTouchEvent(MotionEvent ev)方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果是down,说明是一个新的事件
3398 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
3399 onUserInteraction();
3400 }
//调用了PhoneWindow的superDispatchTouchEvent()方法,
//把事件从Activity分发到DecorView
//如果找不到消费当前事件的View,getWindow().superDispatchTouchEvent(ev)会返回false
3401 if (getWindow().superDispatchTouchEvent(ev)) {
3402 return true;
3403 }
//返回Activity的onTouchEvent
3404 return onTouchEvent(ev);
3405 }
3406
我们看下 onUserInteraction();
public void onContentChanged() {
}
这是一个空房法,开发者可以实现这个方法,屏幕点击按下的时候,可以再这里增加逻辑
getWindow()这里返回的是一个Window对象。在Window对象中superDispatchTouchEvent(ev)方法是一个抽象方法,在其子类PhoneWindow中实现。我们看下PhoneWindow中superDispatchTouchEvent(ev)方法。
1828 @Override
1829 public boolean superDispatchTouchEvent(MotionEvent event) {
1830 return mDecor.superDispatchTouchEvent(event);
1831 }
mDecor是DecorView对象
我们进入DecorView对象中
439 public boolean superDispatchTouchEvent(MotionEvent event) {
440 return super.dispatchTouchEvent(event);
441 }
DecorView的父类是ViewGroup,所以 super.dispatchTouchEvent(event)调用的是ViewGroup里的方法。
我们暂且不去管ViewGroup,回过头来看看Activity的dispatchTouchEvent的返回值onTouchEvent(ev);
3401 if (getWindow().superDispatchTouchEvent(ev)) {
3402 return true;
3403 }
假设这段代码不执行,即getWindow().superDispatchTouchEvent(ev)返回为false;
就会执行 onTouchEvent(ev)
public boolean onTouchEvent(MotionEvent event) {
//点击范围是否超过了window边界,比如Dialog类型的Activity,点击外面dialog消失
3143 if (mWindow.shouldCloseOnTouch(this, event)) {
3144 finish();
//表示事件被当前Activity消耗掉了
3145 return true;
3146 }
3147
3148 return false;
3149 }
3150
我们现在看下ViewGroup的dispatchTouchEvent(MotionEvent ev);
2541 @Override
2542 public boolean dispatchTouchEvent(MotionEvent ev) {
//辅助功能,跨进程调用,与我们的事件分发作用不大
2543 if (mInputEventConsistencyVerifier != null) {
2544 mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
2545 }
2546
2547 // If the event targets the accessibility focused view and this is it, start
2548 // normal event dispatch. Maybe a descendant is what will handle the click.
2549 if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
2550 ev.setTargetAccessibilityFocus(false);
2551 }
2552 //本方法的返回值,表示ViewGroup是否消耗了此事件
2553 boolean handled = false;
//检测该事件是否在安全范围内
2554 if (onFilterTouchEventForSecurity(ev)) {
2555 final int action = ev.getAction();
2556 final int actionMasked = action & MotionEvent.ACTION_MASK;
2557
2558 // Handle an initial down.处理最初的按下事件
2559 if (actionMasked == MotionEvent.ACTION_DOWN) {
/**
当开始一个新的触摸手势时,扔掉所有以前的状态,框架可能由于应用程序的切换,ANR,
或其它一些状态更改而放弃了上一个手势的UP或cancel事件
*/
2560 // Throw away all previous state when starting a new touch gesture.
2561 // The framework may have dropped the up or cancel event for the previous gesture
2562 // due to an app switch, ANR, or some other state change.
//取消并清除所有触摸目标
2563 cancelAndClearTouchTargets(ev);
2564 resetTouchState();//重置所有触摸状态,为新循环做准备
2565 }
2566
2567 // Check for interception.事件拦截的检查
2568 final boolean intercepted;
2569 if (actionMasked == MotionEvent.ACTION_DOWN
2570 || mFirstTouchTarget != null) {
2571 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
2572 if (!disallowIntercept) {//允许拦截,ViewGroup要消耗这个事件
2573 intercepted = onInterceptTouchEvent(ev);
2574 ev.setAction(action); // restore action in case it was changed
2575 } else {
2576 intercepted = false;
2577 }
2578 } else {
2579 // There are no touch targets and this action is not an initial down
2580 // so this view group continues to intercept touches.
2581 intercepted = true;
2582 }
2583
2584 // If intercepted, start normal event dispatch. Also if there is already
2585 // a view that is handling the gesture, do normal event dispatch.
2586 if (intercepted || mFirstTouchTarget != null) {
2587 ev.setTargetAccessibilityFocus(false);
2588 }
2589
2590 // Check for cancelation.
2591 final boolean canceled = resetCancelNextUpFlag(this)
2592 || actionMasked == MotionEvent.ACTION_CANCEL;
2593
2594 // Update list of touch targets for pointer down, if needed.
2595 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
2596 TouchTarget newTouchTarget = null;
2597 boolean alreadyDispatchedToNewTouchTarget = false;
2598 if (!canceled && !intercepted) {
2599
2600 // If the event is targeting accessibility focus we give it to the
2601 // view that has accessibility focus and if it does not handle it
2602 // we clear the flag and dispatch the event to all children as usual.
2603 // We are looking up the accessibility focused host to avoid keeping
2604 // state since these events are very rare.
2605 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
2606 ? findChildWithAccessibilityFocus() : null;
2607
2608 if (actionMasked == MotionEvent.ACTION_DOWN
2609 || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
2610 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
2611 final int actionIndex = ev.getActionIndex(); // always 0 for down
2612 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
2613 : TouchTarget.ALL_POINTER_IDS;
2614
2615 // Clean up earlier touch targets for this pointer id in case they
2616 // have become out of sync.
2617 removePointersFromTouchTargets(idBitsToAssign);
2618
2619 final int childrenCount = mChildrenCount;
2620 if (newTouchTarget == null && childrenCount != 0) {
2621 final float x = ev.getX(actionIndex);
2622 final float y = ev.getY(actionIndex);
2623 // Find a child that can receive the event.
2624 // Scan children from front to back.
2625 final ArrayList<View> preorderedList = buildTouchDispatchChildList();
2626 final boolean customOrder = preorderedList == null
2627 && isChildrenDrawingOrderEnabled();
2628 final View[] children = mChildren;
2629 for (int i = childrenCount - 1; i >= 0; i--) {
2630 final int childIndex = getAndVerifyPreorderedIndex(
2631 childrenCount, i, customOrder);
2632 final View child = getAndVerifyPreorderedView(
2633 preorderedList, children, childIndex);
2634
2635 // If there is a view that has accessibility focus we want it
2636 // to get the event first and if not handled we will perform a
2637 // normal dispatch. We may do a double iteration but this is
2638 // safer given the timeframe.
2639 if (childWithAccessibilityFocus != null) {
2640 if (childWithAccessibilityFocus != child) {
2641 continue;
2642 }
2643 childWithAccessibilityFocus = null;
2644 i = childrenCount - 1;
2645 }
2646
2647 if (!canViewReceivePointerEvents(child)
2648 || !isTransformedTouchPointInView(x, y, child, null)) {
2649 ev.setTargetAccessibilityFocus(false);
2650 continue;
2651 }
2652
2653 newTouchTarget = getTouchTarget(child);
2654 if (newTouchTarget != null) {
2655 // Child is already receiving touch within its bounds.
2656 // Give it the new pointer in addition to the ones it is handling.
2657 newTouchTarget.pointerIdBits |= idBitsToAssign;
2658 break;
2659 }
2660
2661 resetCancelNextUpFlag(child);
2662 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
2663 // Child wants to receive touch within its bounds.
2664 mLastTouchDownTime = ev.getDownTime();
2665 if (preorderedList != null) {
2666 // childIndex points into presorted list, find original index
2667 for (int j = 0; j < childrenCount; j++) {
2668 if (children[childIndex] == mChildren[j]) {
2669 mLastTouchDownIndex = j;
2670 break;
2671 }
2672 }
2673 } else {
2674 mLastTouchDownIndex = childIndex;
2675 }
2676 mLastTouchDownX = ev.getX();
2677 mLastTouchDownY = ev.getY();
2678 newTouchTarget = addTouchTarget(child, idBitsToAssign);
2679 alreadyDispatchedToNewTouchTarget = true;
2680 break;
2681 }
2682
2683 // The accessibility focus didn't handle the event, so clear
2684 // the flag and do a normal dispatch to all children.
2685 ev.setTargetAccessibilityFocus(false);
2686 }
2687 if (preorderedList != null) preorderedList.clear();
2688 }
2689
2690 if (newTouchTarget == null && mFirstTouchTarget != null) {
2691 // Did not find a child to receive the event.
2692 // Assign the pointer to the least recently added target.
2693 newTouchTarget = mFirstTouchTarget;
2694 while (newTouchTarget.next != null) {
2695 newTouchTarget = newTouchTarget.next;
2696 }
2697 newTouchTarget.pointerIdBits |= idBitsToAssign;
2698 }
2699 }
2700 }
2701
2702 // Dispatch to touch targets.//触摸目标
2703 if (mFirstTouchTarget == null) {
2704 // No touch targets so treat this as an ordinary view.没有触摸目标,将此视图视为普通视图
2705 handled = dispatchTransformedTouchEvent(ev, canceled, null,
2706 TouchTarget.ALL_POINTER_IDS);
2707 } else {
2708 // Dispatch to touch targets, excluding the new touch target if we already
2709 // dispatched to it. Cancel touch targets if necessary.
2710 TouchTarget predecessor = null;
2711 TouchTarget target = mFirstTouchTarget;
2712 while (target != null) {
2713 final TouchTarget next = target.next;
2714 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
2715 handled = true;
2716 } else {
2717 final boolean cancelChild = resetCancelNextUpFlag(target.child)
2718 || intercepted;
2719 if (dispatchTransformedTouchEvent(ev, cancelChild,
2720 target.child, target.pointerIdBits)) {
2721 handled = true;
2722 }
2723 if (cancelChild) {
2724 if (predecessor == null) {
2725 mFirstTouchTarget = next;
2726 } else {
2727 predecessor.next = next;
2728 }
2729 target.recycle();
2730 target = next;
2731 continue;
2732 }
2733 }
2734 predecessor = target;
2735 target = next;
2736 }
2737 }
2738
2739 // Update list of touch targets for pointer up or cancel, if needed.
2740 if (canceled
2741 || actionMasked == MotionEvent.ACTION_UP
2742 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
2743 resetTouchState();
2744 } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
2745 final int actionIndex = ev.getActionIndex();
2746 final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
2747 removePointersFromTouchTargets(idBitsToRemove);
2748 }
2749 }
2750
2751 if (!handled && mInputEventConsistencyVerifier != null) {
2752 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
2753 }
2754 return handled;
2755 }
拦截方法只有ViewGroup有,它有两种用法,一种是自定义一种容器,重写
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
一个是在其它类里面调用该类的
RelativeLayout relativeLayout = new RelativeLayout(this);
relativeLayout.requestDisallowInterceptTouchEvent(false);//默认是false
再看下View的这个方法
public boolean dispatchTouchEvent(MotionEvent event) {
……………………………………
12507 if (li != null && li.mOnTouchListener != null
12508 && (mViewFlags & ENABLED_MASK) == ENABLED
//调用外部设置的监听
12509 && li.mOnTouchListener.onTouch(this, event)) {
12510 result = true;
12511 }
12512 //调用View本身的onTouchEcent
12513 if (!result && onTouchEvent(event)) {
12514 result = true;
12515 }
12516 }
12517
12518 if (!result && mInputEventConsistencyVerifier != null) {
12519 mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
12520 }
12521
12522 // Clean up after nested scrolls if this is the end of a gesture;
12523 // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
12524 // of the gesture.
12525 if (actionMasked == MotionEvent.ACTION_UP ||
12526 actionMasked == MotionEvent.ACTION_CANCEL ||
12527 (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
12528 stopNestedScroll();
12529 }
12530
12531 return result;
12532 }
这个mOnTouchListener 是我们自己设置的监听,就是下面这个方法
RelativeLayout relativeLayout = new RelativeLayout(this);
relativeLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return false;
}
});
如果没有设置这个监听,会调用它自己的onTouchEvent方法。
12513 if (!result && onTouchEvent(event)) {
12514 result = true;
12515 }
12516 }
我们看下onTouchEvent(event)方法
public boolean onTouchEvent(MotionEvent event) {
13719 final float x = event.getX();
13720 final float y = event.getY();
13721 final int viewFlags = mViewFlags;
13722 final int action = event.getAction();
13723
13724
13742
13743 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
13744 switch (action) {
13745 case MotionEvent.ACTION_UP:
13746
13757
13770
13799
13820
13821
13843 break;
13844
13845 case MotionEvent.ACTION_CANCEL:
13846
13855 break;
13856
13857 case MotionEvent.ACTION_MOVE:
13858
13873 break;
13874 }
13875
13876 return true;
13877 }
13878
13879 return false;
13880 }
13881
在MotionEvent.ACTION_UP里面,存在我们的onClick事件。在MotionEvent.ACTION_DOWN中,存在我们的长按事件。
我们现在有下面这种情况,几个控件叠压在一起,我们点击它重合的地方,事件是如何处理的呢?
首先会遍历所有的子View,然后获取子View的Z值,根据z值进行处理。
我们最后总结下时间分发的流程
下
1,多指触控的相关API
2,事件冲突
事件冲突分为三种:
(1)外部滑动与内部滑动方向不一致,比如外部是上下滑动,内部是左右滑动
当我们内部控件斜着滑动的时候,外部控件也可能出现滑动的情况。
(2)外部滑动与内部滑动方向一致,外部和内部都是上下或者都是左右滑动
(3)最外部是上下滑动,次外层是左右滑动,最内层又是上下滑动
事件拦截的策略
外部拦截法:所谓外部拦截,是指点击事件都先经过父容器的拦截处理,如果父容器需要就拦截,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法内部做相应的拦击即可。
内部拦截法:
是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器处理,这种方法和android的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,跟外部拦截法相比较为复杂。内部拦截法需要重写子元素的dispatchTouchEvent方法
网上很多自定义控件会造成滑动冲突,就是因为她没使用内部拦截方式去处理滑动事件冲突。以RecyclerView为例,我们ScrollView嵌套一个RecyclerView,即使我们不作任何处理,RecyclerView也可以很顺畅的滑动
我们看下以下布局
<?xml version="1.0" encoding="utf-8"?>
<com.example.shijianfenfa.ConScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.shijianfenfa.ConScrollView1
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="200dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#f00"
android:text="TwoScrollView1" />
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#f00"
android:text="TwoScrollView1" />
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#f00"
android:text="TwoScrollView1" />
</LinearLayout>
</com.example.shijianfenfa.ConScrollView1>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rel_view"
android:layout_width="match_parent"
android:layout_height="400dp"
app:layoutManager="LinearLayoutManager"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:textColor="#f00"
android:text="TwoScrollView" />
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:textColor="#f00"
android:text="TwoScrollView" />
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:textColor="#f00"
android:text="TwoScrollView" />
</LinearLayout>
</LinearLayout>
</com.example.shijianfenfa.ConScrollView>
如果我们不作任何处理的话,ConScrollView1(继承ScrollView)是无法滑动的。我们看下外部拦截法是如何处理的。
package com.example.shijianfenfa;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
import androidx.annotation.RequiresApi;
public class ConScrollView extends ScrollView {
private float mTouchSlop;
private float downY;
public ConScrollView(Context context) {
super(context);
init(context);
}
public ConScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ConScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ConScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
//是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。如果小于这个距离就不触发移动控件,
// 如viewpager就是用这个距离来判断用户是否翻页。
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
//外部拦截法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>down");//false
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>move");//不作任何处理的话会是false false……true。移动一段时间后变为true
float moveY = getY();
if(Math.abs(moveY - downY)>mTouchSlop){
//外部容器不拦截,交给子控件处理。
return false;//如果返回为true,ConScrollView1无法移动
}
break;
case MotionEvent.ACTION_UP:
Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>up");//不打印输出
break;
}
return super.onInterceptTouchEvent(ev);
}
}
内部拦截法
package com.example.shijianfenfa;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
public class ConScrollView1 extends ScrollView {
private float mTouchSlop;
private float downY;
public ConScrollView1(Context context) {
super(context);
init(context);
}
public ConScrollView1(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ConScrollView1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ConScrollView1(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
//内部拦截法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//请求父view不要拦截事件,保证子View能够接收到action_move
downY = ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float moveY = ev.getY();
if (Math.abs(moveY - downY) > mTouchSlop) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
}