在上一篇博客CoordinatorLayout高級用法-自定義Behavior中,我們介紹了如何去自定義一個CoordinatorLayout的Behavior,通過文章也可以看出Behavior在CoordinatorLayout中地位是相當高的,那麼今天我們就來接著上篇博客來從源碼分析一下Behavior的實現思路,如果你對CoordinatorLayout和Behavior還不熟悉的話,建議先去看看上篇博客《CoordinatorLayout高級用法-自定義Behavior》。
這篇文章我們要分析的內容有:
Behavior的實例化 layoutDependsOn和onDependentViewChanged調用過程 onStartNestedScroll和onNestedPreScroll實現原理 Behavior的事件分發過程
大家都知道,我們在view中可以通過app:layout_behavior
然後指定一個字符串來表示使用哪個behavior,稍微去想一下,在CoordinatorLayout中肯定是利用反射機制來完成的behavior的實例化,現在就讓我們從CoordinatorLayout的源碼中找尋答案,來驗證我們的猜想。首先,我們來看看CoordinatorLayout的一個內部類,也是大家熟悉的LayoutParams
,
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* A {@link Behavior} that the child view should obey.
*/
Behavior mBehavior;
...
}
在這裡我們確實看到了behavior的影子,那它是在什麼時候被初始化的呢?繼續看代碼,
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_LayoutParams);
...
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}
a.recycle();
}
在LayoutParams的構造方法中,首先是去檢查了是不是有layout_behavior
,這裡很容易理解,接下來調用了parseBehavior
方法,返回了Behavior的實例,我們非常有理由去看看parseBehavior
到底干了嘛,或許我們要的答案就在裡面!
// 這裡是指定的Behavior的參數類型
static final Class [] CONSTRUCTOR_PARAMS = new Class [] {
Context.class,
AttributeSet.class
};
...
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
// 代表了我們指定的那個behavior的完整路徑
final String fullName;
// 如果是".MyBehavior"
// 則在前面加上程序的包名
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// 這裡我們指定了全名
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor c = constructors.get(fullName);
// 這裡利用反射去實例化了指定的Behavior
// 並且值得注意到是,這裡指定了構造的參數類型
// 也就是說我們在自定義Behavior的時候,必須要有這種類型的構造方法
if (c == null) {
final Class clazz = (Class) Class.forName(fullName, true,
context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
上面的代碼很容易理解,就是利用反射機制去實例化了Behavior,調用的是兩個參數的那個構造方法,這也就是我們在自定義Behavior的時候為什麼一定要去重寫,
public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
這個構造的原因。看來獲取一個Behavior的實例還是很簡單的,那麼,下面就讓我們開始分析Behavior中常用方法調用的機制吧。
在上一篇博客中我們學會了自定義兩種形式的Behavior,其中第一種就是去觀察一個view的狀態變化,也就是涉及到layoutDependsOn
和onDependentViewChanged
兩個方法的調用,現在我們從源碼的角度來分析一下這兩個方法調用的時機和調用的過程,在前一篇博客中我們提到過onDependentViewChanged
這個方法會在view的狀態發生變化後去調用,那在狀態發生變化時必定會執行什麼操作呢?重繪,是的,狀態變化了,那肯定重繪是避免不了的,在CoordinatorLayout
中注冊了一個ViewTreeObserver
,我們可以從這裡入手,因為它可以監聽到view的各種狀態變化,
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
// 實例化了OnPreDrawListener
// 並在下面注冊到了ViewTreeObserver中
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
在onAttachedToWindow
向ViewTreeObserver注冊了一個監聽draw變化的Observer,那在這裡Observer中到底干了嘛呢?
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
dispatchOnDependentViewChanged(false);
return true;
}
}
就兩行代碼,調用了dispatchOnDependentViewChanged
方法,看方法名我們就知道這次找對對象了,懷著激動的心情來看看dispatchOnDependentViewChanged
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
// 遍歷所有的子view
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
// Did it change? if not continue
// 檢查是否變化了,沒有變化直接下一次循環
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
// Update any behavior-dependent views for the change
// 這裡從下一個子view開始
//mDependencySortedChildren有一個排序規則
// selectionSort
// 感興趣的可以看一下mDependencySortedChildren部分。
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
// 獲取到Behavior
final Behavior b = checkLp.getBehavior();
// 這裡調用Behavior的layoutDependsOn來判斷我們的帶有behavior的view是不是依賴這個view
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
// If this is not from a nested scroll and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
// 這裡調用了Behavior的onDependentViewChanged
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
...
}
}
}
}
dispatchOnDependentViewChanged
方法有一個布爾類型的參數,上面我們傳遞的是false, 這裡主要是區分是view引起的狀態變化還是布局引起的,在一些的scroll中也會調用dispatchOnDependentViewChanged
這個方法。
好了,現在我們終於搞懂了onDependentViewChanged
調用機制了,下面我們來看看關於滑動監聽的部分。
在開始源碼之前,我們先來思考個問題,現在有一個view是可以上下滑動的,那這個view的滑動對於父view來說是不是可見的?或者說是可預知的?顯然不是,一個view的滑動對於父布局來說是透明的?所以現在我們不能簡簡單單的從CoordinatorLayout
入手了,而是要從那個可以滑動的view入手,我們選擇NestedScrollView
來進行分析。NestedScrollView
有一個NestedScrollingChildHelper
類型的變量mChildHelper
引起了我們的注意,因為很多看名字很像關於滑動部分的代碼都調用了這個類的一些方法,來看看有哪些吧?
mChildHelper = new NestedScrollingChildHelper(this);
...
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
final int oldScrollY = getScrollY();
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
final int myUnconsumed = dyUnconsumed - myConsumed;
dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
很簡單,不過我們好像發現了一點眉目,這些方法何時調用我們還是不是很清楚,滑動必然和事件有關,我們就來從事件的部分入手吧,畢竟是我們熟悉的地方。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
switch (action & MotionEventCompat.ACTION_MASK) {
...
case MotionEvent.ACTION_DOWN: {
...
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
...
}
...
}
在down的時候我們調用了startNestedScroll
方法,那我們就順著這條線往下看mChildHelper.startNestedScroll(axes)
。
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
// 獲取當前view的parent
ViewParent p = mView.getParent();
View child = mView;
// 一個循環,不斷的往上層去獲取parent
// 直到條件成立,或者沒有parent了 退出
while (p != null) {
// 這裡是關鍵代碼,猜測這裡肯定肯定去調用了CoordinatorLayout的對應方法。
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;
}
在這個方法中一個while循環,不斷的去獲取view的的parent,然後一個ViewParentCompat.onStartNestedScroll
作為條件成立了就return true了,我們有理由猜測ViewParentCompat.onStartNestedScroll
裡去調用了CoordinatorLayout
的相應方法。注意參數,p是我們遍歷到父view,我們先認為是CoordinatorLayout
吧,child是CoordinatorLayout
的直接嵌套著目標view的子view,mView在這裡就是NestedScrollView
了。
public class ViewParentCompat {
static class ViewParentCompatStubImpl implements ViewParentCompatImpl {
@Override
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
return false;
}
}
}
這裡面很簡單,看看parent是不是NestedScrollingParent
類型的,如果是,則調用了onStartNestedScroll
這個方法,而我們的CoordinatorLayout
肯定是實現了NestedScrollingParent
接口的,
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent { }
好了,現在我們終於回到CoordinatorLayout
了,來看看他的onStartNestedScroll
方法,
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
// 調用遍歷出來的這個子view的onStartNestedScroll方法
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
這裡還是去遍歷了所有子view,然後去調用它的onStartNestedScroll
方法,它的返回值,決定了NestedScrollingChildHelper.onStartNestedScroll
是不是要繼續遍歷,如果我們的子view對這個view的滑動感興趣,就返回true,它的遍歷就會結束掉。
好了,現在start的過程我們分析完了,大體的流程就是:
NestedScrollView.onInterceptTouchEvent->NestedScrollingChildHelper.onStartNestedScroll->CoordinatorLayout.onStartNestedScroll
下面的各種滑動調用流程也是一樣的,這裡我們就不再重復分析了,感興趣的可以自己去看一下源碼。
上面的分析其實已經將我們自定義Behavior中使用到的方法的調用流程分析完了,不過我們還是要拓展一下,其實Behavior也是支持事件的傳遞的,在這方面,Behavior好像是一個代理一樣,在CoordinatorLayout的各種事件處理的方法中去調用Behavior的事件處理方法,返回值決定了CoordinatorLayout對事件的消費情況。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors();
}
// 去看看子view中behavior是有要攔截
// 如果要攔截,則我們要攔截
// 在這裡Behavior類似一個代理
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return intercepted;
}
這裡面調用了performIntercept
方法,而且指定了個常量TYPE_ON_INTERCEPT
代表了我們在攔截階段調用的,既然有區分,肯定在別的地方也有調用,答案是肯定的,在onTouch
裡也有對performIntercept
的調用,
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
// 這裡要說道說道
// 兩個條件:1 如果behavior想要攔截
// 2 behavior的onTouchEvent返回true
// 為什麼會有兩個條件呢?
// 解答:第一個條件是正常的分發流程, 很容易理解
//
// 第二個條件是在沒有子view消費事件,所以事件會冒泡到此
// 這時,我們還要繼續詢問behavior是否要消費該事件
// 這裡在performIntercept中執行的是:
// case TYPE_ON_TOUCH: // 從onTouchEvent調用的
// intercepted = b.onTouchEvent(this, child, ev);
// break;
// 當intercepted為true時,表示我們對該down事件感興趣
// 此時 mBehaviorTouchView也有了賦值
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
// 這裡同樣的事件會繼續執行一遍onTouchEvent?
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
// 如果behavior不感興趣
// 輪到自己了,問問自己干不感興趣
// Keep the super implementation correct
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
// 如果behavior執行了事件(並不是攔截了事件,上面的第一個if的第一個條件不成立,第二個條件成立)
// 能執行到這,說明behavior沒有攔截事件,但在事件冒泡的過程中消費了事件
// mBehaviorTouchView是在performIntercept(ev, TYPE_ON_TOUCH)賦值的
// 則給自己執行一個cancel事件
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
if (!handled && action == MotionEvent.ACTION_DOWN) {
}
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return handled;
}
恩,這裡面的代碼注釋已經寫的很明白了,但是需要注意的一點,這一點我很長時間沒有相通,就是為什麼還要在onTouch
裡還要調用一遍performIntercept
,是這樣的,假如現在事件沒有任何子view去消費,那麼事件會冒泡到此,本著把Behavior看作是一個代理的原則,這裡肯定還是要去詢問一下Behavior是不是要執行這個事件,注意這裡說的是執行而不是攔截,這是因為performIntercept
不僅僅會調用Behavior的攔截部分的代碼,也會調用執行的代碼,就是通過第二個參數區分的。可以看到,這裡我們使用了TYPE_ON_TOUCH
。
好了,說了這麼多performIntercept
,是時候來看看performIntercept
的代碼了。
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
final List topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
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();
// 如果現在已經有攔截了的
// 並且現在是down
// 則 所有的behavior會受到一個cancel事件
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT: // 從onInterceptTouchEvent調用的
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH: // 從onTouch調用的
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
// 如果現在還沒有攔截 並且具有behavior
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT: // 從onInterceptTouchEvent調用的
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH: // 從onTouchEvent調用的
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
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;
}
這裡面的代碼也很容易理解,就是去遍歷所有的view,在不同的情景下調用Behavior的onInterceptTouchEvent或onTouch方法。
好了關於Behavior的源碼我們就分析到這裡,相信大家在看完之後會對Behavior有一個全新的認識,而且google已經建議我們使用support design的東西了(沒發現現在的項目默認模板文件就是一個標准的support design布局嗎),所以我們還是有必要對新東西有個更加深入的認識,而且這樣也會有助於我們理解google工程師的思路,在解決一些問題的時候我們完全可以參考一下這些思路。
ok,不扯了,今天就到這裡吧,拜拜。