上
事件分發的流程:
被分發的對象是哪些?被分發的對象是用戶觸摸屏幕而產生的點擊事件,事件主要包括:按下、滑動、擡起和取消。這些事件被封裝成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);
}
}