CoordinatorLayout怎么玩折叠
前言
关于 CoordinatorLayout 的解析之前有一篇文章介绍 CoordinatorLayout补齐资料篇,如果对 CoordinatorLayout 的特性不太清楚的,可以先去了解下,再开始阅读本篇。
需求
大概两个月前,产品妹子携手设计妹子跑过来。
妹子:“小哥哥,给我们 app 的个人信息页加点动态效果吧”。
我:“加加加,想要什么样的效果都可以加”。
于是,妹子拿出了动态效果图。
妹子:“就是滑动的时候背景折叠,这个头像呢大小变化,这个粉丝数量呢从上边移到左边,这个简介呢要消失隐藏。。。。”
我看了眼图,眉头一皱,发现事情并不简单。
回想一下 CoordinatorLayout 的官方例子,感觉还是不太一样,要定制这种效果,只能动手做一个了。
实现
先看效果:
联动的部分主要可以看成三大块:顶部的背景图、中部的个人信息块和底下的 recyclerView,展示效果包括背景图和个人信息块的折叠、位移和隐藏。
这么多元素联动,需要找到一个参考物,这时充当参考物的就是个人信息块的(ps:是一整大块,这一整大块需要折叠,不是里面的元素)。思路是个人信息块随着 CoordinatorLayout 动,其他元素随着个人信息块动。折叠效果需要知道标题栏和状态栏的高度以确定滑动的起始状态和终止状态。位移效果和隐藏效果都是通过view.setTranslationXXX 来实现的。
这里设置了五个 Behavior ,UserInfoBehavior(个人信息背景块)、MineContentBehavior(列表块)、NavigationBarBehavior(几乎所有的元素都在这里面)、AvatarBehavior(圆圆的头像)、LevelBehavior(如何成为专家那行字),后面两个其实也可以并入 NavigationBarBehavior 里面。
先来看看大块的背景是如何配合折叠收缩成一半的,以 MineContentBehavior 为例,复写了 getScrollRange 为例,列表收缩距离到一半。
@Override
int getScrollRange(View v) {
if (isDependOn(v)) { // 最后剩余的距离为显示用户信息的高度的一半+
return -v.getMeasuredHeight() / 2 - mTitleBarHeight + mStatusBarHeight - mDefaultPadding;
} else {
return super.getScrollRange(v);
}
}
再来看看其他元素的移动,以 NavigationBarBehavior 为例,我们在 onLayoutChild 里面确定元素位置:
/**
* 确定NavigationBar的位置
*/
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
parent.onLayoutChild(child, layoutDirection);
mAvatarCiv = (ImageView) parent.findViewById(R.id.ivAvatar);
mTvUserName = parent.findViewById(R.id.tvUserName);
mTvPost = parent.findViewById(R.id.tvPost);
mTvAttention = parent.findViewById(R.id.tvAttention);
mBgaAttentionCount = parent.findViewById(R.id.tvAttentionCount);
mTvPraise = parent.findViewById(R.id.tvPraise);
mBgaPraiseCount = parent.findViewById(R.id.tvPraiseCount);
mTvFans = parent.findViewById(R.id.tvFans);
mBgaFansCount = parent.findViewById(R.id.tvFansCount);
mUserInfoRl = parent.findViewById(R.id.rlUserInfo);
mLlReward = parent.findViewById(R.id.ll_reward);
mTvIntroduction = parent.findViewById(R.id.tv_introduction);
mDivider1 = parent.findViewById(R.id.divider1);
mDivider2 = parent.findViewById(R.id.divider2);
ViewCompat.offsetTopAndBottom(child, 0);
return true;
}
然后在 onDependentViewChanged 计算滑动距离,实现位移和隐藏的效果,在最后动态改变了用户信息北背景的高度。
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
// 让NavigationBar跟随UserInfoRl移动
ViewCompat.setTranslationY(child, ViewCompat.getTranslationY(dependency));
// 用户名
float fraction = Math.abs(ViewCompat.getTranslationY(dependency)) / maxMoveDistance;
ViewCompat.setTranslationX(mTvUserName, (-mAvatarCiv.getMeasuredHeight() + defaultMargin) * 0.2f * fraction);
// 职称
ViewCompat.setTranslationY(mTvPost, (-mTvUserName.getMeasuredHeight() / 2 - mDefaultPadding) * fraction);
float userNameTranslationX = Math.abs(ViewCompat.getTranslationX(mTvUserName));
int translationX = (int) (mTvUserName.getMeasuredWidth() / 2 * fraction);
ViewCompat.setTranslationX(mTvPost, userNameTranslationX + translationX);
// 奖励
if (fraction < 0.7f) {
ViewCompat.setAlpha(mLlReward, 1 - fraction / 0.7f);
ViewCompat.setAlpha(mTvIntroduction, 1 - fraction / 0.7f);
ViewCompat.setAlpha(mDivider1, 1 - fraction / 0.7f);
ViewCompat.setAlpha(mDivider2, 1 - fraction / 0.7f);
} else {
ViewCompat.setAlpha(mLlReward, 0);
ViewCompat.setAlpha(mTvIntroduction, 0);
ViewCompat.setAlpha(mDivider1, 0);
ViewCompat.setAlpha(mDivider2, 0);
}
// 赞、粉丝、关注
ViewCompat.setTranslationX(mTvAttention, fraction);
ViewCompat.setTranslationX(mTvPraise, fraction);
ViewCompat.setTranslationX(mTvFans, fraction);
int offset = UiUtils.sp2px(parent.getContext(), 10);
ViewCompat.setTranslationX(mBgaAttentionCount, (mBgaAttentionCount.getMeasuredWidth() + offset) / density * fraction);
ViewCompat.setTranslationX(mBgaPraiseCount, (mBgaPraiseCount.getMeasuredWidth() + offset) / density * fraction);
ViewCompat.setTranslationX(mBgaFansCount, (mBgaFansCount.getMeasuredWidth() + offset) / density * fraction);
int offsetSize = UiUtils.sp2px(parent.getContext(), 3.5f);
ViewCompat.setTranslationY(mBgaAttentionCount, -offsetSize * fraction);
ViewCompat.setTranslationY(mBgaPraiseCount, -offsetSize * fraction);
ViewCompat.setTranslationY(mBgaFansCount, -offsetSize * fraction);
fraction = 1 - (1 - minTextSize / maxTextSize) * fraction;
fraction = Math.max(fraction, 0.5f);
fraction = Math.min(fraction, 1.0f);
ViewCompat.setPivotY(mBgaAttentionCount, child.getMeasuredHeight() / 2);
ViewCompat.setScaleX(mBgaAttentionCount, fraction);
ViewCompat.setScaleY(mBgaAttentionCount, fraction);
ViewCompat.setPivotY(mBgaPraiseCount, child.getMeasuredHeight() / 2);
ViewCompat.setScaleX(mBgaPraiseCount, fraction);
ViewCompat.setScaleY(mBgaPraiseCount, fraction);
ViewCompat.setPivotY(mBgaFansCount, child.getMeasuredHeight() / 2);
ViewCompat.setScaleX(mBgaFansCount, fraction);
ViewCompat.setScaleY(mBgaFansCount, fraction);
// 动态改变用户信息背景的高度
float currHeight = mUserInfoRl.getTop() + ViewCompat.getTranslationY(dependency);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) mUserInfoRl.getLayoutParams();
currHeight = Math.max(currHeight, minHeight);
currHeight = Math.min(currHeight, maxHeight);
params.height = (int) currHeight;
mUserInfoRl.setLayoutParams(params);
return true;
}
}
代码太多,工程已经上传到 Github,有兴趣的可以去看看。
折叠联动布局