自定義控件
在開(kāi)發(fā)中羹铅,開(kāi)發(fā)者常常會(huì)因?yàn)橄旅嫠膫€(gè)主要原因去自定義 View:
- 讓界面有特定的顯示風(fēng)格霞捡、效果坐漏;
- 讓控件具有特殊的交互方式;
- 優(yōu)化布局;
- 封裝赊琳;
1讓界面有特定的顯示風(fēng)格街夭、效果
在開(kāi)發(fā)中,Android SDK提供了很多控件慨畸,但有時(shí)莱坎,這些控件并不能滿足業(yè)務(wù)需求衣式。例如寸士,想要用一個(gè)折線圖來(lái)展示一組數(shù)據(jù),這時(shí)如果用系統(tǒng)提供的 View 就不能實(shí)現(xiàn)了碴卧,只能通過(guò)自定義 View 來(lái)實(shí)現(xiàn)弱卡。
2 讓控件具有特殊的交互方式
Android SDK提供的控件都有屬于它們自己的特定的交互方式,但有時(shí)住册,控件的默認(rèn)交互方式并不能滿足業(yè)務(wù)的需求婶博。例如,開(kāi)發(fā)者想要縮放 ImageView 中的圖片內(nèi)容荧飞,這時(shí)如果用系統(tǒng)提供的 ImageView 就不能實(shí)現(xiàn)了凡人,只能通過(guò)自定義 ImageView 來(lái)實(shí)現(xiàn)。
3 優(yōu)化布局
有時(shí)叹阔,有些布局如果用系統(tǒng)提供的控件實(shí)現(xiàn)起來(lái)相當(dāng)復(fù)雜挠轴,需要各種嵌套,雖然最終也能實(shí)現(xiàn)了想要的效果耳幢,但性能極差岸晦,此時(shí)就可以通過(guò)自定義 View 來(lái)減少嵌套層級(jí)、優(yōu)化布局睛藻。
4 封裝
有些控件可能在多個(gè)地方使用启上,如大多數(shù) App 里面的底部 Tab,頂部的標(biāo)題欄店印,像這樣的經(jīng)常被用到的控件就可以通過(guò)自定義 View 將它們封裝起來(lái)冈在,以便在多個(gè)地方使用。
自定義ViewGroup
以TopBar為例按摘,講解如何自定義ViewGroup來(lái)實(shí)現(xiàn)封裝的目的包券。可以使用xml文件院峡,也可以全部使用Java代碼兴使。
簡(jiǎn)單案例實(shí)現(xiàn)
首先說(shuō)明這個(gè)TopBar的功能。TopBar是作為放在屏幕最上方的標(biāo)題欄使用的照激。最主要的功能有兩個(gè)发魄,一個(gè)是左側(cè)的返回按鈕,一個(gè)是中心的文本,顯示當(dāng)前界面的標(biāo)題励幼。右側(cè)的功能按鈕或文字在不同的界面有不同的樣式汰寓,所以這里不管。
布局文件代碼
下面就是對(duì)應(yīng)的布局文件的代碼苹粟,
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="48dp"
android:background="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/btn_back"
android:layout_width="34dp"
android:layout_height="28dp"
android:layout_marginStart="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="5dp"
android:src="@drawable/ic_back_nav"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="80dp"
android:layout_marginEnd="80dp"
android:gravity="center"
android:lines="1"
android:textColor="@color/text_333333"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="標(biāo)題" />
</androidx.constraintlayout.widget.ConstraintLayout>
下面就是簡(jiǎn)單的對(duì)應(yīng)的Java代碼有滑。
構(gòu)造方法的詳細(xì)說(shuō)明見(jiàn)下面的小節(jié),這里只要覆寫前兩個(gè)構(gòu)造方法即可嵌削。
public class TopBar extends ConstraintLayout {
private ImageView mIvBack;
public TopBar(@NonNull Context context) {
this(context, null);
}
public TopBar(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// 這行代碼的意思就是加載layout_top_bar這個(gè)xml布局文件到TopBar這個(gè)對(duì)象中毛好。
// 就是將xml代碼與這個(gè)實(shí)例建立聯(lián)系。
LayoutInflater.from(context).inflate(R.layout.layout_top_bar, this);
// init方法是進(jìn)行一些初始化操作
init(attrs);
}
/**
* 一般都會(huì)有的方法苛秕,進(jìn)行初始化操作
*
* @param attrs 初始時(shí)可能用到的參數(shù)肌访,可以用來(lái)調(diào)用一些系統(tǒng)的方法
*/
private void init(AttributeSet attrs) {
// 統(tǒng)一處理設(shè)置左上角按鈕的返回點(diǎn)擊事件,如果是Activity艇劫,就調(diào)用onBackPressed方法
mIvBack = findViewById(R.id.btn_back);
mIvBack.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Context context = getContext();
if (context instanceof Activity) {
((Activity) context).onBackPressed();
}
}
});
}
}
到目前為止吼驶,這個(gè)TopBar其實(shí)只有一個(gè)功能,就是點(diǎn)擊了返回按鈕店煞,能夠返回上一個(gè)界面蟹演。
如何使用
和我們以前使用其他控件一樣使用就好,比如我想讓這個(gè)標(biāo)題欄顷蟀,放在某個(gè)界面的上面酒请,直接在xml文件中使用即可。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".module.animation.TweenActivity">
<cn.com.fkw.test.view.TopBar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
在xml中的預(yù)覽效果:
到這里我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題衩椒,就是標(biāo)題欄的標(biāo)題的文本似乎無(wú)法更改蚌父。
自定義屬性
其實(shí)就這樣使用,強(qiáng)行通過(guò)view.findViewById()的方式毛萌,也能修改自定義內(nèi)部的控件的屬性苟弛,但是不方便。我么可以通過(guò)自定義屬性的形式來(lái)更加方便的定制我們自己想要的屬性阁将。
比如我想在TopBar中添加一個(gè)叫centerText的屬性膏秫,來(lái)指定中間TextView顯示的文字。
實(shí)現(xiàn)方式
- 在values目錄下新建arrts.xml文件做盅。添加成為如下的代碼
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 首先哪個(gè)類要自定義屬性 -->
<declare-styleable name="TopBar">
<!-- 內(nèi)部是一個(gè)個(gè)標(biāo)簽缤削,name是寫在布局文件中的控件的屬性的名字 format是屬性值的格式 -->
<!-- 這個(gè)意思就是TopBar可以有一個(gè)叫centerText的屬性,值是字符串或者字符串引用 -->
<attr name="centerText" format="string" />
</declare-styleable>
</resources>
- 在init方法中獲取屬性值吹榴,然后設(shè)置進(jìn)TextView中
// 通過(guò)attrs獲取xml中寫的屬性值亭敢。這里只有centerText一個(gè)屬性
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.TopBar);
String centerText = typedArray.getString(R.styleable.TopBar_centerText);
// 這里必須調(diào)用typedArray.recycle()方法
typedArray.recycle();
// 然后將屬性設(shè)置給TextView
TextView tv_title = findViewById(R.id.tv_title);
tv_title.setText(centerText);
-
使用。
在布局文件中图筹,使用對(duì)應(yīng)的屬性帅刀。這樣運(yùn)行之后让腹,我們就能看到標(biāo)題欄的文字是微信兩個(gè)字了
<cn.com.fkw.test.view.TopBar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:centerText="微信"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
詳細(xì)解釋
- attrs.xml文件
在這個(gè)文件中都是自定義屬性相關(guān)的內(nèi)容。格式和案例一樣就行扣溺。需要額外說(shuō)明的是format的種類骇窍。
format | 類型 | 說(shuō)明 |
---|---|---|
string | 字符串 | |
integer | 整數(shù) | |
float | 小數(shù) | |
color | 顏色 | |
boolean | 布爾值 | |
dimension | 尺寸 | |
enum | 枚舉 | 在幾個(gè)選項(xiàng)中選擇一個(gè) |
reference | 參考的某一資源ID | |
flag | 位或運(yùn)算 | 在幾個(gè)選項(xiàng)中選擇多個(gè) |
fraction | 百分?jǐn)?shù) |
位或運(yùn)算
<!-- 聲明 -->
<attr name="gravity">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="center_vertical" value="0x10" />
</attr>
<!-- 使用 -->
<TextView android:gravity="bottom|left"/>
通過(guò)代碼設(shè)置屬性
在init方法中,findViewById找到控件,將控件設(shè)置為成員變量,然后提供一些應(yīng)有的方法即可杭跪。
View的構(gòu)造方法
一共有4個(gè),我們一般情況下會(huì)復(fù)寫前兩個(gè)構(gòu)造方法挑随,且互相調(diào)用
/**
* 一般情況下我們都是覆寫 前兩個(gè)構(gòu)造方法
*/
public class CircleView extends View {
/**
* 在Java中創(chuàng)建一個(gè)新的CircleView對(duì)象時(shí)使用。
* 如果你需要在Java中新建這個(gè)View,必須覆寫這個(gè)構(gòu)造方法
*
* @param context
*/
public CircleView(Context context) {
// 這里直接調(diào)用參數(shù)較多的方法,保證自定義View的某些初始化動(dòng)作一定執(zhí)行
this(context, null);
}
/**
* 在xml中添加的控件蛔钙,被渲染成View時(shí),會(huì)調(diào)用這個(gè)方法荠医。
* 所以如果想要在xml中使用這個(gè)控件,這個(gè)構(gòu)造方法必須復(fù)寫桑涎。
*
* @param context
* @param attrs 如果有自定義的屬性彬向,需要用到這個(gè)參數(shù)
*/
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 與主題相關(guān),如果你不需要當(dāng)前的View隨主題的變化而有更改攻冷,就不需要復(fù)寫這個(gè)構(gòu)造方法娃胆。
*/
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 與主題相關(guān),如果你不需要當(dāng)前的View隨主題的變化而有更改等曼,就不需要復(fù)寫這個(gè)構(gòu)造方法里烦。
*/
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
自定義View
用一個(gè)最簡(jiǎn)單的案例演示如何繼承View,實(shí)現(xiàn)自定義View禁谦。
畫一個(gè)圓
Android的View的繪制流程
我們一般需要處理三個(gè)方法胁黑。View顯示在屏幕上,也是經(jīng)歷三個(gè)過(guò)程州泊,測(cè)量丧蘸、布局和繪制。
測(cè)量:onMeasure
在 View 的測(cè)量階段會(huì)執(zhí)行兩個(gè)方法(在測(cè)量階段遥皂,View 的父 View 會(huì)通過(guò)調(diào)用 View 的 measure() 方法將父 View 對(duì) View 尺寸要求傳進(jìn)來(lái)力喷。緊接著 View 的 measure() 方法會(huì)做一些前置和優(yōu)化工作,然后調(diào)用 View 的 onMeasure() 方法演训,并通過(guò) onMeasure() 方法將父 View 對(duì) View 的尺寸要求傳入弟孟。在自定義 View 中,只有需要修改 View 的尺寸的時(shí)候才需要重寫 onMeasure() 方法样悟。在 onMeasure() 方法中根據(jù)業(yè)務(wù)需求進(jìn)行相應(yīng)的邏輯處理拂募,并在最后通過(guò)調(diào)用 setMeasuredDimension() 方法告知父 View 自己的期望尺寸)。
onMeasure() 計(jì)算 View 期望尺寸方法如下:
- 參考父 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 的期望尺寸(通過(guò)調(diào)用 resolveSize() 方法)眯娱;
- 通過(guò) setMeasuredDimension() 保存 View 的期望尺寸(實(shí)際上是通過(guò) setMeasuredDimension() 告知父 View 自己的期望尺寸);
具體的測(cè)量過(guò)程很復(fù)雜。不再贅述爬凑。想要了解徙缴,請(qǐng)看Android自定義View的測(cè)量過(guò)程詳解。我們需要了解的知識(shí)有下面這些:
MeasureSpec,有人叫它測(cè)量規(guī)格嘁信,我更喜歡把它描述成為測(cè)量過(guò)程中必不可少的工具——尺子于样。
這個(gè)尺子有兩種用法,橫著用就叫做widthMeasureSpec潘靖,用來(lái)測(cè)量寬度穿剖,豎著用就叫做heightMeasureSpec,用來(lái)測(cè)量高度的,不管你的自定義View是什么千奇百怪的形狀卦溢,他都是要放在一個(gè)矩形中進(jìn)行包裹展示的糊余,那么為什么會(huì)有這兩個(gè)測(cè)量方式也就不難理解了。
這個(gè)尺子有兩個(gè)重要的功能单寂,第一個(gè)功能自然是測(cè)量值了(Size)贬芥,第二個(gè)功能是測(cè)量的模式(Mode),這兩個(gè)參數(shù)通過(guò)二進(jìn)制將其打包成一個(gè)int(32位)值來(lái)減少對(duì)內(nèi)存的分配,其高2位(31,32位)存放的是測(cè)量模式宣决,而低30位則存儲(chǔ)的是其測(cè)量值蘸劈。測(cè)量模式(specMode)
測(cè)量模式分為三種:
- UNSPECIFIED模式:本質(zhì)就是不限制模式,父視圖不對(duì)子View進(jìn)行任何約束尊沸,View想要多大要多大威沫,想要多長(zhǎng)要多長(zhǎng),這個(gè)在我們寫自定義View中的時(shí)候非常少見(jiàn)洼专,一般都是系統(tǒng)內(nèi)部在設(shè)置ListView或者是ScrollView的時(shí)候才會(huì)用到棒掠。
- EXACTLY模式:該模式其實(shí)對(duì)應(yīng)的場(chǎng)景就是match_parent或者是一個(gè)具體的數(shù)據(jù)(50dp或80px),父視圖為子View指定一個(gè)確切的大小壶熏,無(wú)論子View的值設(shè)置多大句柠,都不能超出父視圖的范圍。
- AT_MOST模式:這個(gè)模式對(duì)應(yīng)的場(chǎng)景就是wrap_content棒假,其內(nèi)容就是父視圖給子View設(shè)置一個(gè)最大尺寸溯职,子View只要不超過(guò)這個(gè)尺寸即可。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 獲取寬
// 方式一:
int width1 = MeasureSpec.getSize(widthMeasureSpec);
// 方式二:
int width2 = getMeasuredWidth();
// 獲取寬
// 方式一:
int height1 = MeasureSpec.getSize(heightMeasureSpec);
// 方式二:
int height2 = getMeasuredHeight();
// 上面補(bǔ)充自己的邏輯帽哑,更改控件的寬高谜酒,最后記得調(diào)用 setMeasuredDimension 將測(cè)量的寬高設(shè)置回去
setMeasuredDimension(100,100);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
setMeasuredDimension(width, height);
}
public static int resolveSize(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;
}
布局:onLayout
layout() : 保存 View 的實(shí)際尺寸。調(diào)用 setFrame() 方法保存 View 的實(shí)際尺寸妻枕,調(diào)用 onSizeChanged() 通知開(kāi)發(fā)者 View 的尺寸更改了僻族,并最終會(huì)調(diào)用 onLayout() 方法讓子 View 布局(如果有子 View 的話粘驰。因?yàn)樽远x View 中沒(méi)有子 View,所以自定義 View 的 onLayout() 方法是一個(gè)空實(shí)現(xiàn))述么;
onLayout() : 空實(shí)現(xiàn)蝌数,什么也不做,因?yàn)樗鼪](méi)有子 View度秘。如果是 ViewGroup 的話顶伞,在 onLayout() 方法中需要調(diào)用子 View 的 layout() 方法,將子 View 的實(shí)際尺寸傳給它們剑梳,讓子 View 保存自己的實(shí)際尺寸唆貌。因此,在自定義 View 中垢乙,不需重寫此方法锨咙,在自定義 ViewGroup 中,需重寫此方法追逮。
繪制:onDraw
在 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() : 繪制背景的方法情妖,不能重寫,只能通過(guò) xml 布局文件或者 setBackground() 來(lái)設(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 前景的方法丑慎,也就是說(shuō)喜喂,想要在主體內(nèi)容之上繪制東西的時(shí)候就可以在該方法中實(shí)現(xiàn)瓤摧。
注意:
Android 里面的繪制都是按順序的,先繪制的內(nèi)容會(huì)被后繪制的蓋住玉吁。
Paint和Canvas
參考資料
Android自定義View的測(cè)量過(guò)程詳解
詳解安卓MeasureSpec及其和match_parent照弥、wrap_content的關(guān)系
match_parent、wrap_parent进副、具體值 和 MeasureSpec 類中 mode 的對(duì)應(yīng)關(guān)系
Android BitmapShader 實(shí)戰(zhàn) 實(shí)現(xiàn)圓形这揣、圓角圖片——hongyang