為了讓我們的自定義看起來和官方的差不多,正經(jīng)一點,對沒錯是正經(jīng)一點。我們的自定義控件要做的全面一點。
BB兩句
- 為什么要自定義控件?
- 為了裝逼
- 為了滿足腦洞
- 為了世界的發(fā)展
自定義屬性,單獨自定義屬性沒啥用,因為自定義屬性是提供給自定義View使用的,所以我們要先創(chuàng)建一個自定義View才能愉快的使用。
前排提示,文章略長,請耐心看完。
流程
- 創(chuàng)建自定義View
- 編寫要用到的屬性
- 使用style給屬性賦值
- 代碼中獲取屬性的值
- 畫出文字
創(chuàng)建自定義View
自定義View的第一步就是要把我們的類寫成View,怎么寫成View呢,只要我們繼承View這個類就可以了,一般情況下我們都是繼承View或者ViewGroup這兩個類進行View擴展。為了方便通常也是直接集成相關方面的View進行修改,如這些TextView、EditText、LinearLayout等等。
首先創(chuàng)建一個類,繼承View,此時應該是這樣的
public class TextView extends View {
}
啊嘞,這好像啥變化都沒有啊-_-! 此時會報錯,提示需要重寫構(gòu)造方法,一般情況下我們需要重寫三個構(gòu)造方法,以滿足各個地方使用的需求,下面介紹使用場景
public TextView(Context context) {
this(context,null);
}
public TextView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
- 第一個構(gòu)造方法簡單來說就是在代碼中實例化的時候執(zhí)行
- 第二個構(gòu)造方法是在XML布局文件中使用的時候執(zhí)行,并且是沒有使用Style來定義要使用的屬性
- 第三個構(gòu)造方法是在XML布局文件中使用并且指定了style的時候執(zhí)行。
注意看第一個方法和第二個方法內(nèi),我是用的this,就是調(diào)用當前類的第二個、第三個構(gòu)造,依次類推,這是為了簡化代碼,直接在第三個方法中初始化一次就行了
好了,簡單結(jié)構(gòu)了解了,就開始走下一步。
編寫要用到的屬性
為什么要自定義屬性?想想你使用的TextView
<TextView
android:text="@string/app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
這些都是屬性,我們的自定義View想要有更好的體驗,我們需要進行自定義一些屬性方便在XML中直接配置。
自定義屬性需要在res/value/attrs.xml中配置,沒有的創(chuàng)建一個,使用declare-styleable標簽進行定義,標簽中的name屬性寫成自定義View的類名,下面看代碼。
<declare-styleable name="TextView">
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>
<attr name="text" format="string"/>
<attr name="gravity" format="enum">
<enum name="center" value="0"/>
<enum name="top" value="1"/>
<enum name="bottom" value="2"/>
<enum name="left" value="3"/>
<enum name="right" value="4"/>
<enum name="center_horizontal" value="5"/>
<enum name="center_vertical" value="6"/>
</attr>
</declare-styleable>
好了,現(xiàn)在定義完了,可以在XML使用了,在使用前,需要先給自定義View設置一個命名控件,以便在代碼中可以找到自定義的屬性。
<!--命名控件的聲明-->
xmlns:app="http://schemas.android.com/apk/res-auto"
<com.github.odriver.viewdemo.view.TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:gravity="center"
app:textSize="16sp"
app:text="@string/app_name"
app:textColor="@color/colorPrimary"
/>
好了,屬性的使用就是這些了。
使用Style給屬性賦值
有時候我們在使用控件的時候為了方便,為了懶,不想寫重復代碼。會使用style達到復用效果,這個時候,就是第三個構(gòu)造方法執(zhí)行,因為使用了style。
<com.github.odriver.viewdemo.view.TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/textStyle"
/>
上面這段代碼是使用style定義屬性的值,下面我們來看看style的定義,style是在style.xml里面定義的。
<style name="textStyle">
<item name="textSize">18sp</item>
<item name="textColor">#FFFFFF</item>
<item name="text">@string/app_name</item>
<item name="gravity">bottom</item>
</style>
在代碼中獲取屬性的值
我們在代碼中使用了屬性,并且設置了屬性的值。但是現(xiàn)在我們只是設置了,還沒有進行處理,也就是相當于沒個*用,所以我們還要繼續(xù),下面我們來獲取屬性的值,并且使用上,下面來看構(gòu)造方法中的代碼。
public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 使用context對象獲得我們自定義的屬性的值,attrs是我們xml中使用的屬性集合,其中包括屬性名和屬性值等相關信息,后面是我們自定義的屬性,也就是我們定義的declare-styleable。
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextView);
// 使用相應的方法獲取各個類型屬性的值,第二個參數(shù)是獲取不到屬性值默認的屬性值。
mTextColor = typedArray.getColor(R.styleable.TextView_textColor, Color.BLACK);
System.out.println("mTextColor = " + mTextColor +"------defColor="+Color.BLACK);
mTextSize = typedArray.getDimension(R.styleable.TextView_textSize, 12);
System.out.println("mTextSize = " + mTextSize +"-----defDimension = " + 12);
mGravity = typedArray.getInt(R.styleable.TextView_gravity, 0);
System.out.println("mGravity = " + mGravity +"-----defAnInt = " + 0);
mText = typedArray.getString(R.styleable.TextView_text);
System.out.println("mText = " + mText);
// 沒啥,google推薦的,使用完及時釋放
typedArray.recycle();
// 創(chuàng)建一個畫筆,我們要在界面上畫出相應的東西,都需要靠這支筆。
mPaint = new Paint();
// 設置抗鋸齒
mPaint.setAntiAlias(true);
}
如果你設置給這些屬性都賦值了,那么我們在獲取值的時候是和默認值不一樣的,可以打印出來看結(jié)果。
畫出文字
自定義屬性的工作基本上已經(jīng)完成了,下面就是把自定義屬性使用上了,我們現(xiàn)在View顯示的還是空蕩蕩的一片,現(xiàn)在我們要讓他顯示出我們設定的文字,首先介紹幾個方法,是我們自定義View是常用的方法。
- onDraw(Canvas canvas) 最重要的方法,用于將我們想要展示的東西繪制到屏幕上,不然看不到。。
- onMeasure(int widthMeasureSpec, int heightMeasureSpec) 測量方法有時候我們設置的是wrap_content或者match_parent又或者是具體值,我們需要在這個方法里進行判斷,計算出我們自身能使用的大小。在自定義ViewGroup的時候該方法也是用來子View顯示的大小和位置的。
- onLayout(boolean changed, int left, int top, int right, int bottom) 用于確定子View的位置,在自定義ViewGroup使用。
- onTouchEvent(MotionEvent event) 處理觸摸事件
- on一大堆。。。
下面我們先讓我們定義的文字顯示在界面上,重寫onDraw方法。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText(mText, 0, 100, mPaint);
}
Canvas的draw方法,以后再講。
現(xiàn)在來看屏幕,注意看白色部分那塊黑色部分,沒錯,那就是我們畫上去的文字。。。

直到現(xiàn)在,我們還沒有使用過任何自定義的屬性,下面就使用上。
畫出指定大小的文字
我們的文字太小了,我們把字體加大,在onDraw(Canvas canvas)方法中修改,還記的我們創(chuàng)建的那個畫筆嗎,全靠配置他來實現(xiàn)。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 設置我們要顯示的字體的大小
mPaint.setTextSize(mTextSize);
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText(mText, 0, 100, mPaint);
}
現(xiàn)在再看界面上

主要就是靠這支畫筆來控制。
畫出指定顏色的文字
我們來給文字加上顏色,還是要通過這個畫筆來實現(xiàn)。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 設置我們要顯示的字體的大小
mPaint.setTextSize(mTextSize);
// 設置文字的顏色
mPaint.setColor(mTextColor);
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText(mText, 0, 100, mPaint);
}
看,我們想要給文字加上顏色,只要用畫筆的setColor就可以了
這個畫筆就和我們現(xiàn)實中的畫筆是一樣的,我們的Canvas就相當于畫布,現(xiàn)實中我們一般的繪畫都是畫布擺在哪里,不做任何動作,全靠畫筆和人控制來進行繪畫,我們需要畫什么顏色,什么大小都是人和畫筆的動作。而Canvas(畫布)就跟他們說一句話:坐上來,自己動。畫面有點污。。。
畫出背景
看下面的代碼我們是為了設置一個背景,而背景在我們自定義View中有些需求是局部背景而不是整個控件背景,所以我們要在畫布上畫出一個背景,原理就是畫出一個矩形,使用顏色填充作為背景。
先看一個小例子來理解一下onDraw方法里面的繪制層級
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 第一層
// 設置畫筆顏色
mPaint.setColor(Color.CYAN);
// 畫出背景,以上面設置的顏色填充
canvas.drawRect(0, 0, 200, 200, mPaint);
// 設置我們要顯示的字體的大小
mPaint.setTextSize(mTextSize);
// 設置文字的顏色
mPaint.setColor(mTextColor);
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText("第一層", 0, 50, mPaint);
//-----------------------------------------------------------//
// 第二層
// 設置畫筆顏色
mPaint.setColor(Color.BLUE);
// 畫出背景,以上面設置的顏色填充
canvas.drawRect(30, 30, 150, 150, mPaint);
// 設置我們要顯示的字體的大小
mPaint.setTextSize(mTextSize);
// 設置文字的顏色
mPaint.setColor(Color.WHITE);
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText("第二層", 30, 100, mPaint);
}

從上面代碼加圖片中的樣式我們不難看出,畫出來的第二層把第一層給覆蓋掉了,我們的代碼也是由上至下執(zhí)行的,最上面的代碼畫出來的東西會被我們在下面寫的代碼給壓在下面。
注意:因為我們使用的畫筆只有一個,在畫出背景的時候我們給為了讓背景顯示特定的顏色填充整個背景,需要在畫矩形的時候給畫筆設置一個顏色,而這個畫筆又只有一個并且現(xiàn)在已經(jīng)設置上了一個顏色,如果下面再使用這個畫筆在畫布上畫其他東西,使用的顏色是現(xiàn)在設置上的顏色,這樣的話我們下面要畫出的文字的顏色就和背景是一個顏色的了,為了給我們的文字設置文字指定的顏色我們需要在沒有在畫布上繪畫的時候重新給畫筆設置一個顏色。
onMeasure方法的使用
我們先把代碼簡化到只畫文字,此時我們還有一個需求就是設置一個背景色,而設置背景顏色這個在我們繼承的View 這個類已經(jīng)幫我們提供好了,那就是android:background這個屬性,下面我們先把這個屬性在布局文件中給我們自定義的View設置上,設置為#555555,也就是灰色,然后我們來看現(xiàn)在顯示的效果。

可以看到,上圖顯示出來的,我們整個屏幕都變成了灰色,為什么,我們設置的也是wrap_content為什么還是全屏呢。
因為我們的View所在的父控件并沒有限制我們當前View可顯示的大小,默認我們的View就充滿整個屏幕了。這樣我們想要設置一個具體的值也是沒有用的,因為他現(xiàn)在是根據(jù)父View能給的大小來顯示。
為了我們能夠方便的管理這個View的大小,我們需要重寫onMeasure方法在父View調(diào)用我們子View的onMeasure方法詢問我們想要使用的大小的時候進行相應的計算,來合理地顯示我們的View。
下面是我們在onMeasure中的邏輯處理。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 指定了精確值,也就是設置了絕對大小,10dp這種
if (widthMode == MeasureSpec.EXACTLY){
mWidth = widthSize;
}else{
// 使用我們的畫筆來測量文字的大小,然后加上左邊padding和右邊padding值,來算出我們想要的寬度
widthSize= mBounds.width() + getPaddingLeft() + getPaddingRight();
// 等于這個模式就相當wrap_content,我們要找最小值
if (widthMode == MeasureSpec.AT_MOST){
// 讓默認的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
widthSize = Math.min(mWidth, widthSize);
}
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 指定了精確值,也就是設置了絕對大小,10dp這種
if (heightMode == MeasureSpec.EXACTLY){
mHeight = heightSize;
}else{
// 使用我們的畫筆來測量文字的大小,然后加上左邊padding和右邊padding值,來算出我們想要的寬度
heightSize= mBounds.height() + getPaddingTop()+ getPaddingBottom();
// 等于這個模式就相當wrap_content,我們要找最小值
if (heightMode == MeasureSpec.AT_MOST){
// 讓默認的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
heightSize = Math.min(mHeight, heightSize);
}
}
setMeasuredDimension(widthSize,heightSize);
}
其中mBounds實在構(gòu)造方法中,初始化畫筆的時候創(chuàng)建的,使用他主要是為了方便獲取文字的寬高,下面貼出初始化代碼。
// 創(chuàng)建一個畫筆,我們要在界面上畫出相應的東西,都需要靠這支筆。
mPaint = new Paint();
// 設置抗鋸齒
mPaint.setAntiAlias(true);
mPaint.setTextSize(mTextSize);
mBounds = new Rect();
mPaint.getTextBounds(mText,0,mText.length(), mBounds);
mWidth = mBounds.width();
mHeight = mBounds.height();
在初始化的時候注意,一定要在下面把默認的寬高也初始化,就是mWidth = mBounds.width()和mHeight = mBounds.height(),從bound對象中拿到寬高,給我們默認的寬高賦值,否則在我們測量的方法中設置如果是wrap_content的時候,我們的view在界面上是看不到的,因為沒有初始化寬高,默認是0,好了下面我們來看現(xiàn)在的效果圖。

可以看到,我們設置的背景成功的顯示了,并且是最小范圍的顯示,下面我們把寬高都設置成** match_parent**

可以看到,我們的文字跑到了最下面,這是為什么呢,下面我們觀察onDraw方法里面的代碼
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText(mText, 0, getMeasuredHeight(), mPaint);
}
注意看我們drawText的第三個參數(shù),這個參數(shù)我們設置的是測量后的高度,參數(shù)本身是用來定義從什么高度的位置開始畫的,我們定義的是測量后的高度,而且我們現(xiàn)在是** match_parent充滿屏幕的,所以現(xiàn)在就是從最底下開始畫文字,而這個文字畫的時候是文字的底部是0點,也就是我們現(xiàn)在文字的底部是在測量后的高度的最大值處,這時候我們想要讓文字從頂部開始畫,就用到我們剛才的Bound對象了,因為前面說了,我們的文字的底部是0點,那么我們文字頂部就是負的高度,也就是說我們從高度為0的這個位置開始畫,我們的文字是在屏幕外面的。。。??床灰娝?*,所以下面代碼改成這個樣子。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText(mText, 0, mBounds.height(), mPaint);
}
來我們看看效果

現(xiàn)在我們看到,文字剛好顯示在屏幕的頂部。
Gravity屬性的創(chuàng)造
看看我們開始的時候定義的declare-styleable,其中還有一個屬性我們現(xiàn)在還沒有適配,gravity,下面我們來適配它。
在前面我們的測量方法已經(jīng)寫好了,所以下面我們適配這個屬性很輕松了,
- top 按照我們上面寫好的代碼,就相當于top,不多做解釋(其實更相當于start,我想偷點懶。。。)
- bottom 還記得我們剛才那個文字跑到底下去了的那個嗎,那個就相當于bottom。
- 等等。。。。下面我們看代碼好了,有注釋的。。。
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import com.github.odriver.viewdemo.R;
/**
* Created by odriver on 16-8-25.
*/
class TextView extends View {
private int mTextColor;
private float mTextSize;
private int mGravity;
private String mText;
private Paint mPaint;
private int mWidth;
private int mHeight;
private Rect mBounds;
public TextView(Context context) {
this(context,null);
}
public TextView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 使用context對象獲得我們自定義的屬性的值,attrs是我們xml中使用的屬性集合,其中包括屬性名和屬性值等相關信息,后面是我們自定義的屬性,也就是我們定義的declare-styleable。
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextView);
// 使用相應的方法獲取各個類型屬性的值,第二個參數(shù)是獲取不到屬性值默認的屬性值。
mTextColor = typedArray.getColor(R.styleable.TextView_textColor, Color.BLACK);
mTextSize = typedArray.getDimension(R.styleable.TextView_textSize, 12);
mGravity = typedArray.getInt(R.styleable.TextView_gravity, 0);
mText = typedArray.getString(R.styleable.TextView_text);
// 沒啥,google推薦的,使用完及時釋放
typedArray.recycle();
// 創(chuàng)建一個畫筆,我們要在界面上畫出相應的東西,都需要靠這支筆。
mPaint = new Paint();
// 設置抗鋸齒
mPaint.setAntiAlias(true);
mPaint.setTextSize(mTextSize);
mBounds = new Rect();
mPaint.getTextBounds(mText,0,mText.length(), mBounds);
mWidth = mBounds.width();
mHeight = mBounds.height();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// top 1 || left 3
float startx = 0;
float starty = mBounds.height();
// center
if (mGravity == 0){
startx = getMeasuredWidth()/2-mBounds.width()/2;
starty = getMeasuredHeight()/2-mBounds.height()/2;
}else if (mGravity == 2){ // bottom
starty = getMeasuredHeight();
}else if (mGravity == 4){ // right
startx = getMeasuredWidth()-mBounds.width();
}else if (mGravity == 5){ // center_horizontal
startx = getMeasuredWidth()/2-mBounds.width()/2;
}else if (mGravity == 6){ // center_vertical
starty = getMeasuredHeight()/2-mBounds.height()/2;
}
// 使用canvas類的drawText方法將我們的文字畫到屏幕上。
canvas.drawText(mText, startx, starty, mPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 指定了精確值,也就是設置了絕對大小,10dp這種
if (widthMode == MeasureSpec.EXACTLY){
mWidth = widthSize;
}else{
// 使用我們的畫筆來測量文字的大小,然后加上左邊padding和右邊padding值,來算出我們想要的寬度
widthSize= mBounds.width() + getPaddingLeft() + getPaddingRight();
// 等于這個模式就相當wrap_content,我們要找最小值
if (widthMode == MeasureSpec.AT_MOST){
// 讓默認的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
widthSize = Math.min(mWidth, widthSize);
}
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 指定了精確值,也就是設置了絕對大小,10dp這種
if (heightMode == MeasureSpec.EXACTLY){
mHeight = heightSize;
}else{
// 使用我們的畫筆來測量文字的大小,然后加上左邊padding和右邊padding值,來算出我們想要的寬度
heightSize= mBounds.height() + getPaddingTop()+ getPaddingBottom();
// 等于這個模式就相當wrap_content,我們要找最小值
if (heightMode == MeasureSpec.AT_MOST){
// 讓默認的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
heightSize = Math.min(mHeight, heightSize);
}
}
setMeasuredDimension(widthSize,heightSize);
}
}
以上就是整個自定義View的相關代碼,style里面配置的都在前面講的有,下面我們來欣賞一下各種擺放姿勢。






好了簡單的自定義控件結(jié)束了,如果你有想法,那么這么多已經(jīng)可以簡單自定義其他的控件了,比如進度條,只需要改改onDraw方法把文字的相關操作去掉就可以了