目錄
1.3 StaggeredGridLayoutManager
前言
文章屬於學習總結 ,如有錯漏之處,敬請指正。
同系列文章
Android控件RecyclerView(一)——大家都知道的RecyclerView
Android控件RecyclerView(三)——ItemDecoration的使用與自定義
1 常用LayoutManager
LayoutManager是RecyclerView中子Item的佈局管理器,可控制Item的位置,回收,顯示,大小,滾動等等。下面簡單介紹幾個LayoutManager。
雖然前文寫有Adapter以及模擬數據的設置,但還是把Item佈局和Adapter貼出來。
Item佈局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitXY"
app:layout_constraintDimensionRatio="3:2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:gravity="center"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView"
tools:text="android" />
</android.support.constraint.ConstraintLayout>
Adapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import cn.xhuww.recyclerview.R
import kotlinx.android.synthetic.main.recycle_item_image_text_vertical.view.*
class ImageTextAdapter : RecyclerView.Adapter<ImageTextAdapter.ViewHolder>() {
var items: List<String> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recycle_item_image_text_vertical, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindView(items[position])
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindView(content: String) {
itemView.imageView.setImageResource(R.mipmap.image)
itemView.textView.text = content
}
}
}
1.1 LinearLayoutManager
LinearLayoutManager爲RecyclerView提供了與ListView類似的功能,單列展示,它有三個構造方法,
LinearLayoutManager(Context context)
LinearLayoutManager(Context context, int orientation, boolean reverseLayout)
LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
第一個構造方法:默認創建一個 vertical(豎向) 的 LinearLayoutManager
第二個構造方法:可選LinearLayoutManager方向,以及是否反轉佈局位置
第三個構造方法:用於在XML中設置 layoutManager屬性
例子如下:
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
下列構造方法對於的效果圖分別對應 圖 1、2、3
LinearLayoutManager(this)
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
圖一與圖二區別爲,數據顯示位置反轉,圖三與圖一二的區別爲,列表展示與滑動方向不同。
1.2 GridLayoutManager
GridLayoutManager爲RecyclerView提供了與GridView類似的功能,網格展示,它有與LinearLayoutManager類似,也有三個構造方法。
GridLayoutManager(Context context, int spanCount)
GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout)
GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
第一個構造方法:默認創建一個 vertical(豎向) 的 GridLayoutManager,spanCount爲網格列數
第二個構造方法:可選GridLayoutManager方向,以及是否反轉佈局位置
第三個構造方法:用於在XML中設置 layoutManager屬性
下列構造方法對於的效果圖分別對應 圖 1、2、3
GridLayoutManager(this, 3)
GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, true)
GridLayoutManager(this, 2, GridLayoutManager.HORIZONTAL, false)
第三個因爲手機屏幕顯示3個顯示不完整,就改爲了顯示兩個,橫向滑動。
1.3 StaggeredGridLayoutManager
StaggeredGridLayoutManager交錯的網格佈局,如果子View寬高一致,那效果就和GridLayoutManager一樣,如果子View寬高不一致,就可以實現瀑布流效果。
該類有兩個構造方法,第二個是針對xml設置layoutManager屬性的。
StaggeredGridLayoutManager(int spanCount, int orientation)
StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
修改一下Item佈局的圖片寬高,去掉縱橫比
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="fitXY"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
然後Adapter中改變一下顯示圖片
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindView(content: String, position: Int) {
if (position % 2 == 0) {
itemView.imageView.setImageResource(R.mipmap.image_positive)
} else {
itemView.imageView.setImageResource(R.mipmap.image)
}
itemView.textView.text = content
}
}
設置豎向的StaggeredGridLayoutManager
val imageTextAdapter = ImageTextAdapter().apply {
//創建含20個字符串的集合 其中 R.string.item_position == 第%1$d個Item
items = (0..20).map { resources.getString(R.string.item_position, it) }
}
val staggeredGridLayoutManager =
StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
recyclerView.apply {
layoutManager = staggeredGridLayoutManager
adapter = imageTextAdapter
}
查看效果
1.4 FlexboxLayoutManager
FlexboxLayoutManager 來自於Google出品的流式佈局 flexbox-layout 支持RecyclerView,畢竟是Google出品,而且很常用,就把他也列舉了出來。
地址:https://github.com/google/flexbox-layout
依賴:
implementation 'com.google.android:flexbox:1.0.0'
針對於FlexboxLayoutManager 這個類,使用方式與LinearLayoutManager類似,至於FlexBox的其他屬性可查看官方文檔
recyclerView.layoutManager = FlexboxLayoutManager(this)
通過RecyclerView,然後設置 FlexboxLayoutManager之後的效果圖
2 自定義LayoutManager
爲什麼要自定義LayoutManager呢?因爲通過自定義LayoutManager可以實現很多炫酷的功能,也能讓我們更清晰的瞭解RecyclerView。
當然現在我還實現不了比較炫酷的功能,下面簡單的實現一個可無限循環橫向滑動的LayoutManager,爲通過RecyclerViewl來實現無限滑動的Banner做準備。
2.1 創建自定義LayoutManager類
創建HorizontalLayoutManager 繼承於LayoutManager,必須重寫方法generateDefaultLayoutParams(),默認返回RecyclerView.LayoutParams。
class HorizontalLayoutManager : RecyclerView.LayoutManager() {
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}
然後替換RecyclerView的LayoutManager
recyclerView.layoutManager = HorizontalLayoutManager()
運行後,會界面一片空白,因爲RecyclerView的子View是在其LayoutManager中繪製的,我們並未寫對應的代碼。
2.2 繪製RecyclerView子View
繪製方法命名肯定離不開onLayout這個單詞,在LayoutManager中可以重寫onLayoutChildren方法繪製子View。
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
//分離並且回收當前附加的所有View
detachAndScrapAttachedViews(recycler)
if (itemCount == 0) {
return
}
//橫向繪製子View,則需要知道 X軸的偏移量
var offsetX = 0
//繪製並添加view
for (i in 0 until itemCount) {
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(view)
val viewHeight = getDecoratedMeasuredHeight(view)
layoutDecorated(view, offsetX, 0, offsetX + viewWidth, viewHeight)
offsetX += viewWidth
}
}
因爲是橫向列表,所以先把Item佈局文件修改i一下,寬度改爲固定值160dp,然後使用HorizontalLayoutManager看效果
圖中的批註解釋了繪製原理,通過循環以及累加x軸偏移量,橫向繪製完所有子View。
layoutDecorated(view, offsetX, 0, offsetX + viewWidth, viewHeight)
offsetX += viewWidth
此時子View已繪製完成,但還無法滑動,所以需要添加滑動代碼。
2.3 添加滑動功能
因爲要實現的是橫向滑動功能,所以只重寫橫向滑動的對應方法,與之對應的還有豎向的方法
//是否可橫向滑動
override fun canScrollHorizontally(): Boolean {
return true
}
override fun scrollHorizontallyBy(
dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State
): Int {
//日誌顯示,左滑dx值爲正數,右滑dx值爲負數
Log.i("TAG", "----------dx:$dx")
/**
* 橫向移動所有子View
* 爲什麼要 * -1 ? 屏幕xy軸原點在左上角,左移則需要View的座標 x - offset 右移則需要 x + offset
* 所以需要 dx * -1
*/
offsetChildrenHorizontal(dx * -1)
return dx
}
此時的效果
橫向滑動實現了,顯示完所有子View後在滑動就是空白了,正常情況下還需要判斷是否滑到頭、尾了,但我需要實現的是無限循環橫向滑動,所以只需往右滑滑倒第0個時然後往左邊繪製並添加最後一個子View,往左滑滑到最後一個時,在右邊在添加第0個子View。實現無限循環滑動。
2.4 實現橫向循環滑動的LayoutManager
既然在滑動時還需要繪製,那麼就需要單獨寫一個繪製方法 fill()。
//爲什麼大多文章都定義方法名爲fill? 我想是因爲Android提供的3個LayoutManager都用的此方法名吧
private fun fill(dx: Int, recycler: RecyclerView.Recycler) {
//左滑
if (dx > 0) {
//得到當前已添加(可見)的最後一個子View
val lastVisibleView = getChildAt(childCount - 1) ?: return
//得到View對應的位置
val layoutPosition = getPosition(lastVisibleView)
/**
* 例如要顯示20個View,當前可見的最後一個View就是第20個,那麼下一個要顯示的就是第一個
* 如果當前顯示的View不是第20個,那麼就顯示下一個,如當前顯示的是第15個View,那麼下一個顯示第16個
* 注意區分 childCount 與 itemCount
*/
val nextView: View = if (layoutPosition == itemCount - 1) {
recycler.getViewForPosition(0)
} else {
recycler.getViewForPosition(layoutPosition + 1)
}
addView(nextView)
measureChildWithMargins(nextView, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(nextView)
val viewHeight = getDecoratedMeasuredHeight(nextView)
val offsetX = lastVisibleView.right
layoutDecorated(nextView, offsetX, 0, offsetX + viewWidth, viewHeight)
} else { //右滑
val firstVisibleView = getChildAt(0) ?: return
val layoutPosition = getPosition(firstVisibleView)
/**
* 如果當前第一個可見View爲第0個,則左側顯示第20個View 如果不是,下一個就顯示前一個
*/
val nextView = if (layoutPosition == 0) {
recycler.getViewForPosition(itemCount - 1)
} else {
recycler.getViewForPosition(layoutPosition - 1)
}
addView(nextView, 0)
measureChildWithMargins(nextView, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(nextView)
val viewHeight = getDecoratedMeasuredHeight(nextView)
val offsetX = firstVisibleView.left
layoutDecorated(nextView, offsetX - viewWidth, 0, offsetX, viewHeight)
}
}
然後在scrollHorizontallyBy中調用即可,實現滑動中繼續繪製,查看此時的效果,實現了橫向無限循環滑動的效果
2.5 缺陷
我們都知道使用RecyclerView,並不需要在額外做佈局複用緩存處理,因爲RecyclerView已經幫我們做好了,那是不是使用自定義的LayoutManager也具有複用與回收功能呢?
Adapter中添加Log,查看創建的View數量,然後分別使用LinearLayoutManager和剛自定義的 HorizontalLayoutManager
private var createViewCount = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
...
Log.i("TAG", "-------------createViewCount:${++createViewCount}")
...
}
分別查看日誌:
LinearLayoutManager
HorizontalLayoutManager
可以發現 LinearLayoutManager 初次只創建了3個View,而且不管怎樣滑動最多也就7個View,而HorizontalLayoutManager 初始化就創建了20個View,而且後面滑動時,創建了的View數量多出了許多。
2.6 完善
2.6.1 繪製數量限制
在前面的基礎上,繪製子View時,超出RecyclerView範圍則不繪製子View,
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
...
var offsetX = 0
//繪製並添加view
for (i in 0 until itemCount) {
...
offsetX += viewWidth
if (offsetX > width){
break
}
}
}
在滑動時,如果當前兩側的最後一個View滑動後還是未完全展示出來,就不繪製下一個View。
還有個問題就:繪製完下一個View後,RecyclerView偏移 dx,當dx大於子View的寬度時,就會出現子View數量未繪製完,RecyclerView顯白色的問題,前面沒這個問題的原因是沒加繪製條件,滑動時在不斷繪製子View,修改後的代碼如下。
private fun fill(dx: Int, recycler: RecyclerView.Recycler) {
//左滑
if (dx > 0) {
while (true) {
//得到當前已添加(可見)的最後一個子View
val lastVisibleView = getChildAt(childCount - 1) ?: break
//如果滑動過後,View還是未完全顯示出來就 不進行繪製下一個View
if (lastVisibleView.right - dx > width)
break
...
}
} else { //右滑
while (true) {
val firstVisibleView = getChildAt(0) ?: break
if (firstVisibleView.left - dx < 0) break
...
}
}
}
2.6.2 回收子View
當子View超出RecyclerView的範圍時,就移除並回收子View
private fun recycleViews(dx: Int, recycler: RecyclerView.Recycler) {
for (i in 0 until itemCount) {
val childView = getChildAt(i) ?: return
//左滑
if (dx > 0) {
//移除並回收 原點 左側的子View
if (childView.right - dx < 0) {
removeAndRecycleViewAt(i, recycler)
}
} else { //右滑
//移除並回收 右側即RecyclerView寬度之以外的子View
if (childView.left - dx > width) {
removeAndRecycleViewAt(i, recycler)
}
}
}
}
因爲滑動時在不斷添加繪製View,所以對應的也應移除回收View
在 scrollHorizontallyBy 中調用
override fun scrollHorizontallyBy(
dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State
): Int {
recycleViews(dx, recycler)
fill(dx, recycler)
offsetChildrenHorizontal(dx * -1)
return dx
}
2.6.3 最終效果
完善代碼後,查看效果,基本達到了要求。
完整代碼
import android.graphics.PointF
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
class HorizontalLayoutManager : RecyclerView.LayoutManager(),
RecyclerView.SmoothScroller.ScrollVectorProvider {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
if (childCount == 0) {
return null
}
val firstChildPos = getPosition(getChildAt(0)!!)
val direction = if (targetPosition < firstChildPos) -1 else 1
return PointF(direction.toFloat(), 0f)
}
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) {
//分離並且回收當前附加的所有View
detachAndScrapAttachedViews(recycler)
if (itemCount == 0) {
return
}
//橫向繪製子View,則需要知道 X軸的偏移量
var offsetX = 0
//繪製並添加view
for (i in 0 until itemCount) {
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(view)
val viewHeight = getDecoratedMeasuredHeight(view)
layoutDecorated(view, offsetX, 0, offsetX + viewWidth, viewHeight)
offsetX += viewWidth
if (offsetX > width) {
break
}
}
}
//是否可橫向滑動
override fun canScrollHorizontally(): Boolean {
return true
}
override fun scrollHorizontallyBy(
dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State
): Int {
recycleViews(dx, recycler)
fill(dx, recycler)
offsetChildrenHorizontal(dx * -1)
return dx
}
private fun fill(dx: Int, recycler: RecyclerView.Recycler) {
//左滑
if (dx > 0) {
while (true) {
//得到當前已添加(可見)的最後一個子View
val lastVisibleView = getChildAt(childCount - 1) ?: break
//如果滑動過後,View還是未完全顯示出來就 不進行繪製下一個View
if (lastVisibleView.right - dx > width)
break
//得到View對應的位置
val layoutPosition = getPosition(lastVisibleView)
/**
* 例如要顯示20個View,當前可見的最後一個View就是第20個,那麼下一個要顯示的就是第一個
* 如果當前顯示的View不是第20個,那麼就顯示下一個,如當前顯示的是第15個View,那麼下一個顯示第16個
* 注意區分 childCount 與 itemCount
*/
val nextView: View = if (layoutPosition == itemCount - 1) {
recycler.getViewForPosition(0)
} else {
recycler.getViewForPosition(layoutPosition + 1)
}
addView(nextView)
measureChildWithMargins(nextView, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(nextView)
val viewHeight = getDecoratedMeasuredHeight(nextView)
val offsetX = lastVisibleView.right
layoutDecorated(nextView, offsetX, 0, offsetX + viewWidth, viewHeight)
}
} else { //右滑
while (true) {
val firstVisibleView = getChildAt(0) ?: break
if (firstVisibleView.left - dx < 0) break
val layoutPosition = getPosition(firstVisibleView)
/**
* 如果當前第一個可見View爲第0個,則左側顯示第20個View 如果不是,下一個就顯示前一個
*/
val nextView = if (layoutPosition == 0) {
recycler.getViewForPosition(itemCount - 1)
} else {
recycler.getViewForPosition(layoutPosition - 1)
}
addView(nextView, 0)
measureChildWithMargins(nextView, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(nextView)
val viewHeight = getDecoratedMeasuredHeight(nextView)
val offsetX = firstVisibleView.left
layoutDecorated(nextView, offsetX - viewWidth, 0, offsetX, viewHeight)
}
}
}
private fun recycleViews(dx: Int, recycler: RecyclerView.Recycler) {
for (i in 0 until itemCount) {
val childView = getChildAt(i) ?: return
//左滑
if (dx > 0) {
//移除並回收 原點 左側的子View
if (childView.right - dx < 0) {
removeAndRecycleViewAt(i, recycler)
}
} else { //右滑
//移除並回收 右側即RecyclerView寬度之以外的子View
if (childView.left - dx > width) {
removeAndRecycleViewAt(i, recycler)
}
}
}
}
}
3. 總結
- RecyclerView自己具有繪製、回收、緩存複用子View的方法,但需要在LayoutManager調用
- 回收View是根據RecyclerView的寬或高來判斷的,所以想要具有緩存複用功能,RecyclerView一定要有確定的寬或高。
- getChildCount() 是得到RecyclerView中顯示的Item個數
- getItemCount() 是得到Adapter中設置的需要顯示的item個數
- getChildAt(int position) 是從當前屏幕顯示的View中的到對應位置的View
- getPosition(View view) 得到View對應Adapter中的索引位置
- recycler.getViewForPosition(position) 是複用View的關鍵