1、View生命週期以及View層級
1.1、View生命週期
View的主要生命週期如下所示, 包括建立、測量(onMeasure)、佈局(onLayout)、繪製(onDraw)以及銷燬等流程。
自定義View主要涉及到onMeasure、onLayout和onDraw這三個過程,其中
(1)自定義View(繼承自View類):主要實現onMeasure和onDraw,
(2)自定義ViewGroup(繼承自ViewGroup類):主要實現onMeasure和onLayout。
1.2、View層級
View層級是一個樹形結構。
onMeasure、onLayout和onDraw這三個過程都是按照View層級從上到下進行的:(1)ViewGroup主要負責onMeasure和onLayout,確定自身及其子View的大小和放置方式,例如LinearLayout通過onMeasure確定尺寸,通過onLayout對子View進行橫向或者縱向佈局;(2)View主要負責onMeasure和onDraw,例如TextView通過onMeasure確定自身尺寸,通過onDraw繪製文字。
2、View測量與MeasureSpec類
View測量中最難的一點就是如何根據View的LayoutParams引數確定其實際的寬高,如:
android:layout_width="10dp"
android:layout_width="match_parent"
android:layout_width="wrap_content"
這三種情況,View的寬度究竟應該是多少?這就要從View的測量過程分析了,
2.1、MeasureSpec類
View類的內部類MeasureSpec用來輔助View的測量,使用一個int型變數measureSpec來表示View測量的模式和具體的尺寸(寬和高各一個measureSpec值)。measureSpec共32位,用高兩位表示測量模式mode, 通過MeasureSpec.getMode(measureSpec)計算獲得, 低30位表示尺寸size,通過MeasureSpec.getSize(measureSpec)計算獲得。
mode共有三種情況:
MeasureSpec.UNSPECIFIED:不對View大小做限制,系統使用
MeasureSpec.EXACTLY:確切的大小,如:10dp
MeasureSpec.AT_MOST:大小不可超過某數值,最大不能超過其父類
2.2、父View的限制 :測量約束,限制最大寬度、最大高度等
View的測量過程受到父View的限制,如對一個ViewGroup測量時,其高度測量模式mode為EXACTLY,高度尺寸size為100dp,其子View的高度測量依據對應的android:layout_height引數來確定:
(1)具體尺寸值,如50dp,則該子View高度測量中mode為EXACTLY,尺寸為50dp;
(2)match_parent,則該子View高度和其父View高度相同,也是確定的,高度測量中mode為EXACTLY,尺寸為100dp;
(3)wrap_content, 則該子View最大高度為100dp, 確切高度需要根據內部邏輯確定,像TextView需要根據文字內容、寬度等綜合確定,於是高度測量中mode為AT_MOST, 尺寸size為100dp。
其他情況類似,如父View的mode分別為AT_MOST、UNSPECIFIED,具體見下表:
高度測量中mode和size確定後,可通過MeasureSpec.makeMeasureSpec(size, mode)來確定heightMeasureSpec,widthMeasureSpec使用同樣的方法確定。該方法的具體實現為ViewGroup.getChildMeasureSpec()方法。
2.3、子View的影響:實際測量
測量過程以LinearLayout作為例子說明:
(1) LinearLayout根據父View的measureSpec以及自身的LayoutParams確定了自身的widthMeasureSpec、heightMeasureSpec後, 呼叫measure(widthMeasureSpec, heightMeasureSpec) -----> onMeasure(widthMeasureSpec, heightMeasureSpec)來進行實際的測量;
(2) 當該LinearLayout方向為vertical時,實際測量中應該計算所有子View的高度之和,作為LinearLayout的測量高度needHeight;
(3) heightMeasureSpec中size為父類給該LinearLayout的限制高度,根據heightMeasureSpec中mode判斷是取needHeight, 還是heightMeasureSpec中size, 然後呼叫setMeasuredDimension將測量的高度和寬度設定進去。
2.4、View的測量過程
Android中View測量是一種遞迴的過程(見下圖),首先View呼叫measure方法,內部呼叫了自身的onMeasure方法,這個方法內部呼叫子View的measure方法(子View同樣會呼叫自身的onMeasure方法),對子View進行測量,儲存子View的測量尺寸,測量完所有的子View後再對自身測量,儲存測量尺寸,之後便可以通過View.getMeasuredWidth()和View.getMeasuredHeight()來獲取View的測量寬高。
3、自定義流式佈局FlowLayout
主要思路:
對FlowLayout的所有子View逐個進行測量,獲得measuredHeight和measuredWidth,在水平方向上根據這個尺寸依次對View進行放置,放不下則另起一行,每一行的高度取該行所有View的measuredHeight最大值。
3.1、單個子View測量
對其指定子View----child的測量程式碼如下,其中paddingLeft、paddingRight、paddingTop、paddingBottom分別是FlowLayout四邊上的padding,widthMeasureSpec以及heightMeasureSpec是FlowLayout中onMeasure中的兩個引數。
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
paddingLeft + paddingRight, child.getLayoutParams().width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec,
paddingTop + paddingBottom, child.getLayoutParams().height);
child.measure(childWidthSpec, childHeightSpec);
於是子View的測量寬、高分別可以通過child.getMeasuredWidth() 和child.getMeasuredHeight()來進行獲得。
3.2、onMeasure:測量與模擬佈局View
//子View的橫向間隔、縱向間隔
private final int horizontalSpace = dp2px(20);
private final int verticalSpace = dp2px(10);
//儲存測量的子View, 每一個元素為一行的子View陣列
private final List<List<View>> allLines = new ArrayList<>();
//記錄每一行的最大高度,用於佈局
private final List<Integer> heights = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
allLines.clear();
heights.clear();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int usedWidth = 0;
int height = 0;
//父佈局對FlowLayout的約束寬高
int seftWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft -
paddingRight;
int seftHeight = MeasureSpec.getSize(heightMeasureSpec) - paddingTop -
paddingBottom;
//FlowLayout的測量寬高
int needHeight = 0;
int needWidth = 0;
List<View> line = new ArrayList<>();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
paddingLeft + paddingRight, child.getLayoutParams().width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec,
paddingTop + paddingBottom, child.getLayoutParams().height);
child.measure(childWidthSpec, childHeightSpec);
if (usedWidth + horizontalSpace + child.getMeasuredWidth() > seftWidth) {
//當前行無法在放下下一個view,則儲存當前行的Views集合以及當前行的最大高度,
heights.add(height + verticalSpace);
allLines.add(line);
//所有行的最大寬度
needWidth = Math.max(needWidth, usedWidth);
//所有行的高度之和
needHeight += height + verticalSpace;
//重置下一行的使用寬度、高度、View集合
usedWidth = 0;
height = 0;
line = new ArrayList<>();
}
//獲取當前行的最大高度,作為當前行的高度
height = Math.max(height, child.getMeasuredHeight());
//記錄已經使用的寬度(第一個元素不需要加橫向間隔
usedWidth += child.getMeasuredWidth() + (line.size() == 0 ? 0 :
horizontalSpace);
//儲存已經測量及模擬佈局的View
line.add(child);
//記錄最後一行的資料
if (i == count - 1) {
heights.add(height + verticalSpace);
allLines.add(line);
needWidth = Math.max(needWidth, usedWidth);
needHeight += height + verticalSpace;
}
}
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//如果mode為MeasureSpec.EXACTLY, 則使用widthMeasureSpec中的size,
//不然使用測量得到的size, 寬高同理
int realWidth = widthMode == MeasureSpec.EXACTLY ? seftWidth : needWidth;
int realHeight = heightMode == MeasureSpec.EXACTLY ? seftHeight : needHeight;
//儲存測量的寬和高
setMeasuredDimension(realWidth + paddingLeft + paddingRight,
//如果只有一行,不需要縱向間隔
realHeight + paddingTop + paddingBottom - (allLines.size() > 0 ?
verticalSpace : 0));
}
3.3、佈局:onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < allLines.size(); i++) {
List<View> line = allLines.get(i);
for (int j = 0; j < line.size(); j++) {
View child = line.get(j);
child.layout(left, top, left + child.getMeasuredWidth(),
top + child.getMeasuredHeight());
//一行中View佈局後每次向後移動child的測量寬 + 橫向間隔
left += child.getMeasuredWidth() + horizontalSpace;
}
//每一行佈局從paddingLeft開始
left = getPaddingLeft();
//佈局完成一行,向下移動當前行的最大高度
top += heights.get(i);
}
}
3.4、測試
測試程式碼如下:
private final List<String> words = Arrays.asList("家用電器", "手機", "運營商", "數碼",
"電腦", "辦公", "電子書", "惠普星系列高清一體機", "格力2匹移動空調");
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_flow);
FlowLayout layout = findViewById(R.id.flow_layout);
for (int i = 0; i < words.size(); i++) {
TextView textView = new TextView(this);
textView.setText(words.get(i));
textView.setBackground(ContextCompat.getDrawable(this,
R.drawable.round_background));
textView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, FlowLayout.dp2px(60)));
//textView.setLayoutParams(new ViewGroup.LayoutParams(
// ViewGroup.LayoutParams.WRAP_CONTENT,
// ViewGroup.LayoutParams.WRAP_CONTENT));
int padding = FlowLayout.dp2px(5);
textView.setPadding(padding, padding, padding, padding);
layout.addView(textView);
}
}
效果圖: