標籤:
很多筒子覺得自訂View是高手的象徵,其實不然。大家覺得自訂View難很多情況下可能是因為自訂View涉及到了太多的類和API,把人搞得暈乎乎的,那麼今天我們就從最簡單的繪圖API開始,帶大家來一步一步深入自訂View的世界。
先來看看我們今天要實現的一個:
整個效果很簡單,就是在螢幕上顯示一個鐘錶,該鐘錶可以自動走動。
OK,那就開始動工吧。
1.準備工作
首先,要實現這個時鐘,我得繼承自View來自己繪製時鐘,因為這種效果沒有辦法繼承已有控制項去完善功能。然後我們來看看我們這裡需要哪些變數?在這篇部落格中我暫時不打算介紹自訂屬性以及View的測量,這裡我只想介紹繪圖API,所以View的大小以及鐘錶錶針的顏色等我都暫時先給一個固定的值。OK,那麼我們需要的變數主要就是下面幾個:
/** * 繪製錶盤的畫筆 */ private Paint circlePaint; /** * 繪製錶盤數字 */ private Paint numPaint; /** * 繪製表心 */ private Paint dotPaint; /** * 時針 */ private Paint hourPaint; /** * 分針 */ private Paint minutePaint; /** * 秒針 */ private Paint secondPaint; /** * View寬度,預設256dp */ private int width; /** * View高度,預設256dp */ private int height; /** * 日曆類,用來擷取目前時間 */ private Calendar calendar; /** * 當前時針顏色 */ private int hourColor; /** * 當前分針顏色 */ private int minuteColor; /** * 當前秒針顏色 */ private int secondColor; /** * 時針寬度 */ private int hourWidth; /** * 分針寬度 */ private int minuteWidth; /** * 秒針寬度 */ private int secondWidth;
一共就是這麼多個變數。
2.關於構造方法
大家看到,當我繼承View之後,系統要求我實現它的構造方法,構造方法主要有四個,如下:
1.
public ClockView(Context context)
該構造方法是當我在Java代碼中new一個View的時候調用的。
2.
public ClockView(Context context, AttributeSet attrs)
該構造方法是當我在布局檔案中添加一個View時調用的。
3.
public ClockView(Context context, AttributeSet attrs, int defStyleAttr)
很多筒子看到第三個參數defStyleAttr之後,誤以為如果我在布局檔案中寫了style就會調用該構造方法,其實不然,這個構造方法系統並不會自己去調用(大家有興趣可以自己寫一個style,然後在這個方法中列印日誌,看看該方法究竟會不會調用),要由我們自己顯式調用(可以在第二個構造方法中調用)。那麼這裡的defStyleAttr究竟是什麼意思呢?正如這個參數的字面意思,它是我們為自訂的View指定的一個預設樣式。(後面部落格我們再來詳細說一下這個方法)。
另外,還有一個高版本使用的構造方法,我們這裡暫不做介紹。
一般情況下,我們需要在構造方法中完成一些初始化的操作,比如讀取在XML檔案中定義的屬性,或者初始化畫筆等等,因為我們的控制項既有可能是通過Java代碼執行個體化的,也有可能是在布局檔案中通過xml添加的。如前所述,如果我們在Java代碼中初始化控制項,那麼將調用第一個構造方法,如果我們在xml布局檔案中添加控制項,那麼將調用第二個構造方法。這時問題來了,那我們的初始化控制項的方法應該寫在那個構造方法中呢?你可以按下面這種方式來寫:
public ClockView(Context context) { super(context); initView(); } public ClockView(Context context, AttributeSet attrs) { super(context,attrs); initView(); }
在兩個構造方法中分別調用初始化的方法,這種方式沒有問題,但是顯得你的思路沒有條理,參考Android提供的其他控制項的源碼,我建議在初始化控制項時按照下面這種方式來寫:
//在代碼中建立控制項時調用 public ClockView(Context context) { this(context, null); } //在布局檔案中建立View時調用 public ClockView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ClockView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); }
雖然結果都一樣,但是下面這種寫法顯得你思路很清晰。
3.初始化控制項
我們在準備工作中定義了許多變數,包括鐘錶的顏色,指標的顏色等等許多變數,那麼接下來我們需要在initView這個方法中來初始化這些變數,以供下一步使用,OK,我們來看一看初始化代碼:
private void initView() { //擷取目前時間的執行個體 calendar = Calendar.getInstance(); //時鐘預設寬高 width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 256, getResources().getDisplayMetrics()); height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 256, getResources().getDisplayMetrics()); //初始化錶針的顏色 hourColor = Color.RED; minuteColor = Color.GREEN; secondColor = Color.BLUE; //初始化錶針的寬度 hourWidth = 8; minuteWidth = 5; secondWidth = 2; //初始化各種畫筆 circlePaint = new Paint(); //去鋸齒 circlePaint.setAntiAlias(true); //設定畫筆顏色 circlePaint.setColor(Color.GREEN); //設定畫筆style為描邊 circlePaint.setStyle(Paint.Style.STROKE); //設定描邊的寬度 circlePaint.setStrokeWidth(6); dotPaint = new Paint(); dotPaint.setAntiAlias(true); dotPaint.setColor(Color.RED); dotPaint.setStyle(Paint.Style.FILL); numPaint = new Paint(); numPaint.setColor(Color.RED); numPaint.setAntiAlias(true); //文本對齊 numPaint.setTextAlign(Paint.Align.CENTER); hourPaint = new Paint(); hourPaint.setColor(hourColor); hourPaint.setStyle(Paint.Style.FILL); hourPaint.setStrokeWidth(hourWidth); minutePaint = new Paint(); minutePaint.setColor(minuteColor); minutePaint.setStyle(Paint.Style.FILL); minutePaint.setStrokeWidth(minuteWidth); secondPaint = new Paint(); secondPaint.setColor(secondColor); secondPaint.setStyle(Paint.Style.FILL); secondPaint.setStrokeWidth(secondWidth); }
首先是獲得一個目前時間的執行個體,因為我需要根據手機上的時間來顯示鐘錶的時間,其次就是對錶針的各種屬性和畫筆進行初始化,這裡的東西都很簡單,我就不再一一細說,大家看代碼注釋,相信都能看得懂。
4.繪製鐘錶
上面所有的工作做完之後,接下來就是繪製鐘錶了,繪製工作我們放在了onDraw方法中執行,在自訂控制項時,如果該控制項是我們繼承自View來實現的,那麼基本上這個控制項就是需要我們自己來繪製了。
OK,那我們來看看鐘錶的繪製吧。
鐘錶不算複雜,但是我們也需要一步一步來:
首先是繪製錶盤,這個最簡單:
//1.圓心X軸座標,2.圓心Y軸座標,3.半徑,4.畫筆 int radius = width / 2 - 10; //畫錶盤 canvas.drawCircle(width / 2, height / 2, radius, circlePaint);
radius表示錶盤的半徑,通過drawCircle繪製一個圓環,四個參數分別是圓環的中心點座標,圓環的半徑以及繪製圓環的畫筆。
圓環畫好之後,那麼接下來就是繪製表心了,也就是錶盤正中心那個紅色的圓心。
canvas.drawCircle(width / 2, height / 2, 15, dotPaint);
很簡單吧。
OK,兩個最簡單的東東畫完之後,那麼接下來就是繪製錶盤的時幅了,時幅的繪製除了數字之外,還有一個綠色的短線,我們一共要畫十二個這種東西,那麼這個要怎麼繪製呢?思路有很多,你可以按照高中的數學知識,計算出每一個數位座標以及每一個短線起始位置和結束位置的座標,然後繪製出來,毫無疑問,這種方式的計算量太大,那我們這裡採取一個簡單的方式:我每次只在十二點的那個位置上進行繪製,如果需要繪製一點,那麼我把畫布逆時針旋轉30度到十二點的位置,然後畫上1和一個短線之後再將畫布順時針旋轉30度,如果是繪製2點,那麼我把畫布逆時針旋轉60度到十二點的位置,然後繪製上2和一個短線,繪製完成之後再將畫布順時針旋轉60度,思路就是這樣,下面我們來看看代碼:
for (int i = 1; i < 13; i++) { //在旋轉之前儲存畫布狀態 canvas.save(); canvas.rotate(i * 30, width / 2, height / 2); //1.2表示起點座標,3.4表示終點座標,5.畫筆 canvas.drawLine(width / 2, height / 2 - radius, width / 2, height / 2 - radius + 10, circlePaint); //畫錶盤數字1.要繪製的文本,2.文本x軸座標,3.文本基準,4.文本畫筆 canvas.drawText(i + "", width / 2, height / 2 - radius + 22, numPaint); //恢複畫布狀態 canvas.restore(); }
我用一個迴圈來繪製這十二個刻度,在每次旋轉畫布之前,我都通過一個canvas.save()方法來儲存畫布當前的狀態,儲存之後再對畫布進行旋轉操作,旋轉完成之後就是畫線畫數字,這些都很簡單,在繪製完成之後,我需要調用canvas的restore()方法,該方法可以讓畫布恢複到旋轉之前的角度。一般情況下,canvas.save()方法和canvas.restore()方法都是成對出現的,這一點大家要注意。
OK,這些東西都繪製完成之後,接下來就該繪製錶針了,錶針的繪製思路和上面一樣,也是先旋轉錶盤,然後繪製錶針,繪製完成之後,再把錶盤旋轉回之前的狀態。這裡我就不再詳細說明了,大家看代碼:
//獲得當前小時 int hour = calendar.get(Calendar.HOUR); canvas.save(); //旋轉螢幕 canvas.rotate(hour * 30, width / 2, height / 2); //畫時針 canvas.drawLine(width / 2, height / 2 + 20, width / 2, height / 2 - 90, hourPaint); canvas.restore(); int minute = calendar.get(Calendar.MINUTE); canvas.save(); canvas.rotate(minute * 6, width / 2, height / 2); canvas.drawLine(width / 2, height / 2 + 30, width / 2, height / 2 - 110, minutePaint); canvas.restore(); int second = calendar.get(Calendar.SECOND); canvas.save(); canvas.rotate(second * 6, width / 2, height / 2); canvas.drawLine(width / 2, height / 2 + 40, width / 2, height / 2 - 130, secondPaint); canvas.restore();
OK,所有的事情做完之後,我們可以在布局檔案中添加如下代碼,來查看我們的工作做得怎麼樣:
<org.mobiletrain.clockview.ClockView android:layout_width="match_parent" android:layout_height="match_parent"/>
添加完成之後,運行,不出意外的話,你的App上應該已經顯示了一個時鐘了,但是這個時鐘是靜止的,那麼我們該怎麼讓時鐘動起來呢?其實很簡答,我們只需要每隔一秒重新擷取calendar執行個體,然後重繪鐘錶即可,所以,在onDraw方法的開始和結束,我們還要分別添加如下兩行代碼:
1.開始處添加:
calendar = Calendar.getInstance();
這行代碼用來擷取最新的時間的執行個體
2.結束處添加:
postInvalidateDelayed(1000);
這行代碼用來重繪鐘錶,不過重繪是在1秒之後。
OK,至此,我們的自訂鐘錶就完成了,完整的代碼應該是這個樣子:
/** * Created by wangsong on 2016/3/29. */public class ClockView extends View { /** * 繪製錶盤的畫筆 */ private Paint circlePaint; /** * 繪製錶盤數字 */ private Paint numPaint; /** * 繪製表心 */ private Paint dotPaint; /** * 時針 */ private Paint hourPaint; /** * 分針 */ private Paint minutePaint; /** * 秒針 */ private Paint secondPaint; /** * View寬度,預設256dp */ private int width; /** * View高度,預設256dp */ private int height; /** * 日曆類,用來擷取目前時間 */ private Calendar calendar; /** * 當前時針顏色 */ private int hourColor; /** * 當前分針顏色 */ private int minuteColor; /** * 當前秒針顏色 */ private int secondColor; /** * 時針寬度 */ private int hourWidth; /** * 分針寬度 */ private int minuteWidth; /** * 秒針寬度 */ private int secondWidth; //在代碼中建立控制項時調用 public ClockView(Context context) { this(context, null); } //在布局檔案中建立View時調用 public ClockView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ClockView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { //擷取目前時間的執行個體 calendar = Calendar.getInstance(); //時鐘預設寬高 width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 256, getResources().getDisplayMetrics()); height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 256, getResources().getDisplayMetrics()); //初始化錶針的顏色 hourColor = Color.RED; minuteColor = Color.GREEN; secondColor = Color.BLUE; //初始化錶針的寬度 hourWidth = 8; minuteWidth = 5; secondWidth = 2; //初始化各種畫筆 circlePaint = new Paint(); //去鋸齒 circlePaint.setAntiAlias(true); //設定畫筆顏色 circlePaint.setColor(Color.GREEN); //設定畫筆style為描邊 circlePaint.setStyle(Paint.Style.STROKE); //設定描邊的寬度 circlePaint.setStrokeWidth(6); dotPaint = new Paint(); dotPaint.setAntiAlias(true); dotPaint.setColor(Color.RED); dotPaint.setStyle(Paint.Style.FILL); numPaint = new Paint(); numPaint.setColor(Color.RED); numPaint.setAntiAlias(true); //文本對齊 numPaint.setTextAlign(Paint.Align.CENTER); hourPaint = new Paint(); hourPaint.setColor(hourColor); hourPaint.setStyle(Paint.Style.FILL); hourPaint.setStrokeWidth(hourWidth); minutePaint = new Paint(); minutePaint.setColor(minuteColor); minutePaint.setStyle(Paint.Style.FILL); minutePaint.setStrokeWidth(minuteWidth); secondPaint = new Paint(); secondPaint.setColor(secondColor); secondPaint.setStyle(Paint.Style.FILL); secondPaint.setStrokeWidth(secondWidth); } //繪製View @Override protected void onDraw(Canvas canvas) { calendar = Calendar.getInstance(); //1.圓心X軸座標,2.圓心Y軸座標,3.半徑,4.畫筆 int radius = width / 2 - 10; //畫錶盤 canvas.drawCircle(width / 2, height / 2, radius, circlePaint); canvas.drawCircle(width / 2, height / 2, 15, dotPaint); for (int i = 1; i < 13; i++) { //在旋轉之前儲存畫布狀態 canvas.save(); canvas.rotate(i * 30, width / 2, height / 2); //1.2表示起點座標,3.4表示終點座標,5.畫筆 canvas.drawLine(width / 2, height / 2 - radius, width / 2, height / 2 - radius + 10, circlePaint); //畫錶盤數字1.要繪製的文本,2.文本x軸座標,3.文本基準,4.文本畫筆 canvas.drawText(i + "", width / 2, height / 2 - radius + 22, numPaint); //恢複畫布狀態 canvas.restore(); } //獲得當前小時 int hour = calendar.get(Calendar.HOUR); canvas.save(); //旋轉螢幕 canvas.rotate(hour * 30, width / 2, height / 2); //畫時針 canvas.drawLine(width / 2, height / 2 + 20, width / 2, height / 2 - 90, hourPaint); canvas.restore(); int minute = calendar.get(Calendar.MINUTE); canvas.save(); canvas.rotate(minute * 6, width / 2, height / 2); canvas.drawLine(width / 2, height / 2 + 30, width / 2, height / 2 - 110, minutePaint); canvas.restore(); int second = calendar.get(Calendar.SECOND); canvas.save(); canvas.rotate(second * 6, width / 2, height / 2); canvas.drawLine(width / 2, height / 2 + 40, width / 2, height / 2 - 130, secondPaint); canvas.restore(); //每隔1秒重繪View,重繪會調用onDraw()方法 postInvalidateDelayed(1000); }}
以上。
android自訂View之鐘錶誕生記