Android自定義控件1

轉自這位牛人,自定義控件系列的其他文章,請去他的博客欣賞,自己是學一點轉一點,慢慢消化。

尊重原創轉載請註明: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:

  1. public class CustomView extends View {  
  2. }  
在View類中沒有提供無參的構造方法,這時我們的IDE會提示我們你得明確地聲明一個和帶有父類一樣簽名列表的構造方法:

這時我們點擊“Add constructor CustomView(Context context)”,IDE就會自動爲我們生成一個帶有Context類型簽名的構造方法:

  1. public class CustomView extends View {  
  2.     public CustomView(Context context) {  
  3.         super(context);  
  4.     }  
  5. }  
Context是什麼你不用管,只管記住它包含了許多各種不同的信息穿梭於Android中各類組件、控件等等之間,說得不恰當點就是一個裝滿信息的信使,Android需要它從裏面獲取需要的信息。

這樣我們就定義了一個屬於自己的自定義View,我們嘗試將它添加到Activity:

  1. public class MainActivity extends Activity {  
  2.     private LinearLayout llRoot;// 界面的根佈局  
  3.   
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_main);  
  8.   
  9.         llRoot = (LinearLayout) findViewById(R.id.main_root_ll);  
  10.         llRoot.addView(new CustomView(this));  
  11.     }  
  12. }  
運行後發現什麼也沒有,空的!因爲我們的CustomView本來就什麼都沒有!但是添加到我們的界面後沒有什麼問題對吧!Perfect!那我們再直接在xml文檔中引用它呢:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:id="@+id/main_root_ll"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.sigestudio.customviewdemo.views.CustomView  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="match_parent" />  
  10.   
  11. </LinearLayout>  
這時我們還原Activity中的代碼:

  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.     }  
  8. }  
再次運行後發現IDE報錯了:

大致意思是無法解析我們的CustomView類找不到方法,爲什麼呢?我們在xml文件引用我們的CustomView類時爲其指定了兩個android自帶的兩個屬性:layout_width和layout_height,當我們需要使用類似的屬性(比如更多的什麼id啊、padding啊、margin啊之類)時必須在自定義View的構造方法中添加一個AttributeSet類型的簽名來解析這些屬性:

  1. public class CustomView extends View {  
  2.     public CustomView(Context context) {  
  3.         super(context);  
  4.     }  
  5.   
  6.     public CustomView(Context context, AttributeSet attrs) {  
  7.         super(context, attrs);  
  8.     }  
  9. }  
再次運行發現一切又恢復了正常。現在我們來往我們的View裏畫點東西,畢竟自定義View總得有點什麼才行對吧!Android給我們提供了一個onDraw(Canvas canvas)方法來讓我們繪製自己想要的東西:

  1. @Override  
  2. protected void onDraw(Canvas canvas) {  
  3.     super.onDraw(canvas);  
  4. }  
我們想要畫些什麼直接在這個方法裏面畫即可,在現實世界中,我們畫畫需要兩樣東西:筆(或者任何能塗畫的東西)和紙(或者任何能被畫的東西),同樣地,Android也給我們提供了這兩樣東西:Paint和Canvas,一個是畫筆而另一個呢當然是畫布啦~~,我們可以看到在onDraw方法中,畫布Canvas作爲簽名被傳遞進來,也就是說這個畫布是Android爲我們準備好的,不需要你去管,當然你也可以自定義一張畫布在上面繪製自己的東西並將其傳遞給父類,但是一般我們不建議這樣去做!有人會問這畫布是怎麼來的?在這裏我不想跟大家深究其原理,否則長篇大論也過於繁瑣打擊各位菜鳥哥的學習興趣。但是我可以這樣跟大家說,如果在一張大的畫布(界面)上面有各種各樣小的畫布(界面中的各種控件),那麼這些小的畫布該如何確定其大小呢?自己去想哈哈!
草!又跑題了!

畫布有了,差一支畫筆,簡單!我們new一個唄!程序猿的好處就在萬事萬物都可以自己new!女朋友也能自己new,隨便new!!~~~:

  1. @Override  
  2. protected void onDraw(Canvas canvas) {  
  3.     super.onDraw(canvas);  
  4.     Paint paint = new Paint();  
  5.     paint.setAntiAlias(true);  
  6. }  
實例化了一個Paint對象後我們爲其設置了抗鋸齒(一種讓圖像邊緣顯得更圓滑光澤動感的碉堡算法):setAntiAlias(true),但是我們發現這是IDE又警告了!!!說什麼“Avoid object allocations during draw/layout operations (preallocate and reuse instead)”:


Why?Why?說白了就是不建議你在draw或者layout的過程中去實例化對象!爲啥?因爲draw或layout的過程有可能是一個頻繁重複執行的過程,我們知道new是需要分配內存空間的,如果在一個頻繁重複的過程中去大量地new對象內存爆不爆我不知道,但是浪費內存那是肯定的!所以Android不建議我們在這兩個過程中去實例化對象。既然都這樣說了我們就改改唄:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;  
  3.   
  4.     public CustomView(Context context) {  
  5.         this(context, null);  
  6.     }  
  7.   
  8.     public CustomView(Context context, AttributeSet attrs) {  
  9.         super(context, attrs);  
  10.   
  11.         // 初始化畫筆  
  12.         initPaint();  
  13.     }  
  14.   
  15.     /** 
  16.      * 初始化畫筆 
  17.      */  
  18.     private void initPaint() {  
  19.         // 實例化畫筆並打開抗鋸齒  
  20.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  21.     }  
  22.   
  23.     @Override  
  24.     protected void onDraw(Canvas canvas) {  
  25.         super.onDraw(canvas);  
  26.     }  
  27. }  
現實世界中,我們畫畫的畫筆是多種多樣的,有馬克筆、鉛筆、圓珠筆、毛筆、水彩筆、熒光筆等等等等……而這些筆的屬性也各自不同,像鉛筆按照炭顆粒的粗糙度可以分爲2B、3B、4B、5B、HB當然還有SB,而水彩筆也有各種不同的顏色,馬克筆就更霸氣了不說了!同樣地在Android的畫筆裏,現實有的它也有,沒有的它還有!我們可以用Paint的各種setter方法來設置各種不同的屬性,比如setColor()設置畫筆顏色,setStrokeWidth()設置描邊線條,setStyle()設置畫筆的樣式:


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


既然初步瞭解了Paint和Canvas,我們不妨就嘗試在我們的畫布上繪製一點東西,比如一個圓環?我們先來設置好畫筆的屬性:

  1. /** 
  2.  * 初始化畫筆 
  3.  */  
  4. private void initPaint() {  
  5.     // 實例化畫筆並打開抗鋸齒  
  6.     mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  7.   
  8.     /* 
  9.      * 設置畫筆樣式爲描邊,圓環嘛……當然不能填充不然就麼意思了 
  10.      *  
  11.      * 畫筆樣式分三種: 
  12.      * 1.Paint.Style.STROKE:描邊 
  13.      * 2.Paint.Style.FILL_AND_STROKE:描邊並填充 
  14.      * 3.Paint.Style.FILL:填充 
  15.      */  
  16.     mPaint.setStyle(Paint.Style.STROKE);  
  17.   
  18.     // 設置畫筆顏色爲淺灰色  
  19.     mPaint.setColor(Color.LTGRAY);  
  20.   
  21.     /* 
  22.      * 設置描邊的粗細,單位:像素px 
  23.      * 注意:當setStrokeWidth(0)的時候描邊寬度並不爲0而是隻佔一個像素 
  24.      */  
  25.     mPaint.setStrokeWidth(10);  
  26. }  
然後在我們的onDraw方法中繪製Cricle即可:

  1. @Override  
  2. protected void onDraw(Canvas canvas) {  
  3.     super.onDraw(canvas);  
  4.   
  5.     // 繪製圓環  
  6.     canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2200, mPaint);  
  7. }  
這裏要注意哦!drawCircle表示繪製的是圓形,但是在我們的畫筆樣式設置爲描邊後其繪製出來的就是一個圓環!其中drawCircle的前兩個參數表示圓心的XY座標,這裏我們用到了一個工具類獲取屏幕尺寸以便將其圓心設置在屏幕中心位置,第三個參數是圓的半徑,第四個參數則爲我們的畫筆!

這裏有一點要注意:在Android中設置數字類型的參數時如果沒有特別的說明,參數的單位一般都爲px像素。

好了,我們來運行下我們的Demo看看結果:

一個灰常漂亮的圓環展現在我們眼前!怎麼樣是不是很爽,這算是我們寫的第一個View,當然這只是第一步,雖然只是一小步,但必定會是影響人類進步的一大步!……Fuck!

不過一個簡單地畫一個圓恐怕難以滿足各位的胃口對吧,那我們嘗試讓它動起來?比如讓它的半徑從小到大地不斷變化,那怎麼實現好呢?大家如果瞭解動畫的原理就會知道,一個動畫是由無數張連貫的圖片構成的,這些圖片之間快速地切換再加上我們眼睛的視覺暫留給我們造成了在“動”的假象。那麼原理有了實現就很簡單了,我們不斷地改變圓環的半徑並且重新去畫並展示不就成了?同樣地,在Android中提供了一個叫invalidate()的方法來讓我們重繪我們的View。現在我們重新構造一下我們的代碼,添加一個int型的成員變量作爲半徑值的引用,再提供一個setter方法對外設置半徑值,並在設置了該值後調用invalidate()方法重繪View:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.   
  5.     private int radiu;// 圓環半徑  
  6.   
  7.     public CustomView(Context context) {  
  8.         this(context, null);  
  9.     }  
  10.   
  11.     public CustomView(Context context, AttributeSet attrs) {  
  12.         super(context, attrs);  
  13.         mContext = context;  
  14.   
  15.         // 初始化畫筆  
  16.         initPaint();  
  17.     }  
  18.   
  19.     /** 
  20.      * 初始化畫筆 
  21.      */  
  22.     private void initPaint() {  
  23.         // 實例化畫筆並打開抗鋸齒  
  24.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  25.   
  26.         /* 
  27.          * 設置畫筆樣式爲描邊,圓環嘛……當然不能填充不然就麼意思了 
  28.          *  
  29.          * 畫筆樣式分三種: 
  30.          * 1.Paint.Style.STROKE:描邊 
  31.          * 2.Paint.Style.FILL_AND_STROKE:描邊並填充 
  32.          * 3.Paint.Style.FILL:填充 
  33.          */  
  34.         mPaint.setStyle(Paint.Style.STROKE);  
  35.   
  36.         // 設置畫筆顏色爲淺灰色  
  37.         mPaint.setColor(Color.LTGRAY);  
  38.   
  39.         /* 
  40.          * 設置描邊的粗細,單位:像素px 
  41.          * 注意:當setStrokeWidth(0)的時候描邊寬度並不爲0而是隻佔一個像素 
  42.          */  
  43.         mPaint.setStrokeWidth(10);  
  44.     }  
  45.   
  46.     @Override  
  47.     protected void onDraw(Canvas canvas) {  
  48.         super.onDraw(canvas);  
  49.   
  50.         // 繪製圓環  
  51.         canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, radiu, mPaint);  
  52.     }  
  53.   
  54.     public synchronized void setRadiu(int radiu) {  
  55.         this.radiu = radiu;  
  56.   
  57.         // 重繪  
  58.         invalidate();  
  59.     }  
  60. }  
那麼OK,我們在Activity中開一個線程,通過Handler來定時間斷地設置半徑的值並刷新界面:

  1. public class MainActivity extends Activity {  
  2.     private CustomView mCustomView;// 我們的自定義View  
  3.   
  4.     private int radiu;// 半徑值  
  5.   
  6.     @SuppressLint("HandlerLeak")  
  7.     private Handler mHandler = new Handler() {  
  8.         @Override  
  9.         public void handleMessage(Message msg) {  
  10.             // 設置自定義View的半徑值  
  11.             mCustomView.setRadiu(radiu);  
  12.         }  
  13.     };  
  14.   
  15.     @Override  
  16.     protected void onCreate(Bundle savedInstanceState) {  
  17.         super.onCreate(savedInstanceState);  
  18.         setContentView(R.layout.activity_main);  
  19.   
  20.         // 獲取控件  
  21.         mCustomView = (CustomView) findViewById(R.id.main_cv);  
  22.   
  23.         /* 
  24.          * 開線程 
  25.          */  
  26.         new Thread(new Runnable() {  
  27.             @Override  
  28.             public void run() {  
  29.                 /* 
  30.                  * 確保線程不斷執行不斷刷新界面 
  31.                  */  
  32.                 while (true) {  
  33.                     try {  
  34.                         /* 
  35.                          * 如果半徑小於200則自加否則大於200後重置半徑值以實現往復 
  36.                          */  
  37.                         if (radiu <= 200) {  
  38.                             radiu += 10;  
  39.   
  40.                             // 發消息給Handler處理  
  41.                             mHandler.obtainMessage().sendToTarget();  
  42.                         } else {  
  43.                             radiu = 0;  
  44.                         }  
  45.   
  46.                         // 每執行一次暫停40毫秒  
  47.                         Thread.sleep(40);  
  48.                     } catch (InterruptedException e) {  
  49.                         e.printStackTrace();  
  50.                     }  
  51.                 }  
  52.             }  
  53.         }).start();  
  54.     }  
  55.   
  56.     @Override  
  57.     protected void onDestroy() {  
  58.         super.onDestroy();  
  59.         // 界面銷燬後清除Handler的引用  
  60.         mHandler.removeCallbacksAndMessages(null);  
  61.     }  
  62. }  
運行後的效果我就不演示了,項目源碼會共享。

但是有一個問題,這麼一個類似進度條的效果我還要在Activity中處理一些邏輯多不科學!浪費代碼啊!還要Handler來傳遞信息,Fuck!就不能在自定義View中一次性搞定嗎?答案是肯定的,我們修改下CustomView的代碼讓其實現Runnable接口,這樣就爽多了:

  1. public class CustomView extends View implements Runnable {  
  2.     private Paint mPaint;// 畫筆  
  3.     private Context mContext;// 上下文環境引用  
  4.   
  5.     private int radiu;// 圓環半徑  
  6.   
  7.     public CustomView(Context context) {  
  8.         this(context, null);  
  9.     }  
  10.   
  11.     public CustomView(Context context, AttributeSet attrs) {  
  12.         super(context, attrs);  
  13.         mContext = context;  
  14.   
  15.         // 初始化畫筆  
  16.         initPaint();  
  17.     }  
  18.   
  19.     /** 
  20.      * 初始化畫筆 
  21.      */  
  22.     private void initPaint() {  
  23.         // 實例化畫筆並打開抗鋸齒  
  24.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  25.   
  26.         /* 
  27.          * 設置畫筆樣式爲描邊,圓環嘛……當然不能填充不然就麼意思了 
  28.          *  
  29.          * 畫筆樣式分三種: 
  30.          * 1.Paint.Style.STROKE:描邊 
  31.          * 2.Paint.Style.FILL_AND_STROKE:描邊並填充 
  32.          * 3.Paint.Style.FILL:填充 
  33.          */  
  34.         mPaint.setStyle(Paint.Style.STROKE);  
  35.   
  36.         // 設置畫筆顏色爲淺灰色  
  37.         mPaint.setColor(Color.LTGRAY);  
  38.   
  39.         /* 
  40.          * 設置描邊的粗細,單位:像素px 
  41.          * 注意:當setStrokeWidth(0)的時候描邊寬度並不爲0而是隻佔一個像素 
  42.          */  
  43.         mPaint.setStrokeWidth(10);  
  44.     }  
  45.   
  46.     @Override  
  47.     protected void onDraw(Canvas canvas) {  
  48.         super.onDraw(canvas);  
  49.   
  50.         // 繪製圓環  
  51.         canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, radiu, mPaint);  
  52.     }  
  53.   
  54.     @Override  
  55.     public void run() {  
  56.         /* 
  57.          * 確保線程不斷執行不斷刷新界面 
  58.          */  
  59.         while (true) {  
  60.             try {  
  61.                 /* 
  62.                  * 如果半徑小於200則自加否則大於200後重置半徑值以實現往復 
  63.                  */  
  64.                 if (radiu <= 200) {  
  65.                     radiu += 10;  
  66.   
  67.                     // 刷新View  
  68.                     invalidate();  
  69.                 } else {  
  70.                     radiu = 0;  
  71.                 }  
  72.   
  73.                 // 每執行一次暫停40毫秒  
  74.                 Thread.sleep(40);  
  75.             } catch (InterruptedException e) {  
  76.                 e.printStackTrace();  
  77.             }  
  78.         }  
  79.     }  
  80. }  
而我們的Activity呢也能擺脫繁瑣的代碼邏輯:

  1. public class MainActivity extends Activity {  
  2.     private CustomView mCustomView;// 我們的自定義View  
  3.   
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_main);  
  8.   
  9.         // 獲取控件  
  10.         mCustomView = (CustomView) findViewById(R.id.main_cv);  
  11.   
  12.         /* 
  13.          * 開線程 
  14.          */  
  15.         new Thread(mCustomView).start();  
  16.     }  
  17. }  
運行一下看看唄!肏!!!報錯了:


Why!因爲我們在非UI線程中更新了UI!而在Android中非UI線程是不能直接更新UI的,怎麼辦?用Handler?NO!Android給我們提供了一個更便捷的方法:postInvalidate();用它替代我們原來的invalidate()即可:

  1. @Override  
  2. public void run() {  
  3.     /* 
  4.      * 確保線程不斷執行不斷刷新界面 
  5.      */  
  6.     while (true) {  
  7.         try {  
  8.             /* 
  9.              * 如果半徑小於200則自加否則大於200後重置半徑值以實現往復 
  10.              */  
  11.             if (radiu <= 200) {  
  12.                 radiu += 10;  
  13.   
  14.                 // 刷新View  
  15.                 postInvalidate();  
  16.             } else {  
  17.                 radiu = 0;  
  18.             }  
  19.   
  20.             // 每執行一次暫停40毫秒  
  21.             Thread.sleep(40);  
  22.         } catch (InterruptedException e) {  
  23.             e.printStackTrace();  
  24.         }  
  25.     }  
  26. }  
運行效果不變。

源碼地址:傳送門

溫馨提示:自定義控件其實很簡單系列文章每週一、週四更新一篇~

下集精彩預告:Paint爲我們提供了大量的setter方法去設置畫筆的屬性,而Canvas呢也提供了大量的drawXXX方法去告訴我們能畫些什麼,那麼小夥伴們知道這些方法是怎麼用的又能帶給我們怎樣炫酷的效果呢?鎖定本臺敬請關注:自定義控件其實很簡單1/6

自定義控件其實很簡單1/6已更新

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章