轉自這位牛人,自定義控件系列的其他文章,請去他的博客欣賞,自己是學一點轉一點,慢慢消化。
尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!
炮兵鎮樓
自定義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 {
- }
這時我們點擊“Add constructor CustomView(Context context)”,IDE就會自動爲我們生成一個帶有Context類型簽名的構造方法:
- public class CustomView extends View {
- public CustomView(Context context) {
- super(context);
- }
- }
這樣我們就定義了一個屬於自己的自定義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));
- }
- }
- <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>
- public class MainActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
- }
大致意思是無法解析我們的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);
- }
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }
草!又跑題了!
畫布有了,差一支畫筆,簡單!我們new一個唄!程序猿的好處就在萬事萬物都可以自己new!女朋友也能自己new,隨便new!!~~~:
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- Paint paint = new Paint();
- paint.setAntiAlias(true);
- }
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);
- }
- }
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);
- }
- @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);
- }
這裏有一點要注意:在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();
- }
- }
- 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();
- }
- }
- }
- }
- 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()即可:
- @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();
- }
- }
- }
源碼地址:傳送門
溫馨提示:自定義控件其實很簡單系列文章每週一、週四更新一篇~
下集精彩預告:Paint爲我們提供了大量的setter方法去設置畫筆的屬性,而Canvas呢也提供了大量的drawXXX方法去告訴我們能畫些什麼,那麼小夥伴們知道這些方法是怎麼用的又能帶給我們怎樣炫酷的效果呢?鎖定本臺敬請關注:自定義控件其實很簡單1/6