一.前言
-
先看效果(大佬们请忽略水印):
-
卡片层叠列表的实现效果已经发布成插件,集成地址: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方法;快速实现只需要按照前言中的方式进行配置即可,若想要了解细节,可以看插件代码中的注释(写的还是比较详细);