解碼自定義View-佈局繪製與測量

一.引言

  最近在作重慶掌廳的項目,發現使用ViewGroup包裹的子view,常常子view的高度測量不許,爲了瞭解這個問題的本質緣由,簡單寫了幾個demo做爲演示,自定義View是比較重要的,尤爲是佈局和測量流程
  測量和佈局的方法實際上是比較少的,一個是 onMeasure,一個是 onLayout,這個是佈局的核心所在。
下面咱們就開始佈局過程:java

二.佈局過程:

  • 肯定每一個view的位置和尺寸

  通俗來講就是:計算每一個 view 的尺寸以及相對於他們的父view的相對位置,目的是幫助咱們寫佈局文件的難。ide

  • 做用:
    View繪製和觸摸反饋作支持

三.佈局的本質

A.從總體來看:

1. 測量過程:
  從根 View 遞歸調用每個子Viewmeasure()方法。讓每一個子view進行自我測量,每次測量以後,它會算出每一個子view,都會算出本身的指望尺寸局,根據本身指望的尺寸得出子view,實際尺寸。佈局

2. 佈局過程:
  從根 View 遞歸調用到每一級的 layout() 方法,把測量過程得出的子view位置和尺寸傳給子 view,子view保存。ui

3. 爲何要有兩個流程呢?
  主要是測量過程比較複雜,可能測量某個子view,直接獲得它的位置和尺寸,可是有的時候,你可能須要測量屢次,你一次不行可能須要測量屢次。spa

B.從個體來看:
  1. 運行前,開發者在 xml 文件寫入對View的佈局要求layout_xxxcode

  2. view 在本身的onMeasure中,根據開發者在xml中寫對子view的要求,和本身的可用空間,得出子view的具體尺寸要求。xml

  3. view在本身的onMeasure()中根據本身的父view的特性算出本身的指望值。遞歸

    • 若是是viewGroup,還會在這裏調用每一個子viewmeasure()進行測量。
  4. view在子view計算出指望尺寸後,得出子view的實際尺寸和位置。
      開發

  5. view在本身的layout()方法中,將view傳進本身的實際尺寸和位置保存。get

    • 若是是viewGroup,還會在onLayout裏面每一個字viewonLayut()把它們尺寸位置傳給它們。

二.具體開發:

a.extend已有的view,簡單修改他們的尺寸
public class SquareImageView extends AppCompatImageView {
    public SquareImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

   /** 1. 重寫`onMeasure()` */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 2. ⽤ getMeasuredWidth() 和 getMeasuredSize() 獲取到測量出的尺寸
        int measuredWidth = getMeasuredWidth();
        int measuredHeight = getMeasuredHeight();
        // 3. 計算最終的尺寸
        int size = Math.max(measuredWidth, measuredHeight);
        // 4.用 setMeasuredDimension 把最終的結果保存起來


        setMeasuredDimension(size, size);
    }
}
複製代碼

  須要注意的是在自定義view裏面是不能用 onLayout 去獲取 view 寬高的,由於父view可能會被測量屢次,拿到的寬高並不必定是真實的寬高。

  咱們須要獲取的是測量階段算出來的view的寬高,getWidthgetHeight其實是在layout裏面獲取的寬高。即右 - 左邊距;那個實際的寬度和高度

  通常狀況下,測量的結果都同樣,你的寬度和高度在測量過程是拿不到的,還沒佈局,因此保險起見只能用getMeasureWidthgetMeasureHeight。就至關於你論文的初稿。真正定稿是getWidthgetHeight

  因此只要遵循一個原則,在繪製階段,只能用getMeasureWidthgetMeasureHeight,在觸摸反饋和佈局階段只能用getWidthgetHeight

b.徹底自定義View的尺寸
  • 重寫 onMeasure
  • 計算本身的尺寸
  • resolveSize()resolveSizeAndState() 修正結果
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    // 1.MeasureSpec.getMode(measureSpec) 和 MeasureSpec.getSize(measureSpec) 取出對本身的尺寸類型和具體尺寸
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
            // 若是measure spec 的model是 AT_MOST,表示view對子view的尺寸只限制了上限
                if (specSize < size) {
                // 若是計算出的size 不大於 spec 的size,而且在 resolveSizeAndState 會添加標誌 MEASURED_STATE_TOO_SMALL 這個能夠輔助父view的測量
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
            // 若是模式是`EXACTLY`,表示父view對子view作出了精確的限制,因此就放棄計算的size直接選擇measureSpec的size
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            // 若是measure spec 的 model 是 UNSPECIFIED , 表示父 view 對 子 view 沒有任何尺寸限制,因此直接選用計算出來的size,忽略 spec 的 size
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
複製代碼
  • 使用 setMeasureDimension(width,height)保存結果
@Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       int size = (int) ((PADDING + radius) * 2);

       int measuredWidth = resolveSize(size, widthMeasureSpec);
       int measuredHeight = resolveSize(size, heightMeasureSpec);

       setMeasuredDimension(measuredWidth, measuredHeight);
   }
複製代碼
c.自定義Layout: 重寫onMeasure()onLayout(): TagLayout

時間關係,明天總結~

四. 參考資料

相關文章
相關標籤/搜索