Android開發(fā)之CoordinatorLayout打造滑動越界彈性放大圖片效果

寫這篇文章的初衷來自最近項(xiàng)目中的一個(gè)需求,查閱了網(wǎng)上的一些資料,貌似大家都熱衷于用ScrollView+HeaderView去實(shí)現(xiàn),根據(jù)手勢判斷晤愧,去做圖片的矩陣放大,然后不斷的讓ImageView去requestLayout重新計(jì)算高度蛉腌,也不是說不行官份,只是當(dāng)布局嵌套層級多的時(shí)候,這種重復(fù)測量的方式烙丛,性能上會出現(xiàn)問題的舅巷,好了,話不多說河咽,直接進(jìn)入正題钠右。

這里再補(bǔ)充一下,之前寫過一篇《高仿美團(tuán)APP頁面滑動標(biāo)題欄漸變效果》有一些朋友問我另類的實(shí)現(xiàn)方式和其它的適配問題忘蟹,這幾天會抽時(shí)間修整并同步到github飒房,當(dāng)然更推薦用本篇文章的實(shí)現(xiàn)方式去做。

CoordinatorLayout是Google在android.support.design包中提供的一個(gè)新的視圖布局媚值,它繼承于ViewGroup狠毯,其布局方式類似于FrameLayout,同時(shí)引入了一套全新的事件處理方式Behavior褥芒,它允許開發(fā)者自定義Behavior來實(shí)現(xiàn)一些復(fù)雜的UI交互效果垃你,通過組合和代理模式將View的事件處理邏輯抽取出來,達(dá)到更漂亮的松散耦合喂很。

下面來看下我們今天要實(shí)現(xiàn)的最終效果圖:


下拉越界彈性放大效果

認(rèn)識CoordinatorLayout

CoordinatorLayout是一個(gè)“布局協(xié)調(diào)者”惜颇,用來協(xié)調(diào)布局內(nèi)子View之間的關(guān)系(狀態(tài),大小少辣,位置等)凌摄,可以讓開發(fā)者靈活的定制協(xié)調(diào)規(guī)則。

關(guān)于子View之間的協(xié)調(diào)規(guī)則漓帅,我們需要用到上文提到的Behavior锨亏,它是CoordinatorLayout類下的一個(gè)抽象類痴怨,在實(shí)現(xiàn)類中需要指定一個(gè)泛型View,這個(gè)泛型View也就是CoordinatorLayout下所作用的子View器予,通常情況下浪藻,我們指定View即可。

    public static abstract class Behavior<V extends View> {
        ...
}

還有一點(diǎn)很重要乾翔,當(dāng)我們在自定義Behavior的時(shí)候爱葵,一定要覆寫帶參的構(gòu)造方法,因?yàn)樵贑oordinatorLayout源碼中parseBehavior是通過反射來調(diào)用這個(gè)構(gòu)造方法的反浓,關(guān)鍵代碼如下:

        public Behavior(Context context, AttributeSet attrs) {
        }
  static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
            Context.class,
            AttributeSet.class
    };
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                        context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);

下面來說下如何制定協(xié)調(diào)規(guī)則萌丈,我們可以把“布局協(xié)調(diào)”分成兩大類:
1、View之間的相互關(guān)系
2雷则、View與滑動之間的相互關(guān)系

View之間的相互關(guān)系

既然是View之間的相互關(guān)系辆雾,那么這里的View肯定不會只有一個(gè),也從中引出了“作用View”和“被依賴View”的概念月劈,作用View隨著被依賴View狀態(tài)的變化而變化度迂,有點(diǎn)類似于觀察模式中的觀察者和被觀察者(當(dāng)被觀察者的狀態(tài)發(fā)生變化,通知觀察者猜揪,觀察者也要做出對應(yīng)狀態(tài)的變化)惭墓,這里我們需要關(guān)注layoutDependsOn與onDependentViewChanged兩個(gè)方法:
layoutDependsOn:作用View是否依賴被依賴View,依賴為true湿右,不依賴為false。
onDependentViewChanged:當(dāng)被依賴View發(fā)生狀態(tài)變化時(shí)罚勾,作用View應(yīng)該跟隨做出怎么樣的變化毅人。

舉個(gè)例子,我們設(shè)置2個(gè)TextView尖殃,讓其中一個(gè)TextView(作用View)隨著另一個(gè)TextView(被依賴View)位置的移動而移動丈莺,效果圖如下:


<?xml version="1.0" encoding="utf-8"?>
<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">

    <TextView
        android:id="@+id/tv_layout_child"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_margin="5dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text="Child"
        android:textAllCaps="false"
        android:textColor="#FFFFFF"
        app:layout_behavior="com.lcw.coordinatorlayout.MoveViewBehavior" />

    <TextView
        android:id="@+id/tv_layout_dependency"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_gravity="right"
        android:layout_margin="5dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:text="Dependency"
        android:textColor="#FFFFFF" />

</android.support.design.widget.CoordinatorLayout>
   findViewById(R.id.tv_layout_dependency).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ViewCompat.offsetTopAndBottom(v, 30);
            }
        });
package com.lcw.coordinatorlayout;

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;

/**
 * 跟隨所依賴的View的Y軸移動
 * Create by: chenwei.li
 * Date: 2018/5/20
 * Time: 上午10:39
 * Email: lichenwei.me@foxmail.com
 */
public class MoveViewBehavior extends CoordinatorLayout.Behavior<View> {

    public MoveViewBehavior() {
    }

    /**
     * 需要重寫構(gòu)造方法,在CoordinatorLayout源碼中是通過反射拿到Behavior的
     *
     * @param context
     * @param attrs
     */
    public MoveViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 確定是否依賴dependency
     *
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        //return dependency instanceof TextView;
        return dependency.getId() == R.id.tv_layout_dependency;
    }

    /**
     * 如果確定依賴dependency送丰,那么child跟隨dependency需要做出什么變化(child的位置缔俄,大小,狀態(tài)等)
     *
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int offsetY = dependency.getTop() - child.getTop();
        ViewCompat.offsetTopAndBottom(child, offsetY);
        return super.onDependentViewChanged(parent, child, dependency);
    }
}

我們簡單分析一下器躏,我們自定義了一個(gè)Behavior俐载,讓其繼承CoordinatorLayout.Behavior并指定泛型為通用View(TextView也可以),然后覆寫它的構(gòu)造方法登失,在layoutDependsOn方法中遏佣,我們對View的id(或類型)進(jìn)行判斷,確定被依賴的View揽浙,這里的child指作用View状婶,dependency指被依賴的View意敛,然后我們在onDependentViewChanged方法中,讓作用View跟隨被依賴View做出位置調(diào)整膛虫,也就是child跟隨dependency位置的移動而移動草姻,然后xml里就非常簡單了,我們指定app:layout_behavior為我們的自定義Behavior稍刀,并作用在作用View上撩独,這里需要注意的是,指定app:layout_behavior屬性的一定要是CoordinatorLayout的直接子View才會生效掉丽,因?yàn)檫@個(gè)屬性存在于CoordinatorLayout.LayoutParams里跌榔。

通過上面的操作我們讓View的邏輯操作與業(yè)務(wù)解耦,我們不需要在activity或者fragment里再去寫一些關(guān)于View與View之間的關(guān)系事件捶障,我們只需要定義好Behavior僧须,然后在xml布局文件中配置即可,當(dāng)然這些還不夠项炼,我們繼續(xù)往下看担平。

View與滑動之間的關(guān)系

開門見山,先介紹基礎(chǔ)的2個(gè)方法锭部,onStartNestedScroll和onNestedPreScroll:
onStartNestedScroll:是否要讓Behavior處理滑動暂论,處理返回true,不處理返回false拌禾,如果返回為false則不走Behavior剩下的方法取胎。
onNestedPreScroll:具體怎么處理滑動的方法。

舉個(gè)例子湃窍,我們設(shè)置2個(gè)ScrollView闻蛀,讓其中一個(gè)ScrollView(隨著另一個(gè)ScrollView滑動而滑動,效果圖如下:


<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/ns_layout_child"
        android:layout_width="80dp"
        android:layout_height="match_parent"
        android:layout_margin="5dp"
        android:background="@color/colorAccent"
        android:textColor="#FFFFFF"
        app:layout_behavior="com.lcw.coordinatorlayout.ScrollerViewBehavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="A\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\n"
            android:textSize="20sp" />
    </android.support.v4.widget.NestedScrollView>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/ns_layout_dependency"
        android:layout_width="80dp"
        android:layout_height="match_parent"
        android:layout_gravity="right"
        android:layout_margin="5dp"
        android:background="@color/colorPrimary"
        android:textColor="#FFFFFF">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="A\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\nA\nB\nC\nD\nE\nF\nG\n"
            android:textSize="20sp" />
    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>
package com.lcw.coordinatorlayout;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;

/**
 * 跟隨所依賴的View滾動
 * Create by: chenwei.li
 * Date: 2018/5/20
 * Time: 上午10:39
 * Email: lichenwei.me@foxmail.com
 */
public class ScrollerViewBehavior extends CoordinatorLayout.Behavior<View> {

    public ScrollerViewBehavior() {
    }

    /**
     * 需要重寫構(gòu)造方法您市,在CoordinatorLayout源碼中是通過反射拿到Behavior的
     *
     * @param context
     * @param attrs
     */
    public ScrollerViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    /**
     * 是否要處理滑動
     *
     * @param coordinatorLayout
     * @param child
     * @param directTargetChild
     * @param target
     * @param axes
     * @param type
     * @return
     */
    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        return true;
    }

    /**
     * 具體滑動處理
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dx
     * @param dy
     * @param consumed
     * @param type
     */
    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        int offsetY = target.getScrollY();
        child.setScrollY(offsetY);
    }
}

我們在onStartNestedScroll方法中返回true觉痛,代表我們要處理滑動,然后我們在onNestedPreScroll方法中監(jiān)聽target(被依賴View)在Y軸的滑動距離茵休,然后讓其child(作用View)也跟隨滑動薪棒。
關(guān)于滑動所涉及的方法還有很多,一起來看下:

onStartNestedScroll:當(dāng)手指按下屏幕的時(shí)候觸發(fā)榕莺,用來決定是否要讓Behavior處理這次滑動俐芯,true為處理,false為不處理钉鸯,如果不處理泼各,那么Behavior的后續(xù)方法也就不會在再調(diào)用了,方法中也提供了一些輔助參數(shù)亏拉,比如type扣蜻,可以用來判斷用戶動作逆巍,比如是TYPE_TOUCH按住屏幕拖動,TYPE_NON_TOUCH快速拉動屏幕等莽使。

onNestedScrollAccepted:在Behavior處理這次滑動前調(diào)用(onStartNestedScroll返回true)锐极,可以在這里做一些初始化操作。

onNestedPreScroll:滑動即將開始芳肌,這個(gè)方法有個(gè)參數(shù) int[] consumed灵再,可以用來表示做了多少位移,假設(shè)用戶滑動了100px亿笤,你做了 90px 的位移翎迁,那么就需要把 consumed[1] 改成 90(下標(biāo) 0、1 分別對應(yīng) x净薛、y 軸)汪榔,這樣就可以讓后續(xù)的方法去處理這10px。

onNestedScroll:上一個(gè)方法結(jié)束后肃拜,剩下的滑動位移(dxUnconsumed痴腌、dyUnconsumed)未處理的,可以在這里處理燃领。

onNestedPreFling:當(dāng)用戶快速滑動屏幕士聪,產(chǎn)生慣性滑動的時(shí)候,會觸發(fā)此方法猛蔽,這個(gè)方法參數(shù)中提供了滑動方向與速度剥悟。

onStopNestedScroll:滑動停止的時(shí)候調(diào)用,如果沒有發(fā)生慣性滑動曼库,那么會直接到這個(gè)方法区岗。

以上這些方法不需要都覆寫,可以我們根據(jù)需要凉泄,靈活使用即可躏尉。

好了蚯根,到這里我們對CoordinatorLayout的基礎(chǔ)知識就已經(jīng)講完了后众,接下來我們結(jié)合AppBarLayout、CollapsingToolbarLayout颅拦、Toolbar打造我們絢麗酷炫的頂部欄效果吧蒂誉。

首先我們來實(shí)現(xiàn)一個(gè)基礎(chǔ)的Material Design效果(不帶下拉越界彈性放大ImageView),也是Google在開發(fā)者大會上給我們示范的效果距帅,來看下效果圖:


基礎(chǔ)的Material Design效果

先來看下xml布局:

<?xml version="1.0" encoding="utf-8"?>
<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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="@color/md_deep_purple_600"
            app:expandedTitleMarginEnd="64dp"
            app:expandedTitleMarginStart="48dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:minHeight="100dp"
                android:scaleType="centerCrop"
                android:src="@mipmap/test"
                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />

        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

有點(diǎn)懵右锨,一下子多出這么控件和屬性,不著急碌秸,我們從上往下一個(gè)個(gè)解釋:

CoordinatorLayout: 這個(gè)上文已經(jīng)說的夠多了绍移,就不再做過多闡述悄窃。

AppBarLayout:它繼承于LinearLayout,布局方向?yàn)榇怪滨褰眩梢园阉?dāng)成垂直布局方向的LinearLayout來用轧抗,它擴(kuò)展一些功能,比如它可以監(jiān)聽到某個(gè)View的滾動狀態(tài)瞬测,然后通知它布局內(nèi)的子View做出相對應(yīng)的狀態(tài)改變横媚,而這個(gè)子View應(yīng)該如何變化取決于子View的layout_scrollFlags屬性設(shè)置,這里有scroll月趟、enterAlways灯蝴、enterAlwaysCollapsed、exitUntilCollapsed孝宗、snap5種屬性值穷躁,后面四種作用于第一種之上,也就是不能離開scroll單獨(dú)存在碳褒,我們分別來看下:

<?xml version="1.0" encoding="utf-8"?>
<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.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?android:attr/actionBarSize"
            android:theme="@style/ThemeOverlay.AppCompat.Dark"
            app:layout_scrollFlags="屬性值"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="文字內(nèi)容" />

    </android.support.v4.widget.NestedScrollView>


</android.support.design.widget.CoordinatorLayout>

scroll:當(dāng)屬性值為scroll的時(shí)候折砸,會隨著NestedScrollView的滾動一起滾動(好像是ScrollView的HeaderView一樣,融為一體)

app:layout_scrollFlags="scroll"

enterAlways: 當(dāng)屬性值為scroll|enterAlways的時(shí)候沙峻,當(dāng)NestedScrollView往下滾的時(shí)候睦授,先響應(yīng)View的滾動,再響應(yīng)NestedScrollView的往下滾動摔寨。

app:layout_scrollFlags="scroll|enterAlways"

exitUntilCollapsed:當(dāng)屬性值為scroll|exitUntilCollapsed的 時(shí)候去枷,在往上滾動的時(shí)候,會優(yōu)先把View縮小為最小高度(minHeight)是复,然后再響應(yīng)NestedScrollView的滾動删顶。

app:layout_scrollFlags="scroll|exitUntilCollapsed"

enterAlwaysCollapsed:當(dāng)屬性值為scroll|enterAlways| enterAlwaysCollapsed的時(shí)候,在往下滾的時(shí)候淑廊,會先把最小高度的View展示出來逗余,然后等NestedScrollView向下滾動結(jié)束,才繼續(xù)響應(yīng)View的滾動事件季惩,撐開為View的最大高度录粱。

app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"

snap:這是一個(gè)滾動比例設(shè)置,類似于ViewPager滑動比例画拾,來決定它是否切換頁面啥繁,而這里就是來確定是否滾出布局。

可能有朋友會問青抛,為什么AppbarLayout可以知道NestedScrollView的滑動狀態(tài)旗闽,請看NestedScrollView布局中的Behavior(appbar_scrolling_view_behavior),這個(gè)是Google為我們提供的(AppbarLayout類下的ScrollingViewBehavior),至于原理相信認(rèn)真讀過上文的你應(yīng)該可以知道了适室,有興趣的朋友自己看下源碼實(shí)現(xiàn)吧嫡意。

Toolbar:簡單點(diǎn)來說,它是用來取代ActionBar的捣辆,但是它比ActionBar更強(qiáng)大鹅很,除了完成ActionBar能實(shí)現(xiàn)的功能,它還可以隨意定制位置罪帖,配合CollapsingToolbarLayout還可以展現(xiàn)出更強(qiáng)大的功能促煮。

CollapsingToolbarLayout:是對Toolbar再一層包裝的ViewGroup,用來實(shí)現(xiàn)折疊效果整袁,需要作為AppbarLayout的直接子View菠齿,它具備如下功能:
折疊Title(Collapsing title):當(dāng)布局內(nèi)容全部顯示出來時(shí),title是最大的坐昙,但是隨著View逐步移出屏幕頂部绳匀,title變得越來越小。你可以通過調(diào)用setTitle函數(shù)來設(shè)置title炸客。

內(nèi)容紗布(Content scrim):根據(jù)滾動的位置是否到達(dá)一個(gè)閥值疾棵,來決定是否對View“蓋上紗布””韵桑可以通過setContentScrim(Drawable)來設(shè)置紗布的圖片.

狀態(tài)欄紗布(Status bar scrim):根據(jù)滾動位置是否到達(dá)一個(gè)閥值決定是否對狀態(tài)欄“蓋上紗布”是尔,你可以通過setStatusBarScrim(Drawable)來設(shè)置紗布圖片,但是只能在LOLLIPOP設(shè)備上面有作用开仰。

視差滾動子View(Parallax scrolling children):子View可以選擇在當(dāng)前的布局當(dāng)時(shí)是否以“視差”的方式來跟隨滾動拟枚。(PS:其實(shí)就是讓這個(gè)View的滾動的速度比其他正常滾動的View速度稍微慢一點(diǎn))。將布局參數(shù)app:layout_collapseMode設(shè)為parallax众弓。

將子View位置固定(Pinned position children):子View可以選擇是否在全局空間上固定位置恩溅,這對于Toolbar來說非常有用,因?yàn)楫?dāng)布局在移動時(shí)谓娃,可以將Toolbar固定位置而不受移動的影響脚乡。 將app:layout_collapseMode設(shè)為pin。

好了滨达,到這里就全部介紹完了奶稠,我們回頭看下剛才的布局文件,首先根布局是CoordinatorLayout弦悉,用來協(xié)調(diào)子View(AppBarLayout和RecyclerView)窒典,AppBarLayout包裹CollapsingToolbarLayout蟆炊,而CollapsingToolbarLayout包裹ToolBar形成一個(gè)增強(qiáng)型的Toolbar(包含圖片和效果遮罩)指定了滾動行為scrollFlags為scroll|exitUntilCollapsed稽莉, 指定了內(nèi)容紗布contentScrim為具體顏色值,圖片設(shè)置了滾動視差collapseMode為parallax涩搓,Toolbar指定了collapseMode為pin污秆,各屬性值的具體含義劈猪,這里不再重復(fù)解釋。

學(xué)了大半天的自定義Behavior沒有派上用場良拼,心里癢癢的战得,現(xiàn)在我們就來實(shí)現(xiàn)一個(gè)基于官方給的基礎(chǔ)Material Design效果的擴(kuò)展,實(shí)現(xiàn)AppbarLayout滑動到底后繼續(xù)往下拉庸推,越界放大圖片的效果常侦。

<?xml version="1.0" encoding="utf-8"?>
<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:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:layout_behavior="com.lcw.ui.widget.AppbarZoomBehavior">

        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:fitsSystemWindows="true"
            app:contentScrim="@color/md_deep_purple_600"
            app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed|exitUntilCollapsed">

            <ImageView
                android:id="@+id/iv_img"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax" />

            <android.support.v7.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:theme="@style/ThemeOverlay.AppCompat.Dark"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

        </android.support.design.widget.CollapsingToolbarLayout>


    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>

這里是xml布局文件,基本和上面的官方示例一樣贬媒,這里設(shè)置ImageView包括它的父布局fitsSystemWindows為true聋亡,是為了可以讓內(nèi)容延伸到系統(tǒng)狀態(tài)欄,讓其更加美觀际乘,然后重點(diǎn)就在于AppbarLayout設(shè)置里的Behavior了坡倔。

package com.lcw.ui.widget;

import android.animation.ValueAnimator;
import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;

import com.lcw.fun.shareforgank.R;

/**
 * 頭部下拉放大Behavior
 * Create by: chenwei.li
 * Date: 2018/5/26
 * Time: 下午9:54
 * Email: lichenwei.me@foxmail.com
 */
public class AppbarZoomBehavior extends AppBarLayout.Behavior {

    private ImageView mImageView;
    private int mAppbarHeight;//記錄AppbarLayout原始高度
    private int mImageViewHeight;//記錄ImageView原始高度

    private static final float MAX_ZOOM_HEIGHT = 500;//放大最大高度
    private float mTotalDy;//手指在Y軸滑動的總距離
    private float mScaleValue;//圖片縮放比例
    private int mLastBottom;//Appbar的變化高度

    private boolean isAnimate;//是否做動畫標(biāo)志


    public AppbarZoomBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
        boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
        init(abl);
        return handled;
    }

    /**
     * 進(jìn)行初始化操作,在這里獲取到ImageView的引用脖含,和Appbar的原始高度
     *
     * @param abl
     */
    private void init(AppBarLayout abl) {
        abl.setClipChildren(false);
        mAppbarHeight = abl.getHeight();
        mImageView = (ImageView) abl.findViewById(R.id.iv_img);
        if (mImageView != null) {
            mImageViewHeight = mImageView.getHeight();
        }
    }

    /**
     * 是否處理嵌套滑動
     *
     * @param parent
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     * @param type
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
        isAnimate = true;
        return true;
    }

    /**
     * 在這里做具體的滑動處理
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dx
     * @param dy
     * @param consumed
     * @param type
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
        if (mImageView != null && child.getBottom() >= mAppbarHeight && dy < 0 && type == ViewCompat.TYPE_TOUCH) {
            zoomHeaderImageView(child, dy);
        } else {
            if (mImageView != null && child.getBottom() > mAppbarHeight && dy > 0 && type == ViewCompat.TYPE_TOUCH) {
                consumed[1] = dy;
                zoomHeaderImageView(child, dy);
            } else {
                if (valueAnimator == null || !valueAnimator.isRunning()) {
                    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
                }

            }
        }

    }


    /**
     * 對ImageView進(jìn)行縮放處理罪塔,對AppbarLayout進(jìn)行高度的設(shè)置
     *
     * @param abl
     * @param dy
     */
    private void zoomHeaderImageView(AppBarLayout abl, int dy) {
        mTotalDy += -dy;
        mTotalDy = Math.min(mTotalDy, MAX_ZOOM_HEIGHT);
        mScaleValue = Math.max(1f, 1f + mTotalDy / MAX_ZOOM_HEIGHT);
        ViewCompat.setScaleX(mImageView, mScaleValue);
        ViewCompat.setScaleY(mImageView, mScaleValue);
        mLastBottom = mAppbarHeight + (int) (mImageViewHeight / 2 * (mScaleValue - 1));
        abl.setBottom(mLastBottom);
    }


    /**
     * 處理慣性滑動的情況
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param velocityX
     * @param velocityY
     * @return
     */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
        if (velocityY > 100) {
            isAnimate = false;
        }
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }


    /**
     * 滑動停止的時(shí)候,恢復(fù)AppbarLayout养葵、ImageView的原始狀態(tài)
     *
     * @param coordinatorLayout
     * @param abl
     * @param target
     * @param type
     */
    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
        recovery(abl);
        super.onStopNestedScroll(coordinatorLayout, abl, target, type);
    }

    ValueAnimator valueAnimator;

    /**
     * 通過屬性動畫的形式征堪,恢復(fù)AppbarLayout、ImageView的原始狀態(tài)
     *
     * @param abl
     */
    private void recovery(final AppBarLayout abl) {
        if (mTotalDy > 0) {
            mTotalDy = 0;
            if (isAnimate) {
                valueAnimator = ValueAnimator.ofFloat(mScaleValue, 1f).setDuration(220);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float value = (float) animation.getAnimatedValue();
                        ViewCompat.setScaleX(mImageView, value);
                        ViewCompat.setScaleY(mImageView, value);
                        abl.setBottom((int) (mLastBottom - (mLastBottom - mAppbarHeight) * animation.getAnimatedFraction()));
                    }
                });
                valueAnimator.start();
            } else {
                ViewCompat.setScaleX(mImageView, 1f);
                ViewCompat.setScaleY(mImageView, 1f);
                abl.setBottom(mAppbarHeight);
            }
        }
    }
}

我們自定義了Behavior关拒,由于是作用在AppbarLayout的请契,所以我們對應(yīng)繼承實(shí)現(xiàn)AppbarLayout下的Behavior抽象類,然后覆寫帶參的構(gòu)造方法夏醉,在AppbarLayout布局的時(shí)候會調(diào)用onLayoutChild方法 爽锥,所以我們可以在這里獲取ImageView和AppbarLayout的原始高度,方便后面做對比和恢復(fù)狀態(tài)操作畔柔,然后在onNestedPreScroll方法里做具體的嵌套滑動處理氯夷,當(dāng)AppbarLayout當(dāng)前的高度大于或者等于原始高度且手指向下滑動(dy<0)且為手指按住屏幕拖動的時(shí)候,我們對ImageView做放大操作同時(shí)使AppbarLayout的高度變大靶擦,反之腮考,當(dāng)滿足其他條件且手指的方向是相反(往上滑),則讓ImageView做縮小操作同時(shí)使AppbarLayout的高度恢復(fù)玄捕,然后我們在onStopNestedScroll方法里處理滑動停止時(shí)候的狀態(tài)踩蔚,這里利用屬性動畫讓高度恢復(fù),這里在fling(慣性滑動)中進(jìn)行了一個(gè)速度限制枚粘,這個(gè)是避免當(dāng)用戶滑動的時(shí)候手抖出發(fā)快速滑動馅闽,導(dǎo)致頻繁觸發(fā)屬性動畫出現(xiàn)的位置顯示錯(cuò)誤。

好了,到這里內(nèi)容就結(jié)束了福也,有什么疑問局骤,歡迎大家在評論給我留言~

源碼下載:

這里附上源碼地址(歡迎Star,歡迎Fork):整理中暴凑,稍后放出峦甩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市现喳,隨后出現(xiàn)的幾起案子凯傲,更是在濱河造成了極大的恐慌,老刑警劉巖嗦篱,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泣洞,死亡現(xiàn)場離奇詭異,居然都是意外死亡默色,警方通過查閱死者的電腦和手機(jī)球凰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腿宰,“玉大人呕诉,你說我怎么就攤上這事〕远龋” “怎么了甩挫?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長椿每。 經(jīng)常有香客問我伊者,道長,這世上最難降的妖魔是什么间护? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任亦渗,我火速辦了婚禮,結(jié)果婚禮上汁尺,老公的妹妹穿的比我還像新娘法精。我一直安慰自己,他們只是感情好痴突,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布搂蜓。 她就那樣靜靜地躺著,像睡著了一般辽装。 火紅的嫁衣襯著肌膚如雪帮碰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天拾积,我揣著相機(jī)與錄音殉挽,去河邊找鬼丰涉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛此再,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播玲销,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼输拇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贤斜?” 一聲冷哼從身側(cè)響起策吠,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瘩绒,沒想到半個(gè)月后猴抹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锁荔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年蟀给,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阳堕。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跋理,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出恬总,到底是詐尸還是另有隱情前普,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布壹堰,位于F島的核電站拭卿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏贱纠。R本人自食惡果不足惜峻厚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谆焊。 院中可真熱鬧目木,春花似錦、人聲如沸懊渡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剃执。三九已至誓禁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肾档,已是汗流浹背摹恰。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工辫继, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人俗慈。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓姑宽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親闺阱。 傳聞我的和親對象是個(gè)殘疾皇子炮车,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,501評論 25 707
  • 5CoordinatorLayout與AppBarLayout--嵌套滑動 上文我們說了AppBarLayout的...
    chefish閱讀 6,161評論 4 19
  • CoordinatorLayout與滾動的處理 CoordinatorLayout實(shí)現(xiàn)了多種Material De...
    cxm11閱讀 6,567評論 1 15
  • 我是在經(jīng)過下班后賺錢100天里頭腦獲得躍遷的,其實(shí)一開始總聽別人說躍遷躍遷的酣溃,我就不是太明白具體什么是躍...
    笑而不語_87ec閱讀 178評論 0 0
  • 今天晚上吃過飯瘦穆,我和媽媽、哥哥去散步赊豌。我問媽媽咱們?nèi)ツ膬貉?我想了想又說咱們?nèi)ソ∩砥鞑哪抢锿姘煽富颉N覀兘?jīng)過...
    唐康凱閱讀 133評論 0 0