Android-自定義View前傳-View的三大流程-Layout
Android-自定義View前傳-View的三大流程-Layout
參考
- 《Android開發藝術探索》
- https://github.com/hongyangAndroid/FlowLayout
寫在前頭
在之前的文章中 , 我們學習了Android View的 Measure的流程, 本篇文章來學習一下View的 Layout
的過程。 學完了這一篇文章後,我們可以嘗試自己去自定義一個自己的Layout。
Overview
我對於Layout過程的理解:Layout的過程就是給Child安家的過程
Layout的過程主要是放在 ViewGroup
中的,ViewGroup不僅需要定位自己,還需要定位Child。
View和ViewGroup
Layout流程的起點也是在 ViewRootImpl 中的 performTraversals
方法中。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; mInLayout = true; final View host = mView; if (host == null) { return; } try { //首先調用了host的layout方法 host = mView = DecorView host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); //.... }
我們接著來看View的layout方法
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //調用 setOpticalFrame或者 setFrame 來確定自己的位置 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {、 //調用onLayout onLayout(changed, l, t, r, b); //..... } protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; // Remember our drawn bit int drawn = mPrivateFlags & PFLAG_DRAWN; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); //確定4個點 //這四個點一旦確定了那麽View在ViewGroup中的位置也就確定了 mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); //......
View的 layout
方法主要是做了:
- 通過
setFrame
確定了自己的位置,一篇Left,Top,Right,Bottom這幾個值確定了,那麽View的位置也就確定了。 - 緊接著調用了
onLayout
方法。
LinearLayout的onLayout
View是不需要實現onLayout方法的,只用ViewGroup才需要實現。由於各種ViewGroup的布局方式的不同,無法統一,所以ViewGroup也並沒有實現onLayout
. 而是將onLayout的過程放到了子類中。我們還是通過 LinearLayout
來學習。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
onLayout根據 Orientation 屬性來調用 layoutVertical
或者 layoutHorizontal
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
//獲取Child可用的空間
int childSpace = width - paddingLeft - mPaddingRight;
//Child的Group
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//根據Gravity確定初始的childTop
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//LayoutView
//遍歷
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {//過濾Child的Visibility是GONE的情況
//獲取Child的大小
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//獲取LP
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
//處理Child的Gravity
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
//水平居中
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
//居右
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
//childTop 加上 Divider
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
//加上Margin
childTop += lp.topMargin;
//調用Child的layout方法
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
上面的代碼還是比較清晰的,首先是有一個childTop變量,來確定child與ViewGroup頂部的距離,通過不斷的遍歷Child然後不斷增加childTop的值,這樣就實現了LinearLayout的垂直布局的效果。當然其中也有處理LinearLayout和Child的Gravity的過程。
FlowLayout
通過學習LinearLayout的Layout的過程,發現其實Layout的過程就是確定View的Left,Top,Right,Bottom 4個值的過程,學習了 Measure
和 Layout
的過程以後,我們就已經可以著手做一個自己的Layout了,這裏我選的是模仿 hongyang大神的FlowLayout。效果圖如下:
package layout
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
/**
* Created by ShyCoder on 2019/1/16.
*/
class MyFlowLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
: ViewGroup(context, attrs, defStyleAttr) {
constructor(context: Context?, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context?) : this(context, null)
init {
}
/**
* 存貯所有的View根據行來存貯
* */
private val mAllViews = mutableListOf<List<View>>()
/**
* 存貯每一行View的高度
* */
private val mHeightList = mutableListOf<Int>()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//從MeasureSpec獲取Mode和Size
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getMode(heightMeasureSpec)
//計算Wrap_content的情況
var totalHeight = 0//總高度
var totalWidth = 0//總寬度
var lineWidth = 0//當前行的寬度
var lineHeight = 0//當前行的高度
val viewCount = childCount
for (i in 0.until(viewCount)) {
val child = getChildAt(i)
//如果是GONE狀態不用測量
if (child.visibility == View.GONE) {
continue
}
//測量Child
measureChild(child, widthMeasureSpec, heightMeasureSpec)
val lp = child.layoutParams as MarginLayoutParams
//計算child的width = 測量後寬度+左右的兩個Margin
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
//計算child的Height = 測量後高度 + 上下兩個Margin
val childHeight = child.measuredHeight + lp.bottomMargin + lp.topMargin
//需要換行時候的處理方式
//如果已有的行寬+當前child的寬度> FlowLayout的寬度(減去左右的Padding)
if (childWidth + lineWidth > widthSize - this.paddingLeft + this.paddingRight) {
totalWidth = Math.max(totalWidth, lineWidth)
lineWidth = childWidth
totalHeight += lineHeight
lineHeight = childHeight
} else {//如果不需要換行增加行寬
lineWidth += childWidth
//獲取最大高度
lineHeight = Math.max(lineHeight, childHeight)
}
//最後一個View
if (i == viewCount - 1) {
totalWidth = Math.max(totalWidth, lineWidth)
totalHeight += lineHeight
}
}
this.setMeasuredDimension(
//如果MeasureSpec的Mode是EXACTLY 的話,測量後大小等會傳進來的測量大小,
//否則則是我們自己計算的大小
if (widthMode == MeasureSpec.EXACTLY) widthSize else totalWidth,
if (heightMode == MeasureSpec.EXACTLY) heightSize else totalHeight
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
this.mAllViews.clear()
this.mHeightList.clear()
val viewCount = this.childCount
var lineHeight = 0
var lineWidth = 0
var lineViews = mutableListOf<View>()
for (i in 0.until(viewCount)) {
val child = this.getChildAt(i)
if (child.visibility == View.GONE) {
continue
}
val lp = child.layoutParams as MarginLayoutParams
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
//行上無法繼續放置View
if (childWidth + lineWidth > this.width - this.paddingLeft - this.paddingRight) {
//添加line height
mHeightList.add(lineHeight)
lineWidth = 0
//添加正行的View到集合中
this.mAllViews.add(lineViews)
lineViews = mutableListOf()
}
//區當前行的View的最大高度
lineHeight = StrictMath.max(lineHeight, childHeight)
lineWidth += childWidth
//向行上添加View
lineViews.add(child)
}
//進行Layout
var left = this.paddingLeft
var top = this.paddingTop
for (i in 0.until(this.mAllViews.size)) {
val lineViews = mAllViews[i]
val lineHeight = mHeightList[i]
left = this.paddingLeft
for (j in 0.until(lineViews.size)) {
val child = lineViews[j]
val lp = child.layoutParams as MarginLayoutParams
//view的四個邊
val l = left + lp.leftMargin
val t = top + lp.topMargin
val r = l + child.measuredWidth
val b = t + child.measuredHeight
//調用Child的Layout方法
child.layout(l, t, r, b)
left += child.measuredWidth + lp.leftMargin + lp.rightMargin
}
top += lineHeight
}
}
}
在onMeasure對wrap_content的情況進行了處理,計算出所需要的大小。
在onLayout方法中,首先計算出每一行的高度並存儲和獲取每一行的View的List進行存儲,當這些都計算完之後,進行layout操作。
在layout操作的時候,首先是在同一行上的View的top是統一的,每當這一行的View處理完成之後,就執行換行的操作,即增加view的大小。
寫在最後
本篇文章就到此結束了,Layout的過程還是相對簡單的,在下一篇文章呢,我們將會學習View的最後一個流程 Draw
流程。
Android-自定義View前傳-View的三大流程-Layout