Android 自定義 View 最少必要知識(shí)

1. 什么是自定義 View?

1.1 定義

在 Android 系統(tǒng)中官疲,界面中所有能看到的元素都是 View耀怜。默認(rèn)情況下,Android 系統(tǒng)為開發(fā)者提供了很多 View茶宵,比如用于展示文本信息的 TextView危纫,用于展示圖片的 ImageView 等等。但有時(shí)乌庶,這并不能滿足開發(fā)者的需求种蝶,例如,開發(fā)者想要用一個(gè)餅狀圖來展示一組數(shù)據(jù)瞒大,這時(shí)如果用系統(tǒng)提供的 View 就不能實(shí)現(xiàn)了螃征,只能通過自定義 View 來實(shí)現(xiàn)。那到底什么是自定義 View 呢透敌?

自定義 View 就是通過繼承 View 或者 View 的子類盯滚,并在新的類里面實(shí)現(xiàn)相應(yīng)的處理邏輯(重寫相應(yīng)的方法)踢械,以達(dá)到自己想要的效果。

1.2 繼承結(jié)構(gòu)

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

image

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

1.3 視圖體系用到的設(shè)計(jì)模式

Android View 體系如下:

image

仔細(xì)觀察话瞧,你會(huì)發(fā)現(xiàn),Android View 的體系結(jié)構(gòu)和設(shè)計(jì)模式中的組合模式的結(jié)構(gòu)如出一轍:

image

Android View 體系結(jié)構(gòu)中的 ViewGroup 對(duì)應(yīng)于組合模式中抽象構(gòu)件(Component 和 Composite)寝姿,Android View 體系結(jié)構(gòu)中的 View 對(duì)應(yīng)于組合模式中的葉子構(gòu)件(Leaf):

Android View 構(gòu)件 Composite Pattern 構(gòu)件
ViewGroup Component交排、Composite
View Leaf

2. 為什么要自定義 View?

大多數(shù)情況下会油,開發(fā)者常常會(huì)因?yàn)橄旅嫠膫€(gè)原因去自定義 View:

  1. 讓界面有特定的顯示風(fēng)格个粱、效果;
  2. 讓控件具有特殊的交互方式翻翩;
  3. 優(yōu)化布局都许;
  4. 封裝;

2.1 讓界面有特定的顯示風(fēng)格嫂冻、效果

默認(rèn)情況下胶征,Android 系統(tǒng)為開發(fā)者提供了很多控件,但有時(shí)桨仿,這并不能滿足開發(fā)者的需求睛低。例如,開發(fā)者想要用一個(gè)餅狀圖來展示一組數(shù)據(jù)服傍,這時(shí)如果用系統(tǒng)提供的 View 就不能實(shí)現(xiàn)了钱雷,只能通過自定義 View 來實(shí)現(xiàn)。

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

2.2 讓控件具有特殊的交互方式

默認(rèn)情況下吹零,Android 系統(tǒng)為開發(fā)者提供的控件都有屬于它們自己的特定的交互方式罩抗,但有時(shí),控件的默認(rèn)交互方式并不能滿足開發(fā)者的需求灿椅。例如套蒂,開發(fā)者想要縮放 ImageView 中的圖片內(nèi)容,這時(shí)如果用系統(tǒng)提供的 ImageView 就不能實(shí)現(xiàn)了茫蛹,只能通過自定義 ImageView 來實(shí)現(xiàn)操刀。

2.3 優(yōu)化布局

有時(shí),有些布局如果用系統(tǒng)提供的控件實(shí)現(xiàn)起來相當(dāng)復(fù)雜婴洼,需要各種嵌套骨坑,雖然最終也能實(shí)現(xiàn)了想要的效果,但性能極差窃蹋,此時(shí)就可以通過自定義 View 來減少嵌套層級(jí)卡啰、優(yōu)化布局静稻。

2.4 封裝

有些控件可能在多個(gè)地方使用,如大多數(shù) App 里面的底部 Tab匈辱,像這樣的經(jīng)常被用到的控件就可以通過自定義 View 將它們封裝起來振湾,以便在多個(gè)地方使用。

3. 如何自定義 View亡脸?

在說「如何自定義 View押搪?」之前,我們需要知道「自定義 View 都包括哪些內(nèi)容」浅碾?

自定義 View 包括三部分內(nèi)容:

  1. 布局(Layout)
  2. 繪制(Drawing)
  3. 觸摸反饋(Event Handling)

布局階段:確定 View 的位置和尺寸大州。
繪制階段:繪制 View 的內(nèi)容。
觸摸反饋:確定用戶點(diǎn)擊了哪里垂谢。

其中布局階段包括測量(measure)和布局(layout)兩個(gè)過程厦画,另外,布局階段是為繪制和觸摸反饋階段做支持的滥朱,它并沒有什么直接作用根暑。正是因?yàn)樵诓季蛛A段確定了 View 的尺寸和位置,繪制階段才知道往哪里繪制徙邻,觸摸反饋階段才知道用戶點(diǎn)的是哪里排嫌。

另外,由于觸摸反饋是一個(gè)大的話題缰犁,限于篇幅淳地,就不在這里講解了,后面有機(jī)會(huì)的話帅容,我會(huì)再補(bǔ)上一篇關(guān)于觸摸反饋的文章颇象。

在自定義 View 和自定義 ViewGroup 中,布局和繪制流程雖然整體上都是一樣的并徘,但在細(xì)節(jié)方面夯到,自定義 View 和自定義 ViewGroup 還是不一樣的,所以饮亏,接下來分兩類進(jìn)行討論:

  • 自定義 View 布局、繪制流程
  • 自定義 ViewGroup 布局阅爽、繪制流程

3.1 自定義 View 布局路幸、繪制流程

「自定義 View 布局、繪制」主要包括三個(gè)階段:

  1. 測量階段(measure)
  2. 布局階段(layout)
  3. 繪制階段(draw)

3.1.1 自定義 View 測量階段

在 View 的測量階段會(huì)執(zhí)行兩個(gè)方法(在測量階段付翁,View 的父 View 會(huì)通過調(diào)用 View 的 measure() 方法將父 View 對(duì) View 尺寸要求傳進(jìn)來简肴。緊接著 View 的 measure() 方法會(huì)做一些前置和優(yōu)化工作,然后調(diào)用 View 的 onMeasure() 方法百侧,并通過 onMeasure() 方法將父 View 對(duì) View 的尺寸要求傳入砰识。在自定義 View 中能扒,只有需要修改 View 的尺寸的時(shí)候才需要重寫 onMeasure() 方法。在 onMeasure() 方法中根據(jù)業(yè)務(wù)需求進(jìn)行相應(yīng)的邏輯處理辫狼,并在最后通過調(diào)用 setMeasuredDimension() 方法告知父 View 自己的期望尺寸):

  • measure()
  • onMeasure()

measure() : 調(diào)度方法初斑,主要做一些前置和優(yōu)化工作,并最終會(huì)調(diào)用 onMeasure() 方法執(zhí)行實(shí)際的測量工作膨处;

onMeasure() : 實(shí)際執(zhí)行測量任務(wù)的方法见秤,主要用與測量 View 尺寸和位置。在自定義 View 的 onMeasure() 方法中真椿,View 根據(jù)自己的特性和父 View 對(duì)自己的尺寸要求算出自己的期望尺寸鹃答,并通過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸。

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

  1. 參考父 View 的對(duì) View 的尺寸要求和實(shí)際業(yè)務(wù)需求計(jì)算出 View 的期望尺寸:

    • 解析 widthMeasureSpec突硝;
    • 解析 heightMeasureSpec测摔;
    • 將「根據(jù)實(shí)際業(yè)務(wù)需求計(jì)算出 View 的尺寸」根據(jù)「父 View 的對(duì) View 的尺寸要求」進(jìn)行相應(yīng)的修正得出 View 的期望尺寸(通過調(diào)用 resolveSize() 方法);
  2. 通過 setMeasuredDimension() 保存 View 的期望尺寸(實(shí)際上是通過 setMeasuredDimension() 告知父 View 自己的期望尺寸);

注意:
多數(shù)情況下解恰,這里的期望尺寸就是 View 的最終尺寸锋八。不過最終 View 的期望尺寸和實(shí)際尺寸是不是一樣還要看它的父 View 會(huì)不會(huì)同意。View 的父 View 最終會(huì)通過調(diào)用 View 的 layout() 方法告知 View 的實(shí)際尺寸修噪,并且在 layout() 方法中 View 需要將這個(gè)實(shí)際尺寸保存下來查库,以便繪制階段和觸摸反饋階段使用,這也是 View 需要在 layout() 方法中保存自己實(shí)際尺寸的原因——因?yàn)槔L制階段和觸摸反饋階段要使用盎魄怼樊销!

3.1.2 自定義 View 布局階段

在 View 的布局階段會(huì)執(zhí)行兩個(gè)方法(在布局階段,View 的父 View 會(huì)通過調(diào)用 View 的 layout() 方法將 View 的實(shí)際尺寸(父 View 根據(jù) View 的期望尺寸確定的 View 的實(shí)際尺寸)傳給 View脏款,View 需要在 layout() 方法中將自己的實(shí)際尺寸保存(通過調(diào)用 View 的 setFrame() 方法保存围苫,在 setFrame() 方法中,又會(huì)通過調(diào)用 onSizeChanged() 方法告知開發(fā)者 View 的尺寸修改了)以便在繪制和觸摸反饋階段使用撤师。保存 View 的實(shí)際尺寸之后剂府,View 的 layout() 方法又會(huì)調(diào)用 View 的 onLayout() 方法,不過 View 的 onLayout() 方法是一個(gè)空實(shí)現(xiàn)剃盾,因?yàn)樗鼪]有子 View):

  • layout()
  • onLayout()

layout() : 保存 View 的實(shí)際尺寸腺占。調(diào)用 setFrame() 方法保存 View 的實(shí)際尺寸,調(diào)用 onSizeChanged() 通知開發(fā)者 View 的尺寸更改了痒谴,并最終會(huì)調(diào)用 onLayout() 方法讓子 View 布局(如果有子 View 的話衰伯。因?yàn)樽远x View 中沒有子 View,所以自定義 View 的 onLayout() 方法是一個(gè)空實(shí)現(xiàn))积蔚;

onLayout() : 空實(shí)現(xiàn)意鲸,什么也不做,因?yàn)樗鼪]有子 View。如果是 ViewGroup 的話怎顾,在 onLayout() 方法中需要調(diào)用子 View 的 layout() 方法读慎,將子 View 的實(shí)際尺寸傳給它們,讓子 View 保存自己的實(shí)際尺寸槐雾。因此夭委,在自定義 View 中,不需重寫此方法蚜退,在自定義 ViewGroup 中闰靴,需重寫此方法。

注意:
layout() & onLayout() 并不是「調(diào)度」與「實(shí)際做事」的關(guān)系钻注,layout() 和 onLayout() 均做事蚂且,只不過職責(zé)不同。

3.1.3 自定義 View 繪制階段

在 View 的繪制階段會(huì)執(zhí)行一個(gè)方法——draw()幅恋,draw() 是繪制階段的總調(diào)度方法杏死,在其中會(huì)調(diào)用繪制背景的方法 drawBackground()、繪制主體的方法 onDraw()捆交、繪制子 View 的方法 dispatchDraw() 和 繪制前景的方法 onDrawForeground():

  • draw()

draw() : 繪制階段的總調(diào)度方法淑翼,在其中會(huì)調(diào)用繪制背景的方法 drawBackground()、繪制主體的方法 onDraw()品追、繪制子 View 的方法 dispatchDraw() 和 繪制前景的方法 onDrawForeground()玄括;

drawBackground() : 繪制背景的方法,不能重寫肉瓦,只能通過 xml 布局文件或者 setBackground() 來設(shè)置或修改背景遭京;

onDraw() : 繪制 View 主體內(nèi)容的方法,通常情況下泞莉,在自定義 View 的時(shí)候哪雕,只用實(shí)現(xiàn)該方法即可;

dispatchDraw() : 繪制子 View 的方法鲫趁。同 onLayout() 方法一樣斯嚎,在自定義 View 中它是空實(shí)現(xiàn),什么也不做挨厚。但在自定義 ViewGroup 中堡僻,它會(huì)調(diào)用 ViewGroup.drawChild() 方法,在 ViewGroup.drawChild() 方法中又會(huì)調(diào)用每一個(gè)子 View 的 View.draw() 讓子 View 進(jìn)行自我繪制疫剃;

onDrawForeground() : 繪制 View 前景的方法苦始,也就是說,想要在主體內(nèi)容之上繪制東西的時(shí)候就可以在該方法中實(shí)現(xiàn)慌申。

注意:
Android 里面的繪制都是按順序的,先繪制的內(nèi)容會(huì)被后繪制的蓋住。如蹄溉,你在重疊的位置「先畫圓再畫方」和「先畫方再畫圓」所呈現(xiàn)出來的結(jié)果是不同的咨油,具體表現(xiàn)為下表:

image


image

3.1.4 自定義 View 布局、繪制流程時(shí)序圖

image

3.2 自定義 ViewGroup 布局柒爵、繪制流程

「自定義 ViewGroup 布局役电、繪制」主要包括三個(gè)階段:

  1. 測量階段(measure)
  2. 布局階段(layout)
  3. 繪制階段(draw)

3.2.1 自定義 ViewGroup 測量階段

同自定義 View 一樣,在自定義 ViewGroup 的測量階段會(huì)執(zhí)行兩個(gè)方法:

  • measure()
  • onMeasure()

measure() : 調(diào)度方法棉胀,主要做一些前置和優(yōu)化工作法瑟,并最終會(huì)調(diào)用 onMeasure() 方法執(zhí)行實(shí)際的測量工作;

onMeasure() : 實(shí)際執(zhí)行測量任務(wù)的方法唁奢,與自定義 View 不同霎挟,在自定義 ViewGroup 的 onMeasure() 方法中,ViewGroup 會(huì)遞歸調(diào)用子 View 的 measure() 方法麻掸,并通過 measure() 將 ViewGroup 對(duì)子 View 的尺寸要求(ViewGroup 會(huì)根據(jù)開發(fā)者對(duì)子 View 的尺寸要求酥夭、自己的父 View(ViewGroup 的父 View) 對(duì)自己的尺寸要求和自己的可用空間計(jì)算出自己對(duì)子 View 的尺寸要求)傳入,對(duì)子 View 進(jìn)行測量脊奋,并把測量結(jié)果臨時(shí)保存熬北,以便在布局階段使用。測量出子 View 的實(shí)際尺寸之后诚隙,ViewGroup 會(huì)根據(jù)子 View 的實(shí)際尺寸計(jì)算出自己的期望尺寸讶隐,并通過 setMeasuredDimension() 方法告知父 View(ViewGroup 的父 View) 自己的期望尺寸。

具體流程如下:

  1. 運(yùn)行前久又,開發(fā)者在 xml 中寫入對(duì) ViewGroup 和 ViewGroup 子 View 的尺寸要求 layout_xxx巫延;
  2. ViewGroup 在自己的 onMeasure() 方法中,根據(jù)開發(fā)者在 xml 中寫的對(duì) ViewGroup 子 View 的尺寸要求籽孙、自己的父 View(ViewGroup 的父 View) 對(duì)自己的尺寸要求和自己的可用空間計(jì)算出自己對(duì)子 View 的尺寸要求烈评,并調(diào)用每個(gè)子 View 的 measure() 將 ViewGroup 對(duì)子 View 的尺寸要求傳入,測量子 View 尺寸犯建;
  3. ViewGroup 在子 View 計(jì)算出期望尺寸之后(在 ViewGroup 的 onMeasure() 方法中讲冠,ViewGroup 遞歸調(diào)用每個(gè)子 View 的 measure() 方法,子 View 在自己的 onMeasure() 方法中會(huì)通過調(diào)用 setMeasuredDimension() 方法告知父 View(ViewGroup) 自己的期望尺寸)适瓦,得出子 View 的實(shí)際尺寸和位置竿开,并暫時(shí)保存計(jì)算結(jié)果,以便布局階段使用玻熙;
  4. ViewGroup 根據(jù)子 View 的尺寸和位置計(jì)算自己的期望尺寸否彩,并通過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸。如果想要做的更好嗦随,可以在「 ViewGroup 根據(jù)子 View 的尺寸和位置計(jì)算出自己的期望尺寸」之后列荔,再結(jié)合 ViewGroup 的父 View 對(duì) ViewGroup 的尺寸要求進(jìn)行修正(通過 resolveSize() 方法)敬尺,這樣得出的 ViewGroup 的期望尺寸更符合 ViewGroup 的父 View 對(duì) ViewGroup 的尺寸要求。

3.2.2 自定義 ViewGroup 布局階段

同自定義 View 一樣贴浙,在自定義 ViewGroup 的布局階段會(huì)執(zhí)行兩個(gè)方法:

  • layout()
  • onLayout()

layout() : 保存 ViewGroup 的實(shí)際尺寸砂吞。調(diào)用 setFrame() 方法保存 ViewGroup 的實(shí)際尺寸,調(diào)用 onSizeChanged() 通知開發(fā)者 ViewGroup 的尺寸更改了崎溃,并最終會(huì)調(diào)用 onLayout() 方法讓子 View 布局蜻直;

onLayout() : ViewGroup 會(huì)遞歸調(diào)用每個(gè)子 View 的 layout() 方法,把測量階段計(jì)算出的子 View 的實(shí)際尺寸和位置傳給子 View袁串,讓子 View 保存自己的實(shí)際尺寸和位置概而。

3.2.3 自定義 ViewGroup 繪制階段

同自定義 View 一樣,在自定義 ViewGroup 的繪制階段會(huì)執(zhí)行一個(gè)方法——draw()囱修。draw() 是繪制階段的總調(diào)度方法赎瑰,在其中會(huì)調(diào)用繪制背景的方法 drawBackground()、繪制主體的方法 onDraw()蔚袍、繪制子 View 的方法 dispatchDraw() 和 繪制前景的方法 onDrawForeground():

  • draw()

draw() : 繪制階段的總調(diào)度方法乡范,在其中會(huì)調(diào)用繪制背景的方法 drawBackground()、繪制主體的方法 onDraw()啤咽、繪制子 View 的方法 dispatchDraw() 和 繪制前景的方法 onDrawForeground()晋辆;

在 ViewGroup 中,你也可以重寫繪制主體的方法 onDraw()宇整、繪制子 View 的方法 dispatchDraw() 和 繪制前景的方法 onDrawForeground()瓶佳。但大多數(shù)情況下,自定義 ViewGroup 是不需要重寫任何繪制方法的。因?yàn)橥ǔG闆r下,ViewGroup 的角色是容器蟀悦,一個(gè)透明的容器野芒,它只是用來盛放子 View 的叹谁。

3.2.4 自定義 ViewGroup 布局、繪制流程時(shí)序圖

image

3.3 自定義 View 步驟

  1. 自定義屬性的聲明與獲取;
  2. 重寫測量階段相關(guān)方法(onMeasure())傻工;
  3. 重寫布局階段相關(guān)方法(onLayout()(僅 ViewGroup 需要重寫));
  4. 重寫繪制階段相關(guān)方法(onDraw() 繪制主體孵滞、dispatchDraw() 繪制子 View 和 onDrawForeground() 繪制前景)中捆;
  5. onTouchEvent();
  6. onInterceptTouchEvent()(僅 ViewGroup 有此方法)坊饶;

4. 實(shí)戰(zhàn)演練

4.1 自定義 View

4.1.1 自定義 View ——自定義 View 的繪制內(nèi)容

自定義 View泄伪,它的內(nèi)容是「三個(gè)半徑不同、顏色不同的同心圓」匿级,效果圖如下:

image
  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 構(gòu)造函數(shù)中獲取自定義 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. 重寫測量階段相關(guān)方法(onMeasure())

由于不需要自定義 View 的尺寸蟋滴,所以染厅,不用重寫該方法。

  1. 重寫布局階段相關(guān)方法(onLayout()(僅 ViewGroup 需要重寫))

由于沒有子 View 需要布局脓杉,所以糟秘,不用重寫該方法。

  1. 重寫繪制階段相關(guān)方法(onDraw() 繪制主體球散、dispatchDraw() 繪制子 View 和 onDrawForeground() 繪制前景)
//4. 重寫 onDraw() 方法,自定義 View 內(nèi)容
@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. 重寫測量階段相關(guān)方法(onMeasure())屋讶;
    //由于不需要自定義 View 的尺寸,所以不用重寫該方法
//    @Override
//    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//    }

    //3. 重寫布局階段相關(guān)方法(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. 重寫繪制階段相關(guān)方法(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 中應(yīng)用 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>

最終效果如下:

image

此時(shí)乐疆,即使你在 xml 中將 CircleView 的寬、高聲明為「match_parent」贬养,你會(huì)發(fā)現(xiàn)最終的顯示效果都是一樣的挤土。

主要原因是:默認(rèn)情況下,View 的 onMeasure() 方法在通過 setMeasuredDimension() 告知父 View 自己的期望尺寸時(shí)误算,會(huì)調(diào)用 getDefaultSize() 方法仰美。在 getDefaultSize() 方法中,又會(huì)調(diào)用 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 獲取建議的最小寬度和最小高度儿礼,并根據(jù)最小尺寸和父 View 對(duì)自己的尺寸要求進(jìn)行修正咖杂。最主要的是,在 getDefaultSize() 方法中修正的時(shí)候蚊夫,會(huì)將 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 一視同仁诉字,直接返回父 View 對(duì) View 的尺寸要求:

//1. 默認(rèn) 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;
}

正是因?yàn)樵?getDefaultSize() 方法中處理的時(shí)候这橙,將 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 一視同仁奏窑,所以才有了上面「在 xml 中應(yīng)用 CircleView 的時(shí)候,無論將 CircleView 的尺寸設(shè)置為 match_parent 還是 wrap_content 效果都一樣」的現(xiàn)象屈扎。

具體分析如下:

開發(fā)者對(duì) View 的尺寸要求 View 的父 View 對(duì) 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 對(duì) View 的尺寸要求」是 View 的父 View 根據(jù)「開發(fā)者對(duì)子 View 的尺寸要求」、「自己的父 View(View 的父 View 的父 View) 對(duì)自己的尺寸要求」和「自己的可用空間」計(jì)算出自己對(duì)子 View 的尺寸要求鹰晨。

另外墨叛,由執(zhí)行結(jié)果可知止毕,上表中的 specSize 實(shí)際上等于 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 的尺寸和繪制內(nèi)容

自定義 View,它的內(nèi)容是「三個(gè)半徑不同漠趁、顏色不同的同心圓」扁凛,效果圖如下:

image
  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 構(gòu)造函數(shù)中獲取自定義 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. 重寫測量階段相關(guān)方法(onMeasure())
//2. onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //2.1 根據(jù) View 特點(diǎn)或業(yè)務(wù)需求計(jì)算出 View 的尺寸
    mWidth = (int)(mRadius * 2);
    mHeight = (int)(mRadius * 2);

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

    //2.3 通過 setMeasuredDimension() 保存 View 的期望尺寸(通過 setMeasuredDimension() 告知父 View 的期望尺寸)
    setMeasuredDimension(mWidth, mHeight);
}
  1. 重寫布局階段相關(guān)方法(onLayout()(僅 ViewGroup 需要重寫))

由于沒有子 View 需要布局,所以闯传,不用重寫該方法谨朝。

  1. 重寫繪制階段相關(guān)方法(onDraw() 繪制主體、dispatchDraw() 繪制子 View 和 onDrawForeground() 繪制前景)
//4. 重寫 onDraw() 方法甥绿,自定義 View 內(nèi)容
@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. 重寫測量階段相關(guān)方法(onMeasure());
    //由于不需要自定義 View 的尺寸图谷,所以不用重寫該方法
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //2.1 根據(jù) View 特點(diǎn)或業(yè)務(wù)需求計(jì)算出 View 的尺寸
        mWidth = (int)(mRadius * 2);
        mHeight = (int)(mRadius * 2);

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

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

    //3. 重寫布局階段相關(guān)方法(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. 重寫繪制階段相關(guān)方法(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 中應(yīng)用 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>

最終效果如下:

image

當(dāng)在 xml 中將 MeasuredCircleView 的寬、高聲明為「match_parent」時(shí)嫉沽,顯示效果跟 CircleView 顯示效果一樣辟犀。

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

但是,當(dāng)在 xml 中將 MeasuredCircleView 的寬绸硕、高聲明為「wrap_content」時(shí)堂竟,顯示效果是下面這個(gè)樣子:

image

其實(shí),也很好理解:

開發(fā)者對(duì) View 的尺寸要求 View 的父 View 對(duì) 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玻佩,標(biāo)簽布局出嘹,效果圖如下:

image

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

  1. 自定義屬性的聲明與獲纫Т蕖税稼;
  2. 重寫測量階段相關(guān)方法(onMeasure());
  3. 重寫布局階段相關(guān)方法(onLayout()(僅 ViewGroup 需要重寫))垮斯;
  4. 重寫繪制階段相關(guān)方法(onDraw() 繪制主體郎仆、dispatchDraw() 繪制子 View 和 onDrawForeground() 繪制前景);
  5. onTouchEvent()兜蠕;
  6. onInterceptTouchEvent()(僅 ViewGroup 有此方法)扰肌;

只不過,大多數(shù)情況下熊杨,ViewGroup 不需要「自定義屬性」和「重寫繪制階段相關(guān)方法」曙旭,但有些時(shí)候還是需要的盗舰,如,開發(fā)者想在 ViewGroup 的所有子 View 上方繪制一些內(nèi)容桂躏,就可以通過重寫 ViewGroup 的 onDrawForeground() 來實(shí)現(xiàn)钻趋。

  1. 自定義屬性的聲明與獲取

在自定義 ViewGroup 中「自定義屬性的聲明與獲取」的方法與在自定義 View 中「自定義屬性的聲明與獲取」的方法一樣,且因?yàn)榇蠖鄶?shù)情況下剂习,在自定義 ViewGroup 中是不需要自定義屬性的蛮位,所以,在這里就不自定義屬性了鳞绕。

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

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

    //2.2 ViewGroup 根據(jù)「開發(fā)者在 xml 中寫的對(duì) ViewGroup 子 View 的尺寸要求」、「自己的父 View(ViewGroup 的父 View)對(duì)自己的尺寸要求」和
    //「自己的可用空間」計(jì)算出自己對(duì)子 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 暫時(shí)保存子 View 的尺寸骡苞,以便布局階段和繪制階段使用
        Rect childBound;
        if(mChildrenBounds.size() <= i){
            childBound = new Rect();
            mChildrenBounds.add(childBound);
        }else{
            childBound = mChildrenBounds.get(i);
        }
        //此處不能用 child.getxxx() 獲取子 View 的尺寸值垂蜗,因?yàn)樽?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 將「根據(jù)子 View 的實(shí)際尺寸計(jì)算出的自己(ViewGroup)的尺寸」結(jié)合「自己父 View 對(duì)自己的尺寸要求」進(jìn)行修正贴见,并通
    //過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸
    int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
    int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
    setMeasuredDimension(measuredWidth, measuredHeight);
}

//重寫generateLayoutParams()
//2.2.1 在自定義 ViewGroup 中調(diào)用 measureChildWithMargins() 方法計(jì)算 ViewGroup 對(duì)子 View 的尺寸要求時(shí),
//必須在 ViewGroup 中重寫 generateLayoutParams() 方法躲株,因?yàn)?measureChildWithMargins() 方法中用到了 MarginLayoutParams片部,
//如果不重寫 generateLayoutParams() 方法,那調(diào)用 measureChildWithMargins() 方法時(shí)霜定,MarginLayoutParams 就為 null档悠,
//所以在自定義 ViewGroup 中調(diào)用 measureChildWithMargins() 方法時(shí),必須重寫 generateLayoutParams() 方法望浩。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
  1. 重寫布局階段相關(guān)方法(onLayout()(僅 ViewGroup 需要重寫))
//3. 重寫布局階段相關(guān)方法(onLayout()(僅 ViewGroup 需要重寫))辖所;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (int i = 0; i < getChildCount(); i++) {
        //應(yīng)用測量階段計(jì)算出的子 View 的尺寸值布局子 View
        View child = getChildAt(i);
        Rect childBound = mChildrenBounds.get(i);
        child.layout(childBound.left, childBound.top, childBound.right, childBound.bottom);
    }
}
  1. 重寫繪制階段相關(guān)方法(onDraw() 繪制主體、dispatchDraw() 繪制子 View 和 onDrawForeground() 繪制前景)

默認(rèn)情況下磨德,自定義 ViewGroup 時(shí)是不需要重寫任何繪制階段的方法的缘回,因?yàn)?ViewGroup 的角色是容器,一個(gè)透明的容器典挑,它只是用來盛放子 View 的酥宴。

注意:

  • 默認(rèn)情況下,系統(tǒng)會(huì)自動(dòng)調(diào)用 View Group 的 dispatchDraw() 方法您觉,所以不需要重寫該方法拙寡;
  • 出于效率的考慮,ViewGroup 默認(rèn)會(huì)繞過 draw() 方法顾犹,換而直接執(zhí)行 dispatchDraw()倒庵,以此來簡化繪制流程褒墨。所以如果你自定義了一個(gè) ViewGroup ,并且需要在它的除 dispatchDraw() 方法以外的任何一個(gè)繪制方法內(nèi)繪制內(nèi)容擎宝,你可能會(huì)需要調(diào)用 View.setWillNotDraw(false) 方法來切換到完整的繪制流程(是「可能」而不是「必須」的原因是郁妈,有些 ViewGroup 是已經(jīng)調(diào)用過 setWillNotDraw(false) 了的,例如 ScrollView)绍申。除了可以通過調(diào)用 View.setWillNotDraw(false) 方法來切換到完整的繪制流程之外噩咪,你還可以通過給 ViewGroup 設(shè)置背景來切換到完整的繪制流程。
  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. 重寫測量階段相關(guān)方法(onMeasure())奔脐;
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

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

        //2.2 ViewGroup 根據(jù)「開發(fā)者在 xml 中寫的對(duì) ViewGroup 子 View 的尺寸要求」俄周、「自己的父 View(ViewGroup 的父 View)對(duì)自己的尺寸要求」和
        //「自己的可用空間」計(jì)算出自己對(duì)子 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 暫時(shí)保存子 View 的尺寸峦朗,以便布局階段和繪制階段使用
            Rect childBound;
            if(mChildrenBounds.size() <= i){
                childBound = new Rect();
                mChildrenBounds.add(childBound);
            }else{
                childBound = mChildrenBounds.get(i);
            }
            //此處不能用 child.getxxx() 獲取子 View 的尺寸值,因?yàn)樽?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 將「根據(jù)子 View 的實(shí)際尺寸計(jì)算出的自己(ViewGroup)的尺寸」結(jié)合「自己父 View 對(duì)自己的尺寸要求」進(jìn)行修正,并通
        //過 setMeasuredDimension() 方法告知父 View 自己的期望尺寸
        int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
        int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    //2.2.1 在自定義 ViewGroup 中調(diào)用 measureChildWithMargins() 方法計(jì)算 ViewGroup 對(duì)子 View 的尺寸要求時(shí)橄维,
    //必須在 ViewGroup 中重寫 generateLayoutParams() 方法尺铣,因?yàn)?measureChildWithMargins() 方法中用到了 MarginLayoutParams,
    //如果不重寫 generateLayoutParams() 方法挣郭,那調(diào)用 measureChildWithMargins() 方法時(shí)迄埃,MarginLayoutParams 就為 null,
    //所以在自定義 ViewGroup 中調(diào)用 measureChildWithMargins() 方法時(shí)兑障,必須重寫 generateLayoutParams() 方法侄非。
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    //3. 重寫布局階段相關(guān)方法(onLayout()(僅 ViewGroup 需要重寫));
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            //應(yīng)用測量階段計(jì)算出的子 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 中應(yīng)用 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>

最終效果如下:

image

5. 相關(guān)問題

5.1 大方向

  1. Activity流译、Window逞怨、View 之間的關(guān)系
  2. View 是如何顯示出來的?
    • View 是如何顯示出來的福澡?
    • View 新增子 View 的時(shí)候是將子 View 添加到原來的 View Tree叠赦,那 Toast 顯示的時(shí)候呢?它是怎樣顯示的?
  3. View(ViewGroup) 布局除秀、繪制流程
  4. View(ViewGroup) 事件分發(fā)

5.2 小細(xì)節(jié)

  1. 用過 View 中的 onSaveInstanceState()/onRestoreInstanceState() 嗎糯累?一般在什么情況下使用?
  2. onMeasure() 會(huì)執(zhí)行多次嗎册踩?為什么泳姐?舉例說明
    • 能手動(dòng)觸發(fā)嗎?如果能暂吉,怎么做胖秒?如果能觸發(fā),會(huì)出現(xiàn)什么情況慕的?
  3. onLayout() 會(huì)執(zhí)行多次嗎阎肝?為什么?
    • 能手動(dòng)觸發(fā)嗎肮街?如果能风题,怎么做?如果能觸發(fā)嫉父,會(huì)出現(xiàn)什么情況俯邓?
  4. onDraw() 會(huì)執(zhí)行多次嗎?為什么熔号?
    • 能手動(dòng)觸發(fā)嗎?如果能鸟整,怎么做引镊?如果能觸發(fā),會(huì)出現(xiàn)什么情況篮条?
  5. requestLayout() 作用弟头、使用場景、注意事項(xiàng)
  6. invalidate() 作用涉茧、使用場景赴恨、注意事項(xiàng)
  7. postInvalidate() 作用、使用場景伴栓、注意事項(xiàng)
  8. invalidate()伦连、postInvalidate() 異同
  9. scrollBy、scrollTo 作用钳垮、使用場景惑淳、注意事項(xiàng)、二者的區(qū)別

5.3 如何優(yōu)化自定義 View饺窿?

  1. 如何優(yōu)化自定義 View歧焦?
  2. 如何優(yōu)化自定義 ViewGroup?

6. 如何拓展肚医?

  1. 結(jié)合 Drawable
  2. 結(jié)合動(dòng)畫绢馍,讓 View 的內(nèi)容變化顯得更加流暢

7. 總結(jié)

自定義 View 包括三部分內(nèi)容:

  • 布局(Layout)
  • 繪制(Drawing)
  • 觸摸反饋(Event Handling)

其中布局階段確定了 View 的位置和尺寸向瓷,該階段主要是為了后面的繪制和觸摸反饋?zhàn)鲋С郑焕L制階段主要用于繪制 View 的內(nèi)容(大多數(shù)情況下舰涌,只用實(shí)現(xiàn) OnDraw 方法(Where)方法猖任、按照指定順序調(diào)用相關(guān) API(How)即可實(shí)現(xiàn)自定義繪制(What));觸摸反饋階段確定了用戶點(diǎn)擊了哪里舵稠,三者相輔相成超升,缺一不可。


參考文檔

  1. View
  2. ViewGroup
  3. HenCoder
  4. Android面試解密-自定義View
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哺徊,一起剝皮案震驚了整個(gè)濱河市室琢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌落追,老刑警劉巖盈滴,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異轿钠,居然都是意外死亡巢钓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門疗垛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來症汹,“玉大人,你說我怎么就攤上這事贷腕”痴颍” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵泽裳,是天一觀的道長瞒斩。 經(jīng)常有香客問我,道長涮总,這世上最難降的妖魔是什么胸囱? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮瀑梗,結(jié)果婚禮上烹笔,老公的妹妹穿的比我還像新娘。我一直安慰自己抛丽,他們只是感情好箕宙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著铺纽,像睡著了一般柬帕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天陷寝,我揣著相機(jī)與錄音锅很,去河邊找鬼。 笑死凤跑,一個(gè)胖子當(dāng)著我的面吹牛爆安,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仔引,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼扔仓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咖耘?” 一聲冷哼從身側(cè)響起翘簇,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎儿倒,沒想到半個(gè)月后版保,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夫否,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年彻犁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凰慈。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汞幢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出微谓,到底是詐尸還是另有隱情急鳄,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布堰酿,位于F島的核電站,受9級(jí)特大地震影響张足,放射性物質(zhì)發(fā)生泄漏触创。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一为牍、第九天 我趴在偏房一處隱蔽的房頂上張望哼绑。 院中可真熱鬧,春花似錦碉咆、人聲如沸抖韩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茂浮。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間席揽,已是汗流浹背顽馋。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留幌羞,地道東北人寸谜。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像属桦,于是被迫代替她去往敵國和親熊痴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容