一、前言
RecyclerView 是從5.0推出的 MD 風格的控件。RecyclerView 之前有 ListView、GridView,但是功能很有限,例如 ListView 只能實現垂直方向上的滑動等。但是存在則合理,ListView 卻沒有被官方標記為 @Deprecated
,有興趣的同學可以去找下相關資料,主要看下 RecyclerView 和 ListView 的布局重用機制。在 ListView 文檔上可以發現一句話
For a more modern, flexible, and performant approach to displaying lists, use?RecyclerView
翻譯為:要獲得更現代、更靈活、更高效的列表顯示方法,請使用 RecyclerView 就是說 RecyclerView 很牛逼
A flexible view for providing a limited window into a large data set
本文主題是 RecyclerView#ItemDecoration
。Decoration:裝飾,裝潢;裝飾品;裝飾器。顧名思義就是給 Item 一些打扮的。ItemDecoration 允許應用程序從適配器的數據集中為特定的 ItemViews 添加特殊的圖形和布局偏移量。這對于在 Item 之間繪制分隔線,突出顯示,分組等等非常有用。 下面進入主題。
二、效果
看圖
描述:RecyclerView 最上面有一個塊紅色的條,滾動是紅色條也跟著向上滾;除了最后一個每個 Item 都有一條分割線,并且分割線距離左邊有一定的距離;前個 Item 右邊有一個圖標。
三、實現步驟
ItemDecoration 是一個抽象類一共有6個方法,其中三個標記為 @Deprecated
, 所以真正用的方法是以下三個:
getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State)
onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
1、方法介紹
按照執行順序先后: 1. getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State)
為特定的 ItemView 設置偏移量,此方法在 RecyclerView 測量 ItemView 后執行。 參數說明: 1)outRect:ItemView 邊界,可用理解為原來 ItemView padding。 例如:outRect.set(50, 50, 50, 50)
,參數順序為 “左上右下”,原來的 ItemView 上下左右都會擴展 50 像素,如下圖
?2)view:RecyclerView 的 ItemView(將被裝飾的View),outRect.set() 設置的邊界針對的是這個 View
?3)parent:RecyclerView
?4)state:當前 RecyclerView 的狀態
2. onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
此方法在 RecyclerView 的 onDraw(Canvas c)
方法中調用,在 ItemView 下層繪制內容,繪制的內容可能會被 ItemView 遮擋住 1)c:畫布,和自定義 View 那樣把內容繪制在畫布上。 如圖:假設只有一個 ItemView, 紅色區域是繪制的內容,大小是 100x100 像素從頂點開始繪制 c.drawRect(Rect(0, 0, 100, 100), mPaint)
,在 getItemOffsets
設置 outRect.set(50, 50, 0, 0)
3. onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
此方法在 RecyclerView 的 draw(Canvas c)
方法中調用和 onDraw(Canvas c)
一樣,區別在于此方法繪制的內容有可能會覆蓋 ItemView。 還是上面的例子,如果 c.drawRect(Rect(0, 0, 100, 100), mPaint)
放在 onDrawOver()
效果如下圖:
ItemView 的三個方法就簡單講到這里,下面上代碼。
2、分割線代碼
新建一個類 ItemLineDivider.kt
, 貼出部分代碼
class ItemLineDivider(@RecyclerView.Orientation var orientation: Int = VERTICAL) : RecyclerView.ItemDecoration() {//邊界private val mBounds: Rect = Rect()private val mPaint = Paint()@ColorIntvar dividerColor: Int = Color.GRAYset(value) {mPaint.color = value}private val defaultSize = 1//默認1像素var hasEndDivider = true//是否要最后一個item的分割線var dividerWidth = defaultSize//豎線寬度,單位pxvar dividerHeight = defaultSize//橫線高度,單位px/**分割線左邊間距*/var leftSpace: Int = 0/**分割線右邊間距*/var topSpace: Int = 0/**分割線上方間距*/var rightSpace: Int = 0/**分割線下方間距*/var bottomSpace: Int = 0init {mPaint.color = dividerColormPaint.isAntiAlias = true}/*** 分割線繪制在ItemView 的下層,* 如果 getItemOffsets 中 outRect 四個參數都是 0, 則 ItemView 有背景的情況會把分割線遮擋*/override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {super.onDraw(c, parent, state)i("onDraw")if (orientation == VERTICAL) {drawVertical(c, parent)} else {drawHorizontal(c, parent)}}/*** 為分割線騰出位置* [outRect] ItemView 邊距*/override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {super.getItemOffsets(outRect, view, parent, state)i("getItemOffsets")if (orientation == VERTICAL) {outRect.set(0, 0, 0, dividerHeight)} else {outRect.set(0, 0, dividerWidth, 0)}}/*** 繪制水平分割線*/private fun drawVertical(c: Canvas, parent: RecyclerView) {c.save()val left: Intval right: Intif (parent.clipToPadding) {left = parent.paddingLeft + leftSpace //左邊坐標right = if (dividerWidth != defaultSize) {//右邊坐標left + dividerWidth//設置寬度,以設置的寬度優先} else {parent.width - parent.paddingEnd - rightSpace}c.clipRect(left, parent.paddingTop, right, parent.height - parent.paddingBottom)} else {left = leftSpaceright = if (dividerWidth != defaultSize) {left + dividerWidth} else {parent.width - rightSpace}}var childCount = parent.childCountif (!hasEndDivider) {//最后一個 Item 不繪制分割線childCount -= 1}for (i in 0 until childCount) {val child = parent.getChildAt(i)parent.getDecoratedBoundsWithMargins(child, mBounds)val bottom: Int = mBounds.bottom + Math.round(child.translationY)val top: Int = bottom - dividerHeightval rect = Rect(left, top, right, bottom)c.drawRect(rect, mPaint)}c.restore()}}
復制代碼
3、頂部條塊代碼
新建一個類 VerticalItemStartLine.kt
class VerticalItemStartLine : RecyclerView.ItemDecoration() {private val mBound = Rect()private val mPaint = Paint()private val defaultSize = 1var lineWidth = defaultSizevar lineHeight = defaultSize@ColorIntvar color = Color.GRAYset(value) {mPaint.color = value}override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {drawVertical(c, parent)}private fun drawVertical(c: Canvas, parent: RecyclerView) {c.save()val child = parent.getChildAt(0)val childIndex = parent.getChildAdapterPosition(child)if (childIndex == 0) {parent.getDecoratedBoundsWithMargins(parent.getChildAt(0), mBound)val left = mBound.leftval right = if (lineWidth == defaultSize) {parent.width} else {lineWidth}val top = mBound.topval bottom = lineHeight + topc.drawRect(Rect(left, top, right, bottom), mPaint)i(mBound.toShortString() + "\nleft=$left, top=$top, right=$right, bottom=$bottom")}c.restore()}override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {if (parent.getChildAdapterPosition(view) == 0) {//只在第一個頭上添加outRect.set(0, lineHeight, 0, 0)}}}
復制代碼
4、右邊標簽代碼
新建一個類 TopThreeItemDrawOver.kt
class TopThreeItemDrawOver(val drawable: Drawable) : RecyclerView.ItemDecoration() {private val width = 100private val height = 100override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {i(drawable.bounds.toShortString())for (i in 0..2) {//把 drawable 畫到前三個 itemView 上val child = parent.getChildAt(i)val index = parent.getChildAdapterPosition(child)val left = parent.width - 50 - widthval right = left + widthval space = (child.height - height) / 2val top = child.top + spaceval bottom = child.bottom - spaceif (index < 3) {drawable.setBounds(left, top, right, bottom)drawable.draw(c)}}}}
復制代碼
5、把上面三個 ItemDecoration 添加到 RecyclerView
private fun init() {val myAdapter = MyAdapter(this, getData())val layoutManager = LinearLayoutManager(this)val itemDecoration = ItemLineDivider(RecyclerView.VERTICAL)itemDecoration.apply {dividerHeight = 5leftSpace = 140hasEndDivider = false}val startItemDecoration = VerticalItemStartLine()startItemDecoration.apply {lineHeight = 100color = Color.RED}val drawOver = TopThreeItemDrawOver(resources.getDrawable(R.drawable.ic_swap_horiz))recycler_view.apply {addItemDecoration(startItemDecoration)//頭部條塊addItemDecoration(itemDecoration)//分割線addItemDecoration(drawOver)//右邊標簽setHasFixedSize(true)setLayoutManager(layoutManager)adapter = myAdapter}myAdapter.notifyDataSetChanged()}
復制代碼
四、總結
通過一個簡單的例子,可以很好的理解高大上的 ItemDecoration,什么分割線啊也不用在 xml 布局文件里設置了。ItemDecoration 還可以實現時間軸、黏附等效果,這里就不舉例了,根據上面的方法解析和例子,再加上自己的想法,可以在 RecyclerView 實現很多效果。我覺得剛開始的話重點去理解 Rect
,坐標,偏移量,就可以很好把一個內容繪制到指定位置了。
參考: RecyclerView 文檔