自定义RecyclerView.LayoutManager实现类实现卡片层叠布局的列表效果

一.前言

  • 先看效果(大佬们请忽略水印):
    在这里插入图片描述

  • 卡片层叠列表的实现效果已经发布成插件,集成地址:implementation ‘com.github.MrFishC:YcrCardLayoutHepler:v1.1’;

  • 先讲解如何快速实现,然后再来讲解插件[支持加载固定数量的数据,无限循环,加载更多]中的实现细节;

二.使用方式

  • 先看代码
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import cn.jack.library_arouter.manager.constants.RouterPathActivity
import com.alibaba.android.arouter.facade.annotation.Route
import com.jack.lib_base.base.view.BaseSimpleActivity
import com.jack.simple_recycleview.databinding.ActivitySimpleRecycleviewBinding
import com.jack.ycr_rv_cardlayout.ConfigManager
import com.jack.ycr_rv_cardlayout.CustomItemTouchHelperCallBackImp
import com.jack.ycr_rv_cardlayout.CustomLayoutManager
import com.jack.ycr_rv_cardlayout.OnItemSwipeListener
import java.util.*
@Route(path = RouterPathActivity.SimpleRv.PAGER_SIMPLE_RV)
class SimpleRecycleViewActivity :
    BaseSimpleActivity<ActivitySimpleRecycleviewBinding>(ActivitySimpleRecycleviewBinding::inflate) {
    
    

    override fun prepareData() {
    
    
        super.prepareData()

        val adapter = MyAdapter()
        mBinding.recyclerView.adapter = adapter
        val manager = ConfigManager()
        val callBackImp = CustomItemTouchHelperCallBackImp(adapter, list, manager)
        callBackImp.setOnSwipedListener(object : OnItemSwipeListener<Int> {
    
    
            override fun onItemSwiping(
                viewHolder: RecyclerView.ViewHolder,
                ratio: Float,
                direction: Int
            ) {
    
    
                when (direction) {
    
    
                    manager.SWIPING_LEFT -> {
    
    
                        println("向左侧滑动")
                    }

                    manager.SWIPING_RIGHT -> {
    
    
                        println("向右侧滑动")
                    }

                    else -> {
    
    
                        println("向未知方向滑动")
                    }
                }
            }

            override fun onItemSwiped(viewHolder: RecyclerView.ViewHolder, t: Int, direction: Int) {
    
    
                when (direction) {
    
    
                    manager.SWIPED_UP -> {
    
    
                        println("从上方滑出")
                    }

                    manager.SWIPED_DOWN -> {
    
    
                        println("从下方滑出")
                    }

                    manager.SWIPED_LEFT -> {
    
    
                        println("从左侧滑出")
                    }

                    manager.SWIPED_RIGHT -> {
    
    
                        println("从右侧滑出")
                    }

                    else -> {
    
    
                        println("从未知方向滑出")
                    }
                }
            }

            @SuppressLint("NotifyDataSetChanged")
            override fun onSwipedAllItem() {
    
    
                println("卡片全部滑出")
                //根据实际业务来实现 加载更多
                mBinding.recyclerView.postDelayed({
    
    
                    initData()
                    Objects.requireNonNull(mBinding.recyclerView.adapter).notifyDataSetChanged()
                }, 1500L)
            }
        })
        val touchHelper = ItemTouchHelper(callBackImp)
        val cardLayoutManager = CustomLayoutManager(mBinding.recyclerView, touchHelper, manager)
        mBinding.recyclerView.layoutManager = cardLayoutManager
        touchHelper.attachToRecyclerView(mBinding.recyclerView)
        initData()
    }

    private fun initData() {
    
    
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
        list.add(R.drawable.icon_common_bg)
    }

    private val list: MutableList<Int> = ArrayList()

    private inner class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
    
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    
    
            val view =
                LayoutInflater.from(parent.context).inflate(R.layout.simple_rv_item, parent, false)
            return MyViewHolder(view)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    
    }
        override fun getItemCount(): Int {
    
    
            return list.size
        }

        inner class MyViewHolder(itemView: View?) : RecyclerView.ViewHolder(
            itemView!!
        )
    }
}
  • 插件集成之后,只需要按照prepareData方法中的代码做一下配置便可以实现卡片层叠的列表;

三.插件实现细节

1.ConfigManager

  • 这里是一些配置信息,列表item的缩放比例,显示列表的item数量,item支持滑出的方向,item层叠的方式等等;

2.CustomLayoutManager

  • 这个是层叠布局实现的关键,通过自定义RecyclerView.LayoutManager的实现类可以实现很多炫酷的UI,其核心在于onLayoutChildren方法,绘制RecycleView的子View:
override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {
    
    
        // 先移除所有view
        removeAllViews()
        // 在布局之前,将所有的子 View 先 Detach 掉,放入到 Scrap 缓存中
        detachAndScrapAttachedViews(recycler)
        val itemCount = itemCount

        // 当数据源个数大于最大显示数时
        if (itemCount > mCManager.DEFAULT_SHOW_ITEM) {
    
    
            // 把数据源倒着循环,这样,第0个数据就在屏幕最上面了            为什么倒序就可以让第0个数据在屏幕最上面  原理是什么 待研究
            for (position in mCManager.DEFAULT_SHOW_ITEM downTo 0) {
    
    
                //从缓冲池中获取到itemView
                val view = recycler.getViewForPosition(position)
                // 将 Item View 加入到 RecyclerView 中
                addView(view)
                // 测量 Item View
                measureChildWithMargins(view, 0, 0)
                // getDecoratedMeasuredWidth(view) 可以得到 Item View 的宽度
                // 所以 widthSpace 就是除了 Item View 剩余的值
                val widthSpace = width - getDecoratedMeasuredWidth(view)
                // 同理
                val heightSpace = height - getDecoratedMeasuredHeight(view)

                // recyclerview布局:在这里默认布局是放在 RecyclerView 中心
                // layoutDecoratedWithMargins: 将child显示在RecyclerView上面,left,top,right,bottom规定了显示的区域
                layoutDecoratedWithMargins(
                    view, widthSpace / 2, heightSpace / 2,
                    widthSpace / 2 + getDecoratedMeasuredWidth(view),
                    heightSpace / 2 + getDecoratedMeasuredHeight(view)
                )

                // 其实屏幕上有 mCManager.DEFAULT_SHOW_ITEM + 1 张卡片,但是我们把第 mCManager.DEFAULT_SHOW_ITEM 张和
                // 第 mCManager.DEFAULT_SHOW_ITEM + 1 张卡片重叠在一起,这样看上去就只有 mCManager.DEFAULT_SHOW_ITEM  张
                // 第CardConfig.DEFAULT_SHOW_ITEM + 1张卡片主要是为了保持动画的连贯性
                if (position == mCManager.DEFAULT_SHOW_ITEM) {
    
    
                    view.scaleX = 1 - (position - 1) * mCManager.DEFAULT_SCALE
                    view.scaleY = 1 - (position - 1) * mCManager.DEFAULT_SCALE
                    when (mCManager.getStackDirection()) {
    
    
                        //从下往上层叠
                        mCManager.UP ->
                            view.translationY =
                                ((position - 1) * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()

                        //从上往下层叠
                        mCManager.DOWN ->
                            view.translationY =
                                (-(position - 1) * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()

                        else -> view.translationY =
                            (-(position - 1) * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()
                    }
                } else if (position > 0) {
    
    
                    view.scaleX = 1 - position * mCManager.DEFAULT_SCALE
                    view.scaleY = 1 - position * mCManager.DEFAULT_SCALE
                    when (mCManager.getStackDirection()) {
    
    
                        //从下往上层叠
                        mCManager.UP ->
                            view.translationY =
                                (position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()

                        //从上往下层叠
                        mCManager.DOWN ->
                            view.translationY =
                                (-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()

                        else -> view.translationY =
                            (-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()
                    }
                } else {
    
    
                    //只有顶层的卡片才能滑动
                    view.setOnTouchListener(mOnTouchListener)
                }
            }
        } else {
    
    
            // 当数据源个数小于或等于最大显示数时
            for (position in itemCount - 1 downTo 0) {
    
    
                val view = recycler.getViewForPosition(position)
                addView(view)
                measureChildWithMargins(view, 0, 0)
                val widthSpace = width - getDecoratedMeasuredWidth(view)
                val heightSpace = height - getDecoratedMeasuredHeight(view)
                // recyclerview 布局
                layoutDecoratedWithMargins(
                    view, widthSpace / 2, heightSpace / 2,
                    widthSpace / 2 + getDecoratedMeasuredWidth(view),
                    heightSpace / 2 + getDecoratedMeasuredHeight(view)
                )
                if (position > 0) {
    
    
                    view.scaleX = 1 - position * mCManager.DEFAULT_SCALE
                    view.scaleY = 1 - position * mCManager.DEFAULT_SCALE
                    when (mCManager.getStackDirection()) {
    
    
                        //从下往上层叠
                        mCManager.UP ->
                            view.translationY =
                                (position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()

                        //从上往下层叠
                        mCManager.DOWN ->
                            view.translationY =
                                (-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()

                        else -> view.translationY =
                            (-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()
                    }
                } else {
    
    
                    view.setOnTouchListener(mOnTouchListener)
                }
            }
        }
    }

3.CustomItemTouchHelperCallBackImp

  • ItemTouchHelper.Callback的实现类,两个核心方法:
    • onChildDraw:这个方法被触发的条件之一是,item滑动的时候,在该方法内部对所有可见的item进行缩放,对最上层的item进行旋转角度的设置,如此,用户体验效果更佳;
override fun onChildDraw(
        c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
        dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
    ) {
    
    
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
        val itemView = viewHolder.itemView
        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
    
    
            //滑动的比例
            var ratio: Float = dX / mCManager.getThreshold(recyclerView)

            // ratio 最大为 1 或 -1
            if (ratio > 1) {
    
    
                ratio = 1f
            } else if (ratio < -1) {
    
    
                ratio = -1f
            }

            //旋转的角度
            itemView.rotation = ratio * mCManager.DEFAULT_ROTATE_DEGREE
            val childCount = recyclerView.childCount

            //卡片滑动过程中   对view进行缩放处理  [这里的逻辑需要跟自定义的RecyclerView.LayoutManager实现类onLayoutChildren方法对应]        具体的缩放效果可以自行通过计算来尝试
            // 当数据源个数大于最大显示数时
            if (childCount > mCManager.DEFAULT_SHOW_ITEM) {
    
    
                //position:从1开始       for循环中定义position的初始值以及其边界,目的是为了让第一张不做处理
                for (position in 1 until childCount - 1) {
    
    
                    val index = childCount - position - 1
                    val view = recyclerView.getChildAt(position)
                    //通过调用setScaleX()和setScaleY()方法,可以实现View的缩放
                    view.scaleX =
                        1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALE
                    view.scaleY =
                        1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALE
                    when (mCManager.getStackDirection()) {
    
    
                        //从下往上层叠
                        mCManager.UP -> view.translationY =
                            (index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y
                        //从上往下层叠
                        mCManager.DOWN -> view.translationY =
                            -(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y

                        else -> view.translationY =
                            -(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y
                    }
                }
            } else {
    
    
                // 当数据源个数小于或等于最大显示数时      for循环中定义position的初始值以及其边界,目的是为了让最后一张不做处理
                for (position in 0 until childCount - 1) {
    
    
                    val index = childCount - position - 1
                    val view = recyclerView.getChildAt(position)
                    view.scaleX =
                        1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALE
                    view.scaleY =
                        1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALE
                    when (mCManager.getStackDirection()) {
    
    
                        //从下往上层叠
                        mCManager.UP ->
                            view.translationY =
                                (index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y

                        //从上往下层叠
                        mCManager.DOWN ->
                            view.translationY =
                                -(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y

                        else -> view.translationY =
                            -(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y
                    }
                }
            }
            //由于增加了上下方向 这里 可以按需添加业务逻辑
            if (ratio != 0f) {
    
    
                if (mListener != null) {
    
    
                    mListener!!.onItemSwiping(
                        viewHolder,
                        ratio,
                        if (ratio < 0) mCManager.SWIPING_LEFT else mCManager.SWIPING_RIGHT
                    )
                }
            } else {
    
    
                if (mListener != null) {
    
    
                    mListener!!.onItemSwiping(viewHolder, ratio, mCManager.SWIPING_NONE)
                }
            }
        }
    }

  • onSwiped:当item滑动的时候触发,这个方法内部做设置回调,设置支持无限循环的情况下集合需要添加顶层被移除的item,同时需要调用适配器的notifyDataSetChanged方法,对所有item“一视同仁”;
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
    
    
        // 移除onTouchListener,防止触摸 滑动之间冲突
        viewHolder.itemView.setOnTouchListener(null)

        val layoutPosition = viewHolder.layoutPosition
        val remove: T = mDataList.removeAt(layoutPosition)

        if (mCManager.isLoopCard()) {
    
    
            mDataList.add(remove)
        }

        //主动调用刷新,否则会出现只有顶层卡片才能滑动
        mAdapter.notifyDataSetChanged()

        //使用接口回调进行拓展1
        if (mListener != null) {
    
    
            when (direction) {
    
    
                ItemTouchHelper.UP -> mListener!!.onItemSwiped(
                    viewHolder,
                    remove,
                    mCManager.SWIPED_UP
                )

                ItemTouchHelper.DOWN -> mListener!!.onItemSwiped(
                    viewHolder,
                    remove,
                    mCManager.SWIPED_DOWN
                )

                ItemTouchHelper.LEFT -> mListener!!.onItemSwiped(
                    viewHolder,
                    remove,
                    mCManager.SWIPED_LEFT
                )

                ItemTouchHelper.RIGHT -> mListener!!.onItemSwiped(
                    viewHolder,
                    remove,
                    mCManager.SWIPED_RIGHT
                )

                else -> mListener!!.onItemSwiped(viewHolder, remove, mCManager.SWIPED_NONE)
            }
        }

        //使用接口回调进行拓展2
        // 当没有数据时回调 mListener
        if (mAdapter.itemCount == 0 && mListener != null && !mCManager.isLoopCard()) {
    
    
            mListener!!.onSwipedAllItem()
        }
    }

四.总结

  • 核心在于两点,其一:自定义RecyclerView.LayoutManager实现类,重写onLayoutChildren方法,对子item进行“绘制”;其二:自定义ItemTouchHelper.Callback实现类,重写onChildDraw和onSwiped方法;快速实现只需要按照前言中的方式进行配置即可,若想要了解细节,可以看插件代码中的注释(写的还是比较详细);

猜你喜欢

转载自blog.csdn.net/itTalmud/article/details/130332860
今日推荐