博客原文:kyleduo.com
前言
這個系列源自前幾天看到一篇使用CoordinatorLayout實現(xiàn)支付寶首頁效果的文章励幼,下載看了效果和源碼,不敢茍同,所以打算自己動手斤儿。實現(xiàn)的過程有點曲折,但也發(fā)現(xiàn)了一些有意思的事情,用三篇文章來記錄并分享給大家往果。
- CoordinatorLayout和Behavior
- 自定義CoordinatorLayout.Behavior
- 支付寶首頁效果實現(xiàn)
文中:CoL代表CoordinatorLayout疆液,ABL表示AppBarLayout,CTL表示CollapsingToolbarLayout棚放,SRL表示SwipeRefreshLayout,RV表示RecyclerView馅闽。
源碼:Github
第一篇文章主要討論Behavior的結(jié)構(gòu)飘蚯、CoordinatorLayout的實現(xiàn)以及CoordinatorLayout和Behavior之間的通信。除了Behavior相關(guān)的內(nèi)容福也,CoordinatorLayout作為官方實現(xiàn)的一個ViewGroup局骤,也有一些在自定義ViewGroup時可以借鑒的內(nèi)容,這些也穿插在這篇文章中暴凑。
Behavior結(jié)構(gòu)
使用CoordinatorLayout結(jié)合ABL峦甩,CTL,SRL/RV可以方便的實現(xiàn)各種MaterialDesign ToolBar效果现喳,還有FloatingActionButton凯傲,SnackBar等控件,可以直接使用嗦篱。CoL的主要功能是為其直接子View提供 協(xié)調(diào)滾動 的統(tǒng)一接口冰单,讓子View可以方便的實現(xiàn)諸如嵌套滾動,跟隨滾動等效果灸促,讓界面更加靈動诫欠;而這個統(tǒng)一接口,就是Behavior浴栽。
我們先來看一下Behavior提供的方法荒叼,大致可以分為4組:
布局相關(guān),這類方法用來重載child的Measure典鸡、Layout相關(guān)回調(diào)被廓。
public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection);
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency);
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency);
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency);
Touch事件相關(guān),這組方法用來攔截和處理Touch事件傳遞萝玷。
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev);
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev);
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child);
NestedScrolling相關(guān)伊者,這組方法用來響應NestedScrolling,更多關(guān)于NestedScrolling的討論可以查看另一篇博客间护。
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes);
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target);
public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY);
其他輔助方法
// 關(guān)聯(lián)/取消關(guān)聯(lián)LayoutParams的時候回調(diào)
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params);
public void onDetachedFromLayoutParams();
public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets);
// 控制Scrim效果亦渗,只有當getScrimOpacity返回值不為0時才繪制。
public int getScrimColor(CoordinatorLayout parent, V child);
public float getScrimOpacity(CoordinatorLayout parent, V child);
// 暫時沒發(fā)現(xiàn)用到的地方
public static void setTag(View child, Object tag);
public static Object getTag(View child);
public boolean onRequestChildRectangleOnScreen(CoordinatorLayout coordinatorLayout, V child, Rect rectangle, boolean immediate);
public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state);
public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child);
// 防止CoL子View間出現(xiàn)遮擋汁尺,獲取child應避免遮擋部分的Rect法精。
public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull Rect rect);
多數(shù)情況下我們只需要關(guān)注前三組方法。通過這些方法我們可以看到Behavior可以做的事情不僅僅是“依賴某個View的變化并且在其變化之后進行響應”這么單一,Behavior實際上可以控制child在CoL中的measure搂蜓,layout以及攔截toush事件狼荞,支持NestedScrolling等等,基本上是除了Draw之外的全部自定義View需要關(guān)注的內(nèi)容了帮碰。
Behavior的使用和自定義我們下一篇文章進行討論相味,這篇文章我們繼續(xù)關(guān)注CoL如何操作Behavior。
設置Behavior
給一個View設置Behavior有兩種方法:在布局文件中指定殉挽;使用@CoordinatorLayout.DefaultBehavior
注解丰涉。
第一種方法適用于多數(shù)情況,需要注意的是斯碌,如果使用自定義Behavior一死,需要覆寫2個參數(shù)的構(gòu)造方法:
public Behavior(Context context, AttributeSet attrs);
因為CoL在解析時是通過反射調(diào)用Behavior的這個構(gòu)造方法創(chuàng)建Behavior對象的:
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
// {...}
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
final Class<Behavior> clazz = (Class<Behavior>) 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);
}
}
第二種方法,適用于自定義View并且自定義Behavior的情況傻唾,比如AppBarLayout:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
// {...}
}
如果在布局文件中不另外指定投慈,這里將調(diào)用Behavior的無參構(gòu)造方法創(chuàng)建對象。
事件分發(fā)
上面看到的Behavior的各個方法冠骄,其調(diào)用者基本都是CoL伪煤。CoL在自己的回調(diào)方法中通過調(diào)用子View Behavior的相關(guān)方法,將事件向下分發(fā)凛辣。
在討論分發(fā)方法之前带族,有一點需要注意:Behavior雖然影響的是子View的布局和行為,但實際是對CoL本身事件處理的代理蟀给。
基本的模式大家可以想到蝙砌,就是遍歷子View,獲取Behavior跋理,然后調(diào)用子Behavior對應的方法择克。這里對幾個有意思的地方進行討論:
Touch事件分發(fā)
Touch事件分發(fā)分兩個方法onInterceptTouchEvent和onTouchEvent。CoL的實現(xiàn)中前普,這兩個方法都調(diào)用performIntercept
方法將是否攔截事件的判斷交給Behavior處理肚邢。每個CoordinatorLayout的子View都有機會攔截事件并響應,注意這里子View并不是在自己的onTouch相關(guān)方法中進行處理拭卿,而是Behavior子類骡湖,有機會代理CoL對事件進行攔截并處理。
出于篇幅考慮這里不貼源碼了峻厚,關(guān)鍵的地方這里解釋一下:
- 在遍歷子View之前响蕴,使用
getTopSortedChildren(topmostChildList);
獲取按照顯示順序由上至下排序過的子View列表。ViewGroup可以覆寫getChildDrawingOrder
自定義子View的繪制順序(這篇文章中有對這個方法的解釋)惠桃,getTopSortedChildren方法會按照繪制順序獲取子View浦夷;在5.0及以上版本中辖试,還要考慮Z軸次序,也就是elevation劈狐,會再進行一次排序罐孝,最終得到真實可靠的自頂之下的子View分發(fā)順序。這對讓子View合理響應Touch事件很重要肥缔,如果自定義Group需要有類似功能莲兢,可以參考CoL的實現(xiàn)。 - Behavior通過覆寫
onInterceptTouchEvent
或者onTouchEvent
并返回true來聲明攔截事件续膳,CoL會將該View緩存到mBehaviorTouchView屬性改艇,后續(xù)事件將直接分發(fā)到該View。直到該View的onTouchEvent方法返回false姑宽。 - 在確定mBehaviorTouchView之后遣耍,CoL會將該View(Z軸)下面View的事件流終止闺阱,具體操作是向這些View分發(fā)一個CANCEL事件炮车。
Behavior可以通過覆寫blocksInteractionBelow方法block下方View的事件。在自己不需要處理事件但同時不希望子View處理事件時酣溃,可以簡單的覆寫這個方法瘦穆。
默認實現(xiàn)邏輯是判斷getScrimOpacity的值>0
NestedScrolling
NestedScrolling是Behavior實現(xiàn)滑動的重要支撐。前文提到Behavior是對CoL自身事件的代理赊豌,所以Behavior對NestedScrolling的支持扛或,就是在代理CoL的NestedScrollingParent接口方法。
更多NestedScrolling相關(guān)信息參見更早的博客:Android Nested Scrolling
需要注意的是關(guān)于NestedScrolling機制中“消費量”的處理
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
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()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
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);
}
}
注意這里是取了所有Behavior消費掉偏移量的最大值碘饼。因為Behavior是代理的角色熙兔,而各個代理的消費對于NestedScrolling機制來說,都會被看做是CoL這個NestedScrollingParent的消費艾恼。各Behavior之間是同級的住涉,所以他們對事件的消費是“重疊”的(可以重復消費),所以這里返回的consumed是取最大值钠绍。
LayoutDependence
這里是其他博客講的比較多的地方舆声,確定View依賴的Dependence,當Dependence變化之后柳爽,會將變化廣播給所有依賴這個View的兄弟View媳握。我要說的有兩點:1. onDependentViewChanged回調(diào)的調(diào)用時機。2. 依賴關(guān)系的存儲磷脯。
- onDependentViewChanged在某個View的大小或者位置發(fā)生變化的時候都會進行回調(diào)蛾找。并且是真正變化之后才會進行回調(diào)。
- 子View之間的依賴關(guān)系通過非循環(huán)有向圖數(shù)據(jù)結(jié)構(gòu)進行存儲赵誓。具體到結(jié)構(gòu)上就是通過一個
Map<Node, List<Node>>
存儲(這個Map并不是JDK的實現(xiàn)腋粥,感興趣的可以看下源碼)晦雨。 - 既然存在依賴關(guān)系,那么在涉及到對子View遍歷的時候隘冲,就要考慮到子View之前的依賴關(guān)系闹瞧。CoL的實現(xiàn)中
prepareChildren
方法構(gòu)建依賴圖并根據(jù)依賴圖進行DFS搜索得到依賴鏈列表,這個列表用在了分發(fā)布局展辞、NestedScrolling奥邮、LayoutDependentChange的過程中。
自定義LayoutParams
自定義ViewGroup很常見罗珍,但是多數(shù)情況下用不到自定義LayoutParams洽腺。LayoutParams正如其名,用了設置布局參數(shù)覆旱,也就是控制ViewGroup如何measure和layout子View蘸朋。如果一個自定義ViewGroup提供了額外的布局參數(shù),那就需要自定義LayoutParams了扣唱。自定義LayoutParams并沒有多么復雜藕坯,這里列幾個需要注意的地方。
基類
如果自定義LayoutParams需要支持margin噪沙,繼承自ViewGroup.MarginLayoutParams
即可炼彪,默認的ViewGroup.LayoutParams并不支持margin。
構(gòu)造方法
LayoutParams默認有多個不同參數(shù)的構(gòu)造方法:
- LayoutParams(Context c, AttributeSet attrs) 適用于解析布局文件時生成LayoutParams正歼,layout相關(guān)的xml屬性辐马,就是在這個構(gòu)造方法里面解析的。
- LayoutParams(int width, int height) 代碼構(gòu)建時只傳入寬高局义。
- LayoutParams(LayoutParams source) LayoutParams轉(zhuǎn)換
- LayoutParams() 無參構(gòu)造函數(shù)
自定義LayoutParams也要覆寫這些構(gòu)造方法并做響應的轉(zhuǎn)換喜爷。
ViewGroup方法
使用自定義LayoutParams的ViewGroup,也需要實現(xiàn)幾個相關(guān)方法萄唇,主要是在解析檩帐、addView的時候生成適合的LayoutParams。
protected LayoutParams generateDefaultLayoutParams();
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p);
public LayoutParams generateLayoutParams(AttributeSet attrs);
// 檢查LayoutParams是否是自定義的LayoutParams類型
protected boolean checkLayoutParams(ViewGroup.LayoutParams p);
具體實現(xiàn)可以參考CoL穷绵。
CoordinatorLayout.LayoutParams
CoL.LP主要實現(xiàn)了基于anchorView的布局和keylines(縱向基準線轿塔,子View可以對齊到keyline)的布局、保存Behavior仲墨、儲存滑動過程中的標記位勾缭,具體實現(xiàn)這里就不展開了,邏輯比較簡單目养。