[toc]android
原由是這樣一張UI美眉給的設計圖:git
起初是有作自定義 View
的想法,後來發現這個想法多多少少有點不成熟,由於沒有時間目前也沒有能力去完成這樣一個控件。github
好在後來找到了 SpannedGridLayoutManager 由做者 Arasthel
完成的一個跨行跨列的 GridLayoutManager
佈局。做者思路很值得借鑑。數組
在這段時間的開發完成以後,對 LayoutManager
產生了一些好奇。它是如何產生這些效果的呢?若是要實現一個相似 LayoutManager
須要完成什麼步驟呢?緩存
由於是從此次的佈局開發產生的興趣,那麼首先下手的地方就是 GridLayoutManager
了。查看 GridLayoutManager
源碼能夠發現你,它存在三個內部類:markdown
LayoutParams
都很熟悉了,它提供給咱們佈局屬性,最基本的 width
、height
,更詳細一點的 margin
屬性,等等。不少容器組件都有其 LayoutParams
的子類。那 GridLayoutManager
的 LayoutParams
中增長了兩個新的變量:mSpanIndex
、mSpanSize
。他們是作什麼的呢?來繼續看。app
SpanSizeLookup
是一個幫助類,用於提供每項 itemview
所佔用跨度數,默認值爲 1
。 它是一個抽象類,來看下其內部都定義了那些方法:ide
public abstract static class SpanSizeLookup {
/**
* @param position item 在 adapter 中的位置
* @return position 指代的 item 所佔用的列數
*/
public abstract int getSpanSize(int position);
/**
* itemview 佔據跨度的下標
*/
public int getSpanIndex(int position, int spanCount){}
}
複製代碼
getSpanIndex()
和 getSpanSize()
是比較重要的兩個方法,除了這兩個方法外還有一些緩存的方法,用於緩存 position
對應 SpanSize
的計算結果。oop
getSpanIndex()
和 getSpanSize()
的含義和做用結合 DefaultSpanSizeLookup
更好理解。佈局
DefaultSpanSizeLookup
是 SpanSizeLookup
的默認實現:
public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
@Override
public int getSpanSize(int position) {
return 1;
}
@Override
public int getSpanIndex(int position, int spanCount) {
return position % spanCount;
}
}
複製代碼
能夠看到 getSpanSize()
返回值默認爲 1
,getSpanIndex()
返回值爲 當前位置 % 總列數
。若是把 GridLayoutMananger
看作一個個小格子,每一個格子下標從 0 至 SpanCount
,一個 SpanCount = 4
的佈局中每一個小格子的下標以下:
0,1,2,3
0,1,2,3
0,1,2,3
...
複製代碼
那麼 positionn = 0
的 itemview
的 spanIndex = 0
, positionn = 2
的 itemview
的 spanIndex = 2
,positionn = 4
的 itemview
的 spanIndex = 0
。
總結一下,在每一行中,SpanIndex
表明 itemview
佔據跨度的起始下標,spanSize
表明 itemview
佔據了多少跨度。若是設置 spanSize = spanCount
:
val gridLayoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return 3
}
}
複製代碼
你會獲得一個使用 GridLayoutManager
繪製的 LinearLayoutManager
佈局。
瞭解了 SpanSize
和 SpanIndex
的含義咱們能夠理所應當的猜想 GridLayoutManager
就是經過它們來肯定每一個 itemview
在容器中的位置的。
如何驗證猜想的真實性呢?固然是去三兄弟 onMeasure()
、onLayout()
、onDraw()
中找了。由於是容器組件因此對於 子View
的測量和佈局確定是在 onLayoutChildren()
裏了。找到 GridLayoutManager
的 onLayoutChildren()
方法:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
super.onLayoutChildren(recycler, state);
...
}
複製代碼
其內依然使用的是其父類 onLayoutChildren
的實現,它的父類是誰呢: LinearLayoutManager
。
這裏插一句,不少前輩都說過,最好帶着問題去看源碼,個人理解是若是隻是想弄明白某個問題,那麼最好的作法是快速定位到具體的代碼實現中,而不是去關注太多目前不須要去理解的代碼,它會分散你的經歷而且影響你的思路。
能夠看到 LinearLayoutManager - onLayoutChildren()
中的代碼茫茫多。想要找到 子View
是如何測量和佈局的,真不知道如何下手。怎麼定位對咱們有用的代碼呢?關鍵詞 子View
,先要有 子View
而後才能談到測量和佈局吧,在 RecyclerView
裏 子View
怎麼來的? 那不就是 onCreateViewHolder()
嘛。
從 onCreateViewHolder()
一級一級向上查找,能夠獲得以下的執行邏輯:
....
-> LinearLayoutManager.onLayoutChild()
-> LinearLayoutManager.fill()
-> LinearLayoutManager.layoutChunk() / GridLayoutManager.layoutChunk()
-> LinearLayoutManager.LayoutState.next()
-> RecyclerView.Recycler.getViewForPosition()
-> RecyclerView.Recycler.tryGetViewHolderForPositionByDeadline()
-> RecyclerView.Adapter.createViewHolder()
-> onCreateViewHolder()
複製代碼
在這條線上去找能夠比較輕鬆的定位到 子View
是用在 layoutChunk()
方法中的,基本到這裏就能夠肯定 itemview
測量和佈局的實現代碼就是在 layoutChunk()
裏了,名字就叫佈局塊嘛。
在開始 layoutChunk()
以前,先了解一個會對理解接下來的流程頗有幫助的點,咱們知道 LinearLayoutManager
是 GridLayoutManager
的父類,而且 GridLayoutManager
重寫了 layoutChunk()
,這也就說明 GridLayout
也是按行繪製的,只是它把每行都分紅了 SpanCount
列,而後在每一個單元格都繪製一個 itemview
。這就是 GridLayoutManager
的繪製邏輯。如圖:
接下來看 layoutChunk()
中具體的實現:
@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
int count = 0;
int consumedSpanCount = 0;
int remainingSpan = mSpanCount;
// 獲取某行中可添加的全部 子View
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
int pos = layoutState.mCurrentPosition;
final int spanSize = getSpanSize(recycler, state, pos);
if (spanSize > mSpanCount) {
throw new IllegalArgumentException("Item at position " + pos + " requires "
+ spanSize + " spans but GridLayoutManager has only " + mSpanCount
+ " spans.");
}
remainingSpan -= spanSize;
if (remainingSpan < 0) {
break; // item did not fit into this row or column
}
View view = layoutState.next(recycler);
if (view == null) {
break;
}
consumedSpanCount += spanSize;
mSet[count] = view;
count++;
}
if (count == 0) {
result.mFinished = true;
return;
}
//一行中全部 itemview 最大的寬度
int maxSize = 0;
//一行中全部 itemview 最大的高度, 通常是一格的高度
float maxSizeInOther = 0; // use a float to get size per span
// we should assign spans before item decor offsets are calculated
assignSpans(recycler, state, count, layingOutInPrimaryDirection);
//在這裏對 子View 進行測量
for (int i = 0; i < count; i++) {
View view = mSet[i];
if (layoutState.mScrapList == null) {
if (layingOutInPrimaryDirection) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (layingOutInPrimaryDirection) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
// 這裏是 ItemDecorations 主要計算 itemview 之間的間隔
calculateItemDecorationsForChild(view, mDecorInsets);
// 測量子View
measureChild(view, otherDirSpecMode, false);
//獲取 View 作佔據佈局的寬度, 包括 marigin + padding + width
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
//獲取 View 作佔據佈局的高度, 包括 marigin + padding + width
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view)
/ lp.mSpanSize;
if (otherSize > maxSizeInOther) {
maxSizeInOther = otherSize;
}
}
//一個佈局塊消耗的最大高度
result.mConsumed = maxSize;
//這裏對 子View 進行佈局,這四個值所表明的就是 view 的佈局區域
int left = 0, right = 0, top = 0, bottom = 0;
if (mOrientation == VERTICAL) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = bottom - maxSize;
} else {
top = layoutState.mOffset;
bottom = top + maxSize;
}
} else {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = right - maxSize;
} else {
left = layoutState.mOffset;
right = left + maxSize;
}
}
for (int i = 0; i < count; i++) {
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
} else {
top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
// 對 ziView 佈局
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable |= view.hasFocusable();
}
Arrays.fill(mSet, null);
}
複製代碼
看過 layoutChunk()
方法的實現,其總共分爲三步:
子View
子View
子View
能夠看到在 layoutChunk()
代碼的開始就執行了一個 while
循環用於獲取當前行中可存放的全部 子View
,用白話解釋就是 」一個 SpanCount = 4
的佈局,一行中可存放四個 SpanSize = 1
的子View,或者兩個 SpanSize = 2
的子View ....「
接下來調用 measureChild()
對全部 子View
進行寬高測量(建議看源碼),主要獲取了 子View
的佈局間隔、margin
並經過這些值計算獲得 ziView
寬高的 MeasureSpace
值。
其中 getSpaceForSpanRange()
比較很差理解,它是用於獲取在垂直方向上 itemview
的寬度。getSpaceForSpanRange()
以下:
/**
* @params startSpan itemview 佔據單元格起始下標
* @params spanSize itemview 佔據單元格總數
*/
int getSpaceForSpanRange(int startSpan, int spanSize) {
if (mOrientation == VERTICAL && isLayoutRTL()) {
return mCachedBorders[mSpanCount - startSpan]
- mCachedBorders[mSpanCount - startSpan - spanSize];
} else {
return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan];
}
}
複製代碼
方法內使用了一個名爲 mCachedBorders
的數組,mCachedBorders
是一個大小爲 spanCount + 1
的數組,數組內存儲的是將 RecyclerView
分紅 spanCount
列的 spanCount + 1
條線的 X
軸座標。數組第一位的值老是 0
。
mCachedBorders
的計算方法以下:
static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
if (cachedBorders == null || cachedBorders.length != spanCount + 1
|| cachedBorders[cachedBorders.length - 1] != totalSpace) {
cachedBorders = new int[spanCount + 1];
}
cachedBorders[0] = 0;
int sizePerSpan = totalSpace / spanCount;
int sizePerSpanRemainder = totalSpace % spanCount;
int consumedPixels = 0;
int additionalSize = 0;
for (int i = 1; i <= spanCount; i++) {
int itemSize = sizePerSpan;
additionalSize += sizePerSpanRemainder;
if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
itemSize += 1;
additionalSize -= spanCount;
}
consumedPixels += itemSize;
cachedBorders[i] = consumedPixels;
}
return cachedBorders;
}
複製代碼
舉例,若是 RecyclerView.width = 1080, SpanCount = 3
,那麼經計算獲得 mCachedBorder
的值就爲 [0, 360, 720, 1080]
。
到這裏就能夠肯定 RecyclerView
是經過 mCachedBorder
搭配 SpanSizeLookup
來計算每一個 itemview
的寬度的了。
到這裏測量的過程其實就結束了,也獲得了每一個 itemview
的寬高了。
接下來就是佈局階段了,這裏比較簡單,就是獲得,四個值 left、top、right、bottom
,有一個比較重要的值 layoutState.mOffset
它是某行開始繪製的像素偏移量:
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
複製代碼
layoutChunkResult.mConsumed
是每一個佈局塊所佔用的最大高度,layoutState.mLayoutDirection
佈局填充的方向:
LAYOUT_START
:-1 從下往上填充佈局LAYOUT_END
:1 從上往下填充佈局若是 GridLayoutManager
是垂直佈局而且從上往下填充佈局的話,layoutState.mOffset
的值就是繪製某行時其繪製起始點的 Y
軸座標。
這裏有一個疑問不太懂,既然在以前 itemview
的測量中已經得到了它的寬高,爲何確認佈局位置的時候仍是用的 mCachedBorders
,不知道這是基於那些考慮。
到這裏 GridLayoutMananger
的佈局邏輯就介紹完了。咱們也知道了經過 GridLayoutManager
來實現UI美眉的設計圖也是不行的了,由於它只支持跨列而不支持跨行,萬幸,有前輩作了 SpannedGridLayoutManager
讚美開源,讚美做者 ^_^
。
在講解 SpannedGridLayoutManager
的具體實現前,先說明一下它的實現邏輯,有助於理解做者的思路。
在 LayoutManager
中最大的難點是肯定 itemview
的佈局屬性,在 GridLayoutMananger
中是經過:
來肯定某個 position
所對應 itemview
的佈局屬性的。
那來看下 SpannedGridLayoutManager
的做者是經過什麼思路來解決這個難題的呢?
好比要實現上面那張UI美眉給的設計圖(如下都是按垂直方向佈局):
首先,做者定義了 SpanSize
類,定義 itemview
佔據單元格的行數和列數:
/**
* Helper to store width and height spans
*/
class SpanSize(val width: Int, val height: Int)
複製代碼
而後,定義了一個列表 freeRects
,用於存儲 RecyclerView
所佔據的整個空間中全部可用矩形範圍:
初始狀態下 freeRects
中只包含一個 Rect
,它的值爲:
Rect(0, 0 - 3, 2147483647)
複製代碼
從 Rect
的值中能夠發現,做者在 Rect
中保存的並非具體的像素距離值,而是單元格數。如圖紅色框區域就是其表明可用的範圍,固然它的底部範圍是很大的,最多能夠佔據 Int.MAX_VALUE
個單元格,這裏我以爲畫上邊線會更好解釋一點,本身內心清楚就行了。
下面咱們來放第一個 itemview
,它的寬高都佔據兩個單元格:
能夠看到當第一個 itemview
被添加到圖上以後,其將可用範圍分紅了兩個,將這兩個可用範圍按照必定規律進行排序。而後添加第二個 itemview
,他的寬高只佔據一個單元格,正好能夠添加到黃色框的部分,添加並更新可用範圍,以下:
再來添加第三個 itemview
,寬高還是佔據一個單元格,仍是添加到黃色框區域:
這時能夠發現,黃色框區域徹底包裹在紅色框區域內,那就能夠將表明黃色框的 Rect
去掉,只保留紅色框區域了,以下:
依次類推,經過這種方式,在不超過可用範圍的狀況下能夠隨意設置任意 itemview
的跨行和跨列。以上僅是做者的基本思路,具體的代碼實現邏輯仍是有些許的不一樣的,下面就來看具體是怎麼作的吧。
首先看代碼結構,它包含兩個內部類:
SpanSize
:定義 itemview
佔據單元格的行數和列數。RectHelper
:其內實現的就是對可用矩形區域的查找、緩存和更新。能夠看到 SpannedGridLayoutManager
和 GridLayoutManager
不一樣,它集成的是 RecyclerView.LayoutMananger
。不要緊依然用上述 GridLayoutManager
的方法找到最接近答案的節點。
protected open fun makeView(position: Int, direction: Direction, recycler: RecyclerView.Recycler): View {
val view = recycler.getViewForPosition(position)
measureChild(position, view)
layoutChild(position, view)
return view
}
複製代碼
就很清晰,拿到 itemview
,測量,佈局。
首先來看 measure
過程:
protected open fun measureChild(position: Int, view: View) {
val freeRectsHelper = this.rectsHelper
val itemWidth = freeRectsHelper.itemSize
val itemHeight = freeRectsHelper.itemSize
val spanSize = spanSizeLookup?.getSpanSize(position) ?: SpanSize(1, 1)
val usedSpan = if (orientation == Orientation.HORIZONTAL) spanSize.height else spanSize.width
if (usedSpan > this.spans || usedSpan < 1) {
throw InvalidSpanSizeException(errorSize = usedSpan, maxSpanSize = spans)
}
// This rect contains just the row and column number - i.e.: [0, 0, 1, 1]
val rect = freeRectsHelper.findRect(position, spanSize)
// Multiply the rect for item width and height to get positions
val left = rect.left * itemWidth
val right = rect.right * itemWidth
val top = rect.top * itemHeight
val bottom = rect.bottom * itemHeight
val insetsRect = Rect()
calculateItemDecorationsForChild(view, insetsRect)
// Measure child
val width = right - left - insetsRect.left - insetsRect.right
val height = bottom - top - insetsRect.top - insetsRect.bottom
val layoutParams = view.layoutParams
layoutParams.width = width
layoutParams.height = height
measureChildWithMargins(view, width, height)
// Cache rect
childFrames[position] = Rect(left, top, right, bottom)
}
複製代碼
這部分代碼就很好理解,首先獲得 position
對應的 SpanSize
,再根據經過 RectHelper.findRect()
獲得適合 itemview
存放的矩形區域,調用 RecyclerView.measureChildWithMargins()
進行對 itemview
進行測量。能夠看到在 RectHelper.findRect()
中 Rect
是存放在 rectsCache
數組中的。在 onLayoutChildren()
方法中能夠找到一個 for
循環,它的做用就是獲得 itemCount
個 itemview
的佈局區域並存儲在 rectsCache
數組中。
而後就是 layout
過程:
protected open fun layoutChild(position: Int, view: View) {
val frame = childFrames[position]
if (frame != null) {
val scroll = this.scroll
val startPadding = getPaddingStartForOrientation()
if (orientation == Orientation.VERTICAL) {
layoutDecorated(view,
frame.left + paddingLeft,
frame.top - scroll + startPadding,
frame.right + paddingLeft,
frame.bottom - scroll + startPadding)
} else {
layoutDecorated(view,
frame.left - scroll + startPadding,
frame.top + paddingTop,
frame.right - scroll + startPadding,
frame.bottom + paddingTop)
}
}
// A new child was layouted, layout edges change
updateEdgesWithNewChild(view)
}
複製代碼
這部分代碼也很簡單,可是這裏面加入了 scroll
的處理,因爲這篇文章不涉及 scroll
,將其排除在外的話理解起來仍是沒有難度的。
其實整個 SpannedGridLayoutManager
中最重要的是對於 itemview
佈局區域的獲取,其大部分代碼都存在於 subtract()
方法中。理解了這個方法也就理解了做者對於佈局的處理。
到這裏也就介紹完了,再次感嘆做者的思路,讚美開源精神。讀完了 GridLayoutManager
和 SpannedGridLayoutManager
的源碼,發現 RecyclerView
對於 itemview
的佈局就好像是在玩兒拼圖遊戲的感受,從左至右,從上到下,依次排列每個 子View
,其實和咱們在作相似拼圖遊戲的時候規定按從左至右,從上到下的規則排列大小不等的塊的處理邏輯是否是很像。那 GridLayoutManager
和 SpannedGridLayoutManager
的做者在實現的時候是否是就是將這種咱們大腦處理相似問題的邏輯給翻譯成代碼了呢。
如今來看下若是要自定一個 LayoutManager
的話須要作些什麼,或者須要重寫那些方法。
class MyLayoutManager extends RecyclerView.LayoutManager {
//==============================================================================================
// * 必須重寫
// ~ 惟一 abstract 方法
//==============================================================================================
/**
* * 必須重寫
* 它是 `LayoutManager` 中惟一的 `abstract` 方法, 爲子類提供 LayoutParams,
* 可使用 RecyclerView.LayoutParams() 對象也能夠自定義的 ViewGroup.LayoutParams 對象.
* 若是自定義話須要重寫以下方法:
* checkLayoutParams(LayoutParams)
* generateLayoutParams(android.view.ViewGroup.LayoutParams)
* generateLayoutParams(android.content.Context,android.util.AttributeSet)
*/
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return null;
}
//==============================================================================================
// * 必須重寫
// ~ 佈局相關
//==============================================================================================
/**
* * 必須重寫
* 主要實現 itemview 的 measure 和 layout 過程
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
}
//==============================================================================================
// * 必須重寫
// ~ 滑動相關
//==============================================================================================
/**
* 是否能夠水平滑動
*/
@Override
public boolean canScrollHorizontally() {
return super.canScrollHorizontally();
}
/**
* 是否能夠垂直滑動
*/
@Override
public boolean canScrollVertically() {
return super.canScrollVertically();
}
/**
* 控制在水平方向上的滑動距離
*/
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
return super.scrollHorizontallyBy(dx, recycler, state);
}
/**
* 控制在垂直方向上的滑動距離
*/
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
return super.scrollVerticallyBy(dy, recycler, state);
}
/**
* 滑動到適配器指定位置
*/
@Override
public void scrollToPosition(int position) {
super.scrollToPosition(position);
}
/**
* 平滑滾動到適配器指定位置
* 建立 SmoothScroller 實例並調用 startSmoothScroll()
*/
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
super.smoothScrollToPosition(recyclerView, state, position);
}
//==============================================================================================
// ~ 狀態相關
//==============================================================================================
@Nullable
@Override
public Parcelable onSaveInstanceState() {
return super.onSaveInstanceState();
}
@Override
public void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
}
}
複製代碼
若是想支持滾動條須要重寫這些方法:
public int computeHorizontalScrollExtent(@NonNull State state) {
return 0;
}
public int computeHorizontalScrollOffset(@NonNull State state) {
return 0;
}
public int computeHorizontalScrollRange(@NonNull State state) {
return 0;
}
public int computeVerticalScrollExtent(@NonNull State state) {
return 0;
}
public int computeVerticalScrollOffset(@NonNull State state) {
return 0;
}
public int computeVerticalScrollRange(@NonNull State state) {
return 0;
}
複製代碼
最重要的其實仍是 onLayoutChildren()
方法了,其餘方法均可以對照 LinearLayoutManager
或者 GridLayoutManager
來寫,或者乾脆跟 GridLayoutManager
同樣,繼承於 LinearLayoutManager
專心於 onLayoutChildren()
實現想要的佈局也是能夠的。
若是對你有幫助的話留下贊吧 ^_^
。