dim.red
在appcompat 22 的時候,google帶來了Support Design,成為實現(xiàn)MD的利器,最近因為要開始使用這個庫,稍微過了下庫的內(nèi)容.
這次主要通過講解當(dāng)前界面是怎么實現(xiàn)的.來學(xué)習(xí)這個庫.
布局
看看這個界面的實現(xiàn),我們主要通過3個方面來了解,
- 子控件的寬高的測量
- 子控件的位置擺放
- 子控件的事件傳遞
1 測量:
因為它們的根控件是CoordinatorLayout .所以我們重點是放在
CoordinatorLayout 的onMeasure方法里面:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
* 省略N多代碼
*/
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
/**
* 省略N多代碼
*/
setMeasuredDimension(width, height);
}
子控件的測量交給他們的Behavior,Behavior 不處理,交給CoordinatorLayout處理 ,Behavior 可以在attr中指定. 可以看出ViewPager的Behavior 是AppBarLayout$ScrollingViewBehavior
,我們進(jìn)入ScrollingViewBehavior 中的onMeasureChild方法中
@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child,
int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
|| childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
/**
* 省略N多代碼
*/
if (availableHeight == 0) {
// If the measure spec doesn't specify a size, use the current height
availableHeight = parent.getHeight();
}
final int height = availableHeight - header.getMeasuredHeight()
+ getScrollRange(header);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
? View.MeasureSpec.EXACTLY
: View.MeasureSpec.AT_MOST);
// Now measure the scrolling menu with the correct height
parent.onMeasureChild(child, parentWidthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
return false;
}
可以看出來當(dāng)你的ViewPager的高度不設(shè)置固定的值得話,他的高度會被ScrollingViewBehavior重新賦值,高度為CoordinatorLayout的高度減去AppBarLayout的可滑動范圍.(既getTotalScrollRange())
可以看出:當(dāng)前的ViewPager 的高度比我們當(dāng)前屏幕上看的要高一點.
AppBarLayout 里面有3個范圍比較有意思.
getTotalScrollRange()
:表示總共可以滑動的范圍
它是計算所有l(wèi)ayout_scrollFlags標(biāo)有scroll 的View 的高度減去所有同時標(biāo)有scroll 和 exitUntilCollapsed 的 View 的最小高度.
getDownNestedPreScrollRange()
:表示當(dāng)向下滑動可以滑動的范圍.
它計算了所有l(wèi)ayout_scrollFlags同時標(biāo)記scroll 和 enterAlways 同時不標(biāo)記 enterAlwaysCollapsed的View 的高度 加上既標(biāo)記了scroll 和 enterAlways又標(biāo)記了enterAlwaysCollapsed 的最小高度.
產(chǎn)生的效果是:在下滑的過程中AppBarLayout殘留在屏幕上的最小高度為 AppBarLayout本身的高度減去getDownNestedPreScrollRange()的高度.
getUpNestedPreScrollRange()
:表示當(dāng)向上滑動可以滑動的范圍.
這里返回的是getTotalScrollRange().
產(chǎn)生的效果是:在上滑的過程中AppBarLayout殘留在屏幕上的最小高度為 AppBarLayout本身的高度減去getUpNestedPreScrollRange()的高.
而這三種范圍構(gòu)成了 AppBarLayout 在 RecyclerView 滑動事件的滑動效果.
主意點:
- exitUntilCollapsed只有和scroll一起組合才會有效果;
- enterAlwaysCollapsed 要和scroll 和enterAlways一起使用才有效果.
- 官方說要把帶有scroll flag的view放在前面,這樣收回的view才能讓正常退出欺冀,而固定的view繼續(xù)留在頂部树绩。
那是因為AppBarLayout 是一個 LinearLayout 布局.最后留在屏幕上的東西是 AppBarLayout 的底部,所以需要把要固定的 View 放在最后. - 這里所有的 View 都是 AppBarLayout 的一級 View.二級不太考慮當(dāng)中,
下面放出幾個例子來加深大家對layout_scrollFlags和3中范圍的理解.
第一中 正常情況(scroll):
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#f00"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll" />
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.AppBarLayout>
第2種(minHeight +scroll +exitUntilCollapsed)
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#f00"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:minHeight="20dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed" />
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.AppBarLayout>
第3種(minHeight +scroll +enterAlways+enterAlwaysCollapsed)
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#f00"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:minHeight="20dp"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed" />
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.AppBarLayout>
2 位置擺放
同樣進(jìn)入CoordinatorLayout 的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);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
同樣可以看到它也是先讓Behavior處理.不處理才是CoordinatorLayout自身去處理.
同樣我們?yōu)榱瞬榭碫iewPager 的擺放,我們進(jìn)入ScrollingViewBehavior 中的onLayoutChild方法中.
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
// First lay out the child as normal
super.onLayoutChild(parent, child, layoutDirection);
// Now offset us correctly to be in the correct position. This is important for things
// like activity transitions which rely on accurate positioning after the first layout.
final List<View> dependencies = parent.getDependencies(child);
for (int i = 0, z = dependencies.size(); i < z; i++) {
if (updateOffset(parent, child, dependencies.get(i))) {
// If we updated the offset, break out of the loop now
break;
}
}
return true;
}
先調(diào)用的父類的onLayoutChild 的方法.然后根據(jù)dependencies (其實就是AppBarLayout)的getTopBottomOffsetForScrollingSibling(),其實就是把ViewPager放在AppBarLayout的下方.
3 事件傳遞
Touch事件的話
CoordinatorLayout是會在onInterceptTouchEvent 對所有的攜帶Behavior的第一級View 發(fā)送通知.如果被哪一個Behavior的onInterceptTouchEvent 的攔截,所以的后續(xù)的 Touch動作都分發(fā)給這個Behavior.
注意點:
能接受到事件只有第一級的并且攜帶Behavior的控件.
同時這個事件是通知給所有的攜帶Behavior的控件,也就是說當(dāng)你的點擊事件不在這個 View 的上方,只要這個View 有攜帶 Behavior 都會收到通知,就是說不管你是點擊屏幕上的1還是2,AppBarLayout 都會收到onInterceptTouchEvent事件,所以在復(fù)寫 Behavior 的onInterceptTouchEvent 要特別注意到這個情況.
比如說界面一開始往上滑動. 這個時候點擊事件是被AppBarLayout的Behavior 攔截的. AppBarLayout的Behavior事件會設(shè)置AppBarLayout的setTopAndBottomOffset ,使AppBarLayout產(chǎn)生了往上偏移,所以你可以看到AppBarLayout 往上偏移,那么ViewPager 為啥也向上偏移.因為ViewPager的ScrollingViewBehavior 中
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
對AppBarLayout 進(jìn)行關(guān)聯(lián),當(dāng)AppBarLayout 有變化的時候會通知給
ScrollingViewBehavior 的onDependentViewChanged 方法中.
通過在這個方法中進(jìn)行對ViewPager的位置也進(jìn)行偏移.使他們一起往上偏移.所以看起來想兩個一起往上偏移,這個也是酷酷的.
Scroll 事件
當(dāng)Touch 事件在ViewPager中. 因為ViewPager中的使用的RecyclerView控件,而RecyclerView 是使用Nest來和其他控件一起處理Scroll事件.RecyclerView 的Nest的事件會一層一層的上傳Scroll 事件,被最近的NestedScrollingParent 接受,這里是CoordinatorLayout ,CoordinatorLayout是一個協(xié)調(diào)者的角色,他將Nest的事件分發(fā)給子控件的View的Behavior處理.
在這里都會被AppBarLayout的Behavior接受.它會根據(jù)getTotalScrollRange,getDownNestedPreScrollRange,getUpNestedPreScrollRange來進(jìn)行想對應(yīng)的偏移. 效果在上面已經(jīng)講了.
關(guān)于Nest 來處理 Scroll 事件:
當(dāng) NestedScrollingChild(下面用Child代替) 要開始滑動的時候會發(fā)送 onStartNestedScroll 請求給最近的NestedScrollingParent(下面用Parent代替). 當(dāng)onStartNestedScroll 返回 true 表示同意一起處理 Scroll 事件的時候時候Child會發(fā)送onNestedScrollAccepted 通知 讓Parent去做一些準(zhǔn)備動作,當(dāng)Child 要開始滑動的時候,會先發(fā)送onNestedPreScroll 請求給Parent ,告訴它我現(xiàn)在要滑動多少米了,你覺得行不行,這時候Parent 根據(jù)實際情況告訴Child 現(xiàn)在只允許你滑動多少.然后 Child 根據(jù) onNestedPreScroll 中傳遞回來的信息對滑動距離做相對應(yīng)的調(diào)整.在滑動的過程中 Child 會發(fā)送onNestedScroll通知告知Parent 當(dāng)前 Child 的滑動情況. 當(dāng)要進(jìn)行滑行的時候,會先發(fā)送onNestedFling 請求給Parent,告訴它 我現(xiàn)在要滑行了,你說行不行, 這時候Parent會根據(jù)情況告訴 Child 你是否可以滑行. Child 通過onNestedFling 返回的 Boolean 值來覺得是否進(jìn)行滑行.如果要滑行的話,會在滑行的時候發(fā)送onNestedFling 通知告知 Parent 滑行情況.當(dāng)滑動事件結(jié)束就會發(fā)送onStopNestedScroll 通知 Parent 去做相關(guān)操作.
主意點:
- Parent 告知 Child 現(xiàn)在允許你滑動多少是通過
onNestedPreScroll中的數(shù)組int[] consumed ,consumed[0]表示 Parent 在 X 軸消耗的量, 所以 Child 滑動距離是請求X軸的滑動距離上面減少consumed[0],consumed[1]表示 Y軸上面的消耗.
因為consumed是數(shù)組,所以Child可以完成可以拿到數(shù)據(jù),而不需要onNestedPreScroll 的返回值.
- 重點注意講解中的請求和通知.
尾巴
詳情界面我也大概看了一遍..機(jī)制差不多,其實就是多了CollapsingToolbarLayout這個的好玩的控件.所以這個學(xué)習(xí)筆記不一定有2.呵呵
同時我會在最近的寫一些有意思的 Behavior 出來.
歡迎大家關(guān)注 我的github,我的微博.