前言
下面文章中涉及到的代碼全部可以在我的github上得到:https://github.com/celesteshire/TestView
Android 中已經(jīng)提供了很多的 View 給我們使用,但是有時(shí)候因?yàn)樘厥庑枨蟮脑颍@些 View 并不能滿足需求,這個(gè)時(shí)候就需要自己來設(shè)計(jì) View 。通常在自定義 View 的時(shí)候需要重寫 onDraw() 方法來繪制需要顯示的內(nèi)容,如果這個(gè) View 需要使用 wrap_content 屬性,還需要重寫 onMeasure() 方法,對(duì)于前言不明白的可以看看我的另一篇文章:
在 View 中有以下一些常用的方法:
- onFinishInflate() -- 從 XML 加載組件后的回調(diào)
- onSizeChanged() -- 組件大小發(fā)生變化時(shí)的回調(diào)
- onMeasure() -- 回調(diào)此方法對(duì)組件大小進(jìn)行測(cè)量
- onLayout() -- 回調(diào)該方法來確定顯示的位置
- onTouchEvent() -- 監(jiān)聽到觸摸事件時(shí)的回調(diào)
并不需要重寫以上所有的方法,根據(jù)自己的需求重寫其中的部分方法即可,通常有三種方法來實(shí)現(xiàn)自定義控件。
- 擴(kuò)展 -- 對(duì)現(xiàn)有控件進(jìn)行擴(kuò)展
- 組合 -- 將不同的控件組合在一起形成新的空間
- 重寫 -- 通過重寫來實(shí)現(xiàn)全新的控件
擴(kuò)展
這是自定義 View 中重點(diǎn)方法之一,他可以在原生控件的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,修改 UI 顯示效果等,下面以一個(gè) TextView 為例子,看看如何對(duì)他進(jìn)行擴(kuò)展,比如如何讓一個(gè) TextView 的背景更加豐富,字體絢爛等 。
先看看普通的TextView 。

要修改他的顯示效果,應(yīng)該重寫它的 onDraw() 方法。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
程序調(diào)用 super.onDraw(canvas) 方法來實(shí)現(xiàn)原生控件的繪制,要想在這基礎(chǔ)上進(jìn)行實(shí)現(xiàn)自己的邏輯,可以在方法的前后添加代碼,在方法前添加的,就是繪制在原生 TextView 的底層,在方法后添加的,就是繪制在原生 TextView 的上層。
我用一個(gè)實(shí)例來表現(xiàn)這個(gè)層疊關(guān)系,這是一個(gè)自定義的 TextView,我重寫了 onDraw() 方法。
public class OneTextView extends TextView {
private Paint mPaint;
public OneTextView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
//這部分是在原生控件繪制之前進(jìn)行繪制的,位于原生控件繪制區(qū)域底層,不會(huì)遮擋其他內(nèi)容
//第一個(gè)矩形,大小跟此控件一樣大,位于底層
mPaint.setColor(Color.BLUE);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
//第二個(gè)矩形,稍微小一點(diǎn),但是會(huì)遮擋第一個(gè)矩形
mPaint.setColor(Color.RED);
canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mPaint);
//原生控件開始繪制,會(huì)遮擋上面代碼繪制的內(nèi)容。
super.onDraw(canvas);
//第三個(gè)矩形,這是在原生控件繪制完畢后進(jìn)行繪制的,位于原生控制的上層,會(huì)遮擋所有之前繪制的內(nèi)容
mPaint.setColor(Color.GREEN);
canvas.drawRect(20, 20,200, 100, mPaint);
}
}
看看最后的效果

可以看到最底層的藍(lán)色矩形,第二層的紅色矩形,第三層的文字,第四層的綠色矩形(從底層往上數(shù)),這下可以直觀的理解在繪制過程中的層疊覆蓋關(guān)系了吧,了解這個(gè)對(duì)于以后自定義控件的時(shí)候很有幫助。
下面我進(jìn)行一個(gè)比較復(fù)雜的自定義 TextView 。字體能夠?qū)崿F(xiàn)漸變效果,這里我會(huì)用到 LinearGradient 以及 Matrix ,如果不了解可以查閱:
LinearGradient
Matrix
public class TwoTextView extends TextView {
int mViewWidth;
LinearGradient mLinearGradient;
Matrix mMatrix;
Paint mPaint;
int mTranslate=0;
public TwoTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//在 onSizeChanged 方法中獲取到寬度,并對(duì)各個(gè)類進(jìn)行初始化
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
//得到 父類 TextView 中寫字的那支筆。。。
mPaint = getPaint();
//初始化線性渲染器 不了解的請(qǐng)看上面連接
mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
new int[]{Color.BLUE, Color.YELLOW, Color.RED, Color.GREEN}, null, Shader.TileMode.CLAMP);
//把渲染器給筆套上
mPaint.setShader(mLinearGradient);
//初始化 Matrix
mMatrix = new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
//先讓父類方法執(zhí)行,由于上面我們給父類的 Paint 套上了渲染器,所以這里出現(xiàn)的文字已經(jīng)是彩色的了
super.onDraw(canvas);
if (mMatrix != null) {
//利用 Matrix 的平移動(dòng)作實(shí)現(xiàn)霓虹燈的效果,這里是每次滾動(dòng)1/10
mTranslate += mViewWidth / 10;
//如果滾出了控件邊界,就要拉回來重置開頭,這里重置到了屏幕左邊的空間
if (mTranslate > mViewWidth) {
mTranslate = -mViewWidth/2;
}
//設(shè)置平移距離
mMatrix.setTranslate(mTranslate, 0);
//平移效果生效
mLinearGradient.setLocalMatrix(mMatrix);
//延遲 100 毫秒再次刷新 View 也就是再次執(zhí)行本 onDraw 方法
postInvalidateDelayed(100);
}
}
}

炫酷吧,重點(diǎn)其實(shí)在于如何利用好 LinearGradient 和 Matrix 還有 Paint , Canvas 也很重要,這些在自定義 View 中都是經(jīng)常用到的。
組合
有的時(shí)候其實(shí)可以使用幾個(gè)基本控件組合在一起,形成一個(gè)新的控件。這種方式通常都需要繼承一個(gè)合適的 ViewGroup ,再給他添加指定功能的控件,形成新的空間。通過這種方式創(chuàng)建的控件我們還可以給他指定一些可配置的屬性,增強(qiáng)它的可操控性,下面以一個(gè)標(biāo)題欄為例子來說明如何創(chuàng)建組合控件。
一個(gè)標(biāo)題欄通常貫穿了整個(gè)應(yīng)用程序的大部分界面,大部分布局是左右兩個(gè) Button ,中間一個(gè) TextView ,如果每個(gè)頁(yè)面都去寫一次,那未免太過繁瑣了,我把它抽象出來,形成一個(gè)通用的標(biāo)題欄,并且可以任意更改按鈕文字和標(biāo)題文字等屬性,還要提供接口給調(diào)用者操作點(diǎn)擊事件。
所以,我們需要對(duì)這個(gè)標(biāo)題欄創(chuàng)建一些自定義屬性,只需要在 res 資源目錄的 values 目錄下創(chuàng)建一個(gè) attrs.xml 文件,并添加代碼即可,看看下面的代碼吧。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="titleText" format="string"/>
<attr name="titleTextSize" format="dimension"/>
<attr name="titleColor" format="color"/>
<attr name="leftText" format="string"/>
<attr name="leftTextColor" format="color"/>
<attr name="leftBackground" format="color|reference"/>
<attr name="rightText" format="string"/>
<attr name="rightTextColor" format="color"/>
<attr name="rightBackground" format="color|reference"/>
</declare-styleable>
</resources>
通過 declare-styleable 標(biāo)簽表示使用自定義屬性,通過 name 屬性來表示引用的名稱,通過 format 來確定屬性的類型,這里定義的東西就是我們平時(shí)寫布局文件 XML 的時(shí)候調(diào)用的屬性啦。這里分別設(shè)置了以下屬性:
| 屬性 | 解釋 |
|---|---|
| titleText | 標(biāo)題文本 |
| titleColor | 標(biāo)題文本顏色 |
| titleTextSize | 標(biāo)題文本字體大小 |
| leftText | 左邊按鈕文本 |
| leftTextColor | 左邊按鈕文本顏色 |
| leftBackground | 左邊按鈕背景 |
| rightText | 右邊按鈕文本 |
| rightTextColor | 右邊按鈕文本顏色 |
| rightBackground | 右邊按鈕背景 |
上面是對(duì)這個(gè)自定義標(biāo)題欄的diy參數(shù),那么在布局 XML 文件中應(yīng)該如何使用?
<com.shire.testview.MyTopBar
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/mytopbar"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="#3F51B5"
custom:leftBackground="#ffffff"
custom:leftText="Back"
custom:leftTextColor="#000000"
custom:rightBackground="#ffffff"
custom:rightText="+"
custom:rightTextColor="#000000"
custom:titleColor="#ffffff"
custom:titleText="test"
custom:titleTextSize="8sp"
>
以上,注意第二行加入了一個(gè)命名空間 “http://schemas.android.com/apk/res-auto”,這個(gè)命名空間是使用自定義參數(shù)的時(shí)候使用的,這里我命名為了 custom 。
完成 xml 文件的編寫后,來看看這個(gè)自定義標(biāo)題欄的類應(yīng)該怎么寫,這個(gè)自定義的標(biāo)題欄需要繼承一個(gè) ViewGroup 來包裹多個(gè)普通 View , 這里繼承的是 RelativeLayout。
public class MyTopBar extends RelativeLayout {
//定義了各個(gè)控件的屬性變量
String titleText, leftText, rightText;
int titleColor, leftTextColor, rightTextColor;
float titleTextSize;
Drawable leftBackground, rightBackground;
Button rightButton, leftButton;
TextView titleTextView;
MyTopBarClickListener myTopBarClickListener;
public MyTopBar(Context context, AttributeSet attrs) {
super(context, attrs);
//通過 TypedArray 可以從 XML 文件中取出相應(yīng)的屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
//使用R.styleable.xxxxx 就可以取出對(duì)應(yīng)數(shù)據(jù)類型的具體數(shù)據(jù),第二個(gè)參數(shù)是默認(rèn)值 如果取值為空就會(huì)使用默認(rèn)值
titleText = typedArray.getString(R.styleable.TopBar_titleText);
titleTextSize = typedArray.getDimension(R.styleable.TopBar_titleTextSize, 20);
titleColor = typedArray.getColor(R.styleable.TopBar_titleColor, 0);
leftText = typedArray.getString(R.styleable.TopBar_leftText);
leftTextColor = typedArray.getColor(R.styleable.TopBar_leftTextColor, 0);
leftBackground = typedArray.getDrawable(R.styleable.TopBar_leftBackground);
rightText = typedArray.getString(R.styleable.TopBar_rightText);
rightTextColor = typedArray.getColor(R.styleable.TopBar_rightTextColor, 0);
rightBackground = typedArray.getDrawable(R.styleable.TopBar_rightBackground);
typedArray.recycle();
}
通過上面部分的代碼,我們?nèi)〕隽嗽?XML 文件中進(jìn)行設(shè)置的參數(shù),接下來應(yīng)該是構(gòu)造子控件,賦予參數(shù),并添加到此 ViewGroup 中。
public void initView() {
//創(chuàng)建三個(gè)控件對(duì)象
rightButton = new Button(MainActivity.mContext);
leftButton = new Button(MainActivity.mContext);
titleTextView = new TextView(MainActivity.mContext);
//將之前得到的相應(yīng)屬性賦給相應(yīng)的對(duì)象
rightButton.setText(rightText);
rightButton.setTextColor(rightTextColor);
rightButton.setBackground(rightBackground);
leftButton.setText(leftText);
leftButton.setTextColor(leftTextColor);
leftButton.setBackground(leftBackground);
titleTextView.setText(titleText);
titleTextView.setTextSize(titleTextSize);
titleTextView.setTextColor(titleColor);
//對(duì)各個(gè)子控件設(shè)置布局元素,并將它添加到此 ViewGroup 中。
LayoutParams leftParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
leftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
addView(leftButton, leftParams);
LayoutParams rightParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(rightButton, rightParams);
LayoutParams titleParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
titleParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(titleTextView, titleParams);
}
做完這一步,這個(gè)自定義標(biāo)題欄其實(shí)就已經(jīng)可以顯示了,來看看吧。

雖然丑了一點(diǎn),不過還算能用。。做實(shí)驗(yàn)就不講究那么多了。到此,就實(shí)現(xiàn)了這個(gè)自定義的標(biāo)題欄,可以自己設(shè)置9個(gè)屬性以及其他系統(tǒng)自帶的屬性,那么僅此就夠了嗎?或許還應(yīng)該給兩邊的按鈕加上點(diǎn)擊事件,但是不能直接寫,直接寫的話就成了固定的事件了,要開放一個(gè)接口給調(diào)用者,讓調(diào)用者自己來編寫邏輯代碼,這樣才能起到復(fù)用的作用。
/**
* 定義一個(gè)接口 讓調(diào)用者自己實(shí)現(xiàn)具體邏輯
*/
public interface MyTopBarClickListener {
void leftClick();
void rightClick();
}
有了接口還要給外部調(diào)用者一個(gè)公開的方法來設(shè)置接口
/**
* 開放一個(gè)方法給外部來設(shè)置點(diǎn)擊事件,參數(shù)用接口的形式得到調(diào)用者自己設(shè)置的邏輯
* @param myTopBarClickListener 監(jiān)聽器接口類
*/
public void setOnClickListener(MyTopBarClickListener myTopBarClickListener) {
this.myTopBarClickListener = myTopBarClickListener;
}
現(xiàn)在外部調(diào)用者可以使用 setOnClickListener 這個(gè)方法來編寫點(diǎn)擊事件的邏輯代碼,我們得到這個(gè)接口的實(shí)現(xiàn)時(shí)候就可以用來填充到確切的點(diǎn)擊事件中了。
private void setListener() {
rightButton.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
myTopBarClickListener.rightClick();
}
});
leftButton.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
myTopBarClickListener.leftClick();
}
});
}
最后,外部調(diào)用者只需要這樣,就可以控制兩個(gè)按鈕的點(diǎn)擊事件了。
MyTopBar myTopBar = (MyTopBar) findViewById(R.id.mytopbar);
myTopBar.setOnClickListener(new MyTopBar.MyTopBarClickListener() {
@Override public void leftClick() {
Toast.makeText(MainActivity.this, "左邊被點(diǎn)擊了", Toast.LENGTH_LONG).show();
}
@Override public void rightClick() {
Toast.makeText(MainActivity.this, "右邊被點(diǎn)擊了", Toast.LENGTH_LONG).show();
}
});
上面這段代碼分別為兩個(gè)按鈕設(shè)置了點(diǎn)擊事件,但是具體使用是使用外部調(diào)用者編寫的邏輯。到這里,就實(shí)現(xiàn)由外部調(diào)用者自定義的點(diǎn)擊事件的功能了。那么或許有時(shí)候會(huì)需要更多的功能,比如說有的時(shí)候我只想顯示一個(gè)按鈕,而不是兩個(gè)按鈕都顯示。那么我們可以這樣:
/**
* @param i 要控制的按鈕,0為左邊,1為右邊
* @param x 要顯示還是要隱藏
*/
public void setButtonVisable(int i, boolean x) {
if (x) {
if (i == 0) {
leftButton.setVisibility(View.VISIBLE);
}else {
rightButton.setVisibility(View.VISIBLE);
}
}else {
if (i == 0) {
leftButton.setVisibility(View.GONE);
}else {
rightButton.setTextColor(View.GONE);
}
}
}
在外部調(diào)用下面這行代碼就可以實(shí)現(xiàn)隱藏左邊按鈕的功能了。
myTopBar.setButtonVisable(0,false);

以上,就是組合類型的 View 實(shí)現(xiàn)方式,雖然很丑很簡(jiǎn)單,但是從中可以看出來,通過這種方式可以做出很多復(fù)雜功能的 View 。
重寫
有時(shí)候,不管是繼承原生控件或者是組合原生控件,都不能滿足我們的特殊需求,這種時(shí)候就只能夠自己重頭完全的寫一個(gè)全新的控件了。創(chuàng)建一個(gè)全新的 View 重點(diǎn)在于繪制和交互的部分,通常需要繼承 View 類,并重寫 onDraw() 、onMeasure() 等方法,還可以像剛才的組合控件一樣,引入自定義屬性來豐富控件的可控性。
接下來 我們想實(shí)現(xiàn)一個(gè)效果:中間有一個(gè)實(shí)心圓,外圈是一圈弧線,通過點(diǎn)擊,可以增加弧線的長(zhǎng)度直到360度。我們看看效果圖。

點(diǎn)擊之后的樣子

其中自定義屬性在上面的組合控件中已經(jīng)試用過了,這里為了簡(jiǎn)化代碼就不再使用,來看看繪制和交互的過程吧,我直接貼上所有的代碼,代碼中有詳細(xì)的注釋。
public class MyView extends View {
Paint circlePaint, arcPaint, textPaint;
RectF rectF;
//弧線度數(shù)變量
int i = 10;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
textPaint = new TextPaint(0);
textPaint.setColor(Color.RED);
textPaint.setTextSize(50);
//實(shí)心圓畫筆
circlePaint = new Paint(0);
circlePaint.setColor(Color.BLUE);
//外圈弧線畫筆
arcPaint = new Paint(0);
arcPaint.setColor(Color.GREEN); //設(shè)置畫筆顏色
arcPaint.setStyle(Paint.Style.STROKE); //設(shè)置不填充中間
arcPaint.setStrokeWidth((float) 80.0); //畫筆粗細(xì)
//用來定位弧線的矩形
rectF = new RectF();
rectF.top = 340;
rectF.bottom = 740;
rectF.left = 340;
rectF.right = 740;
}
@Override protected void onDraw(Canvas canvas) {
//這是圓心的坐標(biāo),我的屏幕的1080的,使用540的話就是橫向居中了。
float xy = 540;
//圓圈的半徑 這里是100
float radius = 100;
//畫圓~
canvas.drawCircle(xy, xy, radius, circlePaint);
//畫弧線
canvas.drawArc(rectF, 270, i, false, arcPaint);
//顯示度數(shù)與提示
canvas.drawText(String.valueOf(i), 500, 560, textPaint);
canvas.drawText("點(diǎn)一點(diǎn)會(huì)轉(zhuǎn)圈!", 400, 860, textPaint);
}
/**
* 公開一個(gè)方法,可以更新弧線的度數(shù)~
*/
public void ddd() {
//超過360度就還原到10度
if (i >= 360) {
i = 10;
postInvalidate();
} else {
this.i += 10;
postInvalidate();
}
}
}
這就是自定義控件的全部代碼了,在外部只需要簡(jiǎn)單的調(diào)用,就可以控制弧線的度數(shù),以下是外部調(diào)用的例子。
void initMyView()
{
final MyView myView = (MyView) findViewById(R.id.myview);
myView.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
myView.ddd();
}
});
}
以上所有代碼可以到開頭的 github 地址下載。
總結(jié)
以上就是三種對(duì) View 進(jìn)行創(chuàng)新的方式。掌握好了這個(gè)才能做出美觀的界面,掌握熟練之后還可以造輪子,做出漂亮的,可控性高的控件供重復(fù)使用。做 Android 大部分時(shí)間都是在做用戶界面的交互,這其中 View 相當(dāng)重要。