本篇文章介绍使用CoordinatorLayout的自定义Behavior来实现如下的效果
首先观察下,要实现该效果的话可以拆分成三个步骤
要实现该步骤很简单,其实就是使用了CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout然后通过指定官方提供的behavior来实现子视图收缩展开时的视差效果
.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
.support.design.widget.AppBarLayout
android:id="@+id/appBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:title=" ">
"@+id/imageview_placeholder"
android:layout_width="match_parent"
android:layout_height="255dp"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@mipmap/banner"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.7"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed|snap"/>
.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:fitsSystemWindows="false"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
app:layout_collapseMode="pin"
app:title="">
.support.v7.widget.Toolbar>
.support.design.widget.CollapsingToolbarLayout>
.support.design.widget.AppBarLayout>
.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_overlapTop="15dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/card_view_margin"
android:layout_marginEnd="@dimen/card_view_margin"
android:layout_marginStart="@dimen/card_view_margin"
app:cardCornerRadius="5dp"
app:cardElevation="5dp">
"match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/large_text"
/>
.support.v4.widget.NestedScrollView>
.support.design.widget.CoordinatorLayout>
解释下布局当中几个最重要的属性:
要实现该步骤的话,可以使用AppBarLayout的OffsetChangedListener这个接口,该接口在当AppBarLayout垂直方向上的偏移量发生改变时,会回调并提供一些参数
在CollapsingToolbarLayout里添加布局如下:
<FrameLayout
android:id="@+id/framelayout_title"
android:layout_width="270dp"
android:layout_height="100dp"
android:layout_gravity="bottom"
android:orientation="vertical"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.3">
<RelativeLayout
android:id="@+id/title_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="鲁提辖"
android:textColor="@android:color/white"
android:textSize="30sp"/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_name"
android:layout_centerHorizontal="true">
<TextView
android:id="@+id/tv_region"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="4dp"
android:text="闵行区"
android:textColor="@android:color/white"/>
<TextView
android:id="@+id/tv_sex"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="4dp"
android:layout_toEndOf="@id/tv_region"
android:layout_toRightOf="@id/tv_region"
android:text="男"
android:textColor="@android:color/white"/>
RelativeLayout>
RelativeLayout>
FrameLayout>
在Activity后面添加接口:
public class CustomBehavior1Activity extends AppCompatActivity implements AppBarLayout.OnOffsetChangedListener {
在oncreate内添加apbarlayout的偏移监听
mAppBarLayout.addOnOffsetChangedListener(this);
然后重写onOffsetChanged()方法
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
int maxScroll = appBarLayout.getTotalScrollRange();
float percentage = (float) Math.abs(offset) / (float) maxScroll;
handleAlphaOnTitle(percentage);
handleToolbarTitleVisibility(percentage);
}
private void handleToolbarTitleVisibility(float percentage) {
if (percentage >= PERCENTAGE_TO_SHOW_TITLE_AT_TOOLBAR) {
if (!mIsTheTitleVisible) {
startAlphaAnimation(tvTitleName, ALPHA_ANIMATIONS_DURATION, View.VISIBLE);
mIsTheTitleVisible = true;
}
} else {
if (mIsTheTitleVisible) {
startAlphaAnimation(tvTitleName, ALPHA_ANIMATIONS_DURATION, View.INVISIBLE);
mIsTheTitleVisible = false;
}
}
}
private void handleAlphaOnTitle(float percentage) {
if (percentage >= PERCENTAGE_TO_HIDE_TITLE_DETAILS) {
if (mIsTheTitleContainerVisible) {
startAlphaAnimation(mTitleContainer, ALPHA_ANIMATIONS_DURATION, View.INVISIBLE);
mIsTheTitleContainerVisible = false;
}
} else {
if (!mIsTheTitleContainerVisible) {
startAlphaAnimation(mTitleContainer, ALPHA_ANIMATIONS_DURATION, View.VISIBLE);
mIsTheTitleContainerVisible = true;
}
}
}
public static void startAlphaAnimation(View v, long duration, int visibility) {
AlphaAnimation alphaAnimation = (visibility == View.VISIBLE)
? new AlphaAnimation(0f, 1f)
: new AlphaAnimation(1f, 0f);
alphaAnimation.setDuration(duration);
alphaAnimation.setFillAfter(true);
v.startAnimation(alphaAnimation);
}
解释下要点:
int maxScroll = appBarLayout.getTotalScrollRange();
float percentage = (float) Math.abs(offset) / (float) maxScroll;
通过当前偏移量除以appBarLayout的总滑动长度得到一个百分比,使用这个百分比来判定文字显示隐藏的时机
同样来看下效果:
要实现该步骤,需要使用到CoordinatorLayout的自定义Behavior,在本例中我们的自定义Behavior会使用到两个回调方法:
layoutDependsOn
用来和依赖视图绑定,本例绑定的依赖视图是AppBarLayout,绑定依赖视图后返回true,则onDependentViewChanged内的依赖视图也会是AppBarLayout
onDependentViewChanged
这里可以通过当前依赖视图的位移,计算出一个位移因数(取值 0 - 1),用该位移因数来做一些视图的位移,缩放等等的操作
回调的具体说明可以参考CoordinatorLayout自定义Behavior的简单总结
下面我们来分析下思路:
首先确定依赖视图和子视图,依赖视图我们绑定AppBarLayout,子视图就是CircleImageView,我们通过依赖视图上滑下滑的距离除以总共可滑动的距离来计算出百分比,通过这个百分比计算出相应的参数就能让我们实现CircleImageView的位移与缩放了
有了思路后再看代码应该就比较容易了
public class AvatarBehavior extends CoordinatorLayout.Behavior {
private static final String TAG = AvatarBehavior.class.getSimpleName();
// 缩放动画变化的支点
private static final float ANIM_CHANGE_POINT = 0.2f;
private Context mContext;
// 整个滚动的范围
private int mTotalScrollRange;
// AppBarLayout高度
private int mAppBarHeight;
// AppBarLayout宽度
private int mAppBarWidth;
// 控件原始大小
private int mOriginalSize;
// 控件最终大小
private int mFinalSize;
// 控件最终缩放的尺寸,设置坐标值需要算上该值
private float mScaleSize;
// 原始x坐标
private float mOriginalX;
// 最终x坐标
private float mFinalX;
// 起始y坐标
private float mOriginalY;
// 最终y坐标
private float mFinalY;
// ToolBar高度
private int mToolBarHeight;
// AppBar的起始Y坐标
private float mAppBarStartY;
// 滚动执行百分比[0~1]
private float mPercent;
// Y轴移动插值器
private DecelerateInterpolator mMoveYInterpolator;
// X轴移动插值器
private AccelerateInterpolator mMoveXInterpolator;
// 最终变换的视图,因为在5.0以上AppBarLayout在收缩到最终状态会覆盖变换后的视图,所以添加一个最终显示的图片
private CircleImageView mFinalView;
// 最终变换的视图离底部的大小
private int mFinalViewMarginBottom;
public AvatarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mMoveYInterpolator = new DecelerateInterpolator();
mMoveXInterpolator = new AccelerateInterpolator();
if (attrs != null) {
TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.AvatarImageBehavior);
mFinalSize = (int) a.getDimension(R.styleable.AvatarImageBehavior_finalSize, 0);
mFinalX = a.getDimension(R.styleable.AvatarImageBehavior_finalX, 0);
mToolBarHeight = (int) a.getDimension(R.styleable.AvatarImageBehavior_toolBarHeight, 0);
a.recycle();
}
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, CircleImageView child, View dependency) {
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, CircleImageView child, View dependency) {
if (dependency instanceof AppBarLayout) {
_initVariables(child, dependency);
mPercent = (mAppBarStartY - dependency.getY()) * 1.0f / mTotalScrollRange;
float percentY = mMoveYInterpolator.getInterpolation(mPercent);
setViewY(child, mOriginalY, mFinalY - mScaleSize, percentY);
if (mPercent > ANIM_CHANGE_POINT) {
float scalePercent = (mPercent - ANIM_CHANGE_POINT) / (1 - ANIM_CHANGE_POINT);
float percentX = mMoveXInterpolator.getInterpolation(scalePercent);
scaleView(child, mOriginalSize, mFinalSize, scalePercent);
setViewX(child, mOriginalX, mFinalX - mScaleSize, percentX);
} else {
scaleView(child, mOriginalSize, mFinalSize, 0);
setViewX(child, mOriginalX, mFinalX - mScaleSize, 0);
}
if (mFinalView != null) {
if (percentY == 1.0f) {
// 滚动到顶时才显示
mFinalView.setVisibility(View.VISIBLE);
} else {
mFinalView.setVisibility(View.GONE);
}
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && dependency instanceof CollapsingToolbarLayout) {
// 大于5.0才生成新的最终的头像,因为5.0以上AppBarLayout会覆盖变换后的头像
if (mFinalView == null && mFinalSize != 0 && mFinalX != 0 && mFinalViewMarginBottom != 0) {
mFinalView = new CircleImageView(mContext);
mFinalView.setVisibility(View.GONE);
// 添加为CollapsingToolbarLayout子视图
((CollapsingToolbarLayout) dependency).addView(mFinalView);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mFinalView.getLayoutParams();
// 设置大小
params.width = mFinalSize;
params.height = mFinalSize;
// 设置位置,最后显示时相当于变换后的头像位置
params.gravity = Gravity.BOTTOM;
params.leftMargin = (int) mFinalX;
params.bottomMargin = mFinalViewMarginBottom;
mFinalView.setLayoutParams(params);
mFinalView.setImageDrawable(child.getDrawable());
mFinalView.setBorderColor(child.getBorderColor());
int borderWidth = (int) ((mFinalSize * 1.0f / mOriginalSize) * child.getBorderWidth());
mFinalView.setBorderWidth(borderWidth);
}
if (mFinalView != null && mFinalSize != 0 && mFinalX != 0 && mFinalViewMarginBottom != 0) {
mFinalView.setImageDrawable(child.getDrawable());
}
}
return true;
}
/**
* 初始化变量
*
* @param child
* @param dependency
*/
private void _initVariables(CircleImageView child, View dependency) {
if (mAppBarHeight == 0) {
mAppBarHeight = dependency.getHeight();
mAppBarStartY = dependency.getY();
}
if (mTotalScrollRange == 0) {
mTotalScrollRange = ((AppBarLayout) dependency).getTotalScrollRange();
}
if (mOriginalSize == 0) {
mOriginalSize = child.getWidth();
}
if (mFinalSize == 0) {
mFinalSize = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_size);
}
if (mAppBarWidth == 0) {
mAppBarWidth = dependency.getWidth();
}
if (mOriginalX == 0) {
mOriginalX = child.getX();
}
if (mFinalX == 0) {
mFinalX = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_x);
}
if (mOriginalY == 0) {
mOriginalY = child.getY();
}
if (mFinalY == 0) {
if (mToolBarHeight == 0) {
mToolBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.toolbar_height);
}
int statusBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.status_bar_height);
mFinalY = (mToolBarHeight - mFinalSize) / 2 + mAppBarStartY + statusBarHeight;
}
if (mScaleSize == 0) {
mScaleSize = (mOriginalSize - mFinalSize) * 1.0f / 2;
}
if (mFinalViewMarginBottom == 0) {
mFinalViewMarginBottom = (mToolBarHeight - mFinalSize) / 2;
}
}
public void setViewX(View view, float originalX, float finalX, float percent) {
float calcX = (finalX - originalX) * percent + originalX;
view.setX(calcX);
}
public void setViewY(View view, float originalY, float finalY, float percent) {
float calcY = (finalY - originalY) * percent + originalY;
view.setY(calcY);
}
public static void scaleView(View view, float originalSize, float finalSize, float percent) {
float calcSize = (finalSize - originalSize) * percent + originalSize;
float caleScale = calcSize / originalSize;
view.setScaleX(caleScale);
view.setScaleY(caleScale);
}
}
有几个需要注意的地方:
在CoordinatorLayout内添加CircleImageView并指定自定义Behavior
"@+id/avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="180dp"
android:src="@mipmap/avatar"
app:civ_border_color="@android:color/white"
app:civ_border_width="2dp"
app:layout_anchor="@+id/collapsing"
app:layout_anchorGravity="right"
app:layout_behavior="@string/avatar_behavior"/>
最后看下效果:
代码示例:
MaterialDesignFeatures
参考:
http://blog.csdn.net/github_35180164/article/details/51881247
https://github.com/saulmm/CoordinatorBehaviorExample