实现代码:https://github.com/drchengit/alipay_card
感谢开源:https://github.com/loopeer/CardStackView
实现简单ReyclerView : https://juejin.im/post/5df72c5bf265da33b7553a40
看到支付宝选择银行卡页面,感觉很有趣,决定写篇文章。
效果:
- 选择银行卡会打开银行卡
- 其他银行卡折叠到底部
- 不相关的view隐藏
- 再次点击还原
实现:
实现思路:
表面分析:
- 卡片是个列表,可以整体滑动,要用RecyclerView
- 迷之动画+大量的滑动冲突,常规的recyclerView无法满足,要自定义ReyclerView
- 看阵仗要能对卡片点击后用属性动画进行操作。(
选中的卡片上移,前三个卡片,下移折叠
) - 要对打开状态进行判断,显示、隐藏顶部和其他view,控制列表是否可以滑动之类。
实际思路
网上有类似的开源库:https://github.com/loopeer/CardStackView
- 首先写一个CardStackView继承viewGorup实现reyclerView功能
- 在onLayout中实现折叠卡片和其他的View的个性布局
- 点击后对当前状态判断,调用AnimatorAdapter执行属性动画将卡片移动到对应位置,并处理卡片之外的view的显示状态。
源码分析
我之前写过一篇 如何实现一个简单的RecyclerView 中对自定义有一个简单图画解析,可以看一下。
这篇的我只对列表如何实现高度计算、布局、动画及还原过程的详细解释,我还是推荐先看看源码。
高度计算
onMeasure中调用了checkContentHeightByParent()和** measureChild(),前者计算了view 的可显示区域;measureChild**中确定view所有子控件的高度:
private void measureChild(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = 0;
mTotalLength = 0;//总高度
mTotalLength += getPaddingTop() + getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {//遍历子item
final View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final int totalLength = mTotalLength;
final LayoutParams lp =
(LayoutParams) child.getLayoutParams();
if (needOpen(i)) {//不需要重叠的item计算时用原来的高度
lp.mHeight = child.getMeasuredHeight();//这里mlHeight中储存了高度,布局时能用上
} else {//需要折叠的item计算时用重叠后显示的高度
lp.mHeight = mOverlapGaps;
}
mTotalLength = Math.max(totalLength, totalLength + lp.mHeight + lp.topMargin +
lp.bottomMargin);//累积子item高度即可
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
}
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, mShowHeight);
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
heightSizeAndState);
}
onLayout布局,注意个布局,item 属性动画还原时要使用同样的布局方式。
private void layoutChild() {
int childTop = getPaddingTop();
int childLeft = getPaddingLeft();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
final int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
final LayoutParams lp =
(LayoutParams) child.getLayoutParams();
childTop += lp.topMargin;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);//按照高度布局
childTop += lp.mHeight;//childTop实际是下一个item离上一个item顶部的距离,lp.mHight在onMeasure()中计算的时候就保存了高度
}
}
动画一:开启动画
开启动画内容,选中的item上移,其他卡片item下移折叠
CardStackView缓存了Adapter的viewHolder,通过他对卡片设置点击监听:
继续向下
点击后属性动画类AnimatorAdapter执行动画操作
先看开启动画onItemExpand():
/**
* 打开动画
*/
public void onItemExpand(final CardStackView.ViewHolder viewHolder, int position) {
if (mSet != null && mSet.isRunning()) return;
initAnimatorSet();
final int preSelectPosition = mCardStackView.getCardSelectPosition();
final CardStackView.ViewHolder preSelectViewHolder = mCardStackView.getViewHolder(preSelectPosition);
if (preSelectViewHolder != null) {
preSelectViewHolder.onItemExpand(false);
}
mCardStackView.setCardSelectPosition(position);//设置当前选择卡片,隐藏头部和底部itemView
itemExpandAnimatorSet(viewHolder, position);//执行动画操作,会发现是个空实现
...
mSet.start();
}
itemExpandAnimatorSet(viewHolder, position) 是一个空实现,
被AllMoveDownAnimatorAdapter继承实现了这个方法:
/**
* 打开动画
*/
protected void itemExpandAnimatorSet(final CardStackView.ViewHolder viewHolder, int position) {
final View itemView = viewHolder.itemView;
viewHolder.beforeOpenHeight = itemView.getMeasuredHeight();//记录一下没有完全展开后的高度,恢复时要用
viewHolder.itemView.clearAnimation();
//选中卡片飞到顶部
ObjectAnimator oa = ObjectAnimator.ofFloat(itemView, View.Y, itemView.getY(), mCardStackView.getScrollY() + mCardStackView.getPaddingTop());
mSet.play(oa);
...
int collapsePosition = mCardStackView.getNumBottomShow();//底部折叠的个数
...
for (int i = 0; i < mCardStackView.getChildCount(); i++) {//遍历未选中的卡片
...//这里跳过已经选中的卡片
if (i < collapsePosition) {//前面规定的折叠卡片到底部
childTop = mCardStackView.getShowHeight() - getCollapseStartTop(collapseShowItemCount) + mCardStackView.getScrollY();
ObjectAnimator oAnim = ObjectAnimator.ofFloat(child, View.Y, child.getY(), childTop);
mSet.play(oAnim);
collapseShowItemCount++;
} else {//其他移动到屏幕外
ObjectAnimator oAnim = ObjectAnimator.ofFloat(child, View.Y, child.getY(), mCardStackView.getShowHeight() + mCardStackView.getScrollY());
mSet.play(oAnim);
}
}
}
动画二:还原
onItemCollapse 中 AllMoveDownAnimatorAdapter也实现了还原动画itemCollapseAnimatorSet这个方法:
查看:
/**
* 关闭动画
* @param viewHolder 之前选中的viewHolder
*/
public void onItemCollapse(final CardStackView.ViewHolder viewHolder) {
if (mSet != null && mSet.isRunning()) return;
initAnimatorSet();
...
itemCollapseAnimatorSet(viewHolder);//依然是空实现
mSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
...
mCardStackView.setCardSelectPosition(CardStackView.DEFAULT_SELECT_POSITION);//动画完成取消选中状态,头部和底部view显示
}
});
mSet.start();
}
itemCollapseAnimatorSet还原动画中实质是按照CardStackView类onLayout的布局进行了还原:
/**
* 关闭动画
* @param viewHolder 之前选中的viewHolder
*/
@Override
protected void itemCollapseAnimatorSet(CardStackView.ViewHolder viewHolder) {
int childTop = mCardStackView.getPaddingTop();
for (int i = 0; i < mCardStackView.getChildCount(); i++) {
View child = mCardStackView.getChildAt(i);
child.clearAnimation();
final CardStackView.LayoutParams lp =
(CardStackView.LayoutParams) child.getLayoutParams();
childTop += lp.topMargin;
ObjectAnimator oAnim = ObjectAnimator.ofFloat(child, View.Y, child.getY(), childTop);
mSet.play(oAnim);
if(mCardStackView.getChildSeletPosition()==i&&mCardStackView.needOpen(i)){
childTop += viewHolder.beforeOpenHeight;//这里的高度有点问题做了一些处理,因为打开时高度包含详情,测量高度不对
}else {
childTop +=lp.mHeight;//这个childTop 跟onLayout()是一毛一样的
}
}
}
总结:
- onMeaure计算高度
- onLayout中把所有子view码好
- viewHolder给卡片设置点击监听,点击后调用动画方法
- AllMoveDownAnimatorAdapter 类中实现选中子卡片上升,未选中卡片下滑,同时隐藏顶部和底部view
- AllMoveDownAnimatorAdapter 类中实现还原动画,按照onLayout中的排列方式还原卡片,动画结束后显示顶部和底部view
实现代码:https://github.com/drchengit/alipay_card
感谢开源:https://github.com/loopeer/CardStackView
实现简单ReyclerView : https://juejin.im/post/5df72c5bf265da33b7553a40
我是drchen,一个温润的男子,版权所有,未经允许不得抄袭。