本章代碼GitHub位址:https://github.com/LittleFogCat/AndroidBookNote/tree/master/chapter04_view
4.0 要點
View的繪制流程
measure -> layout -> draw
常用回調
onAttach onVisibilityChanged onDetach
滑動處理
4.1 ViewRoot DecorView
首先是這張Android的視窗層級圖
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwQjMx8CX39CXy8CXycXZpZVZnFWbpN0NlAXayR3cvwFduVWay9WLvRXdh9CXyI3Zv1UZnFWbp9zZuBnL2E2Y4EmYwkjNlFWY0QmNk1yMyIjMzUjNvw1cldWYtl2XkF2bsBXdvw1bp5SdoNnbhlmauMXZnFWbp1CZh9GbwV3Lc9CX6MHc0RHaiojIsJye.png)
可以看到,在一個界面中,包含了一個Window,Window中包含了一個DecorView。DecorView其實是一個FrameLayout,一般包含了且僅包含一個豎直的LinearLayout,這個LinearLayout中又包含了一個TitleView和一個ContentView。我們調用
setContentView(id)
的時候,設定的就是這個ContentView的布局。
ViewRoot是WindowManager和DecorView之間的紐帶。View的三大流程其實都是通過ViewRoot來完成的。
關于ViewRoot的來曆,又是怎麼成為WindowManager和DecorView的紐帶的,書中隻是一筆帶過,這裡來一探究竟。
4.1.*
順着源碼追蹤,隻看有用部分:
當ActivityThread收到一個Activity啟動消息時,會調用
handleLaunchActivity(ActivityClientRecord, Intent, String)
方法,handleLaunchActivity方法中有以下幾句:
WindowManagerGlobal.initialize();
Activity a = performLaunchActivity(r, customIntent);
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
可以看到,
handleLaunchActivity
中調用了
performLaunchActivity
和
handleResumeActivity
兩個方法。
在
performLaunchActivity()
中,Activity被建立(onCreate也是在這裡調用的),并且通過
Activity.attach()
方法将Window和Activity綁定。
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
在
handleResumeActivity()
方法中,DecorView會被添加到Window中。(同時這個方法裡面也有一個
performResumeActivity()
方法,在這裡調用Activity.onResume())。
最後,我們會調用Activity的makeVisible()方法,并通知AMS我們的Activity已經resume了。
ViewManager wm = a.getWindowManager();
// 将DecorView添加到Window中,但此時其是不可見的
wm.addView(decor, l);
// ==> mDecor.setVisibility(View.VISIBLE);
r.activity.makeVisible();
ActivityManager.getService().activityResumed(token);
繼續跟蹤
wm.addView(View, ViewGroup.LayoutParams)
,會在WindowManagerGlobal這個單例類中找到:
ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
這裡的view即是decorView。至此,我們成功的建立起了WindowManager -> ViewRootImpl -> DecorView的關系。
4.2 MeasureSpec
4.2.1 MeasureSpec
網上講MeasureSpec的有很多。簡單的來講,MeasureSpec就是這個View的大小(不準确,但是可以這樣簡化了解)。它是一個32位的整型,高2位代表SpecMode,低30位代表SpecSize。
SpecMode有三種:
- UNSPECIFIED 父容器不對View做限制。
- EXACTLY 精确測量模式,即View的最終大小。在View中設定具體數字大小,或者match_parent都是這個模式。
- AT_MOST 可用大小模式,View的大小不會超過這個值。對應的是wrap_content。
4.2.2 MeasureSpec和LayoutParams的對應關系
我們在LayoutParams中會定義View的寬高,即布局xml中的
android:layout_width
和
android:layout_height
屬性。一般來講,我們會設定
match_parent
,
wrap_content
或者具體的數值。
同時,我們會通過View的measure方法向其傳遞MeasureSpec。綜合父布局的MeasureSpec和View的LayoutParam,我們可以計算出這個View的MeasureSpec。
抛開UNSPECIFIED不談(一般不用),以下表格表示了如何通過二者确定View具體MeasureSpec的:
LP \ 父SpecMode | EXACTLY | AT_MOST |
---|---|---|
具體數值 | specMode: EXACTLY specSize: View定義的size | specMode: EXACTLY specSize: View定義的size |
wrap_content | specMode: AT_MOST specSize: 父specSize | specMode: AT_MOST specSize: 父specSize |
match_parent | specMode: EXACTLY specSize: 父specSize | specMode: AT_MOST specSize: 父specSize |
可以看出,除非将View的寬高設定為确定的數值,否則其是受到父容器的影響的。具體的measure過程在下一節講到。
4.3 View的工作流程
View的工作流程主要指measuer、layout、draw。
measure測量View的寬高,layout确定View的位置和大小,draw将View繪制在螢幕上。
4.3.1 measure
View通過measure來測量大小。同時,ViewGroup除了測量自己,還會周遊子View并調用其measure方法。
之前我們已經知道,View的大小由MeasureSpec來決定,而MeasureSpec又是通過父布局的MeasureSpec和LayoutParam共同決定的。
通過檢視源碼,我們可以看到,View的measure過程主要是通過在
measure(int, int)
方法中調用
onMeasure(int, int)
進行的。而onMeasure()的預設實作隻有一句:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我們在重寫onMeasure方法的時候,必須要調用
setMeasuredDimension(int measuredWidth, int measuredHeight)
方法,否則系統會抛出異常。這個方法的主要目的是給View的
mMeasuredWidth
和
mMeasuredWidth
變量指派。
也就是說,measure的結果就是,通過調用
measure(int, int)
方法,最終給View的
mMeasuredWidth
和
mMeasuredWidth
變量指派,使得接下來的layout和draw流程順利進行。
而
measure(int, int)
方法的兩個參數是從何而來的呢?
在4.2.2中我們知道了,View的MeasureSpec是通過父布局的MeasureSpec和自身的LayoutParam來進行計算的,而這個過程是在父ViewGroup中就已經完成了的,如4.2.2中表格所示。也就是說,事實上,measure過程絕大多數工作是在父容器裡面就已經完成了的。
在ViewGroup類中有一個
getChildMeasureSpec(int spec, int padding, int childDimension)
方法,在這裡我們可以看到ViewGroup是怎麼确定子View的measureSpec的,截取其中一段:
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
特别地,當View的寬(或者高)設定為wrap_content的時候,檢視4.2.2的表格,我們可以看到,View的SpecMode是AT_MOST,而SpecSize是父布局剩餘的尺寸。也就是說,我們最後給這個View指派的測量大小,也是父布局剩餘尺寸,這跟match_parent是一樣的效果,不符合我們的預期。造成這個結果的原因是,父布局并不知道這個View應該是多大,是以隻能傳遞父布局的SpecSize。是以當我們自定義View的時候,需要重寫onMeasure方法,并在其中加入當View的SpecMode是AT_MOST時,我們期望的測量結果。例如,我們想設定wrap_content時的寬高是100px:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(100, 100);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(100, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 100);
}
}
而ViewGroup的measure過程,除了要測量自身以外,還要測量各個子View,測量完之後再計算出ViewGroup最終的大小。而這個過程根據ViewGroup的不同,最終測量出來的大小也可能是不一樣的,例如LinearLayout和RelativeLayout,他們的測量過程顯然不可能相同,是以ViewGroup并沒有預設實作measure過程,在自定義ViewGroup的時候,必須重寫onMeasure方法,否則會導緻無法顯示。雖然ViewGroup提供了measureChildren(int, int)和measureChild(View, int, int)方法,可以簡便的對子元素進行測量,
4.3.2 layout
在計算好了尺寸之後,我們需要把View挨個放進ViewGroup裡,如同搭積木一般。這個過程就是layout。是以我們可以簡單的認為,layout的過程是為ViewGroup“量身定制”的。
layout過程跟measure很類似,ViewGroup周遊所有的子View,計算出其應在的位置。如同measure的最終結果是将
mMeasuredWidth
和
mMeasuredWidth
變量指派一般,layout的最終結果是給View的
mLeft
mTop
mRight
mBottom
四個變量指派。
在ViewGroup中,onLayout是一個抽象方法,需要我們自己實作,在其中放置我們的子View。舉個簡單的例子,我們要做個子元素豎直排列的布局,并且每個子元素間隔10px,重寫ViewGroup的onLayout方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = t;
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
int childLeft = l;
int childTop = top;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
top += child.getMeasuredHeight() + 10;
}
}
效果如下:
可以看到符合預期。
4.3.3 draw
一般情況下,draw分為以下幾步:
- 繪制背景(drawBackground)
- 繪制自身(onDraw)
- 繪制子元素(dispatchDraw)
- 繪制裝飾(scrollbars)
我們一般隻關心自身的繪制,也就是說,重寫onDraw方法就可以了。對于自定義View的繪制,最重要的莫過于Canvas和Paint的使用。
4.3.4 小結*
View的三大流程,不是并列關系,而是依賴、遞進的關系。也就是說,對于父布局,必須先測量好每個子元素的大小,再确定他們每個的位置,最後才能繪制出他們的圖像。即:
measure -> layout -> draw
4.4 自定義View和ViewGroup
最後來根據本章内容做一下自定義View、ViewGroup。
我的目标是這樣的:
- 自定義View:外圈圓形,包裹了一個五角星。可以自定義圓形和五角星的顔色,以及五角星的旋轉角度。(其實旋轉可以使用
屬性的)android:rotation
- 自定義ViewGroup:将所有的子View從左到右,從上到下,依次排列。
4.4.1 自定義View:StarView
4.4.1.0 定義屬性
首先建立包含五角星各項資料的實體類
Star
:
public class Star {
/**
* 五角星從中心到頂點的距離
*/
private double mCVLength;
/**
* 中心點的坐标
*/
private Point mCenter;
/**
* 五角星旋轉的角度
*/
private double mRotate;
/**
* 五角星5個頂點坐标,順序為:從最上方頂點開始,順時針旋轉的所有頂點。
*/
private Point[] mPoints = new Point[5];
// ...
}
略去其他部分,這裡主要儲存了五角星從中心到頂點的距離(大小)、中心點的坐标(位置)、五角星旋轉的角度(角度),以及五個頂點的坐标(前三個值計算得到)。而我們等下在繪制圖形的過程中,主要用到的就是這五個點的坐标。(至于是怎麼求到的,則是高中知識,過程充滿了血淚不表)
現在開始自定義StarView。建立StarView.java,繼承自View。在
style.xml
中加入如下屬性:
<declare-styleable name="StarView">
<attr name="star_color" format="color" />
<attr name="star_scale" format="float" />
<attr name="star_rotate" format="float" />
</declare-styleable>
分别代表五角星的顔色、五角星占圈内的比例、五角星的旋轉角度。而圓圈背景則直接從
background
屬性擷取,然後再把背景設定成透明:
Drawable bgDrawable = getBackground();
if (bgDrawable instanceof ColorDrawable) {
mBgColor = ((ColorDrawable) bgDrawable).getColor();
} else {
mBgColor = Color.RED;
}
setBackgroundColor(0);
是不是很粗暴?
4.4.1.1 onMeasure
在onMeasure中,我們隻是處理了對于wrap_content的判斷:如果長(寬)是wrap_content,那麼就将其設定為與寬(長)相等,即正方形(實際繪圖區域,即去掉了padding之後的真實繪圖區域)。如果二者皆是wrap_content,那麼就均設為預設大小。
4.4.1.2 onDraw
首先,我們去除了各種padding之後,得到了真實的圓心坐标(cx, cy)、半徑r。半徑的值為真實繪圖區域短邊的一半。然後調用
canvas.drawCircle()
方法繪制出背景圓形。
然後,我們定義的Star類就登場了。
回顧一下,我們建立了Star對象之後,就可以擷取它的5個頂點坐标。知道了坐标,我們就可以通過Path + canvas.DrawPath()來繪圖了。先使用Path對象,按我們平時手工的方法畫一個五角星,然後再drawPath填充顔色。代碼如下:
// draw star
Star star = getStar(mStarScale * r, cx, cy, mStarRotate);
Star.Point points[] = star.getPoints();
mPath.setFillType(Path.FillType.WINDING);
mPath.moveTo(points[0].x, points[0].y);
mPath.lineTo(points[3].x, points[3].y);
mPath.lineTo(points[1].x, points[1].y);
mPath.lineTo(points[4].x, points[4].y);
mPath.lineTo(points[2].x, points[2].y);
mPath.close();
mPaint.setColor(mStarColor);
canvas.drawPath(mPath, mPaint);
其中getStar()方法是為了避免在onDraw中建立對象。
mPath.setFillType(Path.FillType.WINDING)
允許我們完全填充這個路徑内部。具體可以參考相關文章:https://blog.csdn.net/qq_30889373/article/details/78793086
4.4.1.3 完整代碼
/**
* 圓形背景,五角星圖案的自定義View。
*/
public class StarView extends View {
private static final String TAG = "StarView";
private static final int DEFAULT_SIZE_PX = 128;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mStarColor;
private int mBgColor;
private float mStarScale;
private float mStarRotate;
private Path mPath;
public StarView(Context context) {
this(context, null, 0);
}
public StarView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public StarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StarView);
mStarColor = a.getColor(R.styleable.StarView_star_color, Color.YELLOW);
mStarScale = a.getFloat(R.styleable.StarView_star_scale, 0.8f);
mStarRotate = a.getFloat(R.styleable.StarView_star_rotate, 0);
a.recycle();
init();
}
private void init() {
Drawable bgDrawable = getBackground();
if (bgDrawable instanceof ColorDrawable) {
mBgColor = ((ColorDrawable) bgDrawable).getColor();
} else {
mBgColor = Color.RED;
}
setBackgroundColor(0);
mPath = new Path();
}
@Override
@SuppressWarnings("SuspiciousNameCombination")
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 對長寬為wrap_content的判斷
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(DEFAULT_SIZE_PX, DEFAULT_SIZE_PX);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
int drawSize = heightSpecSize - getPaddingTop() - getPaddingBottom();
setMeasuredDimension(drawSize + getPaddingLeft() + getPaddingRight(), heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
int drawSize = widthSpecSize - getPaddingLeft() - getPaddingRight();
setMeasuredDimension(widthSpecSize, drawSize + getPaddingTop() + getPaddingBottom());
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int realWidth = width - paddingLeft - paddingRight;
int realHeight = height - paddingTop - paddingBottom;
if (realWidth <= 0 || realHeight <= 0) {
return;
}
float cx, cy, r; // 圓心坐标x,圓心坐标y,半徑
r = Math.min(realWidth, realHeight) / 2.0f;
cx = paddingLeft + r;
cy = paddingTop + r;
// draw background
if (mBgColor != Color.TRANSPARENT) {
mPaint.setColor(mBgColor);
canvas.drawCircle(cx, cy, r, mPaint);
}
// draw star
Star star = getStar(mStarScale * r, cx, cy, mStarRotate);
Star.Point points[] = star.getPoints();
mPath.setFillType(Path.FillType.WINDING);
mPath.moveTo(points[0].x, points[0].y);
mPath.lineTo(points[3].x, points[3].y);
mPath.lineTo(points[1].x, points[1].y);
mPath.lineTo(points[4].x, points[4].y);
mPath.lineTo(points[2].x, points[2].y);
mPath.close();
mPaint.setColor(mStarColor);
canvas.drawPath(mPath, mPaint);
}
/**
* 由于onDraw中最好不要頻繁的建立對象,是以使用臨時的成員來儲存Star。
*/
private Star mStarTemp;
private Star getStar(double a, double cx, double cy, double rotate) {
if (mStarTemp == null) {
mStarTemp = new Star(a, cx, cy, rotate);
} else {
mStarTemp.setStar(a, cx, cy, rotate);
}
return mStarTemp;
}
}
/**
* 通過輸入五角星的中心點坐标和頂點到中心的長度,計算出五角星每個頂點的坐标。
* <p>
* Created by LittleFogCat on 2019/1/26.
*/
@SuppressWarnings("WeakerAccess")
public class Star {
/**
* 一些計算中常用的常數
*/
private static final double sin18 = sin(18);
private static final double sin36 = sin(36);
private static final double cos18 = cos(18);
private static final double cos36 = cos(36);
/**
* 五角星從中心到頂點的距離
*/
private double mCVLength;
/**
* 中心點的坐标
*/
private Point mCenter;
/**
* 五角星旋轉的角度
*/
private double mRotate;
/**
* 五角星5個頂點坐标,順序為:從最上方頂點開始,順時針旋轉的所有頂點。
*/
private Point[] mPoints = new Point[5];
/**
* 構造函數,構造出一個正置無旋轉的五角星。
*
* @param a 五角星中心到頂點的距離
* @param cx 五角星中心坐标x
* @param cy 五角星中心坐标y
*/
public Star(double a, double cx, double cy) {
this(a, cx, cy, 0);
}
/**
* 主要構造函數。根據五角星中心坐标和中心到頂點的距離,計算出每個頂點的長度。
*
* @param a 五角星中心到頂點的距離
* @param cx 五角星中心坐标x
* @param cy 五角星中心坐标y
* @param rotate 五角星旋轉角度,0度為正置五角星
*/
public Star(double a, double cx, double cy, double rotate) {
mCVLength = a;
mCenter = new Point(cx, cy);
mRotate = rotate;
makeCoordinate();
}
public void setStar(double a, double cx, double cy, double rotate) {
mCVLength = a;
mCenter.x = (float) cx;
mCenter.y = (float) cy;
mRotate = rotate;
makeCoordinate();
}
/**
* 計算頂點坐标。
*/
private void makeCoordinate() {
Point p[] = getPoints();
final double x = mCenter.x;
final double y = mCenter.y;
final double a = mCVLength;
if (mRotate == 0) {
p[0] = new Point(x, y - a);
p[1] = new Point(x + a * cos18, y - a * sin18);
p[2] = new Point(x + a * sin36, y + a * cos36);
p[3] = new Point(x - a * sin36, y + a * cos36);
p[4] = new Point(x - a * cos18, y - a * sin18);
} else {
final double r = mRotate;
for (int i = 0; i < 5; i++) {
p[i] = new Point(x + a * sin(r + 72 * i), y - a * cos(r + 72 * i));
}
}
}
/**
* 擷取五角星的頂點坐标
*
* @return 五角星的頂點坐标
*/
public Point[] getPoints() {
if (mPoints == null || mPoints.length != 5) {
mPoints = new Point[5];
}
return mPoints;
}
/**
* {@link Math#sin(double)} 參數是弧度,這裡轉換為以度數為參數的函數
*
* @param a degree
* @return sin(a)
*/
private static double sin(double a) {
return Math.sin(Math.toRadians(a));
}
/**
* {@link Math#cos(double)} 參數是弧度,這裡轉換為以度數為參數的函數
*
* @param a degree
* @return cos(a)
*/
private static double cos(double a) {
return Math.cos(Math.toRadians(a));
}
public static class Point {
public float x, y;
public Point() {
}
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public Point(double x, double y) {
this.x = (float) x;
this.y = (float) y;
}
}
}
4.4.2 自定義ViewGroup:FlowLayout
我們給他取了一個很好聽的名字,FlowLayout流布局。實際上就是把子View挨個放。雖然寫的時候感覺挺麻煩的,但是其實思路上面很簡單,沒什麼複雜的地方。
4.4.2.0 onMeasure
隻需要處理長(寬)是wrap_content的情況。思路很簡單,挨個取出所有的子View:
- 如果寬是wrap_content,那麼用變量儲存最長行的寬度,本行寬度和本行剩餘寬度;如果本行剩餘寬度比這個子View小,那麼就到下一行繼續排,比較本行寬度和最長寬度;最後哪一行的寬度最寬,setMeasuredDimension的width就是它了(當然,不能超過parent的寬度);
- 如果高是wrap_content,那麼和1中相同的排法,不同的就是記錄每行最高的View高度,然後把他們全加起來,得到的就是總的高度了(當然,不能超過parent的高度);
- 如果寬高都是wrap_content,那麼就是1和2的結合。
4.4.2.1 onLayout
排布方式已經在onMeasure中說過了,是以onLayout隻需要簡單的算一下子View的上下左右坐标即可。
需要注意的是,為了支援margin屬性,我們需要自定義LayoutParams,繼承自ViewGroup.MarginLayoutParams,然後重寫
generateLayoutParams()
方法。
4.4.2.2 完整代碼
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
super(context);
init();
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
final int childCount = getChildCount();
measureChildrenWithMargins(widthMeasureSpec, heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
int totalWidth = getPaddingLeft() + getPaddingRight();
int totalHeight = getPaddingTop() + getPaddingBottom();
int rowWidth = getPaddingLeft() + getPaddingRight();
int rowHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (widthSize - rowWidth < childWidth) { // 行剩餘空間不足,需要換行
totalHeight += childHeight;
rowHeight = childHeight;
rowWidth = childWidth + getPaddingLeft() + getPaddingRight();
} else {
rowWidth += childWidth;
if (childHeight > rowHeight) {
rowHeight = childHeight;
totalHeight += childHeight - rowHeight;
}
}
if (totalWidth < rowWidth) {
totalWidth = rowWidth;
}
}
setMeasuredDimension(totalWidth, totalHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
int totalWidth = getPaddingLeft() + getPaddingRight();
int rowWidth = getPaddingLeft() + getPaddingRight();
int rowHeight = getPaddingTop() + getPaddingBottom();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (widthSize - rowWidth < childWidth) { // 行剩餘空間不足,需要換行
rowHeight = childHeight;
rowWidth = childWidth + getPaddingLeft() + getPaddingRight();
} else {
rowWidth += childWidth;
if (childHeight > rowHeight) {
rowHeight = childHeight;
}
}
if (totalWidth < rowWidth) {
totalWidth = rowWidth;
}
}
setMeasuredDimension(totalWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
int totalHeight = getPaddingTop() + getPaddingBottom();
int rowWidth = getPaddingLeft() + getPaddingRight();
int rowHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (widthSize - rowWidth < childWidth) { // 行剩餘空間不足,需要換行
totalHeight += childHeight;
rowHeight = childHeight;
rowWidth = childWidth + getPaddingLeft() + getPaddingRight();
} else {
rowWidth += childWidth;
if (childHeight > rowHeight) {
rowHeight = childHeight;
totalHeight += childHeight - rowHeight;
}
}
}
setMeasuredDimension(widthSize, totalHeight);
}
}
protected void measureChildrenWithMargins(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int childCount = getChildCount();
int widthUsed = getPaddingLeft() + getPaddingRight();
int heightUsed = getPaddingTop() + getPaddingBottom();
int rowHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (child.getVisibility() == View.GONE) {
continue;
}
if (widthUsed + lp.width + lp.leftMargin + lp.rightMargin > widthSpecSize) {
widthUsed = getPaddingLeft() + getPaddingRight();
rowHeight = 0;
}
measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
int measuredWidth = child.getMeasuredWidth();
int measuredHeight = child.getMeasuredHeight();
widthUsed += measuredWidth;
if (measuredHeight > rowHeight) {
rowHeight = measuredHeight;
heightUsed += measuredHeight - rowHeight;
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
final int childCount = getChildCount();
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int rowHeight = 0;
int childTop = paddingTop;
int childLeft = paddingLeft;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int left, top, right, bottom;
if (childLeft + childWidth > width) { // 換行
childTop += rowHeight;
left = paddingLeft + lp.leftMargin;
top = childTop + lp.topMargin;
right = left + childWidth;
bottom = top + childHeight;
child.layout(left, top, right, bottom);
childLeft = right + lp.rightMargin;
rowHeight = childHeight + lp.topMargin + lp.bottomMargin;
} else {
left = childLeft + lp.leftMargin;
top = childTop + lp.topMargin;
right = left + childWidth;
bottom = top + childHeight;
child.layout(left, top, right, bottom);
childLeft = right + lp.rightMargin;
rowHeight = Math.max(rowHeight, childHeight + lp.topMargin + lp.bottomMargin);
}
}
}
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
5 其他
本文同時釋出于簡書:https://www.jianshu.com/p/49b89104d828
本章GitHub位址:https://github.com/LittleFogCat/AndroidBookNote/tree/master/chapter04_view