仿支付宝银行卡选择页面

实现代码:https://github.com/drchengit/alipay_card
感谢开源:https://github.com/loopeer/CardStackView
实现简单ReyclerView : https://juejin.im/post/5df72c5bf265da33b7553a40

看到支付宝选择银行卡页面,感觉很有趣,决定写篇文章。

效果:

  • 选择银行卡会打开银行卡
  • 其他银行卡折叠到底部
  • 不相关的view隐藏
  • 再次点击还原
image

实现:

image

实现思路:

表面分析:

  • 卡片是个列表,可以整体滑动,要用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,通过他对卡片设置点击监听:

image

继续向下
image

点击后属性动画类AnimatorAdapter执行动画操作
image

先看开启动画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) 是一个空实现,

image

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);
        }
    }
}

动画二:还原

onItemCollapseAllMoveDownAnimatorAdapter也实现了还原动画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还原动画中实质是按照CardStackViewonLayout的布局进行了还原:

/**
 * 关闭动画
 *  @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,一个温润的男子,版权所有,未经允许不得抄袭。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,374评论 0 27
  • 什么是View android中的UI界面是由View和ViewGroup组合而成的。View就是屏幕中一个能进行...
    Cooke_阅读 463评论 0 0
  • 他是一个妄想症患者,他认为自己是一部书的主角,同时也是作者,三年前被送进医院,药物似乎对他毫无作用。 问:“你知道...
    _柏焱_阅读 1,465评论 0 1
  • 一、子元素设置属性1.绝对定位结合margin:auto 2.绝对定位结合margin设置指定距离 3.绝对定位结...
    冰雪_666阅读 100评论 0 1