基本的事件分發流程
對于一次從父布局到自布局的觸摸事件流程分發,關鍵便是在三個方法上的流程處理
dispatchTouchEvent()
,
onInterceptTouchEvent()
,
onTouchEvent()
。由于和
NestScroll
相關,是以不細緻分析到
View
層面上的事件分發。
對于事件分發的觸摸大緻可分為按下(DOWN),移動(MOVE),擡起(UP)。按照這三個事件,流程分析如下:
- 按下(DOWN):首先調用父控件的
進行事件分發,然後在該方法中,調用dispatchTouchEvent()
方法,如果傳回onInterceptTouchEvent()
表示事件被打斷,則直接調用父控件的true
方法。如果沒有被打斷,會周遊子控件的onTouchEvent()
方法,子控件通過dispatchTouchEvent()
去判斷是否處理事件,如果處理事件,會儲存處理觸摸事件的控件對象。onTouchEvent()
- 移動(MOVE):該事件在父控件的流程相似,差別在于當父控件不處理時,直接擷取
時的處理對象,由該儲存的處理對象消耗事件。DOWN
- 擡起(UP): 該事件和
事件基本相似,不做分析。MOVE
由該流程,不然發現一個問題,事件很容易的向子控件傳遞和處理,但是子控件無法向父控件傳遞觸摸事件。例如:當子控件滑動時,滑到一半突然不想處理事件了,想讓父控件滑動會,或者子控件滑動到極限,然後讓父控件滑動,這兩種情況在如上的流程中很難去實作。
那麼對于這種情況的處理,推出了2個關鍵的接口以及輔助實作的類,
// 接口
NestedScrollingParent
NestedScrollingChild
// 輔助實作類
NestedScrollingChildHelper
NestedScrollingParentHelper
而這篇文章關鍵點便在這四個方法的分析。因為這四個方法是一個整體流程,在單獨分析時,初始可能會有很多疑惑,此時隻需要記憶,等整體流程時便會豁然開朗。
目标Demo
有兩個方框,按住橙色方塊可以跟随手指滑動,當垂直滑動一定距離,再滑動時,由紫色方快滑動,橙色不在移動。
具體的布局代碼如下:
<com.spearbothy.custombehavior.nestscroll.NestParent xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="100dp"
android:background="#f0f" />
<com.spearbothy.custombehavior.nestscroll.NestChild
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="200dp"
android:background="#88ff7f3c" />
</com.spearbothy.custombehavior.nestscroll.NestParent>
其中
NestChild
表示子控件,實作
NestedScrollingChild
接口。
而
NestParent
表示父控件,實作
NestedScrollingParent
接口
NestedScrollingChild
和 NestedScrollingChildHelper
NestedScrollingChild
NestedScrollingChildHelper
首先考慮怎麼實作子控件,子控件的目的便是在将要滑動時,通知父控件,父控件可以選擇消耗或者不消耗這次滑動,然後子控件根據父控件的情況處理此次滑動,滑動完了,再将滑動的情況通知以下父控件,那麼我們就能實作了上述不足中的兩種情況。
首先看
NestedScrollingChild
是一個接口,接口是什麼,是用來定義一系列規範的,即定義具體的方法和作用,不管具體的實作。那麼看一下這個接口都有那些方法:
請注意:這些方法都是目前控件需要實作的方法,其中目的表示我們要在這些方法中實作哪些功能。
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
設定和擷取該控件是否可以嵌套滾動,是否參與這整個嵌套滾動的流程,和普通滑動區分,他們沒有什麼關系。
public boolean startNestedScroll(int axes);
開始滾動滑動時調用此方法,參數為滑動的方向。目的:該方法需要實作通知父控件,我要開始滾動滑動。
hasNestedScrollingParent();
目前的滾動該是否有父控件正在處理。
//dx:x軸的偏移量
//dy:y軸偏移量
//consumed:位移消耗量,由父控件為其指派(數組作為參數的特性?),用以表示父控件消耗了多少位移。
// offsetInWindow: 目前控件的位置索引。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
目前控件準備滑動時的事件分發,這裡的參數比較多,其中dx,dy是我們需要根據手指的觸摸計算得出,而後兩個數組,隻需要傳入size為2的數組能夠存儲x,y的值即可。目的:告訴父控件,我要滑動了和将要滑動的偏移量。父控件可以根據自己的情況是否消耗滑動。
// dxConsumed:目前控件x軸消耗
// dyConsumed:目前控件y軸消耗
// dxUnconsumed,dyUnconsumed:x,y軸未消耗的
// offsetInWindow : 忽略..
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
目前控件滑動完成之後調用。目的:通知父控件我滑動完了,你看看要咋辦吧。
// 滑動的速度
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
目前控件開始慣性滑動時調用。目的:告訴父控件,我要開始慣性滑動了。
// consumed : 目前控件是否消費了慣性滑動
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
目前控件慣性滑動結束後調用。目的:告訴父控件我慣性滑動完了,你看着辦吧。
public void stopNestedScroll();
停止滾動滑動時調用此方法。目的:告訴父控件,我的滑動完成了。
到這裡,所有方法已經分析完了,上面的分析中,主要突出了什麼時機調用和要做的事情。具體的傳回值等的沒有解釋,此時解釋會更加混淆。我們隻需要了解他們的調用時機和目的就夠了。
分析完了,就要實作這個接口并根據上面的定義實作方法。如果此時,我們定義一個
View
并實作
NestedScrollingChild
方法時,會發現
View
已經實作了這些方法,但是,我們千萬不要認為,我們不需要重寫,因為
View
中的方法是在21的時候加入的。如果我們不重寫接口中的方法,在低版本時編譯會報錯。
掐指一算,這麼多的方法要實作,瘋了~~~~突然想到,
View
中不是有實作嗎,我們複制過來行不,當然可以。但這樣會不會很麻煩,此時便是
NestedScrollingChildHelper
類的登場。注意,為了相容,我們需要忽略
View
中有關于此的實作。
從名字上可以看出,他的目的就是為了輔助我們實作
NestedScrollingChild
接口的,我們隻需要把這些方法的實作扔給該輔助類即可。具體實作如下:
public class NestChild extends View implements NestedScrollingChild {
private static final String TAG = NestChild.class.getSimpleName();
private final NestedScrollingChildHelper mChildHelper;
public NestChild(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, );
}
public NestChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 生成輔助類,并傳入目前控件
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
Log.i(TAG, "setNestedScrollingEnabled");
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean startNestedScroll(int axes) {
Log.i(TAG, "startNestedScroll");
return mChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
Log.i(TAG, "stopNestedScroll");
mChildHelper.stopNestedScroll();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
Log.i(TAG, "dispatchNestedScroll");
// 滾動之後将剩餘滑動傳給父類
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
// 子View滾動之前将滑動距離傳給父類
Log.i(TAG, "dispatchNestedPreScroll");
return mChildHelper.dispatchNestedPreScroll(dx, dy,
consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY,
consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}
大功告成,是不是很爽。而根據我們之前的定義,我們重寫
onTouchEvent()
來實作流程。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 啟動滑動,傳入方向
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
// 記錄y值
mOldY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int y = (int) event.getRawY();
// 計算y值的偏移
int offsetY = y - mOldY;
Log.i(TAG, mConsumed[] + ":" + mConsumed[] + "--" + mOffset[] + ":" + mOffset[]);
// 通知父類,如果傳回true,表示父類消耗了觸摸
if (dispatchNestedPreScroll(, offsetY, mConsumed, mOffset)) {
offsetY -= mConsumed[];
}
int unConsumed = ;
float targetY = getTranslationY() + offsetY;
if (targetY > - && targetY < ) {
setTranslationY(targetY);
} else {
unConsumed = offsetY;
offsetY = ;
}
// 滾動完成之後,通知目前滑動的狀态
dispatchNestedScroll(, offsetY, , unConsumed, mOffset);
Log.i(TAG, mConsumed[] + ":" + mConsumed[] + "--" + mOffset[] + ":" + mOffset[]);
mOldY = y;
break;
case MotionEvent.ACTION_UP:
// 滑動結束
stopNestedScroll();
break;
default:
break;
}
return true;
}
分析一下這個流程,在
ACTION_DOWN
時,記錄
y
值,并調用
startNestedScroll()
通知滑動開始。然後,在
ACTION_MOVE
中,根據每次的y值偏移,調用
dispatchNestedPreScroll()
通知父控件,我要開始偏移了,然後根據
mConsumed
判斷父控件消耗的偏移量,并擷取剩餘偏移量,然後開始處理自己的滾動,滾動完成之後通知父控件,目前控件滾動狀态(已滑動的和未消耗的)。
子控件的編寫到此結束,到這裡可能依然一頭霧水,但不要緊,下面通過實作父控件,這些方法就能串聯起來。
NestedScrollingParent
和 NestedScrollingParentHelper
NestedScrollingParent
NestedScrollingParentHelper
在
NestedScrollingChild
中,其大部分方法需要手動調用,因為其作為事件的第一處理者,事件的所有他最清楚。而
NestedScrollingParent
,其中的方法不需要我們手動調用,他通常是作為回調的方法,在方法裡處理子控件通知的回調。
首先看一下
NestedScrollingParent
中所有定義的方法:
// child : 忽略(後面說)
// target: 滑動的目标view
// nestedScrollAxes: 滑動的方向
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
開始滑動時的回調,傳回true表示父控件要處理觸摸。和
child
的
startNestedScroll()
對應
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
接受到開始滑動,在這個方法裡做一些初始化。和
child
的
startNestedScroll()
對應。
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
子控件準備滾動之前的通知,和
child
的
dispatchNestedPreScroll()
相對應。如果該方法消費了dx或dy,則在
consumed[0|1]
的對應索引上添加消耗的置。
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
子控件滑動完成之後的通知,和
child
的
dispatchNestedScroll()
對應。
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
子控件開始慣性滑動之前的通知,傳回
true
表示父控件處理滑動。和
child
的
dispatchNestedPreFling()
對應。
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
子控件慣性滑動完成之後的通知,和
child
的
dispatchNestedFling()
對應。其中
consumed
表示子控件是否消費了滑動。
public void onStopNestedScroll(View target);
滑動結束的回調,和
child
的
stopNestedScroll()
對應。
public int getNestedScrollAxes();
擷取目前滑動的方向。
可以看到,大部分方法是以
onXXX
命名的,他們和
onClickListener
類似,這些方法中主要對相應事件的回調做處理,對于目前,就是對子控件的滑動狀态回調做處理。
對于
NestedScrollingParent
,起需要我們實作的不多,主要的是對回調通知做處理,是以相應的輔助類
NestedScrollingParentHelper
隻是做了最基本的狀态的情況與儲存。
那麼實作如下:
public class NestParent extends LinearLayout implements NestedScrollingParent {
private static final String TAG = NestParent.class.getSimpleName();
NestedScrollingParentHelper mParentHelper;
public NestParent(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, );
}
public NestParent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mParentHelper = new NestedScrollingParentHelper(this);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
// 滑動的child , 目标child , 兩者唯一
// child 嵌套滑動的子控件(目前控件的子控件) , target , 手指觸摸的控件
Log.i(TAG, "onStartNestedScroll:" + child.getClass().getSimpleName() + ":" + target.getClass().getSimpleName());
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
Log.i(TAG, "onStopNestedScroll" + target.getClass().getSimpleName());
mParentHelper.onStopNestedScroll(target);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.i(TAG, "onNestedScroll" + target.getClass().getSimpleName());
Log.i(TAG, "dxUnconsumed:" + dxUnconsumed + "dyUnconsumed:" + dyUnconsumed);
getChildAt().setTranslationY(getChildAt().getTranslationY() + dyUnconsumed);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
Log.i(TAG, "onNestedPreScroll" + target.getClass().getSimpleName());
// 開始滑動之前
Log.i(TAG, consumed[] + ":" + consumed[]);
// consumed[1] = 10;// 消費10px
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
// 慣性滑動
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
// 垂直滾動
return mParentHelper.getNestedScrollAxes();
}
可以看到
NestedScrollingParentHelper
中主要處理了
getNestedScrollAxes()
,
onNestedScrollAccepted()
,
onNestedScrollAccepted()
,如果看源碼,其實就是儲存滑動方向狀态和釋放。
流程分析
到此,代碼工作基本上結束,我在上面添加了一些log,讓我們運作以下程式。簡單的滑動後
log
如下。
NestChild: startNestedScroll
NestParent: onStartNestedScroll
// 循環
NestChild: dispatchNestedPreScroll
NestParent: onNestedPreScroll
// 子控件消耗
NestChild: dispatchNestedScroll
NestParent: onNestedScroll
NestChild: dispatchNestedPreScroll
NestParent: onNestedPreScroll
NestChild: dispatchNestedScroll
NestParent: onNestedScroll
// ......
NestChild: stopNestedScroll
NestParent: onStopNestedScroll
基本的log如下,通過此log可總結流程如下
-
: 子控件開始滑動,調用該方法通知父類滑動即将開始。(DOWN)child:startNestedScroll
-
:父控件收到子控件滑動狀态的通知。parent: onStartNestedScroll
-
:子控件開始滑動的回調,和第一種差別在于此時有具體的滑動距離。child:dispatchNestedPreScroll
-
:父控件收到子控件準備滑動的通知,根據情況是否消耗滑動。parent:onNestedPreScroll
-
:子控件滑動完之後調用,通知父控件child: dispatchNestedScroll
-
:父控件收到子控件滑動之後的通知parent:onNestedScrollNestChild
-
: 子控件滑動結束的通知。child:stopNestedScroll:
-
: 父控件收到子控件滑動結束的通知parent : onStopNestedScroll
根據上面的分析,可以看到整個流程都是
child
和
parent
一一對應的。
簡單的源碼分析
首先從
child
的
startNestedScroll()
方法,其調用
mChildHelper.startNestedScroll(axes)
,再往下跟如下
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
// 擷取處理嵌套滑動的父控件
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
在該方法中,首先判斷是否有父類在滾動,如果沒有,判斷目前控件是否可以嵌套滾動,然後擷取他的父控件,判斷父控件是否處理嵌套滾動,如果處理,則結束循環。否則,以目前父控件為跟,擷取父控件的父控件,繼續判斷。
在的
parent
中有參數
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
和
child
,通過改遠嗎不難發現,
target
就為循環的
child
,而
child
為循環中的
target
。
mView
再看一下準備滑動的實作方法
mChildHelper.dispatchNestedPreScroll(dx, dy, consumed,offsetInWindow)
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
// 是否可以嵌套滑動的基本判斷
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != || dy != ) {
int startX = ;
int startY = ;
if (offsetInWindow != null) {
// 儲存子控件的位置狀态
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[];
startY = offsetInWindow[];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[];
}
consumed = mTempNestedScrollConsumed;
}
consumed[] = ;
consumed[] = ;
// 調用父控件,事件分發
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
if (offsetInWindow != null) {
// 重新計算子控件的位置并擷取偏移
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[] -= startX;
offsetInWindow[] -= startY;
}
return consumed[] != || consumed[] != ;
} else if (offsetInWindow != null) {
offsetInWindow[] = ;
offsetInWindow[] = ;
}
}
return false;
}
剩下的方法大體就是如上邏輯,不在多做分析。
系統控件中已經預設實作兩個接口的類
- 實作
的類NestedScrollingChild
NestedScrollView
,
HorizontalGridView
,
RecyclerView
,
SwipeRefreshLayout
,
VerticalGridView
- 實作
的類NestedScrollingParent
NestedScrollView
,
CoordinatorLayout
,
SwipeRefreshLayout
總結
對于Android中基礎的事件體系,一旦子控件擷取到事件并處理,父控件很難在處理滑動。而
NestedScrolling
的機制便是負責子控件擷取完事件之後的滑動分發。能夠通過
NestedScrolling
機制,在必要時,将子控件的滑動事件交給父控件去處理。