之前我们已经讲解了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 滚动多少,HeaderImageView也跟着滚动多少。 同时也需要距离边界值处理。
@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。
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
判断方向然后单独分析,下面我用一个图解释下。
根据动画效果图我们可以看到,在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就非常好理解,一共就两步,第一步、设置初始化位置;第二步根据平移大小计算比例,进行相关位置的计算。
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);
}
}