講解過程:
1.測量方式分析,比對不同測量模式的差異,包括應用分析。
2.分析源碼中view的具體繪製流程
3.測量的實際應用
1.測量方式分析,對不同測量模式的差異,包括應用分析。
說起測量我們最直觀和常見的是onMeasure方法。
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
這裏會有兩個參數,分別對應的是view的寬度和高度的測量規格。該參數是一個32位的int類型數據,
該參數包含了兩條信息,分別是測量模式和測量值(寬度或者高度值)
/**
* 獲取測量模式
*/
int specMode = MeasureSpec.getMode(measureSpec);
/**
* 獲取view的測量值
*/
int specSize = MeasureSpec.getSize(measureSpec);
32位的高兩位表示測量模式,低30位表示測量值。
測量模式分爲三種模式。
1.EXACTLY match_parent 或者確定值模式,默認是這種測量模式(如果你使用的是view的子類,那麼測量模式會有變化)
2.AT_MOST wrap_content 需要重寫onMeasure方法 最大值模式
3.UNSPECIFIED 自定義控件有用
剛纔說默認的測量模式是確定值模式,現在就來看下。
自定義TextView MyTextView類
public class MyTextView extends android.support.v7.widget.AppCompatTextView {
Context context;
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.context=context;
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.macbook.demoproject.measure.MyTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ddddddddddddddd"
android:layout_marginLeft="5dp"
android:layout_marginRight="6dp"
android:paddingLeft="4dp"
android:paddingRight="3dp"
android:textColor="@color/colorAccent"
android:textSize="30sp"
android:background="#00ff00" />
</LinearLayout>
顯示效果
這是因爲我們繼承的是TextView ,它本身已經實現了onMeasure 方法,所以不會顯示 全屏,現在我們改下,將TextView 改爲View Group
public class MyGroupMeasureVeiw extends ViewGroup {
public MyGroupMeasureVeiw(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.macbook.demoproject.measure.MyGroupMeasureVeiw
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright">
</com.example.macbook.demoproject.measure.MyGroupMeasureVeiw>
</LinearLayout>
所以,如果繼承了viewGroup 不重寫onmeasure 默認是會全屏顯示的,也就是精確值模式。那麼上邊我們看到,如果繼承了TextView 同樣使用了wrap_content 顯示的是最大值效果,那是因爲TextView 重寫onMeasure方法,重新計算了view的測量模式。
下面我們就來新建一個自定義的類繼承自ViewGroup,實現控件填充自動收縮大小。
package com.example.macbook.demoproject.measure;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by LCT
* Time:2019/3/13 11:26.
* Annotation:
*/
public class MyGroupMeasureVeiw extends ViewGroup {
private static final String TAG = "MyGroupMeasureVeiw";
Context context;
int widthL;
/**
* 父類最大寬度
*/
int maxViewGroupWidth = 0;
public MyGroupMeasureVeiw(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
int d = (int) context.getResources().getDisplayMetrics().xdpi;
widthL = context.getResources().getDisplayMetrics().widthPixels;
int dsdd = context.getResources().getDisplayMetrics().heightPixels;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childCount = getChildCount();
if (childCount == 0) {//如果沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
setMeasuredDimension(0, 0);
} else {
int height = getTotleHeight();
int width = maxViewGroupWidth != 0 ? maxViewGroupWidth : getMaxChildWidth();
//如果寬高都是包裹內容最大值模式
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我們將高度設置爲所有子View的高度相加,寬度設爲子View中最大的寬度
setMeasuredDimension(width, height);
} else if (heightMode == MeasureSpec.AT_MOST) {
//如果只有高度是包裹內容最大值模式
//寬度設置爲ViewGroup自己的測量寬度,高度設置爲所有子View的高度總和
setMeasuredDimension(widthSize, height);
} else if (widthMode == MeasureSpec.AT_MOST) {
//如果只有寬度是包裹內容
//寬度設置爲子View中寬度最大的值,高度設置爲ViewGroup自己的測量值
setMeasuredDimension(width, heightSize);
}
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//記錄當前的高度位置
int childCount = getChildCount();
/**
* 保存一行中高度最大的一個view的高度包括margin值
*/
int maxHeight = 0;
/**
* y軸方向每次需要累加的view的高度,用於計算Y值得大小
*/
int maxYHeight = 0;
/**
* view x軸方向最大寬度用於判斷view是否需要換行
*/
int maxWidth = 0;
/**
* 左邊距離
*/
int x = 0;
/**
* 右邊距離
*/
int y = 0;
/**
* 右側距離
*/
int xw = 0;
/**
*
*/
int yh = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
/**
* 獲取當前view的高度
*/
int height = childView.getMeasuredHeight();
int heightds = childView.getHeight();
/**
* 獲取當前view的寬度
*/
int width = childView.getMeasuredWidth();
/**
* 獲取當前view的寬度包含margin值因爲在實際的控件擺放中是需要將margin值空出來的
*/
int viewWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
x += lp.leftMargin;
y = lp.topMargin + maxYHeight;
xw = x + width;
yh = y + height;
if (maxWidth <= widthL && (maxWidth + viewWidth) >= widthL) { //view在本行放滿了累加本行最大高度
/**
* 換行只和y軸方向座標有關x方向恢復默認
*/
x = lp.leftMargin;
xw = x + width;
maxWidth = viewWidth;
maxYHeight += maxHeight;
y = maxYHeight + lp.topMargin;
yh = y + height;
Log.d(TAG, "getTotleHeight: maxHeight:" + y);
} else {
maxWidth += viewWidth;
//獲取一行中高度最大的view的高度
int hHeight = height + lp.topMargin + lp.bottomMargin;
if (maxHeight < hHeight) {
maxHeight = hHeight;
}
Log.d(TAG, "getTotleHeight: maxWidth:" + maxWidth);
}
Log.d(TAG, "getTotleHeight onLayout: l" + x + "t: " + y + "r: " + xw + "b: " + yh);
childView.layout(x, y, xw, yh);
/**
*view擺放成功後將 marginLeft + width + marginRight 得到X 保存一個完整的view所佔的寬度
*/
x = xw + lp.rightMargin;
}
}
/***
* 獲取子View中寬度最大的值
*/
private int getMaxChildWidth() {
int childCount = getChildCount();
int maxWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
/**
* 獲取當前view的寬度
*/
int width = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//view 寬度累計大於屏幕寬度,將寬度設置爲設備寬度
if (maxWidth <= widthL && (maxWidth + width) >= widthL) {
maxWidth = widthL;
Log.d(TAG, "getTotleHeight: maxHeight:" + maxWidth);
return maxWidth;
} else {
maxWidth += width;
Log.d(TAG, "getTotleHeight: maxWidth:" + maxWidth);
}
}
return maxWidth;
}
/***
* 將所有子View的高度相加
**/
private int getTotleHeight() {
int childCount = getChildCount();
int maxHeight = 0;
int maxYHeight = 0;
int maxWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
/**
* 獲取當前view的高度
*/
int height = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
/**
* 獲取當前view的寬度
*/
int width = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//view在本行放滿了累加本行最大高度,用於計算viewgroup高度
if (maxWidth <= widthL && (maxWidth + width) >= widthL) {
maxViewGroupWidth = widthL;
maxYHeight += maxHeight;
maxHeight = 0;
maxWidth = 0;
Log.d(TAG, "getTotleHeight: maxHeight:" + maxYHeight);
} else {
maxWidth += width;
//獲取一行中高度最大的view的高度
if (maxHeight < height) {
maxHeight = height;
}
maxViewGroupWidth=maxWidth;
Log.d(TAG, "getTotleHeight: maxWidth:" + maxHeight);
}
}
if (maxViewGroupWidth!=widthL) {
maxYHeight=maxHeight;
}
return maxYHeight;
}
}
使用的xml measure_group_layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_dark">
<com.example.macbook.demoproject.measure.MyGroupMeasureVeiw
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright"
android:padding="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:gravity="center"
android:padding="20dp"
android:text="打ffffffffffffff快點1"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打fffffffffdsdfvbfd快點2"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快點3"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快dfdsdaaaaaaaaaaaadad點4"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快點5"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快dsddadf點6"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快dsddadf點7"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:padding="20dp"
android:text="打快dsddadf點8"
android:background="@android:color/white"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:gravity="center"
android:padding="20dp"
android:text="打ffffffffffffff快點1"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打fffffffffdsdfvbfd快點2"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快點3"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快dfdsdaaaaaaaaaaaadad點4"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快點5"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:padding="20dp"
android:text="打快dsddadf點6"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/white"
android:gravity="center"
android:padding="20dp"
android:text="打ffffffffffffff快點1"
android:textColor="@android:color/holo_red_dark"
android:textSize="15sp" />
</com.example.macbook.demoproject.measure.MyGroupMeasureVeiw>
</LinearLayout>
2.分析源碼中view的具體繪製流程
現在我們直接看自定義類MyGroupMeasureVeiw,它繼承自ViewGroup ,默認重寫onLayout方法,現在我們直接看onMeasure方法。
measureChildren(widthMeasureSpec, heightMeasureSpec);用於測量子view測量模式,只有經過測量後才能獲取到子view 的相關信息,比如寬度,高度等。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
這裏傳入的兩個參數分別是高度和寬度的測量模式,,然後將父類的測量模式傳給子類進行操作,調用了measureChild方法,這裏是獲取了所有子類,一個一個測量。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
這裏分別調用了getChildMeasureSpec 根據父類的測量模式來生成子類的測量規則,具體分析下getChildMeasureSpec 方法,針對寬度測量規則看下。
這裏三個參數分別是:父類的寬度測量規則,父類的左右兩側的padding值,以及子類的寬度,這裏我直接在源碼中備註了
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
/**
* 獲取測量模式
*/
int specMode = MeasureSpec.getMode(spec);
/**
* 獲取控件的寬度
*/
int specSize = MeasureSpec.getSize(spec);
/**
* 獲取子類可用的最大寬度
* 這裏減去父類的左右padding剩下的纔是子類可用的
*/
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
/**
* 根據父類的測量模式設置子類的測量模式
*/
switch (specMode) {
// Parent has imposed an exact size on us
/**
* 父類是精確值模式,childDimension大於0或者是MATCH_PARENT 則子類的模式是精確值模式,
* 如果子類是WRAP_CONTENT
* 那麼子類的測量模式爲最大值模式AT_MOST,既子類的寬度等於(padding忽略)父類的寬度
*/
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
/**
* 父類是最大值模式:
* 1.如果子類childDimension >= 0 那麼子類就是精確值模式
* 2.childDimension == LayoutParams.MATCH_PARENT 子類是精確值模式MATCH_PARENT,它的寬度大小和父類的寬度一樣
* 父類是最大值模式,所以子類的模式也是最大值模式AT_MOST
* 3.childDimension == LayoutParams.WRAP_CONTENT 父類都是最大值模式,子類的寬度是WRAP_CONTENT最大值模式,
* 所以子類也是最大值模式AT_MOST 寬度和父類寬度一樣
*
*/
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
/**
* 這個稍微難理解,多用於自定模式
* 1.如果子類是具體值那麼,設置它爲精確值模式
* 2.如果子類是MATCH_PARENT 或者 WRAP_CONTENT 模式,那麼子類的大小有可能和父類一樣大,
* 設置子類爲自定義模式UNSPECIFIED
* View.sUseZeroUnspecifiedMeasureSpec 單獨看下這個,字面意思是說,用戶沒有指定它的的測量模式
* 它的默認值是0,sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M;
* 當targetSdkVersion小於23時爲true resultSize爲0,否則resultSize爲父類的大小
*/
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;
}
//noinspection ResourceType
/**
* 將測量好的模式和測量值生成測量模式
*/
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
3.測量的實際應用
現在知道子類是如何根據父類的測量規則生成自己的測量規則,繼續回到MyGroupMeasureVeiw 類的onMeasure 方法中,
measureChildren已經看完了,他就是將所有子類生成了測量規格。看完了對子view的測量,我們結合代碼需求來設置父veiw的尺寸
假設父類寬高都是最大值模式。要求是子類一次從左到右根據內容大小自適應顯示寬高,當一行放不下時換到另一行顯示,然後計算出父類view的寬高。
getTotleHeight 返回 將所有子View擺放的每行的高度相加
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { //我們將高度設置爲所有子View的高度相加,寬度設爲子View中最大的寬度 setMeasuredDimension(width, height); }
int height = getTotleHeight(); int width = maxViewGroupWidth != 0 ? maxViewGroupWidth : getMaxChildWidth();
這裏獲取到了高度和寬度,也就是說我們根據子類的顯示方式去動態指定了一個父類的寬高,建議將onlayout 方法內容註釋掉來理解。
關於 getTotleHeight() 和 getMaxChildWidth()我們大概看先上面的具體哪內容,備註寫的比較完善,主要就是如何獲根據子類,的位置及寬高,去計算自適應父類的寬高尺寸。
setMeasuredDimension方法用於設置view的寬高,這樣viewGroup及基本的view的測量及簡單應用就完成了。其實就是通過對子類的測量計算,得到一個自己想要的父類的大小。
這樣就完了麼,貌似少了點啥啊,還有常用的兩個方法大概講下吧。
getDefaultSize 和 getSuggestedMinimumWidth方法沒講。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
這個方法就是獲取默認的view的大小的,size可以是我們制定的一個數值也可以獲取系統獲取到的值,例如可以這麼用
setMeasuredDimension( getDefaultSize(30,MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec),MeasureSpec.UNSPECIFIED)),getDefaultSize(200,MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),MeasureSpec.UNSPECIFIED)));
這裏寬度的size設置爲30 ,測量模式我給他設置爲UNSPECIFIED 所以得到的getDefaultSize的返回值就是30 那麼同理,高度是200,這個在自定義的控件中非常重要。
接下來看下getSuggestedMinimumWidth()這裏getSuggestedMinimumHeight()類似,只看一個就好了
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
就是獲取view 所佔的最小值,寬度或者高度。
好了,關於view的測量我們就將到這裏,下一篇我們將接着這篇來看下view 是如何擺放的onLayout方法。