Q1:什麼是View?
View是螢幕上的一塊矩形區域。
Q2:Google為什麼設計View?
設計View的目的是為了解決使用者和應用之間互動。
Q3:怎麼樣互動?
繪製自己與事件處理兩種方式與使用者互動。
Q4:矩形區域在螢幕的位置和大小由誰來決定?
一個介面有很多View或者ViewGroup,google用window先去載入一個超級複合View,用它來包含住所有的其他View,這個超級複合View就叫做DecorView。介面中被包裹的View將能直接或間接被DecorView中的FrameLayout去分配位置和大小(大小的控制還和view自身有關)
Q5:DccorView中的FrameLayout怎麼分配位置?
總結:普通View只能被安排,被巢狀的ViewGroup不僅要被安排,而且還要安排它的子View位置。子View的位置資訊由開發者告訴父View直接控制,不需要子View的參與。
為了證明上總結我們畫一個圓:
class MyView : View{
val paint: Paint//畫筆
//構造方法
constructor(context: Context?) : this(context, null)
//構造方法
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
//構造方法
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
//抗鋸齒
paint = Paint(Paint.ANTI_ALIAS_FLAG)
//設定畫筆顏色
paint.color = Color.BLACK
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(100F,100F,100F,paint)
}
}複製程式碼
然後佈局使用layout開頭的屬性(除了寬度和高度的設定,後邊講解)照樣管用,由此更加驗證了上邊總結。
Q6:怎麼確定View的大小?
開發者向ViewGroup表達我這個子View需要的大小,然後ViewGroup會綜合考慮自己的空間大小以及開發者的請求,然後生成兩個MeasureSpec物件(生成這兩個物件的方法在ViewGroup的getChildMeasureSpec方法中),通過子View的onMeasure方法傳遞給子類,這兩個物件是ViewGroup向子View提出的要求,就相當於告訴子View:“我已經與你的使用者(開發者)商量過了,現在把我們商量確定的結果告訴你,你的寬度不能違反width MeasureSpec物件的要求,你的高度不能違反height MeasureSpec物件的要求,現在,你趕緊根據這個要求確定下自己要多大空間,只許少,不許多哦。然後告訴我”
分析getChildMeasureSpec原始碼
/**
* @param spec 父佈局給的spec
* @param padding 父佈局的邊距
* @param childDimension 開發者在佈局檔案中要求的大小layout_width 和layout_height的值
* @return返回父佈局和開發者商量過後的佈局大小
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取父View的測量模式和大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//計算出我能給子View的最大值
int size = Math.max(0, specSize - padding);
//初始化子View的大小和模式
int resultSize = 0;
int resultMode = 0;
//根據自身模式去給子view的大小和模式賦值
switch (specMode) {
case MeasureSpec.EXACTLY:
// 當子view的LayoutParams>0,即有確切的值,把這個確切的值給子view,
// 同時模式是:EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//如果是撐滿,就把父View能給他的最大值給子View,
// 同時模式是:EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
//如果是包裹內容,也把父View能給他的最大值給它,
//很明顯這裡是包裹內容,但是確給了最大值,記住這裡是個錯誤值,
// 它不是不想給,而是不確定也給不了,需要你自己測量後給我,然後我layout的時候才能修改成正確值
// 也很好的解釋了為什麼MATCH_PARENT和WRAP_CONTENT預設是撐滿父佈局
// 注意這裡模式是不要超過這個值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 道理同上
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//注意這裡模式改變了
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當父view的模式為UNSPECIFIED時,父容器不對view有任何限制,要多大給多大
//一般是滑動父佈局
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 道理同上
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
// 因為父view為UNSPECIFIED,可以滾動的,所以這裡這個限制值沒有意義6.0之前返回是0,6.0之後返回是開發者要求的大小
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 因為父view為UNSPECIFIED,可以滾動的,所以這裡這個限制值沒有意義6.0之前返回是0,6.0之後返回是開發者要求的大小
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}複製程式碼
總結
只要給一個確定大小的值,全部都是恰好模式。值也是開發者給的值。其餘只要遇到包裹內容或者包裹內容模式,子類全部是包裹內容。只要遇到不確定模式,全部是不確定模式。
Q7:MeasureSpec物件中的要求怎麼獲取以及含義是什麼?
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSize = MeasureSpec.getSize(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
var heightSize = MeasureSpec.getSize(heightMeasureSpec)複製程式碼
假設size是100dp,Mode的取值有三種,它們代表了ViewGroup的總體態度:
- EXACTLY 標記,ViewGroup對View說,你只能用100dp,原因是多樣的,可能是你的開發者說要你完全佔據我的空間,而我只有100dp。也可能這是你的開發者的要求,他需要你佔這麼大的空間。
- AT_MOST標記,你最多隻能用100dp。這是因為你的開發者說讓你佔據wrap_content的大小,讓我跟你商量,我又不知道你到底要佔多大區域,但是我告訴你,我只有100dp,你最多也只能用這麼多。
- UNSPECIFIED標記,把你最理想的大小告訴我,我可以滾動,你隨便來
這裡就說明了必須要自己測量大小然後給父View否則wrap_content不管用。
Q8:子View怎麼測量自己需要的大小?
不同的View有不同的態度,但是有幾點基本的規矩是要遵守的:不要違反ViewGroup的規定,google給我們提供方便的方法來遵照這一固定resolveSize(真實需要的寬高(簡單的數學演算法), widthMeasureSpec(父類的要求)),返回值是優化後遵照這一規定的寬高。檢視原始碼發現呼叫resolveSizeAndState(size, measureSpec, 0)方法。這裡多了一個標記,目的是告訴父佈局,我是委屈的寬高。
/**
*
* @param size 需要的真實大小
* @param measureSpec 父佈局的規定
* @param childMeasuredState 不知道是什麼
* @return 返回一個優化後的寬高
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
//獲取父佈局的模式
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {//如果測量的比能給的還要大,就給它我能提供的最大限度
result = specSize | MEASURED_STATE_TOO_SMALL;//標記我想要的更大
} else {
result = size;//如果給的比要求的小,沒有問題,直接賦值
}
break;
case MeasureSpec.EXACTLY:
result = specSize;//直接給我的最大限度,或者開發者要求的值
break;
case MeasureSpec.UNSPECIFIED://給你需要的真實值,因為我可以滾動
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}複製程式碼
Q9:子View按照要求測量自己的大小後怎麼告訴父View?
根據上邊google提供的方法,就可以計算優化出一個滿足父佈局要求的自己的尺寸,這個時候就需要告訴父佈局我需要的真實尺寸,進而去修改上邊在包裹內容時候的錯誤資料,保證包裹內容的資料是正確的,在使用layout的時候就會用矯正過的正確值去渲染控制元件大小。
google給我們提供了方法setMeasuredDimension(計算優化後的真實寬,計算優化有的真實高);
到此當渲染的時候每個view都在自身儲存了自己的位置和大小資訊,關於View的繪製,就是一個方法onDraw,保證在當時測量的大小範圍內去繪製就行。後邊講解繪製的細節。
總結:一般onMeasure方法中的程式碼都是告訴父佈局,我真實的大小:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(resolveSize(真實需要大小, widthMeasureSpec), resolveSize(真實需要大小, heightMeasureSpec))
}複製程式碼
Q10:View中都有哪些方法?
方法基本執行順序:https://blog.csdn.net/anhenzhufeng/article/details/72886181
小結:
- View是一個矩形區域,設計它的目的是為了和使用者互動,互動的方式有自我繪製和事件處理
- View的位置由開發者告訴父佈局,父佈局直接決定位置
- View的大小有開發者和父佈局和自身大小共同商量決定
- 自定義View中常重寫的方法,以及這些方法會改變什麼
認識了什麼是View,知道大小和位置是由哪些程式碼決定的,接下來就是繪製和處理事件,接下來幾篇文章準備寫一些常用的繪製方法和技巧。