寫這篇文章的初衷來自最近項(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ā)者大會上給我們示范的效果距帅,來看下效果圖:
先來看下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一樣,融為一體)
enterAlways: 當(dāng)屬性值為scroll|enterAlways的時(shí)候沙峻,當(dāng)NestedScrollView往下滾的時(shí)候睦授,先響應(yīng)View的滾動,再響應(yīng)NestedScrollView的往下滾動摔寨。
exitUntilCollapsed:當(dāng)屬性值為scroll|exitUntilCollapsed的 時(shí)候去枷,在往上滾動的時(shí)候,會優(yōu)先把View縮小為最小高度(minHeight)是复,然后再響應(yīng)NestedScrollView的滾動删顶。
enterAlwaysCollapsed:當(dāng)屬性值為scroll|enterAlways| enterAlwaysCollapsed的時(shí)候,在往下滾的時(shí)候淑廊,會先把最小高度的View展示出來逗余,然后等NestedScrollView向下滾動結(jié)束,才繼續(xù)響應(yīng)View的滾動事件季惩,撐開為View的最大高度录粱。
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):整理中暴凑,稍后放出峦甩。