#### 一、閑扯篇
自定義View,很多初學Android的童鞋聽到這么一句話絕逼是一臉膜拜!因為在很多初學者眼里,能夠自己去畫一個View絕逼是一件很屌很Cool的事!但是,同樣而言,自定義View對初學者來說卻往往可望而不可及,可望是因為看了很多自定義View的源碼好像并不難,有些自定義View甚至不足百行代碼,不可及呢是因為即便看了很多文章很多類似的源碼依然寫不出一個霸氣的View來。這時會有很多前輩告訴你多看看View類的源碼,看看View類里是如何去處理這些繪制邏輯的,如果你去看了我只能說你是個很好學很有求知欲的孩紙,了解原理是好事,但是并非凡事都要去刨根問底的!如果你做Android開發必須要把Android全部源碼弄懂,我只能呵呵了!你還不如去寫一個系統實在對吧!同樣的道理,寫一個自定義View你非要去花巨量時間研究各類源碼是不值得提倡的,當然哥沒有否定追究原理的意義所在,只是對于一個普通的開發者你沒有必要去深究一些不該值得你關心的東西,特別是一個有良好面向對象思維的猿。舉個生活中簡單的例子,大家都用過吹風,吹風一般都會提供三個檔位:關、冷風、熱風對吧,你去買吹風人家只會告訴你這吹風三個檔位分別是什么功能,我相信沒有哪個傻逼買吹風的會把吹風拆開、電機寫下來一個一個地跟你解說那是啥玩意吧!同樣的,我們自定義View其實Android已經提供了大量類似吹風檔位的方法,你只管在里面做你想做的事情就可,至于Android本身內部是如何實現的,你壓根不用去管!用官方文檔的原話來說就是:Just do you things!初學者不懂如何去自定義View并非是不懂其原理,而是不懂這些類似“檔位”的方法!
#### 二、實踐篇
好了,扯了這么多廢話!我們還是先步入正題,來看看究竟自定義View是如何實現的!在Android中自定義一個View類并定是直接繼承View類或者View類的子類比如TextView、Button等等,這里呢我們也依葫蘆畫瓢直接繼承View自定義一個View的子類CustomView:
~~~
public class CustomView extends View {
}
~~~
在View類中沒有提供無參的構造方法,這時我們的IDE會提示我們你得明確地聲明一個和帶有父類一樣簽名列表的構造方法:

這時我們點擊“Add constructor CustomView(Context context)”,IDE就會自動為我們生成一個帶有Context類型簽名的構造方法:
~~~
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
}
~~~
Context是什么你不用管,只管記住它包含了許多各種不同的信息穿梭于Android中各類組件、控件等等之間,說得不恰當點就是一個裝滿信息的信使,Android需要它從里面獲取需要的信息。
這樣我們就定義了一個屬于自己的自定義View,我們嘗試將它添加到Activity:
~~~
public class MainActivity extends Activity {
private LinearLayout llRoot;// 界面的根布局
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
llRoot = (LinearLayout) findViewById(R.id.main_root_ll);
llRoot.addView(new CustomView(this));
}
}
~~~
運行后發現什么也沒有,空的!因為我們的CustomView本來就什么都沒有!但是添加到我們的界面后沒有什么問題對吧!Perfect!那我們再直接在xml文檔中引用它呢:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_root_ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.sigestudio.customviewdemo.views.CustomView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
~~~
這時我們還原Activity中的代碼:
~~~
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
~~~
再次運行后發現IDE報錯了:

大致意思是無法解析我們的CustomView類找不到方法,為什么呢?我們在xml文件引用我們的CustomView類時為其指定了兩個android自帶的兩個屬性:layout_width和layout_height,當我們需要使用類似的屬性(比如更多的什么id啊、padding啊、margin啊之類)時必須在自定義View的構造方法中添加一個AttributeSet類型的簽名來解析這些屬性:
~~~
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
~~~
再次運行發現一切又恢復了正常。現在我們來往我們的View里畫點東西,畢竟自定義View總得有點什么才行對吧!Android給我們提供了一個onDraw(Canvas canvas)方法來讓我們繪制自己想要的東西:
~~~
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
~~~
我們想要畫些什么直接在這個方法里面畫即可,在現實世界中,我們畫畫需要兩樣東西:筆(或者任何能涂畫的東西)和紙(或者任何能被畫的東西),同樣地,Android也給我們提供了這兩樣東西:Paint和Canvas,一個是畫筆而另一個呢當然是畫布啦~~,我們可以看到在onDraw方法中,畫布Canvas作為簽名被傳遞進來,也就是說這個畫布是Android為我們準備好的,不需要你去管,當然你也可以自定義一張畫布在上面繪制自己的東西并將其傳遞給父類,但是一般我們不建議這樣去做!有人會問這畫布是怎么來的?在這里我不想跟大家深究其原理,否則長篇大論也過于繁瑣打擊各位菜鳥哥的學習興趣。但是我可以這樣跟大家說,如果在一張大的畫布(界面)上面有各種各樣小的畫布(界面中的各種控件),那么這些小的畫布該如何確定其大小呢?自己去想哈哈!
草!又跑題了!
畫布有了,差一支畫筆,簡單!我們new一個唄!程序猿的好處就在萬事萬物都可以自己new!女朋友也能自己new,隨便new!!~~~:
~~~
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
}
~~~
實例化了一個Paint對象后我們為其設置了抗鋸齒(一種讓圖像邊緣顯得更圓滑光澤動感的碉堡算法):setAntiAlias(true),但是我們發現這是IDE又警告了!!!說什么“Avoid object allocations during draw/layout operations (preallocate and reuse instead)”:

Why?Why?說白了就是不建議你在draw或者layout的過程中去實例化對象!為啥?因為draw或layout的過程有可能是一個頻繁重復執行的過程,我們知道new是需要分配內存空間的,如果在一個頻繁重復的過程中去大量地new對象內存爆不爆我不知道,但是浪費內存那是肯定的!所以Android不建議我們在這兩個過程中去實例化對象。既然都這樣說了我們就改改唄:
~~~
public class CustomView extends View {
private Paint mPaint;
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆并打開抗鋸齒
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
~~~
現實世界中,我們畫畫的畫筆是多種多樣的,有馬克筆、鉛筆、圓珠筆、毛筆、水彩筆、熒光筆等等等等……而這些筆的屬性也各自不同,像鉛筆按照炭顆粒的粗糙度可以分為2B、3B、4B、5B、HB當然還有SB,而水彩筆也有各種不同的顏色,馬克筆就更霸氣了不說了!同樣地在Android的畫筆里,現實有的它也有,沒有的它還有!我們可以用Paint的各種setter方法來設置各種不同的屬性,比如setColor()設置畫筆顏色,setStrokeWidth()設置描邊線條,setStyle()設置畫筆的樣式:

Paint集成了所有“畫”的屬性,而Canvas則定義了所有要畫的東西,我們可以通過Canvas下的各類drawXXX方法繪制各種不同的東西,比如繪制一個圓drawCircle(),繪制一個圓弧drawArc(),繪制一張位圖drawBitmap()等等等:

既然初步了解了Paint和Canvas,我們不妨就嘗試在我們的畫布上繪制一點東西,比如一個圓環?我們先來設置好畫筆的屬性:
~~~
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆并打開抗鋸齒
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/*
* 設置畫筆樣式為描邊,圓環嘛……當然不能填充不然就么意思了
*
* 畫筆樣式分三種:
* 1.Paint.Style.STROKE:描邊
* 2.Paint.Style.FILL_AND_STROKE:描邊并填充
* 3.Paint.Style.FILL:填充
*/
mPaint.setStyle(Paint.Style.STROKE);
// 設置畫筆顏色為淺灰色
mPaint.setColor(Color.LTGRAY);
/*
* 設置描邊的粗細,單位:像素px
* 注意:當setStrokeWidth(0)的時候描邊寬度并不為0而是只占一個像素
*/
mPaint.setStrokeWidth(10);
}
~~~
然后在我們的onDraw方法中繪制Cricle即可:
~~~
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制圓環
canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, 200, mPaint);
}
~~~
這里要注意哦!drawCircle表示繪制的是圓形,但是在我們的畫筆樣式設置為描邊后其繪制出來的就是一個圓環!其中drawCircle的前兩個參數表示圓心的XY坐標,這里我們用到了一個工具類獲取屏幕尺寸以便將其圓心設置在屏幕中心位置,第三個參數是圓的半徑,第四個參數則為我們的畫筆!
這里有一點要注意:在Android中設置數字類型的參數時如果沒有特別的說明,參數的單位一般都為px像素。
好了,我們來運行下我們的Demo看看結果:

一個灰常漂亮的圓環展現在我們眼前!怎么樣是不是很爽,這算是我們寫的第一個View,當然這只是第一步,雖然只是一小步,但必定會是影響人類進步的一大步!……Fuck!
不過一個簡單地畫一個圓恐怕難以滿足各位的胃口對吧,那我們嘗試讓它動起來?比如讓它的半徑從小到大地不斷變化,那怎么實現好呢?大家如果了解動畫的原理就會知道,一個動畫是由無數張連貫的圖片構成的,這些圖片之間快速地切換再加上我們眼睛的視覺暫留給我們造成了在“動”的假象。那么原理有了實現就很簡單了,我們不斷地改變圓環的半徑并且重新去畫并展示不就成了?同樣地,在Android中提供了一個叫invalidate()的方法來讓我們重繪我們的View。現在我們重新構造一下我們的代碼,添加一個int型的成員變量作為半徑值的引用,再提供一個setter方法對外設置半徑值,并在設置了該值后調用invalidate()方法重繪View:
~~~
public class CustomView extends View {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private int radiu;// 圓環半徑
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆并打開抗鋸齒
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/*
* 設置畫筆樣式為描邊,圓環嘛……當然不能填充不然就么意思了
*
* 畫筆樣式分三種:
* 1.Paint.Style.STROKE:描邊
* 2.Paint.Style.FILL_AND_STROKE:描邊并填充
* 3.Paint.Style.FILL:填充
*/
mPaint.setStyle(Paint.Style.STROKE);
// 設置畫筆顏色為淺灰色
mPaint.setColor(Color.LTGRAY);
/*
* 設置描邊的粗細,單位:像素px
* 注意:當setStrokeWidth(0)的時候描邊寬度并不為0而是只占一個像素
*/
mPaint.setStrokeWidth(10);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制圓環
canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, radiu, mPaint);
}
public synchronized void setRadiu(int radiu) {
this.radiu = radiu;
// 重繪
invalidate();
}
}
~~~
那么OK,我們在Activity中開一個線程,通過Handler來定時間斷地設置半徑的值并刷新界面:
~~~
public class MainActivity extends Activity {
private CustomView mCustomView;// 我們的自定義View
private int radiu;// 半徑值
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 設置自定義View的半徑值
mCustomView.setRadiu(radiu);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 獲取控件
mCustomView = (CustomView) findViewById(R.id.main_cv);
/*
* 開線程
*/
new Thread(new Runnable() {
@Override
public void run() {
/*
* 確保線程不斷執行不斷刷新界面
*/
while (true) {
try {
/*
* 如果半徑小于200則自加否則大于200后重置半徑值以實現往復
*/
if (radiu <= 200) {
radiu += 10;
// 發消息給Handler處理
mHandler.obtainMessage().sendToTarget();
} else {
radiu = 0;
}
// 每執行一次暫停40毫秒
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 界面銷毀后清除Handler的引用
mHandler.removeCallbacksAndMessages(null);
}
}
~~~
運行后的效果我就不演示了,項目源碼會共享。
但是有一個問題,這么一個類似進度條的效果我還要在Activity中處理一些邏輯多不科學!浪費代碼啊!還要Handler來傳遞信息,Fuck!就不能在自定義View中一次性搞定嗎?答案是肯定的,我們修改下CustomView的代碼讓其實現Runnable接口,這樣就爽多了:
~~~
public class CustomView extends View implements Runnable {
private Paint mPaint;// 畫筆
private Context mContext;// 上下文環境引用
private int radiu;// 圓環半徑
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 初始化畫筆
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
// 實例化畫筆并打開抗鋸齒
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/*
* 設置畫筆樣式為描邊,圓環嘛……當然不能填充不然就么意思了
*
* 畫筆樣式分三種:
* 1.Paint.Style.STROKE:描邊
* 2.Paint.Style.FILL_AND_STROKE:描邊并填充
* 3.Paint.Style.FILL:填充
*/
mPaint.setStyle(Paint.Style.STROKE);
// 設置畫筆顏色為淺灰色
mPaint.setColor(Color.LTGRAY);
/*
* 設置描邊的粗細,單位:像素px
* 注意:當setStrokeWidth(0)的時候描邊寬度并不為0而是只占一個像素
*/
mPaint.setStrokeWidth(10);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制圓環
canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, radiu, mPaint);
}
@Override
public void run() {
/*
* 確保線程不斷執行不斷刷新界面
*/
while (true) {
try {
/*
* 如果半徑小于200則自加否則大于200后重置半徑值以實現往復
*/
if (radiu <= 200) {
radiu += 10;
// 刷新View
invalidate();
} else {
radiu = 0;
}
// 每執行一次暫停40毫秒
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
~~~
而我們的Activity呢也能擺脫繁瑣的代碼邏輯:
~~~
public class MainActivity extends Activity {
private CustomView mCustomView;// 我們的自定義View
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 獲取控件
mCustomView = (CustomView) findViewById(R.id.main_cv);
/*
* 開線程
*/
new Thread(mCustomView).start();
}
}
~~~
運行一下看看唄!肏!!!報錯了:

Why!因為我們在非UI線程中更新了UI!而在Android中非UI線程是不能直接更新UI的,怎么辦?用Handler?NO!Android給我們提供了一個更便捷的方法:postInvalidate();用它替代我們原來的invalidate()即可:
[java] view plain copy print?
@Override
public void run() {
/*
* 確保線程不斷執行不斷刷新界面
*/
while (true) {
try {
/*
* 如果半徑小于200則自加否則大于200后重置半徑值以實現往復
*/
if (radiu <= 200) {
radiu += 10;
// 刷新View
postInvalidate();
} else {
radiu = 0;
}
// 每執行一次暫停40毫秒
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行效果不變。
源碼地址:[傳送門](http://download.csdn.net/detail/aigestudio/8170091)
溫馨提示:自定義控件其實很簡單系列文章每周一、周四更新一篇~
下集精彩預告:Paint為我們提供了大量的setter方法去設置畫筆的屬性,而Canvas呢也提供了大量的drawXXX方法去告訴我們能畫些什么,那么小伙伴們知道這些方法是怎么用的又能帶給我們怎樣炫酷的效果呢?鎖定本臺敬請關注:自定義控件其實很簡單1/6