Android自定義View
為什么要自定義View
自定義View的基本方法
自定義View的最基本的三個方法分別是: onMeasure()弓颈、onLayout()笼裳、onDraw(); View在Activity中顯示出來喜德,要經(jīng)歷測量现横、布局和繪制三個步驟奋构,分別對應(yīng)三個動作:measure手趣、layout和draw晌该。
測量:onMeasure()決定View的大小绿渣;
布局:onLayout()決定View在ViewGroup中的位置朝群;
繪制:onDraw()決定繪制這個View。
自定義控件分類
自定義View: 只需要重寫onMeasure()和onDraw()中符,在沒有現(xiàn)成的View姜胖,需要自己實現(xiàn)的時候,就使用自定義View淀散,一般繼承自View右莱,SurfaceView或其他的View
自定義ViewGroup: 則只需要重寫onMeasure()和onLayout()蚜锨,自定義ViewGroup一般是利用現(xiàn)有的組件根據(jù)特定的布局方式來組成新的組件,大多繼承自ViewGroup或各種Layout
自定義View基礎(chǔ)
視圖View主要分為以下兩類:
類別 解釋 特點
單一視圖 即一個View隧出,如TextView 不包含子View
視圖組 即多個View組成的ViewGroup踏志, 如LinearLayout 包含子View
View類簡介
View類是Android中各種組件的基類,如View是ViewGroup基類胀瞪,ViewGroup是繼承自View類的针余,但是在視圖組中,ViewGroup是父組件凄诞,ViewGroup父組件中會包含多個子View
View表現(xiàn)為顯示在屏幕上的各種視圖
Android中的UI都是有View和ViewGroup組成的
View的構(gòu)造函數(shù)有4個:
// 如果View是在Java代碼里面new的圆雁,則調(diào)用第一個構(gòu)造函數(shù)
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里聲明的,則調(diào)用第二個構(gòu)造函數(shù)
// 自定義屬性是從AttributeSet參數(shù)傳進來的
// 這個方法一般是必須重寫的帆谍,因為在LayoutInfaltor中CreateView的時候伪朽,系統(tǒng)會通過反射調(diào)用該構(gòu)造函數(shù),如果沒有重寫創(chuàng)建View的時候會報錯
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不會自動調(diào)用
// 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用
// 如View有style屬性時
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用
// 不會自動調(diào)用
// 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用
// 如View有style屬性時
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, intdefStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
View的創(chuàng)建繪制流程:
AttributeSet與自定義屬性
系統(tǒng)自帶的View可以在xml中配置屬性汛蝙,對于寫的好的自定義View同樣可以在xml中配置屬性烈涮,為了使自定義的View的屬性可以在xml中配置,需要以下4個步驟:
- 通過 <declare-styleable> 為自定義View添加屬性
- 在xml中為相應(yīng)的屬性聲明屬性值
- 在運行時(一般為構(gòu)造函數(shù))獲取屬性值
- 將獲取到的屬性值應(yīng)用到View
View視圖結(jié)構(gòu)
- PhoneWindow是Android系統(tǒng)中最基本的窗口系統(tǒng)窖剑,繼承自Windows類坚洽,負責(zé)管理界面顯示以及事件響應(yīng)。它是Activity與View系統(tǒng)交互的接口
- DecorView是PhoneWindow中的起始節(jié)點View西土,繼承于View類讶舰,作為整個視圖容器來使用。用于設(shè)置窗口屬性需了。它本質(zhì)上是一個FrameLayout跳昼,DecorView是繼承自FrameLayout的
-
ViewRoot在Activtiy啟動時創(chuàng)建,負責(zé)管理肋乍、布局鹅颊、渲染窗口UI等等
image.png
對于多View的視圖,結(jié)構(gòu)是樹形結(jié)構(gòu):最頂層是ViewGroup住拭,ViewGroup下可能有多個ViewGroup或View挪略,如下
圖:
一定要記住:Android系統(tǒng)無論是measure過程滔岳、layout過程還是draw過程,永遠都是從View樹的根節(jié)點開始測量或計算(即從
樹的頂端開始)挽牢,一層一層谱煤、一個分支一個分支地進行(即樹形遞歸),最終計算整個View樹中各個View禽拔,最終確
定整個View樹的相關(guān)屬性刘离。
Android坐標(biāo)系
Android的坐標(biāo)系定義為:
屏幕的左上角為坐標(biāo)原點
向右為x軸增大方向
向下為y軸增大方向
區(qū)別于一般的數(shù)學(xué)坐標(biāo)系:
View位置(坐標(biāo))描述
View的位置由4個頂點決定的室叉, 4個頂點的位置描述分別由4個值決定,請記琢蛱琛:View的位置是相對于父控件而言的
- Top:子View上邊界到父view上邊界的距離
- Left:子View左邊界到父view左邊界的距離
- Bottom:子View下邊距到父View上邊界的距離
- Right:子View右邊界到父view左邊界的距離
位置獲取方式
View的位置是通過view.getxxx()函數(shù)進行獲燃牒邸:(以Top為例)
// 獲取Top位置
public final int getTop() {
return mTop;
}
// 其余如下:
getLeft(); //獲取子View左上角距父View左側(cè)的距離
getBottom(); //獲取子View右下角距父View頂部的距離
getRight(); //獲取子View右下角距父View左側(cè)的距離
與MotionEvent中 get()和getRaw()的區(qū)別
//get() :觸摸點相對于其所在組件坐標(biāo)系的坐標(biāo)
event.getX();
event.getY();
//getRaw() :觸摸點相對于屏幕默認坐標(biāo)系的坐標(biāo)
event.getRawX();
event.getRawY();
getX()和getRawX()的區(qū)別參照下圖:
getMeasureWidth與getWidth的區(qū)別getMeasureWidth
在measure()過程結(jié)束后就可以獲取到對應(yīng)的值;
通過setMeasuredDimension()方法來進行設(shè)置的.
getWidth
在layout()過程結(jié)束后才能獲取到;
通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的.
Android中顏色相關(guān)內(nèi)容
Android支持的顏色模式:以ARGB8888為例
View樹的繪制流程
View樹的繪制流程是誰負責(zé)的?
view樹的繪制流程是通過ViewRoot去負責(zé)繪制的恼除,ViewRoot這個類的命名有點坑踪旷,最初看到這個名字,翻譯過來是
view的根節(jié)點豁辉,但是事實完全不是這樣令野,ViewRoot其實不是View的根節(jié)點,它連view節(jié)點都算不上徽级,它的主要作用
是View樹的管理者气破,負責(zé)將DecorView和PhoneWindow“組合”起來,而View樹的根節(jié)點嚴(yán)格意義上來說只有
DecorView餐抢;每個DecorView都有一個ViewRoot與之關(guān)聯(lián)现使,這種關(guān)聯(lián)關(guān)系是由WindowManager去進行管理的;
View繪制流程如下圖:
View的添加
View的繪制流程
measure
- 系統(tǒng)為什么要有measure過程旷痕?
- measure過程都干了點什么事碳锈?
- 對于自適應(yīng)的尺寸機制,如何合理的測量一顆View樹苦蒿?
- 那么ViewGroup是如何向子View傳遞限制信息的殴胧?
- ScrollView嵌套ListView問題?
Layout
- 系統(tǒng)為什么要有l(wèi)ayout過程佩迟?
-
layout過程都干了點什么事团滥?
image.png
Draw
- 系統(tǒng)為什么要有draw過程?
-
draw過程都干了點什么事
image.png
LayoutParams
LayoutParams翻譯過來就是布局參數(shù)报强,子View通過LayoutParams告訴父容器(ViewGroup)應(yīng)該如何放置自己灸姊。
從這個定義中也可以看出來LayoutParams與ViewGroup是息息相關(guān)的,因此脫離ViewGroup談LayoutParams是沒
有意義的秉溉。
事實上力惯,每個ViewGroup的子類都有自己對應(yīng)的LayoutParams類,典型的如LinearLayout.LayoutParams和
FrameLayout.LayoutParams等召嘶,可以看出來LayoutParams都是對應(yīng)ViewGroup子類的內(nèi)部類
MarginLayoutParams
MarginLayoutParams是和外間距有關(guān)的父晶。事實也確實如此,和LayoutParams相比弄跌,MarginLayoutParams只是增
加了對上下左右外間距的支持甲喝。實際上大部分LayoutParams的實現(xiàn)類都是繼承自MarginLayoutParams,因為基本
所有的父容器都是支持子View設(shè)置外間距的
屬性優(yōu)先級問題 MarginLayoutParams主要就是增加了上下左右4種外間距铛只。在構(gòu)造方法中埠胖,先是獲取了
margin屬性糠溜;如果該值不合法,就獲取horizontalMargin直撤;如果該值不合法非竿,再去獲取leftMargin和
rightMargin屬性(verticalMargin、topMargin和bottomMargin同理)谋竖。我們可以據(jù)此總結(jié)出這幾種屬性的優(yōu)
先級
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin红柱、topMargin和bottomMargin
屬性覆蓋問題 優(yōu)先級更高的屬性會覆蓋掉優(yōu)先級較低的屬性。此外圈盔,還要注意一下這幾種屬性上的注釋
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
LayoutParams與View如何建立聯(lián)系
在XML中定義View
在Java代碼中直接生成View對應(yīng)的實例對象
addView
/**
* 重載方法1:添加一個子View
* 如果這個子View還沒有LayoutParams豹芯,就為子View設(shè)置當(dāng)前ViewGroup默認的LayoutParams
*/
public void addView(View child) {
addView(child, -1);
}
/**
* 重載方法2:在指定位置添加一個子View
* 如果這個子View還沒有LayoutParams,就為子View設(shè)置當(dāng)前ViewGroup默認的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默認的LayoutParams
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return
null");
}
}
addView(child, index, params);
}
/**
* 重載方法3:添加一個子View
* 使用當(dāng)前ViewGroup默認的LayoutParams驱敲,并以傳入?yún)?shù)作為LayoutParams的width和height
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams(); // 生成當(dāng)前ViewGroup默認的
LayoutParams
params.width = width;
params.height = height;
addView(child, -1, params);
}
/**
* 重載方法4:添加一個子View铁蹈,并使用傳入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
/**
* 重載方法4:在指定位置添加一個子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不合法众眨,將進行轉(zhuǎn)化操作
}
if (preventRequestLayout) { // ② 是否需要阻止重新執(zhí)行布局流程
child.mLayoutParams = params; // 這不會引起子View重新布局(onMeasure->onLayout-
>onDraw)
} else {
child.setLayoutParams(params); // 這會引起子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中幾個與LayoutParams相關(guān)的方法
// 檢查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默認的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT);
}
// 對傳入的LayoutParams進行轉(zhuǎn)化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new SimpleViewGroup.LayoutParams(p);
}
// 對傳入的LayoutParams進行轉(zhuǎn)化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}
LayoutParams常見的子類
在為View設(shè)置LayoutParams的時候需要根據(jù)它的父容器選擇對應(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
測量規(guī)格,封裝了父容器對 view 的布局上的限制娩梨,內(nèi)部提供了寬高的信息( SpecMode 沿腰、 SpecSize ),SpecSize是指
在某種SpecMode下的參考尺寸狈定,其中SpecMode 有如下三種:
UNSPECIFIED 父控件不對你有任何限制颂龙,你想要多大給你多大,想上天就上天纽什。這種情況一般用于系統(tǒng)內(nèi)部措嵌,表示一種測量狀態(tài)。(這個模式主要用于系統(tǒng)內(nèi)部多次Measure的情形芦缰,并不是真的說你想要多大最后就真有多大)
EXACTLY 父控件已經(jīng)知道你所需的精確大小企巢,你的最終大小應(yīng)該就是這么大。
AT_MOST 你的大小不能大于父控件給你指定的size让蕾,但具體是多少浪规,得看你自己的實現(xiàn)。
MeasureSpecs 的意義
通過將 SpecMode 和 SpecSize 打包成一個 int 值可以避免過多的對象內(nèi)存分配探孝,為了方便操作笋婿,其提供了打包 / 解
包方法
MeasureSpec值的確定
MeasureSpec值到底是如何計算得來的呢?
子View的MeasureSpec值是根據(jù)子View的布局參數(shù)(LayoutParams)和父容器的MeasureSpec值計算得來的,具
體計算邏輯封裝在ViewGroup的getChildMeasureSpec()方法里
/**
*
* 目標(biāo)是將父控件的測量規(guī)格和child view的布局參數(shù)LayoutParams相結(jié)合顿颅,得到一個
* 最可能符合條件的child view的測量規(guī)格萌抵。
* @param spec 父控件的測量規(guī)格
* @param padding 父控件里已經(jīng)占用的大小
* @param childDimension child view布局LayoutParams里的尺寸
* @return child view 的測量規(guī)格
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //父控件的測量模式
int specSize = MeasureSpec.getSize(spec); //父控件的測量大小
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 當(dāng)父控件的測量模式 是 精確模式,也就是有精確的尺寸了
case MeasureSpec.EXACTLY:
//如果child的布局參數(shù)有固定值元镀,比如"layout_width" = "100dp"
//那么顯然child的測量規(guī)格也可以確定下來了绍填,測量大小就是100dp,測量模式也是EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局參數(shù)是"match_parent",也就是想要占滿父控件
//而此時父控件是精確模式,也就是能確定自己的尺寸了耽梅,那child也能確定自己大小了
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局參數(shù)是"wrap_content"野崇,也就是想要根據(jù)自己的邏輯決定自己大小,
//比如TextView根據(jù)設(shè)置的字符串大小來決定自己的大小
//那就自己決定唄阱高,不過你的大小肯定不能大于父控件的大小嘛
//所以測量模式就是AT_MOST,測量大小就是父控件的size
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父控件的測量模式 是 最大模式,也就是說父控件自己還不知道自己的尺寸锻霎,但是大小不能超過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);
}
針對上表,這里再做一下具體的說明
對于應(yīng)用層 View 戈盈,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定
對于不同的父容器和view本身不同的LayoutParams奠衔,view就可以有多種MeasureSpec。 1. 當(dāng)view采用固定寬
高的時候塘娶,不管父容器的MeasureSpec是什么归斤,view的MeasureSpec都是精確模式并且其大小遵循
Layoutparams中的大小血柳; 2. 當(dāng)view的寬高是match_parent時官册,這個時候如果父容器的模式是精準(zhǔn)模式,那么
view也是精準(zhǔn)模式并且其大小是父容器的剩余空間难捌,如果父容器是最大模式膝宁,那么view也是最大模式并且其大
小不會超過父容器的剩余空間; 3. 當(dāng)view的寬高是wrap_content時根吁,不管父容器的模式是精準(zhǔn)還是最大化员淫,
view的模式總是最大化并且大小不能超過父容器的剩余空間。 4. Unspecified模式击敌,這個模式主要用于系統(tǒng)內(nèi)
部多次measure的情況下介返,一般來說,我們不需要關(guān)注此模式(這里注意自定義View放到ScrollView的情況 需要
處理)。