自從RecyclerView
出來之后就得到廣泛的應(yīng)用县踢,但是由于其使用和定制化比較復(fù)雜,所以很多時(shí)候都停留在使用階段上巧颈。這片文章簡(jiǎn)單講述一個(gè)自定義一個(gè)簡(jiǎn)單的LayoutManager
.
翻譯
/**
* A <code>LayoutManager</code> is responsible for measuring and positioning item views
* within a <code>RecyclerView</code> as well as determining the policy for when to recycle
* item views that are no longer visible to the user. By changing the <code>LayoutManager</code>
* a <code>RecyclerView</code> can be used to implement a standard vertically scrolling list,
* a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock
* layout managers are provided for general use.
* <p/>
* If the LayoutManager specifies a default constructor or one with the signature
* ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will
* instantiate and set the LayoutManager when being inflated. Most used properties can
* be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case
* a LayoutManager specifies both constructors, the non-default constructor will take
* precedence.
*
*/
翻譯:
一個(gè)LayoutManager
負(fù)責(zé)測(cè)量和定位RecyclerView
的item views, 同時(shí)需要處理在item views不再可見時(shí)的回收工作奋早。通過設(shè)置不同的LayoutManager
, RecyclerView
可以用來實(shí)現(xiàn)標(biāo)準(zhǔn)的縱向滑動(dòng)列表,通用的網(wǎng)格构挤、交錯(cuò)型、橫向滑動(dòng)等效果惕鼓。這幾種LayoutManager
都已經(jīng)提供了筋现。
如果一個(gè)LayoutManager
設(shè)定了默認(rèn)的構(gòu)造器或者一個(gè)參數(shù)分別為Context
, AttributeSet
, int
, int
的構(gòu)造器,RecyclerView
可以通過xml來進(jìn)行實(shí)例化該LayoutManager
, 不用在代碼中設(shè)置箱歧》桑可以在LayoutManager
構(gòu)造器中使用方法getProperties(Context, AttributeSet, int, int)
來獲取自定義的屬性。如果兩種構(gòu)造器都制定了呀邢,那么優(yōu)先使用默認(rèn)的構(gòu)造器洒沦。
功能
從官方文檔中可以看出,一個(gè)LayoutManager
的工作是:測(cè)量价淌、定位和回收RecyclerView
的item views. 簡(jiǎn)而言之申眼,LayoutManager
實(shí)際上就是讓item views合理的顯示在RecyclerView
, 同時(shí)需要進(jìn)行回收肖油。這些工作和定義一個(gè)使用了回收機(jī)制的ViewGroup
非常相似碑诉,就像ListView
一樣,不過它并不考慮數(shù)據(jù)的綁定香拉。
定義一個(gè)有View
回收機(jī)制的ViewGroup
需要處理的工作:
- 測(cè)量(measure)
- 布局(定位, layout)
- 考慮滑動(dòng)
- 回收
View
這里我們先討論一下在LayoutManager
需要怎么處理這些工作病毡。
- 測(cè)量:獲取到
View
后濒翻,需要對(duì)其進(jìn)行測(cè)量,因?yàn)閺?fù)用的原因啦膜,所以這一步必須做 - 布局:對(duì)獲取到的
View
根據(jù)要求放置在界面上有送,同時(shí)需要考慮滑動(dòng) - 滑動(dòng):touch的處理是由
RecyclerView
來完成的,LayoutManager
需要處理的是決定滑動(dòng)方向和修正滑動(dòng)距離 - 回收
View
:關(guān)于如何回收View
都是放在Recycler
中的功戚,LayoutManager
需要告訴那些View
需要被回收
接下來說明一下LayoutManager
做這些工作可能使用到的方法娶眷。
測(cè)量
獲取到View
的過程完全是交由Recycler
來完成似嗤,LinearLayoutManager
并不關(guān)心它是重新構(gòu)造啸臀,還是從緩存中獲取的,然后對(duì)其測(cè)量即可。不過另外需要注意的是View
的margin尺寸乘粒。
下面是測(cè)量方法和另外兩個(gè)計(jì)算一個(gè)child view布局時(shí)所占用的空間尺寸的方法豌注,考慮到了margin:
/**
* Measure a child view using standard measurement policy, taking the padding
* of the parent RecyclerView, any added item decorations and the child margins
* into account.
*
* <p>If the RecyclerView can be scrolled in either dimension the caller may
* pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
*
* @param child Child view to measure
* @param widthUsed Width in pixels currently consumed by other views, if relevant
* @param heightUsed Height in pixels currently consumed by other views, if relevant
*/
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() +
lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() +
lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
/**
* 獲取 child view 橫向上需要占用的空間,margin計(jì)算在內(nèi)
*
* @param view item view
* @return child view 橫向占用的空間
*/
private int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
/**
* 獲取 child view 縱向上需要占用的空間灯萍,margin計(jì)算在內(nèi)
*
* @param view item view
* @return child view 縱向占用的空間
*/
private int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
}
因?yàn)?code>ItemDecoration的存在轧铁,所以獲取尺寸時(shí)使用的是getDecoratedMeasuredWidth(View)
和getDecoratedMeasuredHeight(View)
, 將ItemDecoration
考慮在內(nèi)了。
布局
布局是LayoutManager
最基本的功能旦棉,同時(shí)有比較復(fù)雜的尺寸結(jié)算齿风。如果對(duì)計(jì)算不那么排斥或有比較好的計(jì)算方法的話,實(shí)際上也很簡(jiǎn)單绑洛。
下面是LayoutManager
對(duì)一個(gè)child view進(jìn)行布局(定位)的方法:
/**
* Lay out the given child view within the RecyclerView using coordinates that
* include any current {@link ItemDecoration ItemDecorations} and margins.
*
* <p>LayoutManagers should prefer working in sizes and coordinates that include
* item decoration insets whenever possible. This allows the LayoutManager to effectively
* ignore decoration insets within measurement and layout code. See the following
* methods:</p>
* <ul>
* <li>{@link #layoutDecorated(View, int, int, int, int)}</li>
* <li>{@link #measureChild(View, int, int)}</li>
* <li>{@link #measureChildWithMargins(View, int, int)}</li>
* <li>{@link #getDecoratedLeft(View)}</li>
* <li>{@link #getDecoratedTop(View)}</li>
* <li>{@link #getDecoratedRight(View)}</li>
* <li>{@link #getDecoratedBottom(View)}</li>
* <li>{@link #getDecoratedMeasuredWidth(View)}</li>
* <li>{@link #getDecoratedMeasuredHeight(View)}</li>
* </ul>
*
* @param child Child to lay out
* @param left Left edge, with item decoration insets and left margin included
* @param top Top edge, with item decoration insets and top margin included
* @param right Right edge, with item decoration insets and right margin included
* @param bottom Bottom edge, with item decoration insets and bottom margin included
*
* @see View#layout(int, int, int, int)
* @see #layoutDecorated(View, int, int, int, int)
*/
public void layoutDecoratedWithMargins(View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
注釋翻譯:
在RecyclerView
中定位一個(gè)child view, 考慮了ItemDecoration
和margin的影響救斑。
LayoutManager應(yīng)該盡可能地更加關(guān)注包含ItemDecoration
嵌入時(shí)的尺寸和定位。這個(gè)方法允許LayoutManager
不用考慮受ItemDecoration
影響的尺寸和布局代碼真屯,獲取的直接是最終結(jié)果脸候。
參考的方法:忽略。
參數(shù)
- child: child view
- left, 左側(cè)邊距绑蔫,包括
ItemDecoration
和margin - top, 上邊距
- right, 右邊距
- bottom, 下邊距
可以看出运沦,這個(gè)方法布局時(shí)需要考慮ItemDecoration
的影響和margin, 而進(jìn)行測(cè)量的時(shí)候其實(shí)已經(jīng)包括了ItemDecoration
的影響和margin,我們就不再做過多的計(jì)算工作配深。
滑動(dòng)
滑動(dòng)有兩個(gè)方向携添,一般允許一個(gè)方向的滑動(dòng),實(shí)際上篓叶,也可以同時(shí)允許兩個(gè)方向上的滑動(dòng)薪寓,不過一般沒有這個(gè)需求。
下面是考慮滑動(dòng)時(shí)可能需要處理的幾個(gè)方法:
/**
* Scroll horizontally by dx pixels in screen coordinates and return the distance traveled.
* The default implementation does nothing and returns 0.
*
* @param dx distance to scroll by in pixels. X increases as scroll position
* approaches the right.
* @param recycler Recycler to use for fetching potentially cached views for a
* position
* @param state Transient state of RecyclerView
* @return The actual distance scrolled. The return value will be negative if dx was
* negative and scrolling proceeeded in that direction.
* <code>Math.abs(result)</code> may be less than dx if a boundary was reached.
*/
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
return 0;
}
/**
* Scroll vertically by dy pixels in screen coordinates and return the distance traveled.
* The default implementation does nothing and returns 0.
*
* @param dy distance to scroll in pixels. Y increases as scroll position
* approaches the bottom.
* @param recycler Recycler to use for fetching potentially cached views for a
* position
* @param state Transient state of RecyclerView
* @return The actual distance scrolled. The return value will be negative if dy was
* negative and scrolling proceeeded in that direction.
* <code>Math.abs(result)</code> may be less than dy if a boundary was reached.
*/
public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
return 0;
}
/**
* Query if horizontal scrolling is currently supported. The default implementation
* returns false.
*
* @return True if this LayoutManager can scroll the current contents horizontally
*/
public boolean canScrollHorizontally() {
return false;
}
/**
* Query if vertical scrolling is currently supported. The default implementation
* returns false.
*
* @return True if this LayoutManager can scroll the current contents vertically
*/
public boolean canScrollVertically() {
return false;
}
注釋翻譯:
- scrollHorizontallyBy(int, Recycler, State): 根據(jù)屏幕橫向滑動(dòng)的距離dx計(jì)算實(shí)際滑動(dòng)的距離澜共。默認(rèn)不進(jìn)行滑動(dòng)向叉,返回值為0
- scrollVerticallyBy(int, Recycler, State): 根據(jù)屏幕縱向滑動(dòng)的距離dy計(jì)算實(shí)際滑動(dòng)的距離。默認(rèn)不進(jìn)行滑動(dòng)嗦董,返回值為0
- canScrollHorizontally(): 查詢目前是否支持橫向滑動(dòng)母谎。默認(rèn)返回false
- canScrollVertically(): 查詢目前是否支持橫向滑動(dòng)。默認(rèn)返回false
方法canScrollHorizontally()
和canScrollVertically()
是判斷是否處理RecyclerView
上的滑動(dòng)京革,這個(gè)根據(jù)需求自行實(shí)現(xiàn)奇唤。真正處理滑動(dòng)的是在scrollHorizontallyBy(int, Recycler, State)
和scrollVerticallyBy(int, Recycler, State)
中,手指滑動(dòng)距離作為參數(shù)傳入這兩個(gè)方法中匹摇,而根據(jù)邏輯咬扇,處理滑動(dòng),最后返回處理后的滑動(dòng)距離廊勃,一般情況下懈贺,如果沒有到達(dá)邊界经窖,那么處理后的滑動(dòng)距離和實(shí)際滑動(dòng)距離是一樣的,到達(dá)邊界時(shí)對(duì)滑動(dòng)距離進(jìn)行修正梭灿。
另外需要注意的是在scrollHorizontallyBy(int, Recycler, State)
和scrollVerticallyBy(int, Recycler, State)
中需要進(jìn)行滑動(dòng)時(shí)的布局問題画侣,即滑動(dòng)一定距離之后,實(shí)際上是重新進(jìn)行了布局的堡妒。
回收View
回收View
原因和原理是配乱,在根據(jù)邏輯布局View
時(shí),它超出了用戶的可視范圍皮迟,所以為了性能考慮搬泥,我們應(yīng)該及時(shí)進(jìn)行回收,避免耗費(fèi)過多的內(nèi)存伏尼。而通常的處理方法是佑钾,在用戶的可視范圍上進(jìn)行布局,查看超出邊界的child view, 然后進(jìn)行回收烦粒,當(dāng)然休溶,及時(shí)發(fā)現(xiàn)不可能顯示在界面上的child view, 在布局過程中就可以決定是否需要對(duì)余下的child view進(jìn)行布局。
概念
在RecyclerView
的概念中扰她,有幾種對(duì)回收的概念
- attach/detach: 將item view添加到
RecyclerView
中兽掰,和add/remove不同的是不會(huì)觸發(fā)layout - scrap: 標(biāo)識(shí)一個(gè)item view表示其已經(jīng)從
RecyclerView
中移除,但是實(shí)際上還是在RecyclerView
中 - recycle: 表示一個(gè)沒有parent的
View
的回收處理工作徒役,可以銷毀孽尽,也可以用來復(fù)用
/**
* Temporarily detach and scrap all currently attached child views. Views will be scrapped
* into the given Recycler. The Recycler may prefer to reuse scrap views before
* other views that were previously recycled.
*
* @param recycler Recycler to scrap views into
*/
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
注釋翻譯:
臨時(shí)detach和scrap所有的child view. 這些view將被放置到Recycler
中處理。相對(duì)于之前回收的那些view, Recycler
會(huì)首先使用利用這些views.
除了回收所有的child view之外忧勿,還有很多其他的處理child view的方法杉女,全部列舉在下面,就不再一一翻譯鸳吸,可能會(huì)用到的一個(gè)方法removeAndRecycleView(View, Recycler)
, 表示移除一個(gè)child view, 并交由指定的Recycler
處理熏挎。
/**
* Temporarily detach a child view.
*
* <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
* views currently attached to the RecyclerView. Generally LayoutManager implementations
* will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
* so that the detached view may be rebound and reused.</p>
*
* <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
* {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
* or {@link #removeDetachedView(android.view.View) fully remove} the detached view
* before the LayoutManager entry point method called by RecyclerView returns.</p>
*
* @param child Child to detach
*/
public void detachView(View child) {
final int ind = mChildHelper.indexOfChild(child);
if (ind >= 0) {
detachViewInternal(ind, child);
}
}
/**
* Temporarily detach a child view.
*
* <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
* views currently attached to the RecyclerView. Generally LayoutManager implementations
* will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
* so that the detached view may be rebound and reused.</p>
*
* <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
* {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
* or {@link #removeDetachedView(android.view.View) fully remove} the detached view
* before the LayoutManager entry point method called by RecyclerView returns.</p>
*
* @param index Index of the child to detach
*/
public void detachViewAt(int index) {
detachViewInternal(index, getChildAt(index));
}
private void detachViewInternal(int index, View view) {
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchStartTemporaryDetach(view);
}
mChildHelper.detachViewFromParent(index);
}
/**
* Reattach a previously {@link #detachView(android.view.View) detached} view.
* This method should not be used to reattach views that were previously
* {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
*
* @param child Child to reattach
* @param index Intended child index for child
* @param lp LayoutParams for child
*/
public void attachView(View child, int index, LayoutParams lp) {
ViewHolder vh = getChildViewHolderInt(child);
if (vh.isRemoved()) {
mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh);
} else {
mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh);
}
mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved());
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
}
/**
* Reattach a previously {@link #detachView(android.view.View) detached} view.
* This method should not be used to reattach views that were previously
* {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
*
* @param child Child to reattach
* @param index Intended child index for child
*/
public void attachView(View child, int index) {
attachView(child, index, (LayoutParams) child.getLayoutParams());
}
/**
* Reattach a previously {@link #detachView(android.view.View) detached} view.
* This method should not be used to reattach views that were previously
* {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
*
* @param child Child to reattach
*/
public void attachView(View child) {
attachView(child, -1);
}
/**
* Finish removing a view that was previously temporarily
* {@link #detachView(android.view.View) detached}.
*
* @param child Detached child to remove
*/
public void removeDetachedView(View child) {
mRecyclerView.removeDetachedView(child, false);
}
/**
* Moves a View from one position to another.
*
* @param fromIndex The View's initial index
* @param toIndex The View's target index
*/
public void moveView(int fromIndex, int toIndex) {
View view = getChildAt(fromIndex);
if (view == null) {
throw new IllegalArgumentException("Cannot move a child from non-existing index:"
+ fromIndex);
}
detachViewAt(fromIndex);
attachView(view, toIndex);
}
/**
* Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
*
* <p>Scrapping a view allows it to be rebound and reused to show updated or
* different data.</p>
*
* @param child Child to detach and scrap
* @param recycler Recycler to deposit the new scrap view into
*/
public void detachAndScrapView(View child, Recycler recycler) {
int index = mChildHelper.indexOfChild(child);
scrapOrRecycleView(recycler, index, child);
}
/**
* Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
*
* <p>Scrapping a view allows it to be rebound and reused to show updated or
* different data.</p>
*
* @param index Index of child to detach and scrap
* @param recycler Recycler to deposit the new scrap view into
*/
public void detachAndScrapViewAt(int index, Recycler recycler) {
final View child = getChildAt(index);
scrapOrRecycleView(recycler, index, child);
}
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param child Child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleView(View child, Recycler recycler) {
removeView(child);
recycler.recycleView(child);
}
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param index Index of child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
實(shí)現(xiàn)FlowLayoutManger
看過很多例子,在學(xué)習(xí)LayoutManager
的時(shí)候通常都是以FlowLayoutManager
來作為實(shí)踐的晌砾,因?yàn)槲覀儗?duì)其比較了解坎拐,而且也沒有更好的示例來作為練習(xí)。
FlowLayoutManager
的功能
- 每個(gè)item view從第一行開始進(jìn)行橫向排列养匈,當(dāng)一行不足顯示下一個(gè)item的時(shí)候哼勇,將其布局在下一行
- 支持縱向滑動(dòng)(也可以支持橫向滑動(dòng),不過目前不在考慮范圍內(nèi))
- 每個(gè)item高度不一致的考慮
構(gòu)造器
根據(jù)前面的闡述呕乎,我們實(shí)現(xiàn)兩個(gè)構(gòu)造器积担,分別用于代碼構(gòu)造和xml中使用。
public FlowLayoutManager() {
setAutoMeasureEnabled(true);
}
public FlowLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
setAutoMeasureEnabled(true);
}
注意setAutoMeasureEnabled(boolean)
是表明RecyclerView
的布局是否交由LayoutManager
進(jìn)行處理猬仁,否則應(yīng)該重寫LayoutManager#onMeasure(int, int)
來自定義測(cè)量的實(shí)現(xiàn)帝璧。一般傳true, 除非有特殊需求先誉。
實(shí)現(xiàn)默認(rèn)方法
在LayoutManager
必須實(shí)現(xiàn)這個(gè)方法。
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
就是生成默認(rèn)的LayoutParams
, 參考LinearLayoutManager
, 除非有特殊需要再改變聋溜。
暫時(shí)不考慮滑動(dòng)進(jìn)行布局
只考慮第一次進(jìn)行布局時(shí)
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
// 如果正在進(jìn)行動(dòng)畫谆膳,則不進(jìn)行布局
if (getChildCount() == 0 && state.isPreLayout()) {
return;
}
detachAndScrapAttachedViews(recycler);
// 進(jìn)行布局
layout(recycler, state);
}
/**
* 布局操作
*
* @param recycler
* @param state
*/
private void layout(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 縱向計(jì)算偏移量叭爱,考慮padding
int topOffset = getPaddingTop();
// 橫向計(jì)算偏移量撮躁,考慮padding
int leftOffset = getPaddingLeft();
// 行高,以最高的item作為參考
int maxLineHeight = 0;
final int childCount = getChildCount();
// 當(dāng)?shù)谝淮芜M(jìn)行布局時(shí)
if (childCount == 0) {
for (int i = 0; i < getItemCount(); i++) {
// 獲取一個(gè)item view, 添加到RecyclerView中买雾,進(jìn)行測(cè)量把曼、布局
final View itemView = recycler.getViewForPosition(i);
addView(itemView);
// 測(cè)量,獲取尺寸
measureChildWithMargins(itemView, 0, 0);
final int sizeHorizontal = getDecoratedMeasurementHorizontal(itemView);
final int sizeVertical = getDecoratedMeasurementVertical(itemView);
// 進(jìn)行布局
if (leftOffset + sizeHorizontal <= getHorizontalSpace()) {
// 如果這行能夠布局漓穿,則往后排
// layout
layoutDecoratedWithMargins(itemView, leftOffset, topOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical);
// 修正橫向計(jì)算偏移量
leftOffset += sizeHorizontal;
maxLineHeight = Math.max(maxLineHeight, sizeVertical);
} else {
// 如果當(dāng)前行不夠嗤军,則往下一行挪
// 修正計(jì)算偏移量、行高
topOffset += maxLineHeight;
maxLineHeight = 0;
leftOffset = getPaddingLeft();
// layout
if (topOffset > getHeight() - getPaddingBottom()) {
// 如果超出下邊界
// 移除并回收該item view
removeAndRecycleView(itemView, recycler);
} else {
// 如果沒有超出下邊界晃危,則繼續(xù)布局
layoutDecoratedWithMargins(itemView, leftOffset, topOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical);
// 修正計(jì)算偏移量叙赚、行高
leftOffset += sizeHorizontal;
maxLineHeight = Math.max(maxLineHeight, sizeVertical);
}
}
}
} else {
// nothing
}
}
//...
/**
* @return 橫向的可布局的空間
*/
private int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
考慮滑動(dòng)
因?yàn)槭强v向滑動(dòng),我們將RecyclerView
想象成一個(gè)寬度一定僚饭,長度可變的紙張震叮,長度有其中的item布局來決定。當(dāng)手指滑動(dòng)屏幕時(shí)鳍鸵,實(shí)際上是讓紙張?jiān)谏厦嬉苿?dòng)苇瓣,同時(shí)保證紙張頂部不能低于RecyclerView
頂部,紙張底部不能高于RecyclerView
底部偿乖。
布局亦是這樣击罪,我們假定所有的item view都已經(jīng)布局在張紙上面(不考慮回收),我們滑動(dòng)時(shí)贪薪,只是改變了這張紙相對(duì)于屏幕的滑動(dòng)距離媳禁。
不考慮邊界
/**
* @return 可以縱向滑動(dòng)
*/
@Override
public boolean canScrollVertically() {
return true;
}
// 縱向偏移量
private int mVerticalOffset = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
Log.e(TAG, String.valueOf(dy));
// 如果滑動(dòng)距離為0, 或是沒有任何item view, 則不移動(dòng)
if (dy == 0 || getChildCount() == 0) {
return 0;
}
mVerticalOffset += dy;
detachAndScrapAttachedViews(recycler);
layout(recycler, state, mVerticalOffset);
return dy;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
// 如果正在進(jìn)行動(dòng)畫,則不進(jìn)行布局
if (getChildCount() == 0 && state.isPreLayout()) {
return;
}
detachAndScrapAttachedViews(recycler);
// 進(jìn)行布局
layout(recycler, state, 0);
}
/**
* 布局操作
*
* @param recycler
* @param state
*/
private void layout(RecyclerView.Recycler recycler, RecyclerView.State state, int verticalOffset) {
// 縱向計(jì)算偏移量画切,考慮padding
int topOffset = getPaddingTop();
// 橫向計(jì)算偏移量损话,考慮padding
int leftOffset = getPaddingLeft();
// 行高,以最高的item作為參考
int maxLineHeight = 0;
for (int i = 0; i < getItemCount(); i++) {
// 獲取一個(gè)item view, 添加到RecyclerView中槽唾,進(jìn)行測(cè)量丧枪、布局
final View itemView = recycler.getViewForPosition(i);
addView(itemView);
// 測(cè)量,獲取尺寸
measureChildWithMargins(itemView, 0, 0);
final int sizeHorizontal = getDecoratedMeasurementHorizontal(itemView);
final int sizeVertical = getDecoratedMeasurementVertical(itemView);
// 進(jìn)行布局
if (leftOffset + sizeHorizontal <= getHorizontalSpace()) {
// 如果這行能夠布局庞萍,則往后排
// layout
layoutDecoratedWithMargins(itemView, leftOffset, topOffset - verticalOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical - verticalOffset);
// 修正橫向計(jì)算偏移量
leftOffset += sizeHorizontal;
maxLineHeight = Math.max(maxLineHeight, sizeVertical);
} else {
// 如果當(dāng)前行不夠拧烦,則往下一行挪
// 修正計(jì)算偏移量、行高
topOffset += maxLineHeight;
maxLineHeight = 0;
leftOffset = getPaddingLeft();
// layout
// 不考慮邊界
layoutDecoratedWithMargins(itemView, leftOffset, topOffset - verticalOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical - verticalOffset);
// 修正計(jì)算偏移量钝计、行高
leftOffset += sizeHorizontal;
maxLineHeight = Math.max(maxLineHeight, sizeVertical);
}
}
}
上面示例恋博,對(duì)滑動(dòng)的處理比較簡(jiǎn)單齐佳,記錄總共滑動(dòng)的距離,并在定位時(shí)將滑動(dòng)距離計(jì)算上债沮。
考慮邊界
對(duì)于FlowLayoutManager
來說炼吴,上邊界比較容易處理,下邊界的處理則需要稍加處理疫衩。
上邊界:因?yàn)槊恳恍械捻敳慷际窍嗤墓璞模允种赶禄瑫r(shí),我們考慮第一行的頂部是否已經(jīng)到達(dá)上邊界闷煤,取第一個(gè)item即可童芹。
下邊界:每個(gè)item的高度不一定相同,為每一個(gè)item進(jìn)行布局時(shí)鲤拿,這樣不會(huì)出現(xiàn)問題假褪,但是在手指上滑時(shí),判斷是否到達(dá)下邊界近顷,需要知道最后一行中最高的item是多少生音,所以在計(jì)算的時(shí)候,要將最后一行的所有item考慮在內(nèi)窒升。
注:我們認(rèn)為缀遍,一旦一個(gè)item構(gòu)造完成,那么它的尺寸是不應(yīng)該發(fā)生變化的异剥,如果在滑動(dòng)過程中瑟由,尺寸發(fā)生變化,會(huì)影響到布局的計(jì)算冤寿。
一個(gè)概念和tip: RecyclerView
就是一個(gè)ViewGroup
, 而通常顯示的順序歹苦,也是FlowLayoutManager
layout的順序都是從第一個(gè)開始的,所以可以這樣認(rèn)為督怜,在position較大的item view顯示之前殴瘦,較小position的item view都是經(jīng)過測(cè)量和布局過的。我們可以將布局過的item view的位置保存号杠,在手指下滑蚪腋,即加載position較小的item view時(shí),不再對(duì)其進(jìn)行測(cè)量和布局姨蟋,只需要取出其位置屉凯,使用滑動(dòng)修正當(dāng)前的位置即可。
但是會(huì)如果數(shù)據(jù)改變眼溶,item view的尺寸和布局則會(huì)發(fā)生改變悠砚,原先的記錄則會(huì)被認(rèn)為是臟數(shù)據(jù),所以當(dāng)任何數(shù)據(jù)改變時(shí)堂飞,需要對(duì)其修正灌旧。不過一個(gè)好的修正方案需要有好的策略绑咱,在實(shí)現(xiàn)中,我只采用了清理的方法枢泰,暫時(shí)不能提供更加理想化的策略描融。
下面是完整代碼:
import android.content.Context;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by Mycroft on 2017/1/11.
*/
public final class FlowLayoutManager extends RecyclerView.LayoutManager {
private static final String TAG = FlowLayoutManager.class.getSimpleName();
public FlowLayoutManager() {
setAutoMeasureEnabled(true);
}
public FlowLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
setAutoMeasureEnabled(true);
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
// 如果正在進(jìn)行動(dòng)畫,則不進(jìn)行布局
if (getChildCount() == 0 && state.isPreLayout()) {
return;
}
detachAndScrapAttachedViews(recycler);
// 進(jìn)行布局
layout(recycler, state, 0);
}
/**
* @return 可以縱向滑動(dòng)
*/
@Override
public boolean canScrollVertically() {
return true;
}
// 縱向偏移量
private int mVerticalOffset = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 如果滑動(dòng)距離為0, 或是沒有任何item view, 則不移動(dòng)
if (dy == 0 || getChildCount() == 0) {
return 0;
}
// 實(shí)際滑動(dòng)的距離衡蚂,到達(dá)邊界時(shí)需要進(jìn)行修正
int realOffset = dy;
if (mVerticalOffset + realOffset < 0) {
realOffset = -mVerticalOffset;
} else if (realOffset > 0) {
// 手指上滑窿克,判斷是否到達(dá)下邊界
final View lastChildView = getChildAt(getChildCount() - 1);
if (getPosition(lastChildView) == getItemCount() - 1) {
int maxBottom = getDecoratedBottom(lastChildView);
int lastChildTop = getDecoratedTop(lastChildView);
for (int i = getChildCount() - 2; i >= 0; i--) {
final View child = getChildAt(i);
if (getDecoratedTop(child) == lastChildTop) {
maxBottom = Math.max(maxBottom, getDecoratedBottom(getChildAt(i)));
} else {
break;
}
}
int gap = getHeight() - getPaddingBottom() - maxBottom;
if (gap > 0) {
realOffset = -gap;
} else if (gap == 0) {
realOffset = 0;
} else {
realOffset = Math.min(realOffset, -gap);
}
}
}
realOffset = layout(recycler, state, realOffset);
mVerticalOffset += realOffset;
offsetChildrenVertical(-realOffset);
return realOffset;
}
private final SparseArray<Rect> mItemRects = new SparseArray<>();
/**
* 布局操作
*
* @param recycler
* @param state
* @param dy 用于判斷回收、顯示item, 對(duì)布局/定位本身沒有影響
* @return
*/
private int layout(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {
int firstVisiblePos = 0;
// 縱向計(jì)算偏移量讳窟,考慮padding
int topOffset = getPaddingTop();
// 橫向計(jì)算偏移量让歼,考慮padding
int leftOffset = getPaddingLeft();
// 行高敞恋,以最高的item作為參考
int maxLineHeight = 0;
int childCount = getChildCount();
// 當(dāng)是滑動(dòng)進(jìn)入時(shí)(在onLayoutChildren方法里面丽啡,我們移除了所有的child view, 所以只有可能從scrollVerticalBy方法里面進(jìn)入這個(gè)方法)
if (childCount > 0) {
// 計(jì)算滑動(dòng)后,需要被回收的child view
if (dy > 0) {
// 手指上滑硬猫,可能需要回收頂部的view
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (getDecoratedBottom(child) - dy < topOffset) {
// 超出頂部的item
removeAndRecycleView(child, recycler);
i--;
childCount--;
} else {
firstVisiblePos = i;
break;
}
}
} else if (dy < 0) {
// 手指下滑补箍,可能需要回收底部的view
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (getDecoratedTop(child) - dy > getHeight() - getPaddingBottom()) {
// 超出底部的item
removeAndRecycleView(child, recycler);
} else {
break;
}
}
}
}
// 進(jìn)行布局
if (dy >= 0) {
// 手指上滑,按順序布局item
int minPosition = firstVisiblePos;
if (getChildCount() > 0) {
final View lastVisibleChild = getChildAt(getChildCount() - 1);
// 修正當(dāng)前偏移量
topOffset = getDecoratedTop(lastVisibleChild);
leftOffset = getDecoratedRight(lastVisibleChild);
// 修正第一個(gè)應(yīng)該進(jìn)行布局的item view
minPosition = getPosition(lastVisibleChild) + 1;
// 使用排在最后一行的所有的child view進(jìn)行高度修正
maxLineHeight = Math.max(maxLineHeight, getDecoratedMeasurementVertical(lastVisibleChild));
for (int i = getChildCount() - 2; i >= 0; i--) {
final View child = getChildAt(i);
if (getDecoratedTop(child) == topOffset) {
maxLineHeight = Math.max(maxLineHeight, getDecoratedMeasurementVertical(child));
} else {
break;
}
}
}
// 布局新的 item view
for (int i = minPosition; i < getItemCount(); i++) {
// 獲取item view, 添加啸蜜、測(cè)量坑雅、獲取尺寸
final View itemView = recycler.getViewForPosition(i);
addView(itemView);
measureChildWithMargins(itemView, 0, 0);
final int sizeHorizontal = getDecoratedMeasurementHorizontal(itemView);
final int sizeVertical = getDecoratedMeasurementVertical(itemView);
// 進(jìn)行布局
if (leftOffset + sizeHorizontal <= getHorizontalSpace()) {
// 如果這行能夠布局,則往后排
// layout
layoutDecoratedWithMargins(itemView, leftOffset, topOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical);
final Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical + mVerticalOffset);
// 保存布局信息
mItemRects.put(i, rect);
// 修正橫向計(jì)算偏移量
leftOffset += sizeHorizontal;
maxLineHeight = Math.max(maxLineHeight, sizeVertical);
} else {
// 如果當(dāng)前行不夠衬横,則往下一行挪
// 修正計(jì)算偏移量裹粤、行高
topOffset += maxLineHeight;
maxLineHeight = 0;
leftOffset = getPaddingLeft();
// layout
if (topOffset - dy > getHeight() - getPaddingBottom()) {
// 如果超出下邊界
// 移除并回收該item view
removeAndRecycleView(itemView, recycler);
break;
} else {
// 如果沒有超出下邊界,則繼續(xù)布局
layoutDecoratedWithMargins(itemView, leftOffset, topOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical);
final Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + sizeHorizontal, topOffset + sizeVertical + mVerticalOffset);
// 保存布局信息
mItemRects.put(i, rect);
// 修正計(jì)算偏移量蜂林、行高
leftOffset += sizeHorizontal;
maxLineHeight = Math.max(maxLineHeight, sizeVertical);
}
}
}
} else {
// 手指下滑遥诉,逆序布局新的child
int maxPos = getItemCount() - 1;
if (getChildCount() > 0) {
maxPos = getPosition(getChildAt(0)) - 1;
}
for (int i = maxPos; i >= 0; i--) {
Rect rect = mItemRects.get(i);
// 判斷底部是否在上邊界下面
if (rect.bottom - mVerticalOffset - dy >= getPaddingTop()) {
// 獲取item view, 添加、設(shè)置尺寸噪叙、布局
final View itemView = recycler.getViewForPosition(i);
addView(itemView, 0);
measureChildWithMargins(itemView, 0, 0);
layoutDecoratedWithMargins(itemView, rect.left, rect.top - mVerticalOffset, rect.right, rect.bottom - mVerticalOffset);
}
}
}
return dy;
}
/* 對(duì)數(shù)據(jù)改變時(shí)的一些修正 */
@Override
public void onItemsChanged(RecyclerView recyclerView) {
mVerticalOffset = 0;
mItemRects.clear();
}
@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
mVerticalOffset = 0;
mItemRects.clear();
}
@Override
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
mVerticalOffset = 0;
mItemRects.clear();
}
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
mVerticalOffset = 0;
mItemRects.clear();
}
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
mVerticalOffset = 0;
mItemRects.clear();
}
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, Object payload) {
mVerticalOffset = 0;
mItemRects.clear();
}
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
mVerticalOffset = 0;
mItemRects.clear();
}
/**
* 獲取 child view 橫向上需要占用的空間矮锈,margin計(jì)算在內(nèi)
*
* @param view item view
* @return child view 橫向占用的空間
*/
private int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
/**
* 獲取 child view 縱向上需要占用的空間,margin計(jì)算在內(nèi)
*
* @param view item view
* @return child view 縱向占用的空間
*/
private int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
}
/**
* @return 橫向的可布局的空間
*/
private int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
}
源代碼地址:FlowLayoutManager
參考文章
【Android】掌握自定義LayoutManager(一) 系列開篇 常見誤區(qū)睁蕾、問題苞笨、注意事項(xiàng),常用API子眶。