完全搞懂CoordinatorLayout Behavior 你能做些什麼
完全搞懂CoordinatorLayout Behavior 系列之API講解
完全搞懂CoordinatorLayout Behavior之源碼學習
完全搞懂CoordinatorLayout Behavior之實戰一
之前我們已經講解了CoordinatorLayout Behavior 之間的關係以及與NestedScrollView 是如何聯繫 進行通知回調等操作的,還結合源碼講解了 Behavior相關的幾個方法參數的。
廢話不多說,說說我們今天實戰的效果,下圖是我之前完成的一個半成品,今天我將繼續完善。
最終的效果圖
一、界面分析
上面一共有幾個觀察者?分別是標題欄、 天氣圖標以及背景圖。 說明我們這三個佈局都需要添加一個Behavior觀察NestedScrollView ,如下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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/img_header"
android:layout_width="match_parent"
android:layout_height="@dimen/img_header_height"
app:layout_behavior=".ImageHeaderBehavior"
android:background="@mipmap/home_top_bg"/>
<RelativeLayout
android:layout_width="match_parent"
android:background="@color/blue"
app:layout_behavior=".TitleBarBehavior"
android:layout_height="@dimen/comm_title_bar_height">
<EditText
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="80dp"
android:layout_centerVertical="true"
android:layout_marginRight="60dp"
android:paddingLeft="20dp"
android:paddingEnd="20dp"
android:hint="請輸入關鍵字"
android:background="@drawable/shape_edit_bg"/>
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginRight="10dp"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:src="@mipmap/scan"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_behavior=".WeatherBehavior">
<ImageView
android:id="@+id/img_weather"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:src="@mipmap/weather_sunny" />
<TextView
android:id="@+id/txt_weather"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/img_weather"
android:gravity="center_horizontal"
android:paddingTop="8dp"
android:textStyle="bold"
android:text="晴天"
android:textColor="@color/white"
android:textSize="16dp" />
<TextView
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_weather"
android:layout_toRightOf="@+id/img_weather"
android:gravity="center_horizontal"
android:text="13℃"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="14dp" />
</RelativeLayout>
<android.support.v4.widget.NestedScrollView
android:id="@+id/scroll_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/orange"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/aqua"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/yellow"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/blue"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
二、監聽對象以及初始位置設置
在構造方法中 先得到titlebar 和 HeaderImageView的高度。
public ImageHeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mTitleBarHeight = context.getResources().getDimension(R.dimen.comm_title_bar_height);
mImgHeaderHeight = context.getResources().getDimension(R.dimen.img_header_height);
}
以ImageHeaderBehavior 爲例,我們首先需要讓ImageView可以能夠觀察到NestedScrollView的變化。
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
if (dependency instanceof NestedScrollView) {
// 記錄監聽的NestedScrollView實例,方便初始化位置
mDependency = dependency;
return true;
}
return false;
}
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
Log.d(TAG, "onLayoutChild: child = " + child.getHeight());
//mDependency
mDependency.layout(0, (int) mImgHeaderHeight, parent.getWidth(), (int) (parent.getHeight() + mImgHeaderHeight));
return super.onLayoutChild(parent, child, layoutDirection);
}
mDependency就是我們觀察到的NestedScrollView,拿到實例對象引用給它一個初始化位置, 讓他正好在ImgHeader 下面。所以top設置成mImgHeaderHeight ,同時bottom 也加一個mImgHeaderHeight。
三、監聽NestedScrollView滾動
我們需要達到的目的是NestedScrollView 滾動多少,HeaderImageView也跟着滾動多少。 同時也需要距離邊界值處理。
1、給CoordinatorLayout監聽分配滾動的權限
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
在完全搞懂CoordinatorLayout Behavior 系列之API講解 有講到,只有當這個返回true時,後面的監聽嵌套滾動的方法纔會得到調用。 這裏我們當滑動方向是SCROLL_AXIS_VERTICAL
的時候就返回true。
2、設置滾動監聽
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View target,
int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
先解釋下參數,coordinatorLayout
自然不用說就是根部局元素;
child
就是app:layout_behavior
這個屬性設置的元素;
target
就是目標元素,也就是我們的被監聽者NestedScrollView;
dxConsumed
X軸滑動的距離,豎向滑動時它一直爲0;
dyConsumed
Y軸滑動的距離,如果大於零代表向上滑動,小於零就是向下滑動。
dxUnconsumed
、dyUnconsumed
代表未消耗的距離,就是目標滑動距離減實際消耗距離。
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View target,
int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
//super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
// ImageView 高度減去 titlebar高度,得到的差值就是 NestedScrollView可以滑動的最大距離
float diff = child.getHeight() - mTitleBarHeight;
//向上滑動
if (dyConsumed > 0) {
// 註釋 1
//獲取 NestedScrollView 滑動距離
float translationY = -target.getScrollY();
Log.d(TAG, "onNestedScroll:向上 translationY = " + translationY+" ; diff = "+diff);
if (target.getScrollY() <= diff) {
//NestedScrollView 和ImageView同時向上移動
target.setTranslationY(translationY);
child.setTranslationY(translationY);
}else{
//如果target.getScrollY() > diff 就永遠固定在diff位置。
//如果不加這一行效果會有小瑕疵
target.setTranslationY(-diff);
child.setTranslationY(-diff);
}
}
if (dyConsumed < 0) {
//註釋 2
// child.getY() 獲取ImageView的Y座標 target.getY() 獲取NestedScrollView的Y座標
// child.getY() 小於零代表ImageView 的top在屏幕外面,如果等於零剛好貼住屏幕的最上邊 相對的NestedScrollView 就是緊跟着Image 如果大於child.getHeight() 就與Image 分開了
if (child.getY() <= 0 && target.getY() <= child.getHeight()) {
float translationY = -target.getScrollY();
//本身上面的條件就是符合要求的 會有數值跳動導致不準
if (target.getScrollY() <= diff) { //最大能滑動的寬度是 header圖片的寬度 減去 title高度
target.setTranslationY(translationY);
child.setTranslationY(translationY);
}
}
}
}
上面我額外添加了很多註釋,便於大家理解。可能你們有更好的算法邏輯。我這裏是根據dyConsumed
判斷方向然後單獨分析,下面我用一個圖解釋下。
四 、根據監聽View變化設置ImageView的變化
根據動畫效果圖我們可以看到,在NestedScrollView滑動的時候, HeaderImageView 有一個放大縮小 以及透明度大小的變化。 如何設置呢?onDependentViewChanged 當NestedScrollView 大小或者位置發生變化是都會回調這個方法。 原理部分我們有說到,它是通過一個ViewTreeObserver 監聽繪製的發方法,通過比較上一次和這一次的Rect 。決定onDependentViewChanged是否調用,看一下我是如何使用的。
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
//return super.onDependentViewChanged(parent, child, dependency);
//ImageView 滑動的距離
float translationY = child.getTranslationY();
//通過平移的距離translationY,它的大小已經被固定死了只能是 0 ~ mIgHeaderHeight - mTitleBarHeight 之間。 這裏計算出一個比例。
float progress = 1f - (Math.abs(translationY) / (mIgHeaderHeight - mTitleBarHeight));
// 0.2 只是一個放大縮小的係數, 讓變化更加緩和一些
float scale = 1 + 0.2f * (1.f - progress);
child.setScaleX(scale);
child.setScaleY(scale);
if (progress < 0.3) {
child.setAlpha(0.3f);
} else {
child.setAlpha(progress);
}
return true;
}
五、另外兩個Behavior 源碼
如果理解了上面那個Behavior,那麼這兩個Behavior就非常好理解,一共就兩步,第一步、設置初始化位置;第二步根據平移大小計算比例,進行相關位置的計算。
TitleBarBehavior.java
public class TitleBarBehavior extends CoordinatorLayout.Behavior {
private static final String TAG = "TitleBarBehavior";
private final float mTitleBarHeight;
private final float mImgHeaderHeight;
public TitleBarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mTitleBarHeight = context.getResources().getDimension(R.dimen.comm_title_bar_height);
mImgHeaderHeight = context.getResources().getDimension(R.dimen.img_header_height);
}
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent,
@NonNull View child,
@NonNull View dependency) {
if (dependency instanceof NestedScrollView) {
return true;
}
return super.layoutDependsOn(parent, child, dependency);
}
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
//設置初始位置的平移,讓它完全平移到屏幕外
child.setTranslationY(-mTitleBarHeight);
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
//super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
// int scrollY = target.getScrollY();
// Log.d(TAG, "onNestedScroll: scrollY"+ scrollY);
float translationY = target.getTranslationY();
//target可以滑動的範圍距離
float totalDistance = mImgHeaderHeight - mTitleBarHeight;
float progress = Math.abs(translationY) / totalDistance;
float titleBarTranslationY = -mTitleBarHeight * (1 - progress);
Log.d(TAG, "onNestedScroll: titleBarTranslationY = "+titleBarTranslationY+" ; mTitleBarHeight = "+mTitleBarHeight);
child.setTranslationY(titleBarTranslationY);
child.setAlpha(progress);
}
}
WeatherBehavior.java
public class WeatherBehavior extends CoordinatorLayout.Behavior {
private static final String TAG = "WeatherBehavior";
private float mTitleBarHeight;
private float mWeatherTopMargin;
private float mWeatherLeftMargin;
private float mImgHeaderHeight;
public WeatherBehavior() {
}
public WeatherBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
//得到Weather佈局的寬高
mTitleBarHeight = context.getResources().getDimension(R.dimen.comm_title_bar_height);
mImgHeaderHeight = context.getResources().getDimension(R.dimen.img_header_height);
mWeatherTopMargin = context.getResources().getDimension(R.dimen.weather_top_margin);
mWeatherLeftMargin = context.getResources().getDimension(R.dimen.weather_left_margin);
}
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
if (dependency instanceof NestedScrollView){
return true;
}
return false;
}
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
layoutParams.topMargin = (int) mWeatherTopMargin;
layoutParams.leftMargin = (int) mWeatherLeftMargin;
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return super.onDependentViewChanged(parent, child, dependency);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child, @NonNull View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
//super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
//target可以滑動的總距離
float totalDistance = mImgHeaderHeight - mTitleBarHeight;
float translationY = target.getTranslationY();
float progressY = translationY / totalDistance;
float watherTranslationY = mWeatherTopMargin * progressY;
float translationX = target.getTranslationY();
float progressX = translationX / totalDistance;
float watherTranslationX = mWeatherLeftMargin * progressX;
Log.d(TAG, "onNestedScroll: watherTranslationY = "+ watherTranslationY+ " translationY = "+ translationY);
child.setTranslationY(watherTranslationY);
child.setTranslationX(watherTranslationX);
}
}