CoordinatorLayout系列文章将從淺到深帶你了解與CoordinatorLayout有關的一切
- CoordinatorLayout實作簡單依賴滑動效果
- CoordinatorLayout自定義Behavior實作依賴滾動布局
- CoordinatorLayout源碼解析
源碼基于 com.android.support:design:26.1.0
,不同版本可能有所差異。
一、開始
上一篇Android CoordinatorLayout之自定義Behavior中,我們簡單介紹了
CoordinatorLayout
以及如何自定義
Behavior
。是以這次我們從源碼的角度分析
CoordinatorLayout
的内部實作機制,以便它更好的服務我們!
本文内容主要圍繞
Behavior
展開,還不了解
Behavior
的建議先看上一篇。
二、Behavior的初始化
通常使用
Behavior
是通過給
View
設定
layout_behavior
屬性,屬性值是
Behavior
的路徑,很顯然,這個屬性值最終是綁定在了
LayoutParams
上。可以猜測,和
FrameLayout
等自帶
ViewGroup
類似,
CoordinatorLayout
内部也有一個
LayoutParams
類!嗯,找到了如下代碼段:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
// 是否設定了layout_behavior屬性
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();
if (mBehavior != null) {
// If we have a Behavior, dispatch that it has been attached
mBehavior.onAttachedToLayoutParams(this);
}
首先判斷是否給
View
設定了
layout_behavior
屬性,如果設定了則先得到
Behavior
路徑再去解析
Behavior
,我們重點看下
parseBehavior()
方法:
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
// behavior的全包名路徑
final String fullName;
if (name.startsWith(".")) {
// 如果設定behavior路徑不包含包名,則需要拼接包名
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// 設定了behavior的全包名路徑
fullName = name;
} else {
// 系統内部實作,WIDGET_PACKAGE_NAME代表android.support.design.widget
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
// 通過反射執行個體化behavior
if (c == null) {
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
context.getClassLoader());
// 指定behavior的構造函數有兩個參數
// CONSTRUCTOR_PARAMS = new Class<?>[] {Context.class, AttributeSet.class}
// 這也是我們自定義behavior需要兩個參數的構造函數的原因
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
// 傳回behavior的執行個體
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
parseBehavior()
方法就是使用設定的
Behavior
的路徑進一步通過反射得到
Behavior
執行個體。如果要使用該
Behavior
執行個體,可通過
CoordinatorLayout.LayoutParams
的getBehavior()方法得到:
public Behavior getBehavior() {
return mBehavior;
}
除此之外,還可以在代碼中通過
LayoutParams
給
View
設定
Behavior
,例如修改上一篇demo-1的behavior設定方式:
TextView title = findViewById(R.id.title);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) title.getLayoutParams();
params.setBehavior(new SampleTitleBehavior());
就是通過
CoordinatorLayout.LayoutParams
的
setBehavior()
方法完成的。
最後還有一種就是通過注解,系統的
AppBarLayout
就是用
@CoordinatorLayout.DefaultBehavior()
注解來設定
behavioe
的:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
}
如果某個
View
需要固定設定某個
Behavior
,注解是個不錯的選擇,至于注解的使用時機和原理後邊會提到的。
到此,給
View
設定
Behavior
的方式和原理就基本結束了!
三、CoordinatorLayout的測量、布局
前邊已經分析了
Behavior
的初始化過程,初始化好了,總要用吧,在哪裡用呢?莫急,先看
CoordinatorLayout
在代碼層面是什麼:
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
}
嗯,一個自定義
ViewGroup
,同時實作了
NestedScrollingParent2
接口,既然這樣,
CoordinatorLayout
必然遵循
oMeasure()
、
onLayout()
的執行流程。
是以我們從
CoordinatorLayout
的
onMeasure()
方法開始分析(隻保留了核心代碼):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
final int childCount = mDependencySortedChildren.size();
// 周遊子View,并測量大小
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 得到給View設定的Behavior
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
}
setMeasuredDimension(width, height);
}
先看
prepareChildren()
方法:
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
// 按照View之間的依賴關系,存儲View
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
// mChildDag.getSortedList()會傳回一個按照依賴關系排序後的View集合
// 被依賴的View排在前邊,沒有被依賴的在後邊
mDependencySortedChildren.addAll(mChildDag.getSortedList());
Collections.reverse(mDependencySortedChildren);
}
很明顯
prepareChildren()
就是完成
CoordinatorLayout
中子
View
按照依賴關系的排列,被依賴的
View
排在前面,并将結果儲存在
mDependencySortedChildren
中,在每次測量前都會重新排序。
還記得我們前邊遺留了一個問題嗎?就是注解形式的
Behavior
初始化,答案就在
getResolvedLayoutParams()
中:
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
Class<?> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null &&
(defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
result.setBehavior(
defaultBehavior.value().getDeclaredConstructor().newInstance());
} catch (Exception e) {
Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
" could not be instantiated. Did you forget a default constructor?", e);
}
}
result.mBehaviorResolved = true;
}
return result;
}
其實很簡單,如果沒有通過
layout_behavior
或者
java
代碼給
View
設定
Behavior
,則
LayoutParams
的成員變量
mBehaviorResolved
為
false
,此時如果通注解設定了
Behavior
則會在此完成
Behavior
初始化操作。可見,通過注解設定的
Behavior
被處理的優先級最低。
先跳過
ensurePreDrawListener()
方法,繼續看
onMeasure()
方法剩下的代碼,即周遊
CoordinatorLayout
的子
View
,注意這裡的子
View
從已排序的
mDependencySortedChildren
清單裡得到,先拿到
View
的
LayoutParams
再從中取出
Behavior
,如果
Behavior
非空,并且重寫了
onMeasureChild()
方法,則按照重寫的規則測量該子
View
,否則執行系統的預設測量。可以發現,如果有需要我們可以重寫
Behavior
的
onMeasureChild()
方法,攔截系統的預設onLayoutChild()方法。
繼續分析
onLayout()
方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
其實和
onMeasure()
方法類似,周遊子
View
,确定其位置。同樣,如果有需要我們可以重寫
Behavior
的
onLayoutChild()
方法,攔截系統的預設的
onLayoutChild()
方法。
初見端倪,我
Behavior
就是能為所欲為,想攔截就攔截。類似的
Behavior
攔截操作後邊還會繼續講到!
四、CoordinatorLayout中的依賴、監聽
上一篇我們講到自定義
Behavior
的第一種情況是某個
View
要監聽另一個
View
的位置、尺寸等狀态的變化,需要重寫
layoutDependsOn()
、
onDependentViewChanged()
兩個方法,接下來剖析其中的原理。
View
狀态的變化必然導緻重繪操作,想必有一個監聽狀态變化的接口吧。上邊我們将
onMeasure()
方法時有一個
ensurePreDrawListener()
沒說,答案就在裡邊,現在來看:
void ensurePreDrawListener() {
boolean hasDependencies = false;
final int childCount = getChildCount();
// 判斷是否存在依賴關系
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
// mNeedsPreDrawListener預設為false
// 如果存在依賴關系
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
大緻的作用是判斷
CoordinatorLayout
的子
View
之間是否存在依賴關系,如果存在則注冊監聽繪制的接口:
void addPreDrawListener() {
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
// 給ViewTreeObserver注冊一個監聽繪制的OnPreDrawListener接口
vto.addOnPreDrawListener(mOnPreDrawListener);
}
// Record that we need the listener regardless of whether or not we're attached.
// We'll add the real listener when we become attached.
mNeedsPreDrawListener = true;
}
是以第一次注冊
OnPreDrawListener
接口是在
onMeasure()
裡,并不是在
onAttachedToWindow()
裡邊。看一下
OnPreDrawListener
具體實作:
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
顯然核心就在
onChildViewsChanged(type)
裡邊了,這裡
type
是
EVENT_PRE_DRAW
代表将要繪制,另外還有兩個
type
:
EVENT_NESTED_SCROLL
代表嵌套滾動、
EVENT_VIEW_REMOVED
代表
View
被移除。我們來分析
onChildViewsChanged()
方法(隻保留了核心代碼):
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
if (type != EVENT_VIEW_REMOVED) {
// 檢查子View狀态是否改變
getLastChildRect(child, lastDrawRect);
if (lastDrawRect.equals(drawRect)) {
continue;
}
// 記錄最後一次View的狀态
recordLastChildRect(child, drawRect);
}
// 根據依賴關系更新View
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
// 如果Behavior不為空,并且checkChild依賴child,即重寫了layoutDependsOn
// 則繼續執行if塊,否則目前循環到此結束!
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
// 如果type是EVENT_PRE_DRAW,并且checkChild在嵌套滑動後已經更新
// 則重置标志,進行下一次循環
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// 如果type是EVENT_VIEW_REMOVED,即被依賴的view被移除
// 則需要執行Behavior的onDependentViewRemoved()
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 如果type是EVENT_PRE_DRAW或者EVENT_NESTED_SCROLL
// 并且我們重寫了Behavior的onDependentViewChanged則執行該方法
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// 記錄在嵌套滑動後是否已經更新了View
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
}
onChildViewsChanged()
方法中如果
View
之間有依賴關系,并重寫了相應
Behavior
的
layoutDependsOn()
方法。則會執行
Behavior
的
onDependentViewChanged()
或
onDependentViewRemoved()
方法。這也就解釋了上一篇中第一種情況的原理,可見這裡起關鍵作用的還是
Behavior
。
五、NestedScrolling機制
可能你沒聽過這個概念,但你可能已經使用過它了,例如
CoordinatorLayout
嵌套
RecyclerView
的布局、
NestedScrollView
等,都有用到了這個原理。
NestedScrolling
提供了一套父
View
和子
View
嵌套滑動的互動機制,前提條件是父
View
需要實作
NestedScrollingParent
接口,子
View
需要實作
NestedScrollingChild
接口。按照
NestedScrolling[Parent|Child]
接口的要求(可檢視接口的注釋),實作該接口的
View
需要建立一個
NestedScrolling[Parent|Child]Helper
幫助類執行個體來輔助子
View
和父
View
的互動。
NestedScrollingParent 與 NestedScrollingChild 接口方法是怎樣回調的
執行流程
- 在 Action_Down 的時候,Scrolling child 會調用 startNestedScroll 方法,通過 childHelper 回調 Scrolling Parent 的 startNestedScroll 方法
- 在 Action_move 的時候,Scrolling Child 要開始滑動的時候,會調用dispatchNestedPreScroll 方法,通過 ChildHelper 詢問 Scrolling Parent 是否要先于 Child 進行 滑動,若需要的話,會調用 Parent 的 onNestedPreScroll 方法,協同 Child 一起進行滑動
- 當 ScrollingChild 滑動完成的時候,會調用 dispatchNestedScroll 方法,通過 ChildHelper 詢問 Scrolling Parent 是否需要進行滑動,需要的話,會 調用 Parent 的 onNestedScroll 方法
- 在 Action_down,Action_move 的時候,會調用 Scrolling Child 的stopNestedScroll ,通過 ChildHelper 詢問 Scrolling parent 的 stopNestedScroll 方法。
- 在 Action_down,Action_move 的時候,會調用 Scrolling Child 的stopNestedScroll ,通過 ChildHelper 詢問 Scrolling parent 的 stopNestedScroll 方法。
- 如果需要處理 Fling 動作,我們可以通過 VelocityTrackerCompat 獲得相應的速度,并在 Action_up 的時候,調用 dispatchNestedPreFling 方法,通過 ChildHelper 詢問 Parent 是否需要先于 child 進行 Fling 動作
- 在 Child 處理完 Fling 動作時候,如果 Scrolling Parent 還需要處理 Fling 動作,我們可以調用 dispatchNestedFling 方法,通過 ChildHelper ,調用 Parent 的 onNestedFling 方法
先後構造一個符合
NestedScrolling
機制的場景,前邊我們已經提到了
CoordinatorLayout
實作了
NestedScrollingParent2
接口,
NestedScrollingParent2
又繼承自
NestedScrollingParent
,是以
CoordinatorLayout
做為父
View
的條件是滿足的;其實
RecyclerView
實作了
NestedScrollingChild2
接口,
NestedScrollingChild2
又繼承自
NestedScrollingChild
接口也滿足作為子
View
的條件。
接口可以看作是對
NestedScrolling[Parent|Child]2
接口的擴充,本質和作用類似。
NestedScrolling[Parent|Child]
可以用上一篇自定義
Behavior
的第二種情況的例子來分析:在
CoordinatorLayout
裡邊嵌套一個
RecyclerView
和
TextView
,
TextView
跟随
RecyclerView
的滑動來移動。根據上邊的分析,在
CoordinatorLayout
裡應該有一個
NestedScrollingParentHelper
的執行個體,在
RecyclerView
裡應該有一個
NestedScrollingChildHelper
的執行個體。
從哪裡開始分析呢?因為例子的效果是從
RecyclerView
的滑動開始的,是以就從
RecyclerView
開始吧!滑動必然先進行事件的分發,是以先看它的
onInterceptTouchEvent()
方法(隻保留核心代碼)是否有我們想要的東西:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
}
}
發現了一個
startNestedScroll()
方法,和之前自定義
Behavior
重寫的
onStartNestedScroll()
方法有點像哦!目測找對地方了,繼續看
startNestedScroll()
方法:
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
原來是重寫了
NestedScrollingParent2
接口的方法,
getScrollingChildHelper()
做了什麼呢?
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
就是執行個體化上邊提到的
NestedScrollingChildHelper
,是以
startNestedScroll(axes, type)
就是該幫助類的方法:
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
這裡插一段,
isNestedScrollingEnabled()
代表是否啟用了嵌套滑動,隻有啟用了嵌套滑動,事件才能繼續分發。這個是在哪裡設定的呢?
RecyclerView
的構造函數中:
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
// Re-set whether nested scrolling is enabled so that it is set on all API levels
setNestedScrollingEnabled(nestedScrollingEnabled);
}
最終調用了
NestedScrollingChildHelper
的
setNestedScrollingEnabled()
方法:
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
是以我們自定義的實作
NestedScrollingChild
接口的
View
,也需要設定
setNestedScrollingEnabled(true)
,具體方法了
RecyclerView
中類似。好了插播結束,繼續往下看。
mView
是什麼呢?還記得上邊建立
mScrollingChildHelper
的構造函數嗎?就是:
public NestedScrollingChildHelper(@NonNull View view) {
mView = view;
}
是以
mView
就是
RecyclerView
,則
p
就應該是
CoordinatorLayout
,看下
if
條件
ViewParentCompat.onStartNestedScroll()
裡邊的實作:
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
return false;
}
看到了熟悉的
NestedScrollingParent2
,因為
CoordinatorLayout
實作了該接口,這也更加确定了
parent
就是
CoordinatorLayout
,是以
((NestedScrollingParent2) parent).onStartNestedScroll(child, target, nestedScrollAxes, type)
就回到了
CoordinatorLayout
去執行:
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
// 周遊CoordinatorLayout的子View
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
// 如果目前View有Behavior,則調用其onStartNestedScroll()方法。
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
該方法就是周遊
CoordinatorLayout
的子
View
,如果有
Behavior
則調用它的
onStartNestedScroll
方法,如果傳回
true
,則
Behavior
就攔截了這次事件,進一步可以更新對應的
View
狀态。
到這裡一個
NestedScrolling
機制互動的流程就走完了,先簡單總結一下,事件從
RecyclerView
開始到
NestedScrollingChildHelper
經過
ViewParentCompat
回到了
CoordinatorLayout
最後被
Behavior
處理掉了。
之前我們還重寫了
Behavior
的
onNestedPreScroll()
方法,來處理
RecyclerView
的滑動事件,既然是滑動,那肯定逃不出
onTouchEvent()
的
ACTION_MOVE
事件(隻保留核心代碼):
@Override
public boolean onTouchEvent(MotionEvent e) {
final int action = e.getActionMasked();
switch (action) {
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
} break;
return true;
}
有一個
dispatchNestedPreScroll()
方法,繼續跟進,中間的流程和上的類似,可以自行打斷點跟一遍,我們重點看一下回到
CoordinatorLayout
中的
onNestedPreScroll()
方法:
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
同樣是周遊子
View
,如果可以則執行
View
對應
Behavior
的
onNestedPreScroll()
方法。當然這不是重點,這裡有個
mTempIntPair
數組,對應
Behavior
的
onNestedPreScroll()
方法的
consumed
參數,是以之前我們寫
consumed[1] = dy
,實際是給
mTempIntPair
複制,最終讓父
View
即
CoordinatorLayout
消費掉事件,也就是你滑動的是
RecyclerView
但實際上
CoordinatorLayout
在整體移動!
是以在
NestedScrolling
機制中,當實作了
NestedScrollingChild
接口的子
View
滑動時,現将自己滑動的
dx
、
dy
傳遞給實作了
NestedScrollingParent
接口的父
View
,讓
View
先決定是否要消耗相應的事件,父
View
可以消費全部事件,如果父
View
消耗了部分,則剩下的再由子
View
處理。
六、CoordinatorLayout的TouchEvent
CoordinatorLayout
作為一個自定義
ViewGroup
,必然會重寫
onInterceptTouchEvent()
和
onTouchEvent
來進行事件的攔截和處理。結合之前多次的分析結果,可以猜測這兩個方法最終也由相應子
View
的
Behavior
來處理!
看一下這兩個方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// 重置Behavior的相關記錄,為下次事件做準備
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors();
}
// 是否攔截目前事件是由performIntercept()的傳回值決定的
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
// 同樣是重置Behavior
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// mBehaviorTouchView不為空,表示某個View的Behavior正在處理目前事件
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// 繼續由相應的Behavior處理目前事件
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
return handled;
}
它們都調用了一個共同的方法
performIntercept()
:
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
// 按照 z-order 排序,讓最頂部的View先被處理
getTopSortedChildren(topmostChildList);
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
// 如果已經有Behavior攔截了事件或者是新的攔截,并且不是ACTION_DOWN
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
// 發送cancelEvent給攔截了事件之後的其它子View的Behavior
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
// 如果目前事件還沒被攔截,則先由目前周遊到的子View的Behavior處理
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
// 記錄要處理目前事件的View
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}
相關的說明都在注釋裡了,是以
CoordinatorLayout
的事件處理,還是優先的交給子
View
的
Behavior
來完成。
七、小結
到此,
CoordinatorLayout
的幾個重要的點就分析完了,其實核心還是
CoordinatorLayout
和
Behavior
之間的關系,理清了這個也就明白為什麼
Behavior
可以實作攔截一切的效果!對自定義
Behavior
也是有很大幫助的。可以發現
CoordinatorLayout
最終都是将各種處理優先交給了
Behavior
來完成,是以
CoordinatorLayout
更像是
Behavior
的代理!