View基礎

稀飯_發表於2018-07-05

Q1:什麼是View?

View是螢幕上的一塊矩形區域

Q2:Google為什麼設計View?

設計View的目的是為了解決使用者和應用之間互動

Q3:怎麼樣互動?

繪製自己事件處理兩種方式與使用者互動。

Q4:矩形區域在螢幕的位置和大小由誰來決定?

      一個介面有很多View或者ViewGroup,google用window先去載入一個超級複合View,用它來包含住所有的其他View,這個超級複合View就叫做DecorView。介面中被包裹的View將能直接或間接被DecorView中的FrameLayout去分配位置和大小(大小的控制還和view自身有關

Q5:DccorView中的FrameLayout怎麼分配位置?

layout_*之類的配置雖然在書寫上與子View的屬性在一起,但它們並不是子View的屬性,它們只是父ViewGroup提供給開發者用來安排該View在父ViewGroup中的位置和大小的,同時,這些值由ViewGroup讀取,然後生成一個ViewGroup特定的LayoutParams物件,再把這個物件存入子View中的,這樣,在頁面渲染時候(也就是呼叫layout方法),就可以參考這個LayoutParams中的資訊,然後統一出每個view的正確位置。

總結:普通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中都有哪些方法?

View基礎

方法基本執行順序:https://blog.csdn.net/anhenzhufeng/article/details/72886181

小結:

  • View是一個矩形區域,設計它的目的是為了和使用者互動,互動的方式有自我繪製和事件處理
  • View的位置由開發者告訴父佈局,父佈局直接決定位置
  • View的大小有開發者和父佈局和自身大小共同商量決定
  • 自定義View中常重寫的方法,以及這些方法會改變什麼

認識了什麼是View,知道大小和位置是由哪些程式碼決定的,接下來就是繪製和處理事件,接下來幾篇文章準備寫一些常用的繪製方法和技巧。








相關文章