我们知道RecyclerView 有很多显示的样式可供选择,比如列表的,网格的,瀑布流式的显示样式,这主要取决于RecyclerView.LayoutManager这个类,想前面说的网格的,列表的这些都是继承自这个类实现的,不过这些样式,比如:GridLayoutManager, LinearLayoutManager等这些系统已经帮我们做好了,所以要想实现类似探探的那种效果的话,我们还的想系统这样去继承RecyclerView.LayoutManager来对列表里面的子控件进行布局,显示成我们想要的样子,也就是自定义LayoutManager

最终的效果实现如下图所示:


自定义LayoutManager

首先我们继承RecyclerView.LayoutManager重新它里面必要的方法generateDefaultLayoutParamsonLayoutChildren, onLayoutChildren这个方法就是我们子view布局的方法,下面是卡片布局的实现代码:

public class SwipeCardLayoutManager extends RecyclerView.LayoutManager {

    public SwipeCardLayoutManager(Context context) {
        CardConfig.initConfig(context);
    }

    /**
     * 这个方法一般就是这个写法,基本上是固定的写法
     * @return
     */
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }


    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

        //回收的方法
        detachAndScrapAttachedViews(recycler);
        //获取item的数量
        int itemCount = getItemCount();
        //最底下的卡片item的索引位置
        int bottomPosition ;

        //判断如果item的数量小于卡片最大的显示数量的话,那最底下的索引就为0
        if(itemCount < CardConfig.MAX_SHOW_COUNT){
            bottomPosition = 0;
        }else {
            //卡片的数量减去卡片最大的显示数量就是最底下卡片的所在索引
            bottomPosition = itemCount - CardConfig.MAX_SHOW_COUNT;
        }

        //从最底下的卡片索引开始循环
        for (int i = bottomPosition; i < itemCount; i++) {
            //取出一个item, 注意这里最终会调到tryGetViewHolderForPositionByDeadline
            //也就是说它会先从缓存里面取,缓存没有的话,就会创建一个view
            View itemView = recycler.getViewForPosition(i);
            addView(itemView);

            //测量子View的尺寸
            measureChildWithMargins(itemView, 0, 0);

            //计算剩余空间
            int widthSpace = getWidth() - getDecoratedMeasuredWidth(itemView);
            int heightSpace = getHeight() - getDecoratedMeasuredHeight(itemView);

            //对每个itemView进行布局
            layoutDecoratedWithMargins(itemView, widthSpace/2, heightSpace/2,
                    widthSpace/2 + getDecoratedMeasuredWidth(itemView),
                    heightSpace/2 + getDecoratedMeasuredHeight(itemView));


            int level = itemCount - i - 1;

            if(level > 0){
                //对每个itemView进行平移与缩放
                if(level < CardConfig.MAX_SHOW_COUNT - 1){
                    itemView.setTranslationY(CardConfig.TRANS_Y_GAP * level);

                    float scalValue = 1 - level * CardConfig.SCALE_GAP;
                    itemView.setScaleX(scalValue);
                    itemView.setScaleY(scalValue);
                }else {
                    //最底下的itemView
                    itemView.setTranslationY(CardConfig.TRANS_Y_GAP * (level - 1));
                    float boomScale = 1 - (level - 1) * CardConfig.SCALE_GAP;
                    itemView.setScaleX(boomScale);
                    itemView.setScaleY(boomScale);
                }
            }
        }

    }
}

简单说明下,根据卡片的数量与最大显示的数量计算出最底下卡片的显示位置索引, 然后从这个索引开始到卡片的数量进行循环,再循环里面对子View进行测量布局,因为卡片的显示效果是有层次变化的,所以,还要对每个itemView进行平移缩放,这样的话就完成了卡片的布局。

卡片滑动实现

完成布局之后,接着就是滑动了,滑动的话我们需要借助ItemTouchHelper这个类来实现

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeCardCallBack(mRecyclerView, mAdapter, swipeCardBeans));
        itemTouchHelper.attachToRecyclerView(mRecyclerView);

可以看到ItemTouchHelper需要传入一个Callback, 而这个Callback就是我们做自定义滑动的Callback, 这里我们需要继承ItemTouchHelper.SimpleCallback来实现我们想要的滑动,以下是滑动的具体实现:

public class SwipeCardCallBack extends ItemTouchHelper.SimpleCallback{

    private RecyclerView mRecyclerView;
    private List<SwipeCardBean> mSwipeList;
    private UniversalAdapter<SwipeCardBean> mSwipeAdapter;

    public SwipeCardCallBack(RecyclerView recyclerView, UniversalAdapter<SwipeCardBean> universalAdapter,
                             List<SwipeCardBean> list) {
        super(0, ItemTouchHelper.LEFT |
                ItemTouchHelper.RIGHT | ItemTouchHelper.UP |
                ItemTouchHelper.DOWN);

        mRecyclerView = recyclerView;
        mSwipeAdapter = universalAdapter;
        mSwipeList = list;
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        SwipeCardBean swipeCardBean = mSwipeList.remove(viewHolder.getLayoutPosition());
        mSwipeList.add(0, swipeCardBean);
        mSwipeAdapter.notifyDataSetChanged();
    }

    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
                            @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY,
                            int actionState, boolean isCurrentlyActive) {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);

        double maxDistance = recyclerView.getWidth() * 0.5f;
        double distance = Math.sqrt(dX * dX + dY * dY);

        double fraction = distance / maxDistance;

        if(fraction > 1){
            fraction = 1;
        }

        int itemCount = recyclerView.getChildCount();


        for (int i = 0; i < itemCount; i++) {
            View view = recyclerView.getChildAt(i);

            int level = itemCount - i - 1;
            if(level > 0){
                if(level < CardConfig.MAX_SHOW_COUNT - 1){
                    view.setTranslationY((float) (CardConfig.TRANS_Y_GAP * level - fraction * CardConfig.TRANS_Y_GAP));
                    view.setScaleX((float) (1 - level * CardConfig.SCALE_GAP + fraction * CardConfig.SCALE_GAP));
                    view.setScaleY((float) (1 - level * CardConfig.SCALE_GAP + fraction * CardConfig.SCALE_GAP));
                }
            }
        }
    }

    public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
        return 300;
    }

    /**
     * 滑动距离
     * 互动到一定距离就消失
     * @param viewHolder
     * @return
     */
    @Override
    public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
        return 0.2f;
    }
}public class SwipeCardCallBack extends ItemTouchHelper.SimpleCallback{

    private RecyclerView mRecyclerView;
    private List<SwipeCardBean> mSwipeList;
    private UniversalAdapter<SwipeCardBean> mSwipeAdapter;

    public SwipeCardCallBack(RecyclerView recyclerView, UniversalAdapter<SwipeCardBean> universalAdapter,
                             List<SwipeCardBean> list) {
        super(0, ItemTouchHelper.LEFT |
                ItemTouchHelper.RIGHT | ItemTouchHelper.UP |
                ItemTouchHelper.DOWN);

        mRecyclerView = recyclerView;
        mSwipeAdapter = universalAdapter;
        mSwipeList = list;
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    //这里主要做的是滑动完之后,又将这个数据添加到列表头部,这样的话,就可以一直循环滑动了
        SwipeCardBean swipeCardBean = mSwipeList.remove(viewHolder.getLayoutPosition());
        mSwipeList.add(0, swipeCardBean);
        mSwipeAdapter.notifyDataSetChanged();
    }

    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
                            @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY,
                            int actionState, boolean isCurrentlyActive) {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);

        double maxDistance = recyclerView.getWidth() * 0.5f;
        double distance = Math.sqrt(dX * dX + dY * dY);

        double fraction = distance / maxDistance;

        if(fraction > 1){
            fraction = 1;
        }

        int itemCount = recyclerView.getChildCount();


        for (int i = 0; i < itemCount; i++) {
            View view = recyclerView.getChildAt(i);

            int level = itemCount - i - 1;
            if(level > 0){
                if(level < CardConfig.MAX_SHOW_COUNT - 1){
                    view.setTranslationY((float) (CardConfig.TRANS_Y_GAP * level - fraction * CardConfig.TRANS_Y_GAP));
                    view.setScaleX((float) (1 - level * CardConfig.SCALE_GAP + fraction * CardConfig.SCALE_GAP));
                    view.setScaleY((float) (1 - level * CardConfig.SCALE_GAP + fraction * CardConfig.SCALE_GAP));
                }
            }
        }
    }

    public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
        return 300;
    }

    /**
     * 滑动距离
     * 互动到一定距离就消失
     * @param viewHolder
     * @return
     */
    @Override
    public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
        return 0.2f;
    }
}

可以看到这里面关键的方法是onChildDraw,这个方法就是滑动的时候对子view绘制的方法,在这个方法里面我们做的主要是在滑动的时候, 让其他的卡片做一个持续平移与缩放的效果,这样可以增强用户体验, 其实如果不重写onChildDraw的话也是可以滑动的,只不过滑动的时候有些生硬,不是很自然!至此卡片滑动的实现也说完了!

总结

其实很简单,首先就是布局,布局的话就是继承自定义LayoutManager来对,子view进行重新布局,然后就是滑动,滑动的话借助ItemTouchHelper就可以实现滑动,只不过这里继承了ItemTouchHelper.SimpleCallback重写onChildDraw,在里面实现了滑动的时候让其他卡片持续缩放平移的效果,以增强用户体验!