AppBarLayout
這個(gè)玩意去年就特別火了,主要是因?yàn)楹糜茫凑乙呀?jīng)在 app 里面用CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout+Toolbar實(shí)現(xiàn)了好多讓 iOS 目瞪狗呆的效果。不過話說回來,實(shí)現(xiàn)歸實(shí)現(xiàn),每次實(shí)現(xiàn)都是去找別人的博客,然后一頓 CV 大法,然后屬性參數(shù)到處亂配置,最終效果達(dá)到,然后提交代碼不管。
至于各個(gè)類是干嘛的,有哪些方法,我都不 care。當(dāng)然咯,程序員首先得先滿足產(chǎn)品的需求,CV 大法的前提也是你知道CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout+Toolbar這些東西能夠?qū)崿F(xiàn)你的需求。所以,程序員的見識很重要。這里我給廣大的 Android 猿們推薦一款Chrome瀏覽器插件“掘金”。對,沒錯(cuò),就是稀土掘金發(fā)布的,我覺得這個(gè)插件用起來別稀土掘金的官網(wǎng)簡潔多了,主要是便捷,節(jié)省信息檢索時(shí)間。扯遠(yuǎn)了,會用是程序員的最低要求,想要更進(jìn)一步,當(dāng)然是去看源碼,理解如何實(shí)現(xiàn)。
* AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of
* material designs app bar concept, namely scrolling gestures.
* <p>
* Children should provide their desired scrolling behavior through
* {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute:
* {@code app:layout_scrollFlags}.
*
* <p>
* This view depends heavily on being used as a direct child within a {@link CoordinatorLayout}.
* If you use AppBarLayout within a different {@link ViewGroup}, most of it's functionality will
* not work.
* <p>
* AppBarLayout also requires a separate scrolling sibling in order to know when to scroll.
* The binding is done through the {@link ScrollingViewBehavior} behavior class, meaning that you
* should set your scrolling view's behavior to be an instance of {@link ScrollingViewBehavior}.
* A string resource containing the full class name is available.
……省略了一個(gè) xml 布局 demo
簡單翻譯一下吧,反正我英語不好,翻譯的也不一定對~
AppBarLayout是一個(gè)實(shí)現(xiàn)了許多 MaterialDesign app bar 思想(即滾動手勢)的垂直布局。子控件需要通過setScrollFlags()或 app:"layout_scrollFlags"來提供他們的滑動行為。這個(gè) View 作為一個(gè)子 View,對 CoordinatorLayout 依賴性很強(qiáng),如果CoordinatorLayout不是父View,很多功能會失效。最后一句話翻譯起來有點(diǎn)別扭,就是說 AppBarLayout 需要給他依賴度 View 設(shè)置 ScrollingViewBehavoir 來監(jiān)聽依賴的 View 什么時(shí)候滾動。
按照國際慣例,我們先看一下 attrs 和 public 方法把
attributes
<declare-styleable name="AppBarLayout">
<attr name="elevation"/>
<attr name="android:background"/>
<attr format="boolean" name="expanded"/>
</declare-styleable>
<declare-styleable name="AppBarLayoutStates">
<attr format="boolean" name="state_collapsed"/>
<attr format="boolean" name="state_collapsible"/>
</declare-styleable>
<declare-styleable name="AppBarLayout_Layout">
<attr name="layout_scrollFlags">
<flag name="scroll" value="0x1"/>
<flag name="exitUntilCollapsed" value="0x2"/>
<flag name="enterAlways" value="0x4"/>
<flag name="enterAlwaysCollapsed" value="0x8"/>
<flag name="snap" value="0x10"/>
</attr>
<attr format="reference" name="layout_scrollInterpolator"/>
</declare-styleable>
- expanded 是否展開
- AppBarLayoutStates 我也不知道這玩意是干嘛的,以后知道了再來修改
- layout_scrollFlags 這個(gè)屬性是用來控制子 view 的伴隨滾動處理,一共有5個(gè)值,5個(gè)值之間是可以進(jìn)行或運(yùn)算的,也就是說可以同時(shí)設(shè)置多種狀態(tài)。
為了便于理解這5個(gè)值得效果,我從源碼里面找到了這5個(gè)值的解釋
/**
* The view will be scroll in direct relation to scroll events. This flag needs to be
* set for any of the other flags to take effect. If any sibling views
* before this one do not have this flag, then this value has no effect.
* 1.view 會和滾動事件關(guān)聯(lián)。
* 2.如果要設(shè)置其他任何flag,必須同時(shí)設(shè)置這個(gè) flag
* 3.如果在這個(gè) view 之前,沒有任何同層級 view 設(shè)置過這個(gè) flag,那么這個(gè)值也沒有任何效果
*/
public static final int SCROLL_FLAG_SCROLL = 0x1;
/**
* When exiting (scrolling off screen) the view will be scrolled until it is
* 'collapsed'. The collapsed height is defined by the view's minimum height.
*當(dāng)上拉的時(shí)候,這個(gè) view 也會滾動,直到滾動到最小高度,固定在屏幕頂部
* @see ViewCompat#getMinimumHeight(View)
* @see View#setMinimumHeight(int)
*/
public static final int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED = 0x2;
/**
* When entering (scrolling on screen) the view will scroll on any downwards
* scroll event, regardless of whether the scrolling view is also scrolling. This
* is commonly referred to as the 'quick return' pattern.
* 當(dāng)下拉的時(shí)候,優(yōu)先顯示被隱藏的 view
*/
public static final int SCROLL_FLAG_ENTER_ALWAYS = 0x4;
/**
* An additional flag for 'enterAlways' which modifies the returning view to
* only initially scroll back to it's collapsed height. Once the scrolling view has
* reached the end of it's scroll range, the remainder of this view will be scrolled
* into view. The collapsed height is defined by the view's minimum height.
* 下拉的時(shí)候優(yōu)先顯示被隱藏的 view
* @see ViewCompat#getMinimumHeight(View)
* @see View#setMinimumHeight(int)
*/
public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 0x8;
/**
* Upon a scroll ending, if the view is only partially visible then it will be snapped
* and scrolled to it's closest edge. For example, if the view only has it's bottom 25%
* displayed, it will be scrolled off screen completely. Conversely, if it's bottom 75%
* is visible then it will be scrolled fully into view.
* 就是一個(gè)自動回滾的效果,比如說滑動到25%松手,就會自動滾回0
*/
public static final int SCROLL_FLAG_SNAP = 0x10;
這里的設(shè)計(jì)挺棒的,我簡單提一下,用位運(yùn)算,一個(gè) int 值記錄了5種狀態(tài)的排列組合。一共五個(gè)狀態(tài),五個(gè)不同的 flag,但是源碼里面,就用了一個(gè) int 型的變量就記錄了五個(gè)不同狀態(tài)的排列與組合。正常如果是我們自己寫的話,是不是一不小心就定義了5個(gè)變量去記錄這些值,比如說:mCanScroll,mSnap,然后代碼里面會有類似的代碼:“if(mSnap)do sth”。好了,不扯遠(yuǎn)了,五個(gè) flag 的值分別是1、2、4、8、16,轉(zhuǎn)換成二進(jìn)制分別占了第0、1、2、3、4個(gè)位數(shù),第 n 個(gè)位數(shù)如果為0,則沒有這個(gè) flag,為1則表示有。比如scroll|enterAlways 這個(gè)flag,位運(yùn)算|就是1|4,得到的值是5,然后賦值給了 mFlag,這個(gè) mFlag 則表示scroll、enterAlways兩種狀態(tài),然后如果要判斷是否可以 scroll,則只需要 mFlag&scroll==scroll即可。
類似的代碼設(shè)計(jì)還有 manifeast里面的 android:windowSoftInputMode="adjustPan|adjustResize|stateVisible"
不知道我說明百了沒,沒看懂的小伙伴可以跳過。。。。。
Public methods
- addOnOffsetChangedListener 添加便宜量監(jiān)聽,就是監(jiān)聽 AppBarLayout 的可見高度變化
- removeOnOffsetChangedListener 移除
- setExpanded 設(shè)置展開或者收縮
- generateLayoutParams 生成 LayoutParams。一般用不到
- setOrientation 不用關(guān)心的方法,方向只能是 vertical
- getTotalScrollRange 獲取最大滾動偏移量
- setTargetElevation 設(shè)置 Z 軸高度
CollapsingToolbarLayout
- CollapsingToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar.
- It is designed to be used as a direct child of a {@link AppBarLayout}.
- CollapsingToolbarLayout contains the following features:
- <h4>Collapsing title</h4>
- A title which is larger when the layout is fully visible but collapses and becomes smaller as
- the layout is scrolled off screen. You can set the title to display via
- {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the
- {@code collapsedTextAppearance} and {@code expandedTextAppearance} attributes.
- <h4>Content scrim</h4>
- A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold.
- You can change this via {@link #setContentScrim(Drawable)}.
- <h4>Status bar scrim</h4>
- A scrim which is show or hidden behind the status bar when the scroll position has hit a certain
- threshold. You can change this via {@link #setStatusBarScrim(Drawable)}. This only works
- on {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} devices when we set to fit system
- windows.
- <h4>Parallax scrolling children</h4>
- Child views can opt to be scrolled within this layout in a parallax fashion.
- See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and
- {@link LayoutParams#setParallaxMultiplier(float)}.
- <h4>Pinned position children</h4>
- Child views can opt to be pinned in space globally. This is useful when implementing a
- collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is
- moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}.
- <p><strong>Do not manually add views to the Toolbar at run time</strong>.
- We will add a 'dummy view' to the Toolbar which allows us to work out the available space
- for the title. This can interfere with any views which you add.</p>
咦,copy 出來的類注釋竟然支持 MarkDown 排版,哈哈哈哈
好了,不說題外話,先看類注釋吧~
一共五個(gè)小標(biāo)題
- Collapsing title 折疊標(biāo)題
- Content scrim 內(nèi)容布
- Status bar scrim 狀態(tài)欄布
- parallax scrolling children 視差滾動子 View
- pinned position children 固定子 view 的位置
總結(jié):如果需要折疊標(biāo)題之類的如上功能,則把 AppBarLayout 里面的所有子 view 移到CollapsingToolbarLayout節(jié)點(diǎn)下,然后把CollapsingToolbarLayout作為 AppBarLayout 的唯一子節(jié)點(diǎn)。
attributes
<declare-styleable name="CollapsingToolbarLayout">
<attr format="dimension" name="expandedTitleMargin"/>
<attr format="dimension" name="expandedTitleMarginStart"/>
<attr format="dimension" name="expandedTitleMarginTop"/>
<attr format="dimension" name="expandedTitleMarginEnd"/>
<attr format="dimension" name="expandedTitleMarginBottom"/>
<attr format="reference" name="expandedTitleTextAppearance"/>
<attr format="reference" name="collapsedTitleTextAppearance"/>
<attr format="color" name="contentScrim"/>
<attr format="color" name="statusBarScrim"/>
<attr format="reference" name="toolbarId"/>
<attr format="dimension" name="scrimVisibleHeightTrigger"/>
<attr format="integer" name="scrimAnimationDuration"/>
<attr name="collapsedTitleGravity">
<flag name="top" value="0x30"/>
<flag name="bottom" value="0x50"/>
<flag name="left" value="0x03"/>
<flag name="right" value="0x05"/>
<flag name="center_vertical" value="0x10"/>
<flag name="fill_vertical" value="0x70"/>
<flag name="center_horizontal" value="0x01"/>
<flag name="center" value="0x11"/>
<flag name="start" value="0x00800003"/>
<flag name="end" value="0x00800005"/>
</attr>
<attr name="expandedTitleGravity">
<flag name="top" value="0x30"/>
<flag name="bottom" value="0x50"/>
<flag name="left" value="0x03"/>
<flag name="right" value="0x05"/>
<flag name="center_vertical" value="0x10"/>
<flag name="fill_vertical" value="0x70"/>
<flag name="center_horizontal" value="0x01"/>
<flag name="center" value="0x11"/>
<flag name="start" value="0x00800003"/>
<flag name="end" value="0x00800005"/>
</attr>
<attr format="boolean" name="titleEnabled"/>
<attr name="title"/>
</declare-styleable>
<declare-styleable name="CollapsingToolbarLayout_Layout">
<attr name="layout_collapseMode">
<enum name="none" value="0"/>
<enum name="pin" value="1"/>
<enum name="parallax" value="2"/>
</attr>
<attr format="float" name="layout_collapseParallaxMultiplier"/>
</declare-styleable>
- expandedTitleMargin 展開時(shí) title 的 margin
- expandedTitleTextAppearance 展開時(shí)候title 的文字 style
- contentScrim 在縮放時(shí),內(nèi)容遮蓋的顏色
- statusBarScrim 狀態(tài)欄顏色
- toolbarId 指定了 toolbar 而已,用不用無所謂,源碼里面有就用,沒有就遍歷子 View 找到 toolbar。
- scrimVisibleHeightTrigger 設(shè)置收起多少高度時(shí),顯示內(nèi)容遮蓋顏色
- scrimAnimationDuration 內(nèi)容遮蓋顏色動畫持續(xù)時(shí)間
- collapsedTitleGravity 折疊時(shí),title 的位置
- expandedTitleGravity 展開時(shí),title 的位置
- titleEnabled 是否開啟折疊 title
- layout_collapseMode
- none 跟隨滾動的手勢進(jìn)行折疊
- parallax 視差滾動
- pin 不動
- layout_collapseParallaxMultiplier 滾動因子,取值0-1,1是完全不動
public methods
此處省略 N 個(gè)方法,都是和 attrs對應(yīng)的屬性修改/獲取方法。
問題
可能有些同學(xué)會遇到statusBarScrim不生效的情況,反正我是碰到過,原因是因?yàn)楸幌到y(tǒng)的 statusBar 覆蓋了,在 style 里面或者 activity 里面把狀態(tài)欄設(shè)為透明的就好。
Demo
說了這么久,寫個(gè) demo 吧,把上面講到的東西盡量用一個(gè) demo 演示出來,不過我感覺效果大家應(yīng)該都看到過~~~
就一個(gè)這樣的效果吧,沒有什么特別的特色,當(dāng)然如果讓我自己手?jǐn)]我表示很操蛋~~
1.滑動 ScrollView/RecyclerView 的時(shí)候 優(yōu)先把頂部的圖片頂上去,然后固定TabLayout ,再滾動 ScrollView/RecyclerView 的內(nèi)容,下拉的時(shí)候可以設(shè)置優(yōu)先拖出圖片或者拉到頂部在拖出圖片。
2.Toolbar 的 title 伴隨滾動移動位置和改變顏色,圖片滾動到一定位置的時(shí)候會漸變一個(gè)主題色的蒙版遮蓋住。
xml 代碼實(shí)現(xiàn)
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
...省略內(nèi)容
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:collapsedTitleTextAppearance="@style/ToolbarTextAppearanceTitle"
app:contentScrim="@color/colorPrimary_pinkDark"
app:expandedTitleGravity="center_horizontal|bottom"
app:expandedTitleTextAppearance="@style/expandedToolbarTextAppearance"
app:layout_scrollFlags="scroll|snap|enterAlways"
app:scrimAnimationDuration="2000"
app:scrimVisibleHeightTrigger="40dp"
app:titleEnabled="true">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@mipmap/material_img"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.5"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll"
app:navigationIcon="@mipmap/abc_ic_ab_back_mtrl_am_alpha"
app:title="湖南農(nóng)業(yè)大學(xué)校歌"/>
</android.support.design.widget.CollapsingToolbarLayout>
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
/>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
源碼分析
還是寫點(diǎn)源碼分析吧,感覺不分析一下源碼就相當(dāng)于只學(xué)了一個(gè) api,以后出現(xiàn)類似的特效,然后現(xiàn)有的東西不能滿足定制,我們也能模仿這些效果自己手?jǐn)]出來。
好了,說正事~
今天的源碼分析就不一行一行的看代碼了,我們就根據(jù)上面的效果來分析怎么實(shí)現(xiàn)的把
1.滑動 ScrollView/RecyclerView 的時(shí)候 優(yōu)先把頂部的圖片頂上去,然后固定TabLayout ,再滾動 ScrollView/RecyclerView 的內(nèi)容,下拉的時(shí)候可以設(shè)置優(yōu)先拖出圖片或者拉到頂部在拖出圖片。
2.Toolbar 的 title 伴隨滾動移動位置和改變顏色,圖片滾動到一定位置的時(shí)候會漸變一個(gè)主題色的蒙版遮蓋住。
額,這里不止兩個(gè)點(diǎn),不糾結(jié)了,一個(gè)一個(gè)來吧
- 我們給 ScrollView/RecyclerView 設(shè)置了 Behavior,在滑動的過程中,會調(diào)用 Behavior 里面的onStartNestedScroll、onNestedPreScroll、onNestedScroll、onStopNestedScroll等方法,然后 Behavior 持有對 AppBarLayout 的引用,會在這些方法里面根據(jù)狀態(tài)做一系列的事情。至于這個(gè) Behavior 是怎么調(diào)用的,我會在下一篇里面重點(diǎn)講 Behavior。
- 這里的效果實(shí)現(xiàn)全部由CollapsingToolbarLayout,主要是 title 的位置和顏色, 然后就是mContentScrim和 mStatusBarScrim 這兩個(gè)遮蓋布的繪制,方法很簡單
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// If we don't have a toolbar, the scrim will be not be drawn in drawChild() below.
// Instead, we draw it here, before our collapsing text.
ensureToolbar();
if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) {
mContentScrim.mutate().setAlpha(mScrimAlpha);
mContentScrim.draw(canvas);
}
// Let the collapsing text helper draw its text
if (mCollapsingTitleEnabled && mDrawCollapsingTitle) {
mCollapsingTextHelper.draw(canvas);
}
// Now draw the status bar scrim
if (mStatusBarScrim != null && mScrimAlpha > 0) {
final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
if (topInset > 0) {
mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(),
topInset - mCurrentOffset);
mStatusBarScrim.mutate().setAlpha(mScrimAlpha);
mStatusBarScrim.draw(canvas);
}
}
}
ensureToolbar()再次確保了有一次對子 view toolbar 的引用。然后就是三個(gè) if 控制繪制 contentScrim、CollapsingText、statusBarScrim。
其中CollapsingTextHelper保存了折疊 TextTitle 的各種繪制信息。
可能有人會問,如何控制 contentScrim,剛剛我們在 draw()的方法里面看到了判斷條件,如果mScrimAlpha>0 則繪制,那么我們可以大膽的猜測,肯定是在收縮的過程中根據(jù)高度設(shè)置 mScrimAlpha來控制顏色布的顯示與隱藏。
final void updateScrimVisibility() {
if (mContentScrim != null || mStatusBarScrim != null) {
setScrimsShown(getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger());
}
}
這個(gè)方法控制了 mScrimAlpha,getScrimVisibleHeightTrigger()方法獲取scrimVisibleHeightTrigger這個(gè)屬性大家肯定也不陌生。然后我們通過搜索發(fā)現(xiàn)updateScrimVisibility的調(diào)用在onLayout里面。
熟悉 view 繪制流程的童鞋肯這時(shí)候應(yīng)該都懂了吧。我們在滾動的時(shí)候高度是不斷發(fā)生變化的,而我們的高度發(fā)生變化則會重新 onMeasure,onMeasure 之后則會調(diào)用 onLayout,然后 onLayout里面調(diào)用updateScrimVisibility修改了 mScrimAlpha 的值,最后在 draw 方法里面繪制出來。
好了,就到這里吧,這里沒有酷炫的 demo,什么防簡書首頁、仿知乎等等,但是看懂了這些api,我相信都能夠自己動手防一個(gè)。
有點(diǎn)懶,很多應(yīng)該錄 gif 圖的都沒錄,還請諒解~
不諒解也沒事,反正你也打不到我