Android 自定義 View 最少必要知識

1. 什麼是自定義 View?

1.1 定義

在 Android 系統中,界面中全部能看到的元素都是 View。默認狀況下,Android 系統爲開發者提供了不少 View,好比用於展現文本信息的 TextView,用於展現圖片的 ImageView 等等。但有時,這並不能知足開發者的需求,例如,開發者想要用一個餅狀圖來展現一組數據,這時若是用系統提供的 View 就不能實現了,只能經過自定義 View 來實現。那到底什麼是自定義 View 呢?android

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

1.2 繼承結構

Android 中的全部 UI 元素都是 View 的子類:canvas

PS:因爲涉及的類太多,若是將全部涉及到的類所有加到類圖裏面,類圖將十分大,因此此處只列出了 View 的直接子類。設計模式

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

Android View 體系以下:bash

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

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

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.post

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 裏面的繪製都是按順序的,先繪製的內容會被後繪製的蓋住。如,你在重疊的位置「先畫圓再畫方」和「先畫方再畫圓」所呈現出來的結果是不一樣的,具體表現爲下表:


3.1.4 自定義 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 佈局、繪製流程時序圖

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,它的內容是「三個半徑不一樣、顏色不一樣的同心圓」,效果圖以下:

  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>
複製代碼

最終效果以下:

此時,即便你在 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,它的內容是「三個半徑不一樣、顏色不一樣的同心圓」,效果圖以下:

  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>
複製代碼

最終效果以下:

當在 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」時,顯示效果是下面這個樣子:

其實,也很好理解:

開發者對 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,標籤佈局,效果圖以下:

不管是自定義 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>
複製代碼

最終效果以下:

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
相關文章
相關標籤/搜索