簡介
說到View橄浓,只要是接觸過Android的朋友份氧,應(yīng)該都知道是什么東西,他就是承載各種數(shù)據(jù)顯示的控件诗茎,比如文本常常使用TextView來顯示工坊,圖片->ImageView,列表->RecyclerView敢订;在這篇文章中會從以下幾個方面來介紹我所感悟的View王污。
本文包括:
- 如何自定義View;
自定義View
首先我們需要了解為什么要自定義View楚午?在我看來昭齐,比較常見的其目的有以下兩種:
- 解決系統(tǒng)提供的View難以滿足界面實(shí)現(xiàn)的需求;
- 封裝某一特定View組合矾柜,實(shí)現(xiàn)代碼的復(fù)用阱驾;
那要如何實(shí)現(xiàn)自定義View,下邊會用一些簡單的例子來說明怪蔑;當(dāng)然實(shí)現(xiàn)方式也是多種多樣里覆,我認(rèn)為可以分為以下幾種情況:
-
自定義View:
- 繼承View并進(jìn)一步實(shí)現(xiàn);
- 繼承已有的View并進(jìn)一步實(shí)現(xiàn)缆瓣。
-
自定義ViewGroup:
- 繼承ViewGroup并進(jìn)一步實(shí)現(xiàn)喧枷;
- 繼承已有的ViewGroup并實(shí)現(xiàn);
接下來借用例子逐一實(shí)現(xiàn),首先從自定義View開始割去,繼承View并實(shí)現(xiàn)窟却,接下來要實(shí)現(xiàn)的是以左上角為直角的等腰直角三角形;
第一步:創(chuàng)建一個View取名叫TriangleView繼承自View并實(shí)現(xiàn)其構(gòu)造方法呻逆,如下:
public class TriangleView extends View {
public TriangleView(Context context) {
this(context, null);
}
public TriangleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TriangleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
這一步中有一個細(xì)節(jié)和一個要點(diǎn)夸赫。
細(xì)節(jié)就是構(gòu)造函數(shù)之間的調(diào)用,如果在不清楚你所繼承的View構(gòu)造方法的調(diào)用中是否有其他配置的話(例如主題樣式)咖城,最好還是直接調(diào)用對應(yīng)參數(shù)的super構(gòu)造函數(shù)茬腿;
要點(diǎn)就是必須重寫帶有Attibuteset參數(shù)的構(gòu)造參數(shù),這樣我們才能拿到在布局的xml文件中的參數(shù)宜雀。
第二步:想好自定義View所包含的屬性切平,這里我們只需要三角形的填充色,實(shí)現(xiàn)如下:
首先在資源文件中創(chuàng)建一個declare-styleable資源辐董,一般寫在values下的attrs文件中悴品,如下:
<declare-styleable name="TriangleView">
<attr name="fillColor" format="color" />
</declare-styleable>
這里需要注意的是名字最好與自定義View的名字一致,方便查找简烘,也更加規(guī)范苔严;
attr標(biāo)簽中就是自定義的屬性,包含了名稱和類型孤澎,其中類型有很多種届氢,下邊也整理了一個表格,描述各個屬性的含義覆旭,如下:
format | meaning |
---|---|
color | 顏色值退子,如:"#ff0000"或"R.color.colorAccent" |
dimension | 尺寸,如:單位為sp,dp型将,px的值 |
reference | 資源id寂祥,如:"R.drawable.img_loading" |
boolean | 布爾類型,如:true or false |
float | 浮點(diǎn)小數(shù)七兜,如:0.1 |
integer | 整數(shù)壤靶,如:10 |
string | 字符串,如:"你好啊"或"R.string.app_name" |
fraction | 百分比惊搏,如:20% |
enum | 枚舉,具體使用請參照LinearLayout的orientation屬性 |
flag | 標(biāo)記忧换,即某個屬性可以有多個標(biāo)記恬惯,相對于枚舉而言,枚舉只能選一個值亚茬,這個可以選多個酪耳,例如layout_gravity屬性 |
定義好了,接下來就是設(shè)置屬性和獲取屬性了,屬性的設(shè)置可在xml布局文件中設(shè)置碗暗,例如:
<com.arvin.example.allibraryexample.TriangleView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fillColor="@color/colorAccent" />
注意颈将,在使用自定義屬性是需要該View或者其父View中加上
xmlns:app="http://schemas.android.com/apk/res-auto"
其作用就是能自動找到該View所定義的屬性,使用方式就是
app:自定義屬性
然后就是獲取屬性言疗,如下:
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TriangleView);
fillColor = typedArray.getColor(R.styleable.TriangleView_fillColor, Color.RED);
typedArray.recycle();
attrs中包含了晴圾,在xml布局文件中所有的屬性,系統(tǒng)自帶屬性可通過調(diào)用父類的構(gòu)造函數(shù)獲得噪奄,自定義屬性死姚,即可通過TypedArray獲得,每種自定義屬性的類型都在TypedArray中對應(yīng)著一個get方法勤篮,等我們?nèi)⊥晁阉鲗傩院蠖级荆浀靡猺ecycler掉,原因就是為了復(fù)用這個TypedArray碰缔,具體原因在這里就不再細(xì)說了账劲,總之這個模式這樣用很好。
第三步金抡,定義View的大小
這其中就涉及到一個比較重要的方法了瀑焦,需要重寫
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
}
方法,然后設(shè)置寬度和高度竟终,這里有一點(diǎn)小常識蝠猬,Android中所有的View外形都是一個矩形,只是繪制時统捶,不同的方法導(dǎo)致形狀不同而已榆芦。言歸正傳,這時候就需要講解一下計算大小的規(guī)則了喘鸟,Android提供了一個MeasureSpec匆绣,包含了測量模式,以及測量尺寸什黑,測量模式包括:
UNSPECIFIED
父視圖不對子視圖有任何約束崎淳,它可以達(dá)到所期望的任意尺寸。比如 ListView愕把、ScrollView拣凹,一般自定義 View 中用不到EXACTLY
父視圖為子視圖指定一個確切的尺寸,而且無論子視圖期望多大恨豁,它都必須在該指定大小的邊界內(nèi)嚣镜,對應(yīng)的屬性為 match_parent 或具體值,比如 100dp橘蜜,父控件可以通過MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸菊匿。AT_MOST
父視圖為子視圖指定一個最大尺寸。子視圖必須確保它自己所有子視圖可以適應(yīng)在該尺寸范圍內(nèi),對應(yīng)的屬性為 wrap_content跌捆,這種模式下徽职,父控件無法確定子 View 的尺寸,只能由子控件自己根據(jù)需求去計算自己的尺寸佩厚,這種模式就是我們自定義視圖需要實(shí)現(xiàn)測量邏輯的情況姆钉。
簡單的了解過后我們參考o(jì)nMeasure的默認(rèn)實(shí)現(xiàn),如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public int getMinimumHeight() {
return mMinHeight;
}
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:
result = specSize;
break;
}
return result;
}
代碼很簡單可款,可以發(fā)現(xiàn)育韩,其實(shí)就是通過調(diào)用setMeasuredDimension方法,設(shè)置了寬度和高度闺鲸,默認(rèn)情況下筋讨,wrap_content時,size為0摸恍,而我希望有一點(diǎn)高度和寬度悉罕,所以,我們自己寫一個getSize方法立镶,并讓測量模式為wrap_content時壁袄,有一個默認(rèn)的大小,具體如下:
private int getSize(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:
result = dp2px(DEFAULT_SIZE);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
private int dp2px(float dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
然后畫出來的三角形媚媒,我希望是等邊直角三角形嗜逻,所以要讓寬和高一樣大,這樣簡單處理一下后缭召,大小就設(shè)置完了栈顷,具體的onMeasure實(shí)現(xiàn)如下:
@SuppressWarnings("SuspiciousNameCombination")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int height = getSize(getSuggestedMinimumWidth(), heightMeasureSpec);
if (width > height) {
width = height;
} else {
height = width;
}
setMeasuredDimension(width, height);
}
第四步,重寫onDraw方法嵌巷,這里就是繪制三角形萄凤,代碼很簡單,就不解釋了搪哪,如下:
//在構(gòu)造函數(shù)中調(diào)用靡努,初始化畫筆
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(fillColor);
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(getWidth(), 0);
path.lineTo(0, getHeight());
path.close();
canvas.drawPath(path, mPaint);
}
這樣,以左上角為直角的等邊直角三角形就畫好了晓折。
對于繼承特定View來實(shí)現(xiàn)自定義View惑朦,也不外乎上邊四步,所以就不再介紹了漓概;
接下來就是實(shí)現(xiàn)一個自定義ViewGroup行嗤,其中屬性的創(chuàng)建和獲取以及繪制與自定義View一致,就不再贅述垛耳,而更為重要的就是onMeasure以及onLayout方法,ViewGroup在我看來更像一個管理者,把自己的子View管理起來堂鲜,管理其大小栈雳,安排其位置。
下面缔莲,我們就來做一個自定義的ViewGroup哥纫,功能就是流布局,從上到下痴奏,從左到右蛀骇,把子View依次排列;我們可以想象一下實(shí)現(xiàn)思路读拆,首先擅憔,其ViewGroup的大小如何設(shè)置,很明顯檐晕,需要分別考慮AT_MOST和EXACTLY兩種模式(UNSPECIFIED這里不作考慮)下寬度和高度如何計量暑诸,思路如下:
-
AT_MOST
寬度:只要等于寬度最大的一行的寬度加上paddingLeft和paddingRight后與父容器能給的最大塊度比較,較小的就是最終的寬度辟灰;
高度:把每一行的高度加起來再加上paddingTop和paddingBottom后與父容器能給的最大高度比較个榕,較小的就是最終的高度;
-
EXACTLY
寬度:其父容器能給的最大寬度芥喇;
高度:其父容器能給的最大高度西采;
其中有一個邏輯就是什么時候換行,其實(shí)很明顯继控,就是當(dāng)這一行累積的子View的寬度大于容器的寬度時械馆,最后一個累積的View需要換行。換行后的高度湿诊,就在上一行的下邊開始狱杰。
下邊就是具體的代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int horizontalPadding = getPaddingLeft() + getPaddingRight();
int verticalPadding = getPaddingTop() + getPaddingBottom();
int width = 0;
int height = 0;
int lineWidth = 0;
int lineHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
measureChild(child, widthMeasureSpec, heightMeasureSpec);
int childWidth = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
if (childWidth + lineWidth > widthSize - horizontalPadding) {
width = Math.max(width, lineWidth);
height += lineHeight;
lineWidth = childWidth;
lineHeight = childHeight;
} else {
lineWidth += childWidth;
lineHeight = Math.max(childHeight, lineHeight);
}
if (i == getChildCount() - 1) {
width = Math.max(width, lineWidth) + horizontalPadding;
height += lineHeight + verticalPadding;
}
}
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
heightMode == MeasureSpec.EXACTLY ? heightSize : height);
}
首先獲取到父容器為該view分配的最大測量尺寸以及測量模型,目的就是限制最大的寬度厅须,這里高度就沒有做限制仿畸,子View有多少都給顯示出來;
定義變量朗和,width和height表示最大的寬度和高度错沽;lineWidth和lineHeight為了記錄當(dāng)前行最大寬度和高度;horizontalPadding和verticalPadding就是pading眶拉,很簡單就不解釋了千埃。
然后就是遍歷所有子View,通過measureChild方法忆植,測量一下子View放可,然后就能獲取到子View的大小了(這里我們還支持了margin)谒臼;
接下來就是最重要的邏輯了,就是是否換行的邏輯耀里,即當(dāng)加上當(dāng)前子View后寬度是否比內(nèi)容最大寬度大蜈缤,大則換行,小則繼續(xù)累加冯挎;換行后底哥,行寬從新計算,行高累積房官,遍歷完后趾徽,就能得到一個承載搜索子View的寬高,然后再看測量模型翰守,來決定如何設(shè)值孵奶,MeasureSpec.EXACTLY則就是父容器所設(shè)定的值,MeasureSpec.AT_MOST則就是計算出來的寬高潦俺,這樣就把這個子View的大小測量完畢了拒课。
其中又一點(diǎn)需要注意,是讓View具有margin事示,則需要生成一個LayoutParams早像,如下:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
最后就是把子View排位,這一步和測量時邏輯幾乎相同肖爵,唯一不同就是卢鹦,需要讓子View調(diào)用layout方法,安排自己的位置劝堪,代碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
int horizontalPadding = getPaddingLeft() + getPaddingRight();
int lineWidth = 0;
int lineHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
int childHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
int cl, ct, cr, cb;
if (childWidth + lineWidth > getWidth() - horizontalPadding) {
left = getPaddingLeft();
top += lineHeight;
cl = left + params.leftMargin;
ct = top + params.topMargin;
cr = cl + child.getMeasuredWidth();
cb = ct + child.getMeasuredHeight();
left += childWidth;
lineWidth = childWidth;
lineHeight = childHeight;
} else {
cl = left + params.leftMargin;
ct = top + params.topMargin;
cr = cl + child.getMeasuredWidth();
cb = ct + child.getMeasuredHeight();
left += childWidth;
lineWidth += childWidth;
lineHeight = Math.max(childHeight, lineHeight);
}
child.layout(cl, ct, cr, cb);
}
}
變量解釋:
left就是每一個View的最左邊冀自,top就是每一個View的最上邊;cl,ct,cr,cb分別表示當(dāng)前子View的左上右下的位置秒啦;
邏輯結(jié)構(gòu)都是一樣熬粗,就是在每一個View判斷完是否換行后,計算出其坐標(biāo)余境,并layout驻呐,計算過程也很簡單,如果不換行就改變left的值芳来,保持top不變含末,以及根據(jù)子View設(shè)置右下的位置,換行時即舌,初始化left佣盒,top值在上一行的下邊,這一步layout后記得更改left的值顽聂,這樣子View的布局就搞定了肥惭。測試運(yùn)行盯仪,發(fā)現(xiàn)剛剛好。
還有其他的一些自定義ViewGroup蜜葱,但是掌握了最核心的原理后磨总,發(fā)現(xiàn)每一個都是這樣,所以還有一些實(shí)現(xiàn)方式就不再距離說明了笼沥。
這樣自定義View就完成了。