一.前言
-
先看效果(大佬们请忽略水印):
-
卡片层叠列表的实现效果已经发布成插件,集成地址: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 = adapterval 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 = cardLayoutManagertouchHelper.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) {// 先移除所有viewremoveAllViews()// 在布局之前,将所有的子 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) {//从缓冲池中获取到itemViewval view = recycler.getViewForPosition(position)// 将 Item View 加入到 RecyclerView 中addView(view)// 测量 Item ViewmeasureChildWithMargins(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_SCALEview.scaleY = 1 - (position - 1) * mCManager.DEFAULT_SCALEwhen (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_SCALEview.scaleY = 1 - position * mCManager.DEFAULT_SCALEwhen (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_SCALEview.scaleY = 1 - position * mCManager.DEFAULT_SCALEwhen (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.itemViewif (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {//滑动的比例var ratio: Float = dX / mCManager.getThreshold(recyclerView)// ratio 最大为 1 或 -1if (ratio > 1) {ratio = 1f} else if (ratio < -1) {ratio = -1f}//旋转的角度itemView.rotation = ratio * mCManager.DEFAULT_ROTATE_DEGREEval 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 - 1val view = recyclerView.getChildAt(position)//通过调用setScaleX()和setScaleY()方法,可以实现View的缩放view.scaleX =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEview.scaleY =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEwhen (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_Yelse -> 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 - 1val view = recyclerView.getChildAt(position)view.scaleX =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEview.scaleY =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEwhen (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_Yelse -> 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.layoutPositionval remove: T = mDataList.removeAt(layoutPosition)if (mCManager.isLoopCard()) {mDataList.add(remove)}//主动调用刷新,否则会出现只有顶层卡片才能滑动mAdapter.notifyDataSetChanged()//使用接口回调进行拓展1if (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// 当没有数据时回调 mListenerif (mAdapter.itemCount == 0 && mListener != null && !mCManager.isLoopCard()) {mListener!!.onSwipedAllItem()}}
四.总结
- 核心在于两点,其一:自定义RecyclerView.LayoutManager实现类,重写onLayoutChildren方法,对子item进行“绘制”;其二:自定义ItemTouchHelper.Callback实现类,重写onChildDraw和onSwiped方法;快速实现只需要按照前言中的方式进行配置即可,若想要了解细节,可以看插件代码中的注释(写的还是比较详细);