[Android 自定義 View] —— 深刻總結 onMeasure、 onLayout

在這裏插入圖片描述
個人 Android 知識體系,歡迎 Star https://github.com/daishengda2018/AndroidKnowledgeSystem

onMeasure、onLayout 能夠說是自定 View 的核心,可是不少開發者都沒能理解其含義與做用,也不理解 onMeasure 、 xml 指定大小這兩者的關係與差別,也不能區分 getMeasureWidth 與 getWidth 的本質區別又是什麼。本文將經過理論加實踐的方法帶領你們深刻理解 onMeasure 、onLayout 的定義、流程、具體使用方法與須要注意的細節。php

自定義View —— onMeasure、 onLayout

佈局過程的做用

  • 肯定每一個View的尺寸和位置
  • 做用:爲繪製和觸摸範圍作支持
    • 繪製:知道往哪裏了畫
    • 觸摸返回:知道用戶點的是哪裏

佈局的流程

從總體看

  • 測量流程:從根 View 遞歸調用每一級子 View 的 measure 方法,對它們進行測量。
  • 佈局流程:從根 View 遞歸調用每一級子 View 的 layout 方法,把測量過程得出的子 View 的位置和尺寸傳給子 View,子 View 保存。

從個體看

對於每個 View:java

  1. 運行前,開發者會根據本身的需求在 xml 文件中寫下對於 View 大小的指望值android

  2. 在運行的時候,父 View 會在 onMeaure()中,根據開發者在 xml 中寫的對子 View 的要求, 和自身的實際可用空間,得出對於子 View 的具體尺寸要求git

  3. 子 View 在本身的 onMeasure中,根據 xml 中指定的指望值和自身特色(指 View 的定義者在onMeasrue中的聲明)算出本身的**指望*github

    若是是 ViewGroup 還會在 onMeasure 中,調用每一個子 View 的 measure () 進行測量.面試

  4. 父 View 在子 View 計算出指望尺寸後,得出⼦ View 的實際尺⼨和位置canvas

  5. ⼦ View 在本身的 layout() ⽅法中將父 View 傳進來的實際尺寸和位置保存app

    若是是 ViewGroup,還會在 onLayout() ⾥調用每一個字 View 的 layout() 把它們的尺寸 置傳給它們框架

爲啥須要兩個過程呢?

緣由一

measure 的測量過程可能不止一次,好比有三個子 View 在一個 ViewGroup 裏面,ViewGroup 的寬度是 warp_content,A 的寬度是 match_parent, B 和 C 是 warp_content, 此時 ViewGroup 的寬度是不固定的,怎麼肯定 A 的 match_parent 到底有多大呢?此時是如何測量的呢?ide

以 LinearLayout 爲例:第一次測量 LinearLayout 的大小也是沒有肯定的,因此沒法肯定 A 的 match_parent 到底有多大,這時候的 LinearLayout 會對 A 直接測量爲 0 ,而後測量 B、C 的寬度,由於 B、C 的大小是包裹內容的,在測量後就能夠肯定 LinearLayout 的寬度了:即爲最長的 B 的寬度。

\[外鏈圖片轉存失敗(img-io4cWzMo-1566741015606)(assets/image-20190816011514042.png)\]

這時候再對 A 進行第二次測量,直接設置爲與 LinearLayout 相同的寬度,至此達到了 match_parent 的效果。

\[外鏈圖片轉(img-1YxB5phjh66741015609)(assets/image-20190816011559286.png)(assets/image-20190816011559286.png)\]

若是將 measure 和 layout 的過程糅合在一塊兒,會致使兩次測量的時候進行無用的 layout,消耗了更多的資源,因此爲了性能,將其兩者分開。

緣由二

也是兩者的職責相互獨立,分爲兩個過程,可使流程、代碼更加清晰。

拓展

上面例子中的狀況僅僅存在於 LinearLayout中,每種佈局的測量機制是不一樣的。那麼若是 A B C 三個 View 都是 match_parent LinearLayout 是如何作的呢?

  • 第一輪測量:LinearLayout 沒法肯定本身的大小,因此遇到子 View match_parent 都會測量爲 0

\[外鏈圖片轉存失敗(img-yJZK4eWe-1566741015610)(assets/image-20190816013740231.png)\]

  • 第二輪測量:都沒有大小,LinearLayout 會讓全部子 View 自由測量(父 View 不限制寬度)。每一個測量以後都會變爲和最寬的同樣的寬度。

    \[外鏈圖片轉存失敗(img-OAccRlTA-1566741015611)(assets/image-20190816013805676.png)\]

注意:

  • onMeasure 與 measure() 、onDraw 與 draw 的區別

    onXX 方法是調度過程,而 measure、draw 纔是真正作事情的。能夠從源碼中看到 measure 中調用了 onMeasure 方法。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
           // ……………
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                 // ………………
                }
    }
        
    複製代碼
  • 爲何不把對於尺寸的要求直接交個子 View 而是要交給父 View 呢?

    由於有些場景子 View 的大小須要父 View 進行規劃,例如上面的例子中 LinearLayout 的子 View 設置了 weight。

  • layout() 不多被使用到,由於他的改變沒有通知父 View,這可能會致使佈局重疊等問題 。在下面的「綜合演練 —— 簡單改寫已有 View 的尺寸」中會有一個證實。

##onMeasure 方法

要明確的一個問題是: 何時須要咱們本身實現 onMeasure 方法呢?

答:具體開發的時候有如下三種場景:

  • 當咱們繼承一個已有 View 的時候,簡單改寫他們的尺寸,好比自定義一個正方形的 ImageView,取寬高中較大的值爲邊長。
  • 徹底進行自定義尺寸的計算。好比實現一個繪製圓形的 View 咱們須要在尺寸爲 warp_content 時指定一個大小例以下文中的「綜合演練 —— 徹底自定義 View 的尺寸」。
  • 自定義 Layout,這時候內部全部的子 View 的尺寸和位置都須要咱們本身控制,須要重寫 onMeasure()onLayout()方法。例以下文中的「綜合演練 —— 自定義 Layout」

onLayout 方法

onLayout 方法是 ViewGroup 中用於控制子 View 位置的方法。放置子 View 位置的過程很簡單,只需重寫 onLayout 方法,而後獲取子 View 的實例,調用子 View 的 layout 方法實現佈局。在實際開發中,通常要配合 onMeasure 測量方法一塊兒使用。在下文「綜合演練 —— 自定義 Layout」中會詳細演示。

綜合演練

簡單改寫已有 View 的尺寸實現方形 ImageView

  • 首先來證實一下改寫 layout 方法會存在的問題
/** * 自定義正方形 ImageView * * Created by im_dsd on 2019-08-24 */
public class SquareImageView extends android.support.v7.widget.AppCompatImageView {

    public SquareImageView(Context context) {
        super(context);
    }

    public SquareImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SquareImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

   @Override
    public void layout(int l, int t, int r, int b) {
        // 使用寬高的最大值設置邊長
        int width = r - l;
        int height = b - t;
        int size = Math.max(width, height);
        super.layout(l, t, l + size, t + size);
    }
}
複製代碼

代碼很簡單,獲取寬與高的最大值用於設置正方形 View 的邊長。再看一下佈局文件的設置

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" tools:context=".MainActivity">


    <com.example.dsd.demo.ui.custom.measure.SquareImageView android:background="@color/colorAccent" android:layout_width="200dp" android:layout_height="300dp"/>

    <View android:background="@android:color/holo_blue_bright" android:layout_width="200dp" android:layout_height="200dp"/>
</LinearLayout>
複製代碼

經過佈局文件的描述若是是普通的 View 顯示的狀態應該是這樣的

\[外鏈圖片轉存失敗(img-aPRLST3i-1566741015611)(assets/image-20190824182806710.png)\]

而咱們期待的狀態應該是這樣的:SquareImageView 的寬高均爲 300dp。

\[外鏈圖片轉存失敗(img-kRU6VS9V-1566741015612)(assets/image-20190824184207686.png)\]

可是最終的結果倒是下圖,雖然咱們使用了 LinearLayout 可是咱們經過layout() 方法改變了 SquareImageView 的大小,對於這個變化LinearLayout 並不知道,因此會發生布局重疊的問題。可見通常狀況下不要使用 layout()方法

\[外鏈圖片轉存失敗(img-2AwQ6d3I-1566741015612)(assets/image-20190824183744689.png)\]

  • 經過 onMeasure 方法更改尺寸。
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // super.onMeasure 中已經完成了 View 的測量
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 獲取測量的結果比較後得出最大值
        int height = getMeasuredHeight();
        int width = getMeasuredWidth();
        int size = Math.max(width, height);
        // 將結果設置回去
        setMeasuredDimension(size, size);
    }

複製代碼

總結

簡單來講,更改已有 View 的尺寸主要分爲如下步驟

  1. 重寫 onMeasure()
  2. getMeasureWidthgetMeasureHeight()獲取測量尺寸
  3. 計算最終要的尺寸
  4. setMeasuredDimension(width, height)把結果保存

徹底自定義 View 的尺寸

此處用繪製圓形的 CircleView 作一個例子。對於這個 View 的指望是:View 的大小有內部的圓決定。

\[外鏈圖片轉存失敗(img-ANAQF9RH-1566741015613)(assets/image-20190824191402384.png)\]

首先畫一個圓形看看

/** * 自定義 View 簡單測量 * Created by im_dsd on 2019-08-15 */
public class CircleView extends View {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    /** * 爲了方便簡單,固定尺寸 */
    private static final float PADDING = DisplayUtils.dp2px(20);
    private static final float RADIUS = DisplayUtils.dp2px(80);

    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(Color.RED);
        canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, mPaint);
    }
}
複製代碼
<com.example.dsd.demo.ui.custom.layout.CircleView
        android:background="@android:color/background_dark"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
複製代碼

此時將大小設置爲 wrap_content 包裹布局,結果會是怎麼樣的呢?

\[外鏈圖片轉存失敗(img-KFaFriMr-1566741015613)(assets/image-20190824192535840.png)\]
居然填充了屏幕!根本就沒有包裹內容,此時就須要咱們大展身手了

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 沒有必要再讓 view 本身測量一遍了,浪費資源
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        // 計算指望的 size
        int size = (int) ((PADDING + RADIUS) * 2);
        // 獲取父 View 傳遞來的可用大小
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        // 開始計算
        int result = 0;
        switch (widthMode) {
            // 不超過
            case MeasureSpec.AT_MOST:
                // 在 AT_MOST 模式下,取兩者的最小值
                if (widthSize < size) {
                    result = widthSize;
                } else {
                    result = size;
                }
                break;
            // 精準的
            case MeasureSpec.EXACTLY:
                // 父 View 給多少用多少
                result = widthSize;
                break;
            // 無限大,沒有指定大小
            case MeasureSpec.UNSPECIFIED:
                // 使用計算出的大小
                result = size;
                break;
            default:
                result = 0;
                break;
        }
        // 設置大小
        setMeasuredDimension(result, result);
    }
複製代碼

\[外鏈圖片轉存失敗(img-0xlvjzrm-1566741015614)(assets/image-20190824193549345.png)\]

上面的代碼就是 onMeasure(int,int) 的模板代碼了,要注意一點的是須要註釋 super.onMeasure 方法,此處面試的時候廣泛會問。

// 沒有必要再讓 view 本身測量一遍了,浪費資源
 // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
複製代碼

這段模版代碼其實 Android SDK 裏面早就有了很好的封裝 : resolveSize(int size, int measureSpec)resolveSizeAndState(int size, int measureSpec, int childMeasuredState) ,兩行代碼直接搞定。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 沒有必要再讓 view 本身測量一遍了,浪費資源
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        // 計算指望的 size
        int size = (int) ((PADDING + RADIUS) * 2);
        // 指按期望的 size
        int width = resolveSize(size, widthMeasureSpec);
        int height = resolveSize(size, heightMeasureSpec);
        // 設置大小
        setMeasuredDimension(width, height);
    }

複製代碼

使用的時候徹底能夠這樣作,可是很是建議你們都本身手寫幾遍理解其中的含義,由於面試會問到其中的細節。

還有一點很遺憾,就是 resolveSizeAndState(int, int, int) 很差用。很差用的緣由不是方法有問題,而是不少自定義 View 包括原生的 View 都沒有使用 resolveSizeAndState(int, int, int) 方法,或者沒用指定 sate (state 傳遞父 View 對於子 View 的指望,相比resolveSize(int, in) 方法對於子 View 的控制更好)因此就算設置了,也不會起做用。

總結

徹底自定義 View 的尺寸主要分爲如下步驟:

  1. 重寫 onMeasure()
  2. 計算本身指望的尺寸
  3. resolveSize() 或者 resolveSizeAndState()修正結果
  4. setMeasuredDimension(width, height)保存結果

自定義 Layout

源碼地址

以 TagLayout 爲例一步一步實現一個自定義 Layout。具體指望的效果以下圖:

\[外鏈圖片轉存失敗(img-Iz6Sy7Gd-1566741015614)(assets/image-20190824202927270.png)\]

重寫 onLayout()

在繼承 ViewGroup 的時候 onLayout() 是必需要實現的,這意味着子 View 的位置擺放的規則,所有交由開發者定義。

/** * 自定義 Layout Demo * * Created by im_dsd on 2019-08-11 */
public class TagLayout extends ViewGroup {

    public TagLayout(Context context) {
        super(context);
    }

    public TagLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 此時全部的子 View 都和 TagLayout 同樣大
            child.layout(l, t, r, b);
        }
    }
}

複製代碼

實驗一下是否和指望的效果同樣呢

<?xml version="1.0" encoding="utf-8"?>
<com.example.dsd.demo.ui.custom.layout.TagLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dp" android:padding="5dp" android:background="#ffee00" android:textSize="16sp" android:textStyle="bold" android:text="音樂" />

</com.example.dsd.demo.ui.custom.layout.TagLayout>
複製代碼

\[外鏈圖片轉存失敗(img-9grUdnPa-1566741015615)(assets/image-20190824203849801.png)\]

的確和指望一致。若是想要 TextView 顯示爲 TagLayout 的四分之一呢?

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // 子 View 顯示爲 TagLayout1/4 child.layout(l, t, r / 4, b / 4); } } 複製代碼

效果達成!!!很明顯onLayout能夠很是靈活的控制 View 的位置

\[外鏈圖片轉存失敗(img-i9K9gBGp-1566741015616)(assets/image-20190824204034040.png)\]

再嘗試讓兩個 View 呈對角線佈局呢?

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (i == 0 ){ child.layout(0, 0, (r - l) / 2, (b - t) / 2); } else { child.layout((r - l) / 2, (b - t) / 2, (r - l), (b - t)); } } } 複製代碼

\[外鏈圖片轉存失敗(img-L7XEOX1q-1566741015616)(assets/image-20190824204701652.png)\]

onLayout的方法仍是很簡單的,可是在真正佈局中怎麼獲取 View 的位置纔是難點!如何獲取呢,這時候就須要 onMeasure 的幫助了!

計算

在寫具體的代碼以前,先來搭建大致的框架。主要的思路就是在 onMeasure()方法中計算好子 View 的尺寸和位置信息包括 TagLayout 的具體尺寸,而後在onLayout()方法中擺放子 View。

在計算過程當中涉及到三個難點,具體請看註釋

private List<Rect> mChildRectList = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 沒有必要讓 View 本身算了,浪費資源。 
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 難點1: 計算出對於每一個子 View 的尺寸
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            // 難點2:計算出每個子 View 的位置並保存。
            Rect rect = new Rect(?, ?, ?, ?);
            mChildRectList.add(rect);
        }
        // 難點3:根據全部子 View 的尺寸計算出 TagLayout 的尺寸
        int measureWidth = ?;
        int measureHeight = ?;
        setMeasuredDimension(measureWidth, measureHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mChildRectList.size() == 0) {
            return;
        }
        for (int i = 0; i < getChildCount(); i++) {
            if (mChildRectList.size() <= i) {
                return;
            }
            View child = getChildAt(i);
            // 經過保存好的位置,設置子 View
            Rect rect = mChildRectList.get(i);
            child.layout(rect.left, rect.top, rect.right, rect.bottom);
        }
    }
複製代碼
難點1 :如何計算子 View 的尺寸。

主要涉及兩點:開發者對於子 View 的尺寸設置和父 View 的具體可用空間。獲取開發者對於子 View 尺寸的設置就比較簡單了:

// 獲取開發者對於子 View 尺寸的設置
LayoutParams layoutParams = child.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
複製代碼

獲取父 View (TagLayout) 的可用空間要結合兩點:

  1. TagLayout 的父 View 對於他的尺寸限制
  2. TagLayout 的剩餘空間。咱們用 width 爲例用僞代碼簡單分析一下如何計算子 View 的尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// TagLayout 已經使用過的空間,此處的計算是個難點,此處不是本例子重點,一下子討論
int widthUseSize = 0;
for (int i = 0; i < getChildCount(); i++) {
	View child = getChildAt(i);
  // 獲取開發者對於子 View 尺寸的設置
  LayoutParams layoutParams = child.getLayoutParams();
  int childWidthMode;
  int childWidthSize;
  // 獲取父 View 具體的可用空間
  switch (layoutParams.width) {
  // 若是說子 View 被開發者設置爲 match_parent
  	case LayoutParams.MATCH_PARENT:
    	switch (widthMode) {
      	case MeasureSpec.EXACTLY:
        // TagLayout 爲 EXACTLY 模式下,子 View 能夠填充的部位就是 TagLayout 的可用空間
        case MeasureSpec.AT_MOST:
        // TagLayout 爲 AT_MOST 模式下有一個最大可用空間,子 View 要是想 match_parent 實際上是和 
        // EXACTLY 模式同樣的
        childWidthMode = MeasureSpec.EXACTLY;
        childWidthSize = widthSize - widthUseSize;
        break;
        case MeasureSpec.UNSPECIFIED:
        // 當 TagLayout 爲 UNSPECIFIED 不限制尺寸的時候,意味着可用空間無限大!空間無限大還想
        // match_parent 兩者徹底是悖論,因此咱們也要將子 View 的 mode 指定爲 UNSPECIFIED
        childWidthMode = MeasureSpec.UNSPECIFIED;
        // 此時 size 已經沒有做用了,寫 0 就能夠了
        childWidthSize = 0;
        break;
        }
      case LayoutParams.WRAP_CONTENT:
       break;
      default:
      // 具體設置的尺寸
      break;
}
// 獲取 measureSpec
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode);
複製代碼

補充一下何時會是 UNSPECIFIED 模式呢?好比說橫向或縱向滑動的 ScrollView,他的寬度或者高度的模式就是 UNSPECIFIED

僞代碼僅僅模擬了開發者將子 View 的 size 設置爲 match_parent 的狀況,其餘的狀況讀者要是感興趣能夠本身分析一下。筆者就不作過多的分析了!由於 Android SDK 早就爲咱們提供好了可用的 API: measureChildWithMargins(int, int, int, int)一句話就完成了對於子 View 的測量。

難點2:計算出每個子 View 的位置並保存。
難點3:根據全部子 View 的尺寸計算出 TagLayout 的尺寸

有了 measureChildWithMargins 方法,對於子 View 的測量就很簡單啦。 一口氣解決難點 2 3。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int lineHeightUsed = 0;
        int lineWidthUsed = 0;
        int widthUsed = 0;
        int heightUsed = 0;
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 測量子 View 尺寸。TagLayout 的子 view 是能夠換行的,因此設置 widthUsed 參數爲 0
            // 讓子 View 的尺寸不會受到擠壓。
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            if (widthMode != MeasureSpec.UNSPECIFIED && lineWidthUsed + child.getMeasuredWidth() > widthSize) {
                // 須要換行了
                lineWidthUsed = 0;
                heightUsed += lineHeightUsed;
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            }
            Rect childBound;
            if (mChildRectList.size() >= i) {
                // 不存在則建立
                childBound = new Rect();
                mChildRectList.add(childBound);
            } else {
                childBound = mChildRectList.get(i);
            }
            // 存儲 child 位置信息
            childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(),
                            heightUsed + child.getMeasuredHeight());
            // 更新位置信息
            lineWidthUsed += child.getMeasuredWidth();
            // 獲取一行中最大的尺寸
            lineHeightUsed = Math.max(lineHeightUsed, child.getMeasuredHeight());
            widthUsed = Math.max(lineWidthUsed, widthUsed);
        }

        // 使用的寬度和高度就是 TagLayout 的寬高啦
        heightUsed += lineHeightUsed;
        setMeasuredDimension(widthUsed, heightUsed);
    }
複製代碼

終於寫完代碼啦,運行起來瞧瞧看。

\[外鏈圖片轉存失敗(img-4b1o6kXK-1566741015617)(assets/image-20190825204518467.png)\]

居然奔潰了!經過日誌能夠定位到是

// 對於子 View 的測量
  measureChildWithMargins(child, widthMeasureSpec, widthUsed, 
                                                          heightMeasureSpec, heightUsed);
複製代碼

這一句出了問題,經過源碼得知measureChildWithMargins方法會有一個類型轉換致使了崩潰

protected void measureChildWithMargins(int, int ,int, int) {
	final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
	………………
}
複製代碼

解決辦法就是在 TagLayout 中重寫方法 generateLayoutParams(AttributeSet) 返回一個 MarginLayoutParams 就能夠解決問題了。

@Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
複製代碼

再次運行達到最終目標!

\[外鏈圖片轉存失敗(img-Z60DY2Gw-1566741015617)(assets/image-20190825214300636.png)\]

總結

自定義 Layout 的主要步驟分爲如下幾點:

  1. 重寫 onMeasure()
    • 遍歷每個子 View,用 measureChildWidthMargins() 測量 View
      • MarginLayoutParams 和 generateLayoutParams()
      • 有些子 View 可能須要屢次測量
      • 測量完成後,得出子 View 的實際尺寸和位置,並暫時保存
    • 測量出全部子 View 的位置和尺寸後,計算出本身的尺寸,並用setMeasuredDimension(width, height)保存
  2. 重寫 onLayout()
    • 遍歷每一個子 View,調用它們的 layout() 方法來將位置和尺寸傳遞給它們。

getMeasureWidth 與 getWidth 的區別

getMeasureXX 表明的是 onMeasure 方法結束後(準確的說應該是測量結束後)測量的值,而 getXX 表明的是 layout 階段 right - left、bottom - top 的真實顯示值,因此第一個不一樣點就是賦值的階段不一樣,可見 getXXX 在 layout() 以前一直爲 0, 而 getMeasureXX 可能不是最終值( onMeasure 可能會被調用屢次),可是最終的時候兩者的數值都會是相同的。使用那個還須要看具體的場景。

總結: getMeasureXX 獲取的是臨時的值,而 getXX 獲取的時候最終定稿的值,通常在繪製階段、觸摸反饋階段使用 getXXX,在 onMeasure 階段被迫使用 getMeasureXX 。

本文全部源碼地址

個人 Android 知識體系,歡迎 Star https://github.com/daishengda2018/AndroidKnowledgeSystem

相關文章
相關標籤/搜索