前言
產品有個需求是兩個tab頁面可以左右切換,當時立馬想到我用viewPager+fragment,但是我們知道viewPager默認是可以左右滑動的,而我的需求是隻可點擊不可滑動,於是我就翻了一下viewpager的API發現並沒有可以設置是否可以滑動的相關方法。於是我就想是否可以通過事件的分發機制去攔截它左右滑動的touch。果不其然!
public class myViewpager extends ViewPager {
private boolean canTouch = false;//設置默認不滑動
public myViewpager(@NonNull Context context) {
super(context);
}
public myViewpager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canTouch;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return canTouch;
}
public void setCanTouch(boolean canTouch) {//提供設置是否可以點擊方法默認不可以
this.canTouch = canTouch;
}
}
可以看到這裏重寫了onTnterceptTouchEvent、onTouchEvent並且返回false事情就這樣實現了!
事件分發機制
但是它的事件是如何傳遞的呢?這就得從android的事件分發機制說起:
說到它必須排上三個方法:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
字面意思其實就是事件的分發、攔截、觸摸的處理。
我們知道,安卓中的控件千千萬,也就是說每一個view都有自己的事件分發,那我怎麼能找到他們的規律呢?總不能以偏概全吧?是的,我們Java的三大特性是啥?封裝、繼承、多態我們看它的父類不難發現所有的自定義控件都是繼承view或者是viewgroup,還有一個特殊的就是我們的activity。activity是一個可視化窗口(我是這麼理解的)你把所有的控件擺在上面,當然它也能處理一些事件。因此我們事件的作用對象可以大致分爲:
View的事件分發機制
ViewGroup的事件分發機制
Activity的事件分發機制
產生一個事件
事件是如何產生並進行分發的?
當屏幕被觸摸時(view或者viewGroup),將會產生點擊事件touch,touch是事件的具體實現像:UP(擡起)、DOWN(按下)、MOVE(移動)、CANCEL(取消)。
聽着聽着是不是感覺這個不就是咱們的MotionEvent,還就是,我們的touch細節就被封裝在MotionEvent方法裏面而我們的MotionEvent方法在哪調用了?
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(ev);
}
是的就是我們前面排出的三大方法。想搞懂事件分發機制我們先看看這個方法。
MotionEvent
MotionEvent常用四種類型:
MotionEvent.ACTION_DOWN 按下View(所有事件的開始)
MotionEvent.ACTION_UP 擡起View(與DOWN對應)
MotionEvent.ACTION_MOVE 滑動View
MotionEvent.ACTION_CANCEL 結束事件
當然你打開MotionEvent會發現裏面有很多touch的類型方法如:ACTION_HOVER_MOVE、ACTION_POINTER_UP
總結一下:一般情況下,事件列都是以DOWN事件開始、UP事件結束,中間有無數的MOVE事
事件分發機制流程
上面咱們看了一個重要的方法MotionEvent(處理事件的類型爲事件分發做準備),到這我們看一下事件分發的流程。
事件分發的傳遞順序是:Activity -> ViewGroup -> View
即:點擊事件發生後,事件先傳到Activity、再傳到ViewGroup、最終再傳到 View
這裏說一下View和ViewGroup區別:大白話說,我們去自定義控件的時候可能這個控件不像Button一樣,是一個簡單的繪製的一個距形框上繪製字體,比如LineaLayout,我們自帶的可以包含子佈局的控件,ViewGroup相比較View多一個onLayout方法,也就是設置子佈局擺放位置的相關方法。但是viewGroup最終調用的還是View,所以說viewGroup也是view多一個子類,但是它是一個特殊的view因爲view可以分爲兩類:我本身只是個view,我不會再容下其他View。而我們的特殊View(ViewGroup)我這個view還可以放其他view,我可以約束你,讓你規範。(好難表達)?
事件分發過程
方法 | 作用 | 調用時機 |
---|---|---|
dispatchTouchEvent() | 分發點擊事件 | 當事件傳遞給view時(按下屏幕時機) |
onTouchevent () | 處理點擊事件 | 在dispatchTouchEvent內部事件沒有被父類消耗掉是調用 |
onInterceptTouchEvent() | 判斷是否攔截某個點擊事件,View無改方法 | 在ViewGroup的dispatchTouchEvent內部調用 |
這裏可以看到對於view、viewGroup、Activity他們的事件分發機制是不同的,說到這最難的時候來了,空說無憑啊,他們源碼到底是如何實現的呢?
Activity的事件分發
在Activity中分別調用者三個方法(注意包名)
activity是:package android.app;
View、ViewGroup是:package android.view;
this.dispatchTouchEvent();
view.dispatchTouchEvent()
viewGroup.dispatchTouchEvent()
點開this.dispatchTouchEvent();
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//方法1
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;//方法2
}
return onTouchEvent(ev);//方法3
}
方法1:
看到的是如下的空方法:大概意思是你需要自己去實現,處理通知欄相關的,比如你下滑、上推通知欄,也就是用戶實現交互的方式。
public void onUserInteraction() {
}
方法2:
getWindow().superDispatchTouchEvent(ev)
mDecor.superDispatchTouchEvent(event)
屬於頂層View(DecorView)
a. DecorView類是PhoneWindow類的一個內部類
b. DecorView繼承自FrameLayout,是所有界面的父類
c. FrameLayout是ViewGroup的子類,故DecorView的間接父類 = ViewGroup
*/
@return boolean Return true if this event was consumed
這個是官方的註釋,若getWindow().superDispatchTouchEvent(ev)的返回true表示該事件停止往下傳遞,被消耗掉。
方法3:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
這個是對view的邊界處理,點擊事件在邊界外返回true,否則返回false,而這個mWindow.shouldCloseOnTouch(this, event)內部實現的是對邊界判斷。
同時我們也發現Activity是沒有onInterceptTouchEvent方法的。這是因爲Android裏面只有可以作爲雙親的視圖纔會有onInterceptTouchEvent,因此也只有ViewGroup纔有這個方法,用來截取觸摸事件
這裏簡單總結一下:
這便是Activity的事件傳遞
viewGroup事件分發
嗯,乍一看完全看不懂,要比Activity的dispatchTouchEvent要複雜的多,但是,我們還是可以看懂一點的。我們知道viewgroup是有onInterceptTouchEvent這裏是對事件是否攔截做的處理,源碼裏也有體現
// 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;
}
disallowIntercept = 是否禁用事件攔截的功能(默認是false),可通過調用requestDisallowInterceptTouchEvent()修改
!onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不攔截事件),就會讓第二個值爲true,從而進入到條件判斷的內部
// b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會讓第二個值爲false,從而跳出了這個條件判斷
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
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);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
這裏是對內部view循環判斷處理,找到當前點擊對view
// 條件判斷的內部調用了該View的dispatchTouchEvent()
// 即 實現了點擊事件從ViewGroup到子View的傳遞(具體請看下面的View事件分發機制)
所以有以下事件分發的流程圖
#### View的事件分發機制
view和ViewGroup是在同一個類。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
// 說明:只有以下3個條件都爲真,dispatchTouchEvent()才返回true;否則執行onTouchEvent()
// 1. mOnTouchListener != null
// 2. (mViewFlags & ENABLED_MASK) == ENABLED
// 3. mOnTouchListener.onTouch(this, event)
// 下面對這3個條件逐個分析
/**
* 條件1:mOnTouchListener != null
* 說明:mOnTouchListener變量在View.setOnTouchListener()方法裏賦值
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
// 即只要我們給控件註冊了Touch事件,mOnTouchListener就一定被賦值(不爲空)
}
/**
* 條件2:(mViewFlags & ENABLED_MASK) == ENABLED
* 說明:
* a. 該條件是判斷當前點擊的控件是否enable
* b. 由於很多View默認enable,故該條件恆定爲true
*/
/**
* 條件3:mOnTouchListener.onTouch(this, event)
* 說明:即 回調控件註冊Touch事件時的onTouch();需手動複寫設置,具體如下(以按鈕Button爲例)
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
// 若在onTouch()返回true,就會讓上述三個條件全部成立,從而使得View.dispatchTouchEvent()直接返回true,事件分發結束
// 若在onTouch()返回false,就會使得上述三個條件不全部成立,從而使得View.dispatchTouchEvent()中跳出If,執行onTouchEvent(event)