前言
前面介绍了利用自定义ItemDecoration实现RecyclerView吸顶效果,重点就在onDraw -> ItemView -> onDrawOver的绘制流程,同时注意预留空间的利用。
今天介绍一个新的知识点与demo,利用自定义LayoutManager的方式实现卡片滑动。有时候系统提供的LayoutManager已经不能够满足我们的需求,这个时候我们就需要自定义LayoutManager。
卡片滑动效果介绍
卡片滑动的效果如下图所示。
分析一下卡片效果,顶部的卡片是正常摆放,接着的后面两张会随着位置变化缩小、下移,产生视觉上的叠层效果,再然后的卡片就与第三张重叠摆放。可以滑动移除图片,下面的卡片会自动补全。
- 要实现这样重叠式布局的效果,我们就需要自定义LayoutManager,就像系统自带的
LinearLayoutManager
、GridLayoutManager
一样。
- 滑动移除效果的实现,没有重写
scrollVerticallyBy
以及scrollHorizontallyBy
,这就需要一个重要的帮助类ItemTouchHelper
卡片滑动实现
实现思路
- 摆放策略交给LayoutManager->具体怎么摆,每一个卡片布局策略和数据绑定由Adapter负责
- LayoutManager必须重写
onLayoutChildren
方法,当数据发生改变时,会重新布局,detachAndScrapAttachedViews处理回收,recycler.getViewForPosition(i)获取对应位置的View。
- 测量view,摆放时需要考虑通过ItemDecoration预留的间距
layoutDecoratedWithMargins
- 卡片采用的是倒序摆放,即从第8张到第一张的顺序摆放
- 利用
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){ 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.SimpleCallback
,ItemTouchHelper.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){ 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() } } } } override fun getAnimationDuration( recyclerView: RecyclerView, animationType: Int, animateDx: Float, animateDy: Float ): Long { return 500 } }
|
总结
目前为止,小编已经介绍了RecyclerView自定义用法的两种,自定义ItemDecoration和自定义LayoutManager。这里有一个逻辑上的坑点:顶部的卡片是正常摆放,我们自定义LayoutManager中不用管它,所以在上方两个代码块中都有level>0
和level<CardConfig.MAX_SHOW_COUNT-1
,也就是重绘索引值i=5,6
即第6和7个,在SlideCardLayoutManager
中,第5张卡片即一下的重叠在第6张的下面。这个点烦恼了小编好久,刚开始一直不明白顶部的摆放去哪了,从这点可以看出了解原理看源码的重要性。下一章,我们将对RecyclerView源码机制进行解读,再结合这一章的内容,将会对RecyclerView的原理有个更深层次的理解。
全部代码地址