Android自定義View
為什么要自定義View
自定義View的基本方法
自定義View的最基本的三個(gè)方法分別是: onMeasure()、onLayout()、onDraw(); View在Activity中顯示出來,要經(jīng)歷測(cè)量、布局和繪制三個(gè)步驟,分別對(duì)應(yīng)三個(gè)動(dòng)作:measure、layout和draw。
測(cè)量:onMeasure()決定View的大?。?br>
布局:onLayout()決定View在ViewGroup中的位置;
繪制:onDraw()決定繪制這個(gè)View。
自定義控件分類
自定義View: 只需要重寫onMeasure()和onDraw(),在沒有現(xiàn)成的View,需要自己實(shí)現(xiàn)的時(shí)候,就使用自定義View,一般繼承自View,SurfaceView或其他的View
自定義ViewGroup: 則只需要重寫onMeasure()和onLayout(),自定義ViewGroup一般是利用現(xiàn)有的組件根據(jù)特定的布局方式來組成新的組件,大多繼承自ViewGroup或各種Layout
自定義View基礎(chǔ)
視圖View主要分為以下兩類:
類別 解釋 特點(diǎn)
單一視圖 即一個(gè)View,如TextView 不包含子View
視圖組 即多個(gè)View組成的ViewGroup, 如LinearLayout 包含子View
View類簡介
View類是Android中各種組件的基類,如View是ViewGroup基類,ViewGroup是繼承自View類的,但是在視圖組中,ViewGroup是父組件,ViewGroup父組件中會(huì)包含多個(gè)子View
View表現(xiàn)為顯示在屏幕上的各種視圖
Android中的UI都是有View和ViewGroup組成的
View的構(gòu)造函數(shù)有4個(gè):
// 如果View是在Java代碼里面new的,則調(diào)用第一個(gè)構(gòu)造函數(shù)
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里聲明的,則調(diào)用第二個(gè)構(gòu)造函數(shù)
// 自定義屬性是從AttributeSet參數(shù)傳進(jìn)來的
// 這個(gè)方法一般是必須重寫的,因?yàn)樵贚ayoutInfaltor中CreateView的時(shí)候,系統(tǒng)會(huì)通過反射調(diào)用該構(gòu)造函數(shù),如果沒有重寫創(chuàng)建View的時(shí)候會(huì)報(bào)錯(cuò)
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不會(huì)自動(dòng)調(diào)用
// 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
// 如View有style屬性時(shí)
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用
// 不會(huì)自動(dòng)調(diào)用
// 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
// 如View有style屬性時(shí)
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, intdefStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
View的創(chuàng)建繪制流程:

AttributeSet與自定義屬性
系統(tǒng)自帶的View可以在xml中配置屬性,對(duì)于寫的好的自定義View同樣可以在xml中配置屬性,為了使自定義的View的屬性可以在xml中配置,需要以下4個(gè)步驟:
- 通過 <declare-styleable> 為自定義View添加屬性
- 在xml中為相應(yīng)的屬性聲明屬性值
- 在運(yùn)行時(shí)(一般為構(gòu)造函數(shù))獲取屬性值
- 將獲取到的屬性值應(yīng)用到View
View視圖結(jié)構(gòu)
- PhoneWindow是Android系統(tǒng)中最基本的窗口系統(tǒng),繼承自Windows類,負(fù)責(zé)管理界面顯示以及事件響應(yīng)。它是Activity與View系統(tǒng)交互的接口
- DecorView是PhoneWindow中的起始節(jié)點(diǎn)View,繼承于View類,作為整個(gè)視圖容器來使用。用于設(shè)置窗口屬性。它本質(zhì)上是一個(gè)FrameLayout,DecorView是繼承自FrameLayout的
-
ViewRoot在Activtiy啟動(dòng)時(shí)創(chuàng)建,負(fù)責(zé)管理、布局、渲染窗口UI等等
image.png
對(duì)于多View的視圖,結(jié)構(gòu)是樹形結(jié)構(gòu):最頂層是ViewGroup,ViewGroup下可能有多個(gè)ViewGroup或View,如下
圖:

一定要記?。篈ndroid系統(tǒng)無論是measure過程、layout過程還是draw過程,永遠(yuǎn)都是從View樹的根節(jié)點(diǎn)開始測(cè)量或計(jì)算(即從
樹的頂端開始),一層一層、一個(gè)分支一個(gè)分支地進(jìn)行(即樹形遞歸),最終計(jì)算整個(gè)View樹中各個(gè)View,最終確
定整個(gè)View樹的相關(guān)屬性。
Android坐標(biāo)系
Android的坐標(biāo)系定義為:

屏幕的左上角為坐標(biāo)原點(diǎn)
向右為x軸增大方向
向下為y軸增大方向
區(qū)別于一般的數(shù)學(xué)坐標(biāo)系:
View位置(坐標(biāo))描述
View的位置由4個(gè)頂點(diǎn)決定的, 4個(gè)頂點(diǎn)的位置描述分別由4個(gè)值決定,請(qǐng)記住:View的位置是相對(duì)于父控件而言的
- Top:子View上邊界到父view上邊界的距離
- Left:子View左邊界到父view左邊界的距離
- Bottom:子View下邊距到父View上邊界的距離
- Right:子View右邊界到父view左邊界的距離
位置獲取方式
View的位置是通過view.getxxx()函數(shù)進(jìn)行獲?。海ㄒ訲op為例)
// 獲取Top位置
public final int getTop() {
return mTop;
}
// 其余如下:
getLeft(); //獲取子View左上角距父View左側(cè)的距離
getBottom(); //獲取子View右下角距父View頂部的距離
getRight(); //獲取子View右下角距父View左側(cè)的距離
與MotionEvent中 get()和getRaw()的區(qū)別
//get() :觸摸點(diǎn)相對(duì)于其所在組件坐標(biāo)系的坐標(biāo)
event.getX();
event.getY();
//getRaw() :觸摸點(diǎn)相對(duì)于屏幕默認(rèn)坐標(biāo)系的坐標(biāo)
event.getRawX();
event.getRawY();
getX()和getRawX()的區(qū)別參照下圖:

getMeasureWidth與getWidth的區(qū)別getMeasureWidth
在measure()過程結(jié)束后就可以獲取到對(duì)應(yīng)的值;
通過setMeasuredDimension()方法來進(jìn)行設(shè)置的.
getWidth
在layout()過程結(jié)束后才能獲取到;
通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計(jì)算出來的.
Android中顏色相關(guān)內(nèi)容
Android支持的顏色模式:以ARGB8888為例
View樹的繪制流程
View樹的繪制流程是誰負(fù)責(zé)的?
view樹的繪制流程是通過ViewRoot去負(fù)責(zé)繪制的,ViewRoot這個(gè)類的命名有點(diǎn)坑,最初看到這個(gè)名字,翻譯過來是
view的根節(jié)點(diǎn),但是事實(shí)完全不是這樣,ViewRoot其實(shí)不是View的根節(jié)點(diǎn),它連view節(jié)點(diǎn)都算不上,它的主要作用
是View樹的管理者,負(fù)責(zé)將DecorView和PhoneWindow“組合”起來,而View樹的根節(jié)點(diǎn)嚴(yán)格意義上來說只有
DecorView;每個(gè)DecorView都有一個(gè)ViewRoot與之關(guān)聯(lián),這種關(guān)聯(lián)關(guān)系是由WindowManager去進(jìn)行管理的;
View繪制流程如下圖:

View的添加

View的繪制流程

measure

- 系統(tǒng)為什么要有measure過程?
- measure過程都干了點(diǎn)什么事?
- 對(duì)于自適應(yīng)的尺寸機(jī)制,如何合理的測(cè)量一顆View樹?
- 那么ViewGroup是如何向子View傳遞限制信息的?
- ScrollView嵌套ListView問題?
Layout
- 系統(tǒng)為什么要有l(wèi)ayout過程?
-
layout過程都干了點(diǎn)什么事?
image.png
Draw
- 系統(tǒng)為什么要有draw過程?
-
draw過程都干了點(diǎn)什么事
image.png
LayoutParams
LayoutParams翻譯過來就是布局參數(shù),子View通過LayoutParams告訴父容器(ViewGroup)應(yīng)該如何放置自己。
從這個(gè)定義中也可以看出來LayoutParams與ViewGroup是息息相關(guān)的,因此脫離ViewGroup談LayoutParams是沒
有意義的。
事實(shí)上,每個(gè)ViewGroup的子類都有自己對(duì)應(yīng)的LayoutParams類,典型的如LinearLayout.LayoutParams和
FrameLayout.LayoutParams等,可以看出來LayoutParams都是對(duì)應(yīng)ViewGroup子類的內(nèi)部類
MarginLayoutParams
MarginLayoutParams是和外間距有關(guān)的。事實(shí)也確實(shí)如此,和LayoutParams相比,MarginLayoutParams只是增
加了對(duì)上下左右外間距的支持。實(shí)際上大部分LayoutParams的實(shí)現(xiàn)類都是繼承自MarginLayoutParams,因?yàn)榛?br>
所有的父容器都是支持子View設(shè)置外間距的
屬性優(yōu)先級(jí)問題 MarginLayoutParams主要就是增加了上下左右4種外間距。在構(gòu)造方法中,先是獲取了
margin屬性;如果該值不合法,就獲取horizontalMargin;如果該值不合法,再去獲取leftMargin和
rightMargin屬性(verticalMargin、topMargin和bottomMargin同理)。我們可以據(jù)此總結(jié)出這幾種屬性的優(yōu)
先級(jí)
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
屬性覆蓋問題 優(yōu)先級(jí)更高的屬性會(huì)覆蓋掉優(yōu)先級(jí)較低的屬性。此外,還要注意一下這幾種屬性上的注釋
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
LayoutParams與View如何建立聯(lián)系
在XML中定義View
在Java代碼中直接生成View對(duì)應(yīng)的實(shí)例對(duì)象
addView
/**
* 重載方法1:添加一個(gè)子View
* 如果這個(gè)子View還沒有LayoutParams,就為子View設(shè)置當(dāng)前ViewGroup默認(rèn)的LayoutParams
*/
public void addView(View child) {
addView(child, -1);
}
/**
* 重載方法2:在指定位置添加一個(gè)子View
* 如果這個(gè)子View還沒有LayoutParams,就為子View設(shè)置當(dāng)前ViewGroup默認(rèn)的LayoutParams
* @param index View將在ViewGroup中被添加的位置(-1代表添加到末尾)
*/
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();// 生成當(dāng)前ViewGroup默認(rèn)的LayoutParams
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return
null");
}
}
addView(child, index, params);
}
/**
* 重載方法3:添加一個(gè)子View
* 使用當(dāng)前ViewGroup默認(rèn)的LayoutParams,并以傳入?yún)?shù)作為LayoutParams的width和height
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams(); // 生成當(dāng)前ViewGroup默認(rèn)的
LayoutParams
params.width = width;
params.height = height;
addView(child, -1, params);
}
/**
* 重載方法4:添加一個(gè)子View,并使用傳入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
/**
* 重載方法4:在指定位置添加一個(gè)子View,并使用傳入的LayoutParams
*/
public void addView(View child, int index, LayoutParams params) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
.....
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) { // ① 檢查傳入的LayoutParams是否合法
params = generateLayoutParams(params); // 如果傳入的LayoutParams不合法,將進(jìn)行轉(zhuǎn)化操作
}
if (preventRequestLayout) { // ② 是否需要阻止重新執(zhí)行布局流程
child.mLayoutParams = params; // 這不會(huì)引起子View重新布局(onMeasure->onLayout-
>onDraw)
} else {
child.setLayoutParams(params); // 這會(huì)引起子View重新布局(onMeasure->onLayout-
>onDraw)
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
.....
}
自定義LayoutParams
- 創(chuàng)建自定義屬性
<resources>
<declare-styleable name="xxxViewGroup_Layout">
<!-- 自定義的屬性 -->
<attr name="layout_simple_attr" format="integer"/>
<!-- 使用系統(tǒng)預(yù)置的屬性 -->
<attr name="android:layout_gravity"/>
</declare-styleable>
</resources>
- 繼承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int simpleAttr;
public int gravity;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// 解析布局屬性
TypedArray typedArray = c.obtainStyledAttributes(attrs,
R.styleable.SimpleViewGroup_Layout);
simpleAttr =
typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity,
-1);
typedArray.recycle();//釋放資源
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
- 重寫ViewGroup中幾個(gè)與LayoutParams相關(guān)的方法
// 檢查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默認(rèn)的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT);
}
// 對(duì)傳入的LayoutParams進(jìn)行轉(zhuǎn)化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new SimpleViewGroup.LayoutParams(p);
}
// 對(duì)傳入的LayoutParams進(jìn)行轉(zhuǎn)化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}
LayoutParams常見的子類
在為View設(shè)置LayoutParams的時(shí)候需要根據(jù)它的父容器選擇對(duì)應(yīng)的LayoutParams,否則結(jié)果可能與預(yù)期不一致,
這里簡單羅列一些常見的LayoutParams子類:
ViewGroup.MarginLayoutParams
FrameLayout.LayoutParams
LinearLayout.LayoutParams
RelativeLayout.LayoutParams
RecyclerView.LayoutParams
GridLayoutManager.LayoutParams
StaggeredGridLayoutManager.LayoutParams
ViewPager.LayoutParams
WindowManager.LayoutParams
MeasureSpec
測(cè)量規(guī)格,封裝了父容器對(duì) view 的布局上的限制,內(nèi)部提供了寬高的信息( SpecMode 、 SpecSize ),SpecSize是指
在某種SpecMode下的參考尺寸,其中SpecMode 有如下三種:

UNSPECIFIED 父控件不對(duì)你有任何限制,你想要多大給你多大,想上天就上天。這種情況一般用于系統(tǒng)內(nèi)部,表示一種測(cè)量狀態(tài)。(這個(gè)模式主要用于系統(tǒng)內(nèi)部多次Measure的情形,并不是真的說你想要多大最后就真有多大)
EXACTLY 父控件已經(jīng)知道你所需的精確大小,你的最終大小應(yīng)該就是這么大。
AT_MOST 你的大小不能大于父控件給你指定的size,但具體是多少,得看你自己的實(shí)現(xiàn)。
MeasureSpecs 的意義
通過將 SpecMode 和 SpecSize 打包成一個(gè) int 值可以避免過多的對(duì)象內(nèi)存分配,為了方便操作,其提供了打包 / 解
包方法
MeasureSpec值的確定
MeasureSpec值到底是如何計(jì)算得來的呢?

子View的MeasureSpec值是根據(jù)子View的布局參數(shù)(LayoutParams)和父容器的MeasureSpec值計(jì)算得來的,具
體計(jì)算邏輯封裝在ViewGroup的getChildMeasureSpec()方法里
/**
*
* 目標(biāo)是將父控件的測(cè)量規(guī)格和child view的布局參數(shù)LayoutParams相結(jié)合,得到一個(gè)
* 最可能符合條件的child view的測(cè)量規(guī)格。
* @param spec 父控件的測(cè)量規(guī)格
* @param padding 父控件里已經(jīng)占用的大小
* @param childDimension child view布局LayoutParams里的尺寸
* @return child view 的測(cè)量規(guī)格
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //父控件的測(cè)量模式
int specSize = MeasureSpec.getSize(spec); //父控件的測(cè)量大小
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 當(dāng)父控件的測(cè)量模式 是 精確模式,也就是有精確的尺寸了
case MeasureSpec.EXACTLY:
//如果child的布局參數(shù)有固定值,比如"layout_width" = "100dp"
//那么顯然child的測(cè)量規(guī)格也可以確定下來了,測(cè)量大小就是100dp,測(cè)量模式也是EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局參數(shù)是"match_parent",也就是想要占滿父控件
//而此時(shí)父控件是精確模式,也就是能確定自己的尺寸了,那child也能確定自己大小了
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局參數(shù)是"wrap_content",也就是想要根據(jù)自己的邏輯決定自己大小,
//比如TextView根據(jù)設(shè)置的字符串大小來決定自己的大小
//那就自己決定唄,不過你的大小肯定不能大于父控件的大小嘛
//所以測(cè)量模式就是AT_MOST,測(cè)量大小就是父控件的size
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父控件的測(cè)量模式 是 最大模式,也就是說父控件自己還不知道自己的尺寸,但是大小不能超過size
case MeasureSpec.AT_MOST:
//同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也優(yōu)先滿足孩子的需求??
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//child想要和父控件一樣大,但父控件自己也不確定自己大小,所以child也無法確定自己大小
//但同樣的,child的尺寸上限也是父控件的尺寸上限size
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
//child想要根據(jù)自己邏輯決定大小,那就自己決定唄
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

針對(duì)上表,這里再做一下具體的說明
對(duì)于應(yīng)用層 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定
對(duì)于不同的父容器和view本身不同的LayoutParams,view就可以有多種MeasureSpec。 1. 當(dāng)view采用固定寬
高的時(shí)候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精確模式并且其大小遵循
Layoutparams中的大??; 2. 當(dāng)view的寬高是match_parent時(shí),這個(gè)時(shí)候如果父容器的模式是精準(zhǔn)模式,那么
view也是精準(zhǔn)模式并且其大小是父容器的剩余空間,如果父容器是最大模式,那么view也是最大模式并且其大
小不會(huì)超過父容器的剩余空間; 3. 當(dāng)view的寬高是wrap_content時(shí),不管父容器的模式是精準(zhǔn)還是最大化,
view的模式總是最大化并且大小不能超過父容器的剩余空間。 4. Unspecified模式,這個(gè)模式主要用于系統(tǒng)內(nèi)
部多次measure的情況下,一般來說,我們不需要關(guān)注此模式(這里注意自定義View放到ScrollView的情況 需要
處理)。


