美文网首页
仿支付宝银行卡选择页面

仿支付宝银行卡选择页面

作者: DrChenZeng | 来源:发表于2020-03-19 11:15 被阅读0次

    实现代码: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,一个温润的男子,版权所有,未经允许不得抄袭。

    相关文章

      网友评论

          本文标题:仿支付宝银行卡选择页面

          本文链接:https://www.haomeiwen.com/subject/rpvkyhtx.html