Android 自定義 View 最少必要知識

極速24號發表於2019-09-02

1. 什麼是自定義 View?

1.1 定義

在 Android 系統中,介面中所有能看到的元素都是 View。預設情況下,Android 系統為開發者提供了很多 View,比如用於展示文字資訊的 TextView,用於展示圖片的 ImageView 等等。但有時,這並不能滿足開發者的需求,例如,開發者想要用一個餅狀圖來展示一組資料,這時如果用系統提供的 View 就不能實現了,只能通過自定義 View 來實現。那到底什麼是自定義 View 呢?

自定義 View 就是通過繼承 View 或者 View 的子類,並在新的類裡面實現相應的處理邏輯(重寫相應的方法),以達到自己想要的效果。

1.2 繼承結構

Android 中的所有 UI 元素都是 View 的子類:

Android 自定義 View 最少必要知識

PS:由於涉及的類太多,如果將所有涉及到的類全部加到類圖裡面,類圖將十分大,所以此處只列出了 View 的直接子類。

1.3 檢視體系用到的設計模式

Android View 體系如下:

Android 自定義 View 最少必要知識

仔細觀察,你會發現,Android View 的體系結構和設計模式中的組合模式的結構如出一轍:

Android 自定義 View 最少必要知識

Android View 體系結構中的 ViewGroup 對應於組合模式中抽象構件(Component 和 Composite),Android View 體系結構中的 View 對應於組合模式中的葉子構件(Leaf):

Android View 構件 Composite Pattern 構件
ViewGroup Component、Composite
View Leaf

2. 為什麼要自定義 View?

大多數情況下,開發者常常會因為下面四個原因去自定義 View:

  1. 讓介面有特定的顯示風格、效果;
  2. 讓控制元件具有特殊的互動方式;
  3. 優化佈局;
  4. 封裝;

2.1 讓介面有特定的顯示風格、效果

預設情況下,Android 系統為開發者提供了很多控制元件,但有時,這並不能滿足開發者的需求。例如,開發者想要用一個餅狀圖來展示一組資料,這時如果用系統提供的 View 就不能實現了,只能通過自定義 View 來實現。

If none of the prebuilt widgets or layouts meets your needs, you can create your own View subclass.

2.2 讓控制元件具有特殊的互動方式

預設情況下,Android 系統為開發者提供的控制元件都有屬於它們自己的特定的互動方式,但有時,控制元件的預設互動方式並不能滿足開發者的需求。例如,開發者想要縮放 ImageView 中的圖片內容,這時如果用系統提供的 ImageView 就不能實現了,只能通過自定義 ImageView 來實現。

2.3 優化佈局

有時,有些佈局如果用系統提供的控制元件實現起來相當複雜,需要各種巢狀,雖然最終也能實現了想要的效果,但效能極差,此時就可以通過自定義 View 來減少巢狀層級、優化佈局。

2.4 封裝

有些控制元件可能在多個地方使用,如大多數 App 裡面的底部 Tab,像這樣的經常被用到的控制元件就可以通過自定義 View 將它們封裝起來,以便在多個地方使用。

3. 如何自定義 View?

在說「如何自定義 View?」之前,我們需要知道「自定義 View 都包括哪些內容」?

自定義 View 包括三部分內容:

  1. 佈局(Layout)
  2. 繪製(Drawing)
  3. 觸控反饋(Event Handling)

佈局階段:確定 View 的位置和尺寸。
繪製階段:繪製 View 的內容。
觸控反饋:確定使用者點選了哪裡。

其中佈局階段包括測量(measure)和佈局(layout)兩個過程,另外,佈局階段是為繪製和觸控反饋階段做支援的,它並沒有什麼直接作用。正是因為在佈局階段確定了 View 的尺寸和位置,繪製階段才知道往哪裡繪製,觸控反饋階段才知道使用者點的是哪裡。

另外,由於觸控反饋是一個大的話題,限於篇幅,就不在這裡講解了,後面有機會的話,我會再補上一篇關於觸控反饋的文章。

在自定義 View 和自定義 ViewGroup 中,佈局和繪製流程雖然整體上都是一樣的,但在細節方面,自定義 View 和自定義 ViewGroup 還是不一樣的,所以,接下來分兩類進行討論:

  • 自定義 View 佈局、繪製流程
  • 自定義 ViewGroup 佈局、繪製流程

3.1 自定義 View 佈局、繪製流程

「自定義 View 佈局、繪製」主要包括三個階段:

  1. 測量階段(measure)
  2. 佈局階段(layout)
  3. 繪製階段(draw)

3.1.1 自定義 View 測量階段

在 View 的測量階段會執行兩個方法(在測量階段,View 的父 View 會通過呼叫 View 的 measure() 方法將父 View 對 View 尺寸要求傳進來。緊接著 View 的 measure() 方法會做一些前置和優化工作,然後呼叫 View 的 onMeasure() 方法,並通過 onMeasure() 方法將父 View 對 View 的尺寸要求傳入。在自定義 View 中,只有需要修改 View 的尺寸的時候才需要重寫 onMeasure() 方法。在 onMeasure() 方法中根據業務需求進行相應的邏輯處理,並在最後通過呼叫 setMeasuredDimension() 方法告知父 View 自己的期望尺寸):

  • measure()
  • onMeasure()

measure() : 排程方法,主要做一些前置和優化工作,並最終會呼叫 onMeasure() 方法執行實際的測量工作;

onMeasure() : 實際執行測量任務的方法,主要用與測量 View 尺寸和位置。在自定義 View 的 onMeasure() 方法中,View 根據自己的特性和父 View 對自己的尺寸要求算出自己的期望尺寸,並通過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸。

onMeasure() 計算 View 期望尺寸方法如下:

  1. 參考父 View 的對 View 的尺寸要求和實際業務需求計算出 View 的期望尺寸:

    • 解析 widthMeasureSpec;
    • 解析 heightMeasureSpec;
    • 將「根據實際業務需求計算出 View 的尺寸」根據「父 View 的對 View 的尺寸要求」進行相應的修正得出 View 的期望尺寸(通過呼叫 resolveSize() 方法);
  2. 通過 setMeasuredDimension() 儲存 View 的期望尺寸(實際上是通過 setMeasuredDimension() 告知父 View 自己的期望尺寸);

注意:
多數情況下,這裡的期望尺寸就是 View 的最終尺寸。不過最終 View 的期望尺寸和實際尺寸是不是一樣還要看它的父 View 會不會同意。View 的父 View 最終會通過呼叫 View 的 layout() 方法告知 View 的實際尺寸,並且在 layout() 方法中 View 需要將這個實際尺寸儲存下來,以便繪製階段和觸控反饋階段使用,這也是 View 需要在 layout() 方法中儲存自己實際尺寸的原因——因為繪製階段和觸控反饋階段要使用啊!

3.1.2 自定義 View 佈局階段

在 View 的佈局階段會執行兩個方法(在佈局階段,View 的父 View 會通過呼叫 View 的 layout() 方法將 View 的實際尺寸(父 View 根據 View 的期望尺寸確定的 View 的實際尺寸)傳給 View,View 需要在 layout() 方法中將自己的實際尺寸儲存(通過呼叫 View 的 setFrame() 方法儲存,在 setFrame() 方法中,又會通過呼叫 onSizeChanged() 方法告知開發者 View 的尺寸修改了)以便在繪製和觸控反饋階段使用。儲存 View 的實際尺寸之後,View 的 layout() 方法又會呼叫 View 的 onLayout() 方法,不過 View 的 onLayout() 方法是一個空實現,因為它沒有子 View):

  • layout()
  • onLayout()

layout() : 儲存 View 的實際尺寸。呼叫 setFrame() 方法儲存 View 的實際尺寸,呼叫 onSizeChanged() 通知開發者 View 的尺寸更改了,並最終會呼叫 onLayout() 方法讓子 View 佈局(如果有子 View 的話。因為自定義 View 中沒有子 View,所以自定義 View 的 onLayout() 方法是一個空實現);

onLayout() : 空實現,什麼也不做,因為它沒有子 View。如果是 ViewGroup 的話,在 onLayout() 方法中需要呼叫子 View 的 layout() 方法,將子 View 的實際尺寸傳給它們,讓子 View 儲存自己的實際尺寸。因此,在自定義 View 中,不需重寫此方法,在自定義 ViewGroup 中,需重寫此方法。

注意:
layout() & onLayout() 並不是「排程」與「實際做事」的關係,layout() 和 onLayout() 均做事,只不過職責不同。

3.1.3 自定義 View 繪製階段

在 View 的繪製階段會執行一個方法——draw(),draw() 是繪製階段的總排程方法,在其中會呼叫繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground():

  • draw()

draw() : 繪製階段的總排程方法,在其中會呼叫繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground();

drawBackground() : 繪製背景的方法,不能重寫,只能通過 xml 佈局檔案或者 setBackground() 來設定或修改背景;

onDraw() : 繪製 View 主體內容的方法,通常情況下,在自定義 View 的時候,只用實現該方法即可;

dispatchDraw() : 繪製子 View 的方法。同 onLayout() 方法一樣,在自定義 View 中它是空實現,什麼也不做。但在自定義 ViewGroup 中,它會呼叫 ViewGroup.drawChild() 方法,在 ViewGroup.drawChild() 方法中又會呼叫每一個子 View 的 View.draw() 讓子 View 進行自我繪製;

onDrawForeground() : 繪製 View 前景的方法,也就是說,想要在主體內容之上繪製東西的時候就可以在該方法中實現。

注意:
Android 裡面的繪製都是按順序的,先繪製的內容會被後繪製的蓋住。如,你在重疊的位置「先畫圓再畫方」和「先畫方再畫圓」所呈現出來的結果是不同的,具體表現為下表:

Android 自定義 View 最少必要知識
Android 自定義 View 最少必要知識

3.1.4 自定義 View 佈局、繪製流程時序圖

Android 自定義 View 最少必要知識

3.2 自定義 ViewGroup 佈局、繪製流程

「自定義 ViewGroup 佈局、繪製」主要包括三個階段:

  1. 測量階段(measure)
  2. 佈局階段(layout)
  3. 繪製階段(draw)

3.2.1 自定義 ViewGroup 測量階段

同自定義 View 一樣,在自定義 ViewGroup 的測量階段會執行兩個方法:

  • measure()
  • onMeasure()

measure() : 排程方法,主要做一些前置和優化工作,並最終會呼叫 onMeasure() 方法執行實際的測量工作;

onMeasure() : 實際執行測量任務的方法,與自定義 View 不同,在自定義 ViewGroup 的 onMeasure() 方法中,ViewGroup 會遞迴呼叫子 View 的 measure() 方法,並通過 measure() 將 ViewGroup 對子 View 的尺寸要求(ViewGroup 會根據開發者對子 View 的尺寸要求、自己的父 View(ViewGroup 的父 View) 對自己的尺寸要求和自己的可用空間計算出自己對子 View 的尺寸要求)傳入,對子 View 進行測量,並把測量結果臨時儲存,以便在佈局階段使用。測量出子 View 的實際尺寸之後,ViewGroup 會根據子 View 的實際尺寸計算出自己的期望尺寸,並通過 setMeasuredDimension() 方法告知父 View(ViewGroup 的父 View) 自己的期望尺寸。

具體流程如下:

  1. 執行前,開發者在 xml 中寫入對 ViewGroup 和 ViewGroup 子 View 的尺寸要求 layout_xxx;
  2. ViewGroup 在自己的 onMeasure() 方法中,根據開發者在 xml 中寫的對 ViewGroup 子 View 的尺寸要求、自己的父 View(ViewGroup 的父 View) 對自己的尺寸要求和自己的可用空間計算出自己對子 View 的尺寸要求,並呼叫每個子 View 的 measure() 將 ViewGroup 對子 View 的尺寸要求傳入,測量子 View 尺寸;
  3. ViewGroup 在子 View 計算出期望尺寸之後(在 ViewGroup 的 onMeasure() 方法中,ViewGroup 遞迴呼叫每個子 View 的 measure() 方法,子 View 在自己的 onMeasure() 方法中會通過呼叫 setMeasuredDimension() 方法告知父 View(ViewGroup) 自己的期望尺寸),得出子 View 的實際尺寸和位置,並暫時儲存計算結果,以便佈局階段使用;
  4. ViewGroup 根據子 View 的尺寸和位置計算自己的期望尺寸,並通過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸。如果想要做的更好,可以在「 ViewGroup 根據子 View 的尺寸和位置計算出自己的期望尺寸」之後,再結合 ViewGroup 的父 View 對 ViewGroup 的尺寸要求進行修正(通過 resolveSize() 方法),這樣得出的 ViewGroup 的期望尺寸更符合 ViewGroup 的父 View 對 ViewGroup 的尺寸要求。

3.2.2 自定義 ViewGroup 佈局階段

同自定義 View 一樣,在自定義 ViewGroup 的佈局階段會執行兩個方法:

  • layout()
  • onLayout()

layout() : 儲存 ViewGroup 的實際尺寸。呼叫 setFrame() 方法儲存 ViewGroup 的實際尺寸,呼叫 onSizeChanged() 通知開發者 ViewGroup 的尺寸更改了,並最終會呼叫 onLayout() 方法讓子 View 佈局;

onLayout() : ViewGroup 會遞迴呼叫每個子 View 的 layout() 方法,把測量階段計算出的子 View 的實際尺寸和位置傳給子 View,讓子 View 儲存自己的實際尺寸和位置。

3.2.3 自定義 ViewGroup 繪製階段

同自定義 View 一樣,在自定義 ViewGroup 的繪製階段會執行一個方法——draw()。draw() 是繪製階段的總排程方法,在其中會呼叫繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground():

  • draw()

draw() : 繪製階段的總排程方法,在其中會呼叫繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground();

在 ViewGroup 中,你也可以重寫繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground()。但大多數情況下,自定義 ViewGroup 是不需要重寫任何繪製方法的。因為通常情況下,ViewGroup 的角色是容器,一個透明的容器,它只是用來盛放子 View 的。

3.2.4 自定義 ViewGroup 佈局、繪製流程時序圖

Android 自定義 View 最少必要知識

3.3 自定義 View 步驟

  1. 自定義屬性的宣告與獲取;
  2. 重寫測量階段相關方法(onMeasure());
  3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫));
  4. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景);
  5. onTouchEvent();
  6. onInterceptTouchEvent()(僅 ViewGroup 有此方法);

4. 實戰演練

4.1 自定義 View

4.1.1 自定義 View ——自定義 View 的繪製內容

自定義 View,它的內容是「三個半徑不同、顏色不同的同心圓」,效果圖如下:

Android 自定義 View 最少必要知識
  1. 自定義屬性的宣告與獲取
//1.1 在 xml 中自定義 View 屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--CircleView-->
    <declare-styleable name="CircleView">
        <attr name="circle_radius" format="dimension" />
        <attr name="outer_circle_color" format="reference|color" />
        <attr name="middle_circle_color" format="reference|color" />
        <attr name="inner_circle_color" format="reference|color" />
    </declare-styleable>
</resources>

//1.2 在 View 建構函式中獲取自定義 View 屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
複製程式碼
  1. 重寫測量階段相關方法(onMeasure())

由於不需要自定義 View 的尺寸,所以,不用重寫該方法。

  1. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫))

由於沒有子 View 需要佈局,所以,不用重寫該方法。

  1. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景)
//4. 重寫 onDraw() 方法,自定義 View 內容
@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(mOuterCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
    mPaint.setColor(mMiddleCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
    mPaint.setColor(mInnerCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
複製程式碼
  1. onTouchEvent()

由於 View 不需要和使用者互動,所以,不用重寫該方法。

  1. onInterceptTouchEvent()(僅 ViewGroup 有此方法)

ViewGroup 的方法。

完整程式碼如下:

//1. 自定義屬性的宣告  
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--CircleView-->
    <declare-styleable name="CircleView">
        <attr name="circle_radius" format="dimension" />
        <attr name="outer_circle_color" format="reference|color" />
        <attr name="middle_circle_color" format="reference|color" />
        <attr name="inner_circle_color" format="reference|color" />
    </declare-styleable>
</resources>

//2. CircleView  
public class CircleView extends View {

    private float mRadius;
    private int mOuterCircleColor, mMiddleCircleColor, mInnerCircleColor;
    private Paint mPaint;

    public CircleView(Context context) {
        this(context, null);
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void initData(Context context, AttributeSet attrs) {
        //1. 自定義屬性的宣告與獲取
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
        mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
        mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
        mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
        typedArray.recycle();

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mOuterCircleColor);
    }

    //2. 重寫測量階段相關方法(onMeasure());
    //由於不需要自定義 View 的尺寸,所以不用重寫該方法
//    @Override
//    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//    }

    //3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫));
    //由於沒有子 View 需要佈局,所以不用重寫該方法
//    @Override
//    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//        super.onLayout(changed, left, top, right, bottom);
//    }

    //4. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景);
    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(mOuterCircleColor);
        canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
        mPaint.setColor(mMiddleCircleColor);
        canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
        mPaint.setColor(mInnerCircleColor);
        canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
    }

}

//3. 在 xml 中應用 CircleView  
<?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:gravity="center"
    tools:context=".custom_view_only_draw.CustomViewOnlyDrawActivity">

    <com.smart.a03_view_custom_view_example.custom_view_only_draw.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:circle_radius="@dimen/padding_ninety_six"
        app:inner_circle_color="@color/yellow_500"
        app:middle_circle_color="@color/cyan_500"
        app:outer_circle_color="@color/green_500" />

</LinearLayout>
複製程式碼

最終效果如下:

Android 自定義 View 最少必要知識

此時,即使你在 xml 中將 CircleView 的寬、高宣告為「match_parent」,你會發現最終的顯示效果都是一樣的。

主要原因是:預設情況下,View 的 onMeasure() 方法在通過 setMeasuredDimension() 告知父 View 自己的期望尺寸時,會呼叫 getDefaultSize() 方法。在 getDefaultSize() 方法中,又會呼叫 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 獲取建議的最小寬度和最小高度,並根據最小尺寸和父 View 對自己的尺寸要求進行修正。最主要的是,在 getDefaultSize() 方法中修正的時候,會將 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 一視同仁,直接返回父 View 對 View 的尺寸要求:

//1. 預設 onMeasure 的處理
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

//2. getSuggestedMinimumWidth()
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

//3. getSuggestedMinimumHeight()
protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

//4. getDefaultSize()
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:
        //MeasureSpec.AT_MOST、MeasureSpec.EXACTLY 一視同仁
        result = specSize;
        break;
    }
    return result;
}
複製程式碼

正是因為在 getDefaultSize() 方法中處理的時候,將 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 一視同仁,所以才有了上面「在 xml 中應用 CircleView 的時候,無論將 CircleView 的尺寸設定為 match_parent 還是 wrap_content 效果都一樣」的現象。

具體分析如下:

開發者對 View 的尺寸要求 View 的父 View 對 View 的尺寸要求 View 的期望尺寸
android:layout_width="wrap_content"
android:layout_height="wrap_content"
MeasureSpec.AT_MOST
specSize
specSize
android:layout_width="match_parent"
android:layout_height="match_parent"
MeasureSpec.EXACTLY
specSize
specSize

注:
上表中,「View 的父 View 對 View 的尺寸要求」是 View 的父 View 根據「開發者對子 View 的尺寸要求」、「自己的父 View(View 的父 View 的父 View) 對自己的尺寸要求」和「自己的可用空間」計算出自己對子 View 的尺寸要求。

另外,由執行結果可知,上表中的 specSize 實際上等於 View 的尺寸:

2019-08-13 17:28:26.855 16024-16024/com.smart.a03_view_custom_view_example E/TAG: Width(getWidth()):  1080  Height(getHeight()):  1584
複製程式碼

4.1.2 自定義 View ——自定義 View 的尺寸和繪製內容

自定義 View,它的內容是「三個半徑不同、顏色不同的同心圓」,效果圖如下:

Android 自定義 View 最少必要知識
  1. 自定義屬性的宣告與獲取
//1.1 在 xml 中自定義 View 屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--CircleView-->
    <declare-styleable name="CircleView">
        <attr name="circle_radius" format="dimension" />
        <attr name="outer_circle_color" format="reference|color" />
        <attr name="middle_circle_color" format="reference|color" />
        <attr name="inner_circle_color" format="reference|color" />
    </declare-styleable>
</resources>

//1.2 在 View 建構函式中獲取自定義 View 屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
複製程式碼
  1. 重寫測量階段相關方法(onMeasure())
//2. onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //2.1 根據 View 特點或業務需求計算出 View 的尺寸
    mWidth = (int)(mRadius * 2);
    mHeight = (int)(mRadius * 2);

    //2.2 通過 resolveSize() 方法修正結果
    mWidth = resolveSize(mWidth, widthMeasureSpec);
    mHeight = resolveSize(mHeight, heightMeasureSpec);

    //2.3 通過 setMeasuredDimension() 儲存 View 的期望尺寸(通過 setMeasuredDimension() 告知父 View 的期望尺寸)
    setMeasuredDimension(mWidth, mHeight);
}
複製程式碼
  1. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫))

由於沒有子 View 需要佈局,所以,不用重寫該方法。

  1. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景)
//4. 重寫 onDraw() 方法,自定義 View 內容
@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(mOuterCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
    mPaint.setColor(mMiddleCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
    mPaint.setColor(mInnerCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
複製程式碼
  1. onTouchEvent()

由於 View 不需要和使用者互動,所以,不用重寫該方法。

  1. onInterceptTouchEvent()(僅 ViewGroup 有此方法)

ViewGroup 的方法。

完整程式碼如下:

//1. 自定義屬性的宣告  
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--CircleView-->
    <declare-styleable name="CircleView">
        <attr name="circle_radius" format="dimension" />
        <attr name="outer_circle_color" format="reference|color" />
        <attr name="middle_circle_color" format="reference|color" />
        <attr name="inner_circle_color" format="reference|color" />
    </declare-styleable>
</resources>

//2. MeasuredCircleView
public class MeasuredCircleView extends View {

    private int mWidth, mHeight;
    private float mRadius;
    private int mOuterCircleColor, mMiddleCircleColor, mInnerCircleColor;
    private Paint mPaint;

    public MeasuredCircleView(Context context) {
        this(context, null);
    }

    public MeasuredCircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void initData(Context context, AttributeSet attrs) {
        //1. 自定義屬性的宣告與獲取
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
        mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
        mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
        mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
        typedArray.recycle();

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mOuterCircleColor);
    }

    //2. 重寫測量階段相關方法(onMeasure());
    //由於不需要自定義 View 的尺寸,所以不用重寫該方法
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //2.1 根據 View 特點或業務需求計算出 View 的尺寸
        mWidth = (int)(mRadius * 2);
        mHeight = (int)(mRadius * 2);

        //2.2 通過 resolveSize() 方法修正結果
        mWidth = resolveSize(mWidth, widthMeasureSpec);
        mHeight = resolveSize(mHeight, heightMeasureSpec);

        //2.3 通過 setMeasuredDimension() 儲存 View 的期望尺寸(通過 setMeasuredDimension() 告知父 View 的期望尺寸)
        setMeasuredDimension(mWidth, mHeight);
    }

    //3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫));
    //由於沒有子 View 需要佈局,所以不用重寫該方法
//    @Override
//    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//        super.onLayout(changed, left, top, right, bottom);
//    }

    //4. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景);
    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(mOuterCircleColor);
        canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
        mPaint.setColor(mMiddleCircleColor);
        canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
        mPaint.setColor(mInnerCircleColor);
        canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
    }

}

//3. 在 xml 中應用 MeasuredCircleView  
<?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:gravity="center"
    android:orientation="vertical"
    tools:context=".custom_view_measure_draw.CustomViewMeasureDrawActivity">

    <com.smart.a03_view_custom_view_example.custom_view_measure_draw.MeasuredCircleView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:circle_radius="@dimen/padding_ninety_six"
        app:inner_circle_color="@color/yellow_500"
        app:middle_circle_color="@color/cyan_500"
        app:outer_circle_color="@color/green_500" />
</LinearLayout>
複製程式碼

最終效果如下:

Android 自定義 View 最少必要知識

當在 xml 中將 MeasuredCircleView 的寬、高宣告為「match_parent」時,顯示效果跟 CircleView 顯示效果一樣。

開發者對 View 的尺寸要求 View 的父 View 對 View 的尺寸要求 View 的期望尺寸
android:layout_width="match_parent"
android:layout_height="match_parent"
MeasureSpec.EXACTLY
specSize
specSize

但是,當在 xml 中將 MeasuredCircleView 的寬、高宣告為「wrap_content」時,顯示效果是下面這個樣子:

Android 自定義 View 最少必要知識

其實,也很好理解:

開發者對 View 的尺寸要求 View 的父 View 對 View 的尺寸要求 View 的期望尺寸
android:layout_width="wrap_content"
android:layout_height="wrap_content"
MeasureSpec.AT_MOST
specSize
if(childSize < specSize) childSize
if(childSize > specSize) specSize

4.2 自定義 ViewGroup

自定義 ViewGroup,標籤佈局,效果圖如下:

Android 自定義 View 最少必要知識

無論是自定義 View 還是自定義 ViewGroup,大致的流程都是一樣的:

  1. 自定義屬性的宣告與獲取;
  2. 重寫測量階段相關方法(onMeasure());
  3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫));
  4. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景);
  5. onTouchEvent();
  6. onInterceptTouchEvent()(僅 ViewGroup 有此方法);

只不過,大多數情況下,ViewGroup 不需要「自定義屬性」和「重寫繪製階段相關方法」,但有些時候還是需要的,如,開發者想在 ViewGroup 的所有子 View 上方繪製一些內容,就可以通過重寫 ViewGroup 的 onDrawForeground() 來實現。

  1. 自定義屬性的宣告與獲取

在自定義 ViewGroup 中「自定義屬性的宣告與獲取」的方法與在自定義 View 中「自定義屬性的宣告與獲取」的方法一樣,且因為大多數情況下,在自定義 ViewGroup 中是不需要自定義屬性的,所以,在這裡就不自定義屬性了。

  1. 重寫測量階段相關方法(onMeasure())
//2. 重寫測量階段相關方法(onMeasure());
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //2.1 解析 ViewGroup 的父 View 對 ViewGroup 的尺寸要求
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(widthMeasureSpec);

    //2.2 ViewGroup 根據「開發者在 xml 中寫的對 ViewGroup 子 View 的尺寸要求」、「自己的父 View(ViewGroup 的父 View)對自己的尺寸要求」和
    //「自己的可用空間」計算出自己對子 View 的尺寸要求,並將該尺寸要求通過子 View 的 measure() 方法傳給子 View,讓子 View 測量自己(View)的期望尺寸
    int widthUsed = 0;
    int heightUsed = getPaddingTop();
    int lineHeight = 0;
    int lineWidthUsed = getPaddingLeft();
    int maxRight = widthSize - getPaddingRight();

    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
        //是否需要換行
        if(widthMode != MeasureSpec.UNSPECIFIED && (lineWidthUsed + child.getMeasuredWidth() > maxRight)){
            lineWidthUsed = getPaddingLeft();
            heightUsed += lineHeight + mRowSpace;
            lineHeight = 0;
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
        }

        //2.3 ViewGroup 暫時儲存子 View 的尺寸,以便佈局階段和繪製階段使用
        Rect childBound;
        if(mChildrenBounds.size() <= i){
            childBound = new Rect();
            mChildrenBounds.add(childBound);
        }else{
            childBound = mChildrenBounds.get(i);
        }
        //此處不能用 child.getxxx() 獲取子 View 的尺寸值,因為子 View 只是量了尺寸,還沒有佈局,這些值都是 0
//            childBound.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
        childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());

        lineWidthUsed += child.getMeasuredWidth() + mItemSpace;
        widthUsed = Math.max(lineWidthUsed, widthUsed);
        lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
    }

    //2.4 ViewGroup 將「根據子 View 的實際尺寸計算出的自己(ViewGroup)的尺寸」結合「自己父 View 對自己的尺寸要求」進行修正,並通
    //過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸
    int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
    int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
    setMeasuredDimension(measuredWidth, measuredHeight);
}

//重寫generateLayoutParams()
//2.2.1 在自定義 ViewGroup 中呼叫 measureChildWithMargins() 方法計算 ViewGroup 對子 View 的尺寸要求時,
//必須在 ViewGroup 中重寫 generateLayoutParams() 方法,因為 measureChildWithMargins() 方法中用到了 MarginLayoutParams,
//如果不重寫 generateLayoutParams() 方法,那呼叫 measureChildWithMargins() 方法時,MarginLayoutParams 就為 null,
//所以在自定義 ViewGroup 中呼叫 measureChildWithMargins() 方法時,必須重寫 generateLayoutParams() 方法。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
複製程式碼
  1. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫))
//3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫));
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (int i = 0; i < getChildCount(); i++) {
        //應用測量階段計算出的子 View 的尺寸值佈局子 View
        View child = getChildAt(i);
        Rect childBound = mChildrenBounds.get(i);
        child.layout(childBound.left, childBound.top, childBound.right, childBound.bottom);
    }
}
複製程式碼
  1. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景)

預設情況下,自定義 ViewGroup 時是不需要重寫任何繪製階段的方法的,因為 ViewGroup 的角色是容器,一個透明的容器,它只是用來盛放子 View 的。

注意:

  • 預設情況下,系統會自動呼叫 View Group 的 dispatchDraw() 方法,所以不需要重寫該方法;
  • 出於效率的考慮,ViewGroup 預設會繞過 draw() 方法,換而直接執行 dispatchDraw(),以此來簡化繪製流程。所以如果你自定義了一個 ViewGroup ,並且需要在它的除 dispatchDraw() 方法以外的任何一個繪製方法內繪製內容,你可能會需要呼叫 View.setWillNotDraw(false) 方法來切換到完整的繪製流程(是「可能」而不是「必須」的原因是,有些 ViewGroup 是已經呼叫過 setWillNotDraw(false) 了的,例如 ScrollView)。除了可以通過呼叫 View.setWillNotDraw(false) 方法來切換到完整的繪製流程之外,你還可以通過給 ViewGroup 設定背景來切換到完整的繪製流程。
  1. onTouchEvent()

由於 ViewGroup 不需要和使用者互動,所以,不用重寫該方法。

  1. onInterceptTouchEvent()(僅 ViewGroup 有此方法)

由於 ViewGroup 不需要和使用者互動且 ViewGroup 不需要攔截子 View 的 MotionEvent,所以,不用重寫該方法。

完整程式碼如下:

//1. TabLayout
public class TabLayout extends ViewGroup {

    private ArrayList<Rect> mChildrenBounds;
    private int mItemSpace;
    private int mRowSpace;

    public TabLayout(Context context) {
        this(context, null);
    }

    public TabLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void initData(){
        mChildrenBounds = new ArrayList<>();
        mItemSpace = (int)getResources().getDimension(R.dimen.padding_small);
        mRowSpace = (int)getResources().getDimension(R.dimen.padding_small);
    }

    //2. 重寫測量階段相關方法(onMeasure());
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //2.1 解析 ViewGroup 的父 View 對 ViewGroup 的尺寸要求
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(widthMeasureSpec);

        //2.2 ViewGroup 根據「開發者在 xml 中寫的對 ViewGroup 子 View 的尺寸要求」、「自己的父 View(ViewGroup 的父 View)對自己的尺寸要求」和
        //「自己的可用空間」計算出自己對子 View 的尺寸要求,並將該尺寸要求通過子 View 的 measure() 方法傳給子 View,讓子 View 測量自己(View)的期望尺寸
        int widthUsed = 0;
        int heightUsed = getPaddingTop();
        int lineHeight = 0;
        int lineWidthUsed = getPaddingLeft();
        int maxRight = widthSize - getPaddingRight();

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            //是否需要換行
            if(widthMode != MeasureSpec.UNSPECIFIED && (lineWidthUsed + child.getMeasuredWidth() > maxRight)){
                lineWidthUsed = getPaddingLeft();
                heightUsed += lineHeight + mRowSpace;
                lineHeight = 0;
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            }

            //2.3 ViewGroup 暫時儲存子 View 的尺寸,以便佈局階段和繪製階段使用
            Rect childBound;
            if(mChildrenBounds.size() <= i){
                childBound = new Rect();
                mChildrenBounds.add(childBound);
            }else{
                childBound = mChildrenBounds.get(i);
            }
            //此處不能用 child.getxxx() 獲取子 View 的尺寸值,因為子 View 只是量了尺寸,還沒有佈局,這些值都是 0
//            childBound.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
            childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());

            lineWidthUsed += child.getMeasuredWidth() + mItemSpace;
            widthUsed = Math.max(lineWidthUsed, widthUsed);
            lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
        }

        //2.4 ViewGroup 將「根據子 View 的實際尺寸計算出的自己(ViewGroup)的尺寸」結合「自己父 View 對自己的尺寸要求」進行修正,並通
        //過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸
        int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
        int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    //2.2.1 在自定義 ViewGroup 中呼叫 measureChildWithMargins() 方法計算 ViewGroup 對子 View 的尺寸要求時,
    //必須在 ViewGroup 中重寫 generateLayoutParams() 方法,因為 measureChildWithMargins() 方法中用到了 MarginLayoutParams,
    //如果不重寫 generateLayoutParams() 方法,那呼叫 measureChildWithMargins() 方法時,MarginLayoutParams 就為 null,
    //所以在自定義 ViewGroup 中呼叫 measureChildWithMargins() 方法時,必須重寫 generateLayoutParams() 方法。
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    //3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 需要重寫));
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            //應用測量階段計算出的子 View 的尺寸值佈局子 View
            View child = getChildAt(i);
            Rect childBound = mChildrenBounds.get(i);
            child.layout(childBound.left, childBound.top, childBound.right, childBound.bottom);
        }
    }

    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        return super.onInterceptHoverEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
}

//2. 在 xml 中應用 TabLayout
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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:scrollbars="none"
    tools:context=".MainActivity">

    <com.smart.a04_view_custom_viewgroup_example.custom_layout.TabLayout
        android:id="@+id/tag_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/grey_400"
        android:padding="@dimen/padding_small">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/common_bg"
            android:text="@string/spending_clothes" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/common_bg"
            android:text="@string/spending_others" />

        ...

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/common_bg"
            android:text="@string/november" />

    </com.smart.a04_view_custom_viewgroup_example.custom_layout.TabLayout>

</ScrollView>
複製程式碼

最終效果如下:

Android 自定義 View 最少必要知識

5. 相關問題

5.1 大方向

  1. Activity、Window、View 之間的關係
  2. View 是如何顯示出來的?
    • View 是如何顯示出來的?
    • View 新增子 View 的時候是將子 View 新增到原來的 View Tree,那 Toast 顯示的時候呢?它是怎樣顯示的?
  3. View(ViewGroup) 佈局、繪製流程
  4. View(ViewGroup) 事件分發

5.2 小細節

  1. 用過 View 中的 onSaveInstanceState()/onRestoreInstanceState() 嗎?一般在什麼情況下使用?
  2. onMeasure() 會執行多次嗎?為什麼?舉例說明
    • 能手動觸發嗎?如果能,怎麼做?如果能觸發,會出現什麼情況?
  3. onLayout() 會執行多次嗎?為什麼?
    • 能手動觸發嗎?如果能,怎麼做?如果能觸發,會出現什麼情況?
  4. onDraw() 會執行多次嗎?為什麼?
    • 能手動觸發嗎?如果能,怎麼做?如果能觸發,會出現什麼情況?
  5. requestLayout() 作用、使用場景、注意事項
  6. invalidate() 作用、使用場景、注意事項
  7. postInvalidate() 作用、使用場景、注意事項
  8. invalidate()、postInvalidate() 異同
  9. scrollBy、scrollTo 作用、使用場景、注意事項、二者的區別

5.3 如何優化自定義 View?

  1. 如何優化自定義 View?
  2. 如何優化自定義 ViewGroup?

6. 如何擴充?

  1. 結合 Drawable
  2. 結合動畫,讓 View 的內容變化顯得更加流暢

7. 總結

自定義 View 包括三部分內容:

  • 佈局(Layout)
  • 繪製(Drawing)
  • 觸控反饋(Event Handling)

其中佈局階段確定了 View 的位置和尺寸,該階段主要是為了後面的繪製和觸控反饋做支援;繪製階段主要用於繪製 View 的內容(大多數情況下,只用實現 OnDraw 方法(Where)方法、按照指定順序呼叫相關 API(How)即可實現自定義繪製(What));觸控反饋階段確定了使用者點選了哪裡,三者相輔相成,缺一不可。


參考文件

  1. View
  2. ViewGroup
  3. HenCoder
  4. Android面試解密-自定義View

相關文章