前言

前面介绍了利用自定义ItemDecoration实现RecyclerView吸顶效果,重点就在onDraw -> ItemView -> onDrawOver的绘制流程,同时注意预留空间的利用。

今天介绍一个新的知识点与demo,利用自定义LayoutManager的方式实现卡片滑动。有时候系统提供的LayoutManager已经不能够满足我们的需求,这个时候我们就需要自定义LayoutManager。

卡片滑动效果介绍

卡片滑动的效果如下图所示。

分析一下卡片效果,顶部的卡片是正常摆放,接着的后面两张会随着位置变化缩小、下移,产生视觉上的叠层效果,再然后的卡片就与第三张重叠摆放。可以滑动移除图片,下面的卡片会自动补全。

  • 实现这样的效果突破点在于两点:
  1. 要实现这样重叠式布局的效果,我们就需要自定义LayoutManager,就像系统自带的LinearLayoutManagerGridLayoutManager一样。
  2. 滑动移除效果的实现,没有重写scrollVerticallyBy以及scrollHorizontallyBy,这就需要一个重要的帮助类ItemTouchHelper

SlideCard

卡片滑动实现

  • 实现思路

    1. 摆放策略交给LayoutManager->具体怎么摆,每一个卡片布局策略和数据绑定由Adapter负责
    2. LayoutManager必须重写onLayoutChildren方法,当数据发生改变时,会重新布局,detachAndScrapAttachedViews处理回收,recycler.getViewForPosition(i)获取对应位置的View。
    3. 测量view,摆放时需要考虑通过ItemDecoration预留的间距layoutDecoratedWithMargins
    4. 卡片采用的是倒序摆放,即从第8张到第一张的顺序摆放
    5. 利用ItemTouchHelper实现滑动移除,并且需要重写onChildDraw,实现滑动动画开始时,下方图片缩放效果。TouchHelper是ItemDecoration的子类,在onDraw时根据滑动距离来影响后面view的大小
  • 关键代码块:

class SlideCardLayoutManager:RecyclerView.LayoutManager() {
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
}

override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
super.onLayoutChildren(recycler, state)
detachAndScrapAttachedViews(recycler!!)
val itemCount = itemCount
var bottomPosition = 0
if (itemCount<CardConfig.MAX_SHOW_COUNT){
bottomPosition = 0
}else{
bottomPosition = itemCount - CardConfig.MAX_SHOW_COUNT
}
for (i in bottomPosition until itemCount){
val view = recycler.getViewForPosition(i)
addView(view)
//测量
measureChildWithMargins(view,0,0)
val widthSpace = width - getDecoratedMeasuredHeight(view)
val heightSpace = height - getDecoratedMeasuredHeight(view)
//布局
layoutDecoratedWithMargins(view,
widthSpace/2,
heightSpace/2,
widthSpace/2+getDecoratedMeasuredWidth(view),
heightSpace/2+getDecoratedMeasuredHeight(view))
var level = itemCount -i -1
if (level>0){
if (level<CardConfig.MAX_SHOW_COUNT-1){//提示,这里CardConfig.MAX_SHOW_COUNT=4,可见小编所有代码
view.translationY = CardConfig.TRANS_Y_GAP * level
view.scaleX = 1-CardConfig.SCALE_GAP*level
view.scaleY = 1-CardConfig.SCALE_GAP*level
}else{
view.translationY = CardConfig.TRANS_Y_GAP * (level-1)
view.scaleX = 1-CardConfig.SCALE_GAP*(level-1)
view.scaleY = 1-CardConfig.SCALE_GAP*(level-1)
}
}
}
}
}
  • 注意:这里继承的是ItemTouchHelper.SimpleCallbackItemTouchHelper.Callback需要重写的方法太多。
  • 重写onChildDraw,因为notifyDataSetChanged()会走onDraw流程,而onDraw又会调用onChildDraw。
class SlideCallBack(
private val adapter:SlideCardAdapter,
private val mData:MutableList<SlideCardBean>
):ItemTouchHelper.SimpleCallback(0,15) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}

@SuppressLint("NotifyDataSetChanged")
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val remove = mData.removeAt(viewHolder.layoutPosition)
mData.add(0,remove)
adapter.notifyDataSetChanged()
}

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 maxDistance = recyclerView.width * 0.5f
val distance = sqrt((dX*dX+dY*dY).toDouble())
var fraction = distance/maxDistance
if (fraction>1){
fraction= 1.0
}
val itemCount = recyclerView.childCount
Log.v("ppp","itemCount: $itemCount")
for(i in 0 until itemCount){
val view = recyclerView.getChildAt(i)
val level = itemCount - i - 1
Log.v("pc","level: $level")
if (level>0){
//重绘level=1,2 ->也就是i=5,6
if (level<CardConfig.MAX_SHOW_COUNT-1){
view.translationY = (CardConfig.TRANS_Y_GAP * (level-fraction)).toFloat()
view.scaleX = (1-CardConfig.SCALE_GAP*level+fraction*CardConfig.SCALE_GAP).toFloat()
view.scaleY = (1-CardConfig.SCALE_GAP*level+fraction*CardConfig.SCALE_GAP).toFloat()
}
}
}
}
//s时间
override fun getAnimationDuration(
recyclerView: RecyclerView,
animationType: Int,
animateDx: Float,
animateDy: Float
): Long {
return 500
}
}

总结

目前为止,小编已经介绍了RecyclerView自定义用法的两种,自定义ItemDecoration和自定义LayoutManager。这里有一个逻辑上的坑点:顶部的卡片是正常摆放,我们自定义LayoutManager中不用管它,所以在上方两个代码块中都有level>0level<CardConfig.MAX_SHOW_COUNT-1,也就是重绘索引值i=5,6即第6和7个,在SlideCardLayoutManager中,第5张卡片即一下的重叠在第6张的下面。这个点烦恼了小编好久,刚开始一直不明白顶部的摆放去哪了,从这点可以看出了解原理看源码的重要性。下一章,我们将对RecyclerView源码机制进行解读,再结合这一章的内容,将会对RecyclerView的原理有个更深层次的理解。

全部代码地址