先上效果
一揖庄、繼承AppBarLayout.Behavior
AppBarLayout有一個(gè)默認(rèn)的Behavior牛欢,即AppBarLayout.Behavior铭拧,AppBarLayout.Behavior已注解的方式設(shè)置給AppBarLayout时肿。
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
...
}
1.繼承AppBarLayout.Behavior自定義Behavior
我們可以繼承AppBarLayout.Behavior并重新設(shè)置給AppBarLayout來修改AppBarLayout的默認(rèn)滾動(dòng)行為庇茫,實(shí)現(xiàn)AppBarLayout的彈性越界效果就可以通過這種方式實(shí)現(xiàn)。
繼承AppBarLayout.Behavior需要重寫構(gòu)造方法
public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {
public AppBarLayoutOverScrollViewBehavior() {
}
public AppBarLayoutOverScrollViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
2.將自定義的Behavior設(shè)置給AppBarLayout
可以通過兩種方式將自定義的Behavior設(shè)置給AppBarLayout
-
在布局文件中設(shè)置
<android.support.design.widget.AppBarLayout ... app:layout_behavior="packageName.AppBarLayoutOverScrollViewBehavior"> </android.support.design.widget.AppBarLayout>
-
在代碼中設(shè)置
AppBarLayout appBar = (AppBarLayout) findViewById(R.id.appbar); CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.setBehavior(new AppBarLayoutOverScrollViewBehavior()); appBar.setLayoutParams(params);
設(shè)置完成后螃成,自定義的Behavior就會(huì)生效旦签,但是因?yàn)闆]有重寫任何方法,所以AppBarLayout的滾動(dòng)行為不會(huì)發(fā)生變化寸宏。
二宁炫、Behavior中的回調(diào)方法分析
將自定義的Behavior設(shè)置給AppBarLayout后,可以在自定義的Behavior中重寫滾動(dòng)相關(guān)回調(diào)方法
public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {
...
/**
* AppBarLayout布局時(shí)調(diào)用
*
* @param parent 父布局CoordinatorLayout
* @param abl 使用此Behavior的AppBarLayout
* @param layoutDirection 布局方向
* @return 返回true表示子View重新布局氮凝,返回false表示請(qǐng)求默認(rèn)布局
*/
@Override
public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
return super.onLayoutChild(parent, abl, layoutDirection);
}
/**
* 當(dāng)CoordinatorLayout的子View嘗試發(fā)起嵌套滾動(dòng)時(shí)調(diào)用
*
* @param parent 父布局CoordinatorLayout
* @param child 使用此Behavior的AppBarLayout
* @param directTargetChild CoordinatorLayout的子View羔巢,或者是包含嵌套滾動(dòng)操作的目標(biāo)View
* @param target 發(fā)起嵌套滾動(dòng)的目標(biāo)View(即AppBarLayout下面的ScrollView或RecyclerView)
* @param nestedScrollAxes 嵌套滾動(dòng)的方向
* @return 返回true表示接受滾動(dòng)
*/
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
/**
* 當(dāng)嵌套滾動(dòng)已由CoordinatorLayout接受時(shí)調(diào)用
*
* @param coordinatorLayout 父布局CoordinatorLayout
* @param child 使用此Behavior的AppBarLayout
* @param directTargetChild CoordinatorLayout的子View,或者是包含嵌套滾動(dòng)操作的目標(biāo)View
* @param target 發(fā)起嵌套滾動(dòng)的目標(biāo)View(即AppBarLayout下面的ScrollView或RecyclerView)
* @param nestedScrollAxes 嵌套滾動(dòng)的方向
*/
@Override
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
/**
* 當(dāng)準(zhǔn)備開始嵌套滾動(dòng)時(shí)調(diào)用
*
* @param coordinatorLayout 父布局CoordinatorLayout
* @param child 使用此Behavior的AppBarLayout
* @param target 發(fā)起嵌套滾動(dòng)的目標(biāo)View(即AppBarLayout下面的ScrollView或RecyclerView)
* @param dx 用戶在水平方向上滑動(dòng)的像素?cái)?shù)
* @param dy 用戶在垂直方向上滑動(dòng)的像素?cái)?shù)
* @param consumed 輸出參數(shù)罩阵,consumed[0]為水平方向應(yīng)該消耗的距離竿秆,consumed[1]為垂直方向應(yīng)該消耗的距離
*/
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
/**
* 嵌套滾動(dòng)時(shí)調(diào)用
*
* @param coordinatorLayout 父布局CoordinatorLayout
* @param child 使用此Behavior的AppBarLayout
* @param target 發(fā)起嵌套滾動(dòng)的目標(biāo)View(即AppBarLayout下面的ScrollView或RecyclerView)
* @param dxConsumed 由目標(biāo)View滾動(dòng)操作消耗的水平像素?cái)?shù)
* @param dyConsumed 由目標(biāo)View滾動(dòng)操作消耗的垂直像素?cái)?shù)
* @param dxUnconsumed 由用戶請(qǐng)求但是目標(biāo)View滾動(dòng)操作未消耗的水平像素?cái)?shù)
* @param dyUnconsumed 由用戶請(qǐng)求但是目標(biāo)View滾動(dòng)操作未消耗的垂直像素?cái)?shù)
*/
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
/**
* 當(dāng)嵌套滾動(dòng)的子View準(zhǔn)備快速滾動(dòng)時(shí)調(diào)用
*
* @param coordinatorLayout 父布局CoordinatorLayout
* @param child 使用此Behavior的AppBarLayout
* @param target 發(fā)起嵌套滾動(dòng)的目標(biāo)View(即AppBarLayout下面的ScrollView或RecyclerView)
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @return 如果Behavior消耗了快速滾動(dòng)返回true
*/
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
/**
* 當(dāng)嵌套滾動(dòng)的子View快速滾動(dòng)時(shí)調(diào)用
*
* @param coordinatorLayout 父布局CoordinatorLayout
* @param child 使用此Behavior的AppBarLayout
* @param target 發(fā)起嵌套滾動(dòng)的目標(biāo)View(即AppBarLayout下面的ScrollView或RecyclerView)
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @param consumed 如果嵌套的子View消耗了快速滾動(dòng)則為true
* @return 如果Behavior消耗了快速滾動(dòng)返回true
*/
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
/**
* 當(dāng)定制滾動(dòng)時(shí)調(diào)用
*
* @param coordinatorLayout 父布局CoordinatorLayout
* @param abl 使用此Behavior的AppBarLayout
* @param target 發(fā)起嵌套滾動(dòng)的目標(biāo)View(即AppBarLayout下面的ScrollView或RecyclerView)
*/
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target) {
super.onStopNestedScroll(coordinatorLayout, abl, target);
}
}
可以通過打印log來觀察AppBarLayout在滾動(dòng)時(shí)Behavior中回調(diào)方法的調(diào)用情況。
通過觀察可以發(fā)現(xiàn):
- 上滑時(shí)
- 當(dāng)AppBarLayout由展開到收起時(shí)稿壁,會(huì)依次調(diào)用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onStopNestedScroll()
- 當(dāng)AppBarLayout收起后繼續(xù)向上滑動(dòng)時(shí)幽钢,會(huì)依次調(diào)用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()
- 下滑時(shí)
- 當(dāng)AppBarLayout全部展開時(shí)(即未到頂部時(shí)),會(huì)依次調(diào)用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()
- 當(dāng)AppBarLayout全部展開時(shí)(即到頂部時(shí))傅是,繼續(xù)向下滑動(dòng)屏幕匪燕,會(huì)依次調(diào)用onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()
- 當(dāng)有快速滑動(dòng)時(shí)會(huì)在onStopNestedScroll()前依次調(diào)用onNestedPreFling()->onNestedFling()
所以要修改AppBarLayout的越界行為可以重寫onNestedPreScroll()或onNestedScroll(),因?yàn)锳ppBarLayout收起時(shí)不會(huì)調(diào)用onNestedScroll()喧笔,所以只能選擇重寫onNestedPreScroll()帽驯,具體原因下面會(huì)有說明。
三书闸、重寫Behavior的相關(guān)方法
1.獲取越界時(shí)需要改變尺寸的View
布局時(shí)會(huì)調(diào)用onLayoutChild()界拦,所以在該方法中可獲取需要改變尺寸的View,可以使用View的findViewWithTag方法獲取指定的View梗劫,并初始化屬性享甸。
public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {
private static final String TAG = "overScroll";
private View mTargetView; // 目標(biāo)View
private int mParentHeight; // AppBarLayout的初始高度
private int mTargetViewHeight; // 目標(biāo)View的高度
@Override
public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
// 需要在調(diào)用過super.onLayoutChild()方法之后獲取
if (mTargetView == null) {
mTargetView = parent.findViewWithTag(TAG);
if (mTargetView != null) {
initial(abl);
}
}
return handled;
}
private void initial(AppBarLayout abl) {
// 必須設(shè)置ClipChildren為false,這樣目標(biāo)View在放大時(shí)才能超出布局的范圍
abl.setClipChildren(false);
mParentHeight = abl.getHeight();
mTargetViewHeight = mTargetView.getHeight();
}
...
}
需要在布局文件或代碼中給目標(biāo)View指定tag梳侨,如下:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay"
android:transitionName="picture"
app:layout_behavior="com.zly.exifviewer.widget.behavior.AppBarLayoutOverScrollViewBehavior"
tools:targetApi="lollipop">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentScrim="@color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:statusBarScrim="@color/colorPrimaryDark">
<ImageView
android:id="@+id/siv_picture"
android:layout_width="match_parent"
android:layout_height="200dp"
android:fitsSystemWindows="true"
android:foreground="@drawable/shape_fg_picture"
android:scaleType="centerCrop"
android:tag="overScroll"
app:layout_collapseMode="parallax"
tools:src="@android:drawable/sym_def_app_icon" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:contentInsetEnd="64dp"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.CollapsingToolbarLayout>
</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">
...
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
2.下滑處理
重寫onNestedPreScroll()修改AppBarLayou滑動(dòng)的頂部后的行為
private static final float TARGET_HEIGHT = 500; // 最大滑動(dòng)距離
private float mTotalDy; // 總滑動(dòng)的像素?cái)?shù)
private float mLastScale; // 最終放大比例
private int mLastBottom; // AppBarLayout的最終Bottom值
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
// 1.mTargetView不為null
// 2.是向下滑動(dòng)蛉威,dy<0表示向下滑動(dòng)
// 3.AppBarLayout已經(jīng)完全展開,child.getBottom() >= mParentHeight
if (mTargetView != null && dy < 0 && child.getBottom() >= mParentHeight) {
// 累加垂直方向上滑動(dòng)的像素?cái)?shù)
mTotalDy += -dy;
// 不能大于最大滑動(dòng)距離
mTotalDy = Math.min(mTotalDy, TARGET_HEIGHT);
// 計(jì)算目標(biāo)View縮放比例走哺,不能小于1
mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);
// 縮放目標(biāo)View
ViewCompat.setScaleX(mTargetView, mLastScale);
ViewCompat.setScaleY(mTargetView, mLastScale);
// 計(jì)算目標(biāo)View放大后增加的高度
mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));
// 修改AppBarLayout的高度
child.setBottom(mLastBottom);
} else {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
}
此時(shí)可以實(shí)現(xiàn)下滑越界時(shí)目標(biāo)View放大蚯嫌,AppBarLayout變高的效果。
3.上滑處理
下滑時(shí)目標(biāo)View放大,AppBarLayout變高择示,如果此時(shí)用戶不松開手指束凑,直接上滑,需要目標(biāo)View縮小栅盲,并且AppBarLayout變高汪诉。
默認(rèn)情況下AppBarLayout的滑動(dòng)是通過修改top和bottom實(shí)現(xiàn)的,所以上滑時(shí)谈秫,AppBarLayout為整體向上移動(dòng)扒寄,高度不會(huì)發(fā)生改變,并且AppBarLayout下面的ScrollView也會(huì)向上滾動(dòng)拟烫;而我們需要的是在AppBarLayout的高度大于原始高度時(shí)该编,減小AppBarLayout的高度,top不發(fā)生改變硕淑,并且AppBarLayout下面的ScrollView不會(huì)向上滾動(dòng)课竣。
AppBarLayout上滑時(shí)不會(huì)調(diào)用onNestedScroll(),所以只能在onNestedPreScroll()方法中修改置媳,這也是為什么選擇onNestedPreScroll()方法的原因
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
if (mTargetView != null && dy < 0 && child.getBottom() >= mParentHeight) {
...
} else
// 1.mTargetView不為null
// 2.是向上滑動(dòng)稠氮,dy>0表示向下滑動(dòng)
// 3.AppBarLayout尚未恢復(fù)到原始高度child.getBottom() > mParentHeight
if (mTargetView != null && dy > 0 && child.getBottom() > mParentHeight) {
// 累減垂直方向上滑動(dòng)的像素?cái)?shù)
mTotalDy -= dy;
// 計(jì)算目標(biāo)View縮放比例,不能小于1
mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);
// 縮放目標(biāo)View
ViewCompat.setScaleX(mTargetView, mLastScale);
ViewCompat.setScaleY(mTargetView, mLastScale);
// 計(jì)算目標(biāo)View縮小后減少的高度
mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));
// 修改AppBarLayout的高度
child.setBottom(mLastBottom);
// 保持target不滑動(dòng)
target.setScrollY(0);
} else {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
}
與上滑的邏輯基本一直半开,所以可寫為一個(gè)方法
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
if (mTargetView != null && ((dy < 0 && child.getBottom() >= mParentHeight) || (dy > 0 && child.getBottom() > mParentHeight))) {
scale(child, target, dy);
} else {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
}
private void scale(AppBarLayout abl, View target, int dy) {
mTotalDy += -dy;
mTotalDy = Math.min(mTotalDy, TARGET_HEIGHT);
mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);
ViewCompat.setScaleX(mTargetView, mLastScale);
ViewCompat.setScaleY(mTargetView, mLastScale);
mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));
abl.setBottom(mLastBottom);
target.setScrollY(0);
}
4.還原
當(dāng)AppBarLayout處于越界時(shí),如果用戶松開手指赃份,此時(shí)應(yīng)該讓目標(biāo)View和AppBarLayout都還原到原始狀態(tài)寂拆,重寫onStopNestedScroll()方法
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target) {
recovery(abl);
super.onStopNestedScroll(coordinatorLayout, abl, target);
}
private void recovery(final AppBarLayout abl) {
if (mTotalDy > 0) {
mTotalDy = 0;
// 使用屬性動(dòng)畫還原
ValueAnimator anim = ValueAnimator.ofFloat(mLastScale, 1f).setDuration(200);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
ViewCompat.setScaleX(mTargetView, value);
ViewCompat.setScaleY(mTargetView, value);
abl.setBottom((int) (mLastBottom - (mLastBottom - mParentHeight) * animation.getAnimatedFraction()));
}
});
anim.start();
}
}
5.優(yōu)化
由于用戶在滑動(dòng)時(shí)有可能觸發(fā)快速滑動(dòng),會(huì)導(dǎo)致在AppBarLayout收起后觸發(fā)還原動(dòng)畫抓韩,重新修改AppBarLayout的Bottom纠永,從而顯示錯(cuò)誤,所以當(dāng)發(fā)生快速滑動(dòng)時(shí)需要禁止還原動(dòng)畫谒拴,直接還原到初始狀態(tài)
private boolean isAnimate; //是否有動(dòng)畫
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
// 開始滑動(dòng)時(shí)尝江,啟用動(dòng)畫
isAnimate = true;
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
// 如果觸發(fā)了快速滾動(dòng)且垂直方向上速度大于100,則禁用動(dòng)畫
if (velocityY > 100) {
isAnimate = false;
}
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
private void recovery(final AppBarLayout abl) {
if (mTotalDy > 0) {
mTotalDy = 0;
if (isAnimate) {
ValueAnimator anim = ValueAnimator.ofFloat(mLastScale, 1f).setDuration(200);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
ViewCompat.setScaleX(mTargetView, value);
ViewCompat.setScaleY(mTargetView, value);
abl.setBottom((int) (mLastBottom - (mLastBottom - mParentHeight) * animation.getAnimatedFraction()));
}
});
anim.start();
} else {
ViewCompat.setScaleX(mTargetView, 1f);
ViewCompat.setScaleY(mTargetView, 1f);
abl.setBottom(mParentHeight);
}
}
}