Android自定義控件之圓形時鐘(續)
我們先來看看如何讓時鐘像下圖所示動起來。
首先我們來分析如何讓秒針動起來。不知道大家熟不熟悉逐幀動畫,不熟悉也沒關系,大家小時候上學一定有這樣的經歷:在一本厚厚的書上,在每一頁的同一位置,畫有略有不同的圖案,然后撥動整本書,之后便會奇跡般的呈現出一幅動畫。其實這就是逐幀動畫的原理,我們看到的動畫是由一幅幅圖像組成,之所以我們感覺不出來,是因為這些圖像閃的太快啦,就拿前面書本的例子,如果你將書撥動的越快,那么你看到的動畫就越流暢,相反,如果速度很慢的話,就會明顯看到紙張上的圖案。那么可能有人要問了,這個速度到底快到什么程度,我們才能感覺到是一幅動畫呢?一般來講,我們肉眼能分辨的幀數是24幀,什么意思呢?還拿這個例子講解,如果一秒鐘,你一共撥動了24頁或者更多,那么你就能看到一幅流暢的動畫,完全感覺不到紙張的存在;如果頁數不到24頁,那么我們的肉眼就能看到一張張紙翻過。我們指針的運動其實也是同樣的道理。秒針走完一圈是60秒,而一圈是360度,那么我們可以算出一秒鐘,其實就是360度/60秒 = 6度。也就是說,每經過一秒鐘,我們將秒針的角度加上6度,然后重新調用onDraw方法重繪一次秒針。這樣通過不斷的重繪,我們的指針也就動起來了。那么如何準確的控制這一秒呢?這里我們用到了定時器Timer。代碼如下:
private float mSecondDegree;//秒針的度數
private Timer mTimer = new Timer();
private TimerTask task = new TimerTask() {
@Override
public void run() {//具體的定時任務邏輯
if (mSecondDegree == 360) {//因為圓一圈為360度,所以走滿一圈角度清零
mSecondDegree = 0;
}
mSecondDegree = mSecondDegree + 6;
postInvalidate();
}
};
/**
*開啟定時器
*/
public void start() {
mTimer.schedule(task,0,1000);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setStrokeWidth(2);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 3, mPaint);
mPaint.setStrokeWidth(5);
canvas.drawPoint(getWidth() / 2, getHeight() / 2, mPaint);
mPaint.setStrokeWidth(1);
canvas.translate(getWidth() / 2, getHeight() / 2);
for (int i = 0; i < 360; i++) {
if (i % 30 == 0) {//長刻度
canvas.drawLine(getWidth() / 3 - 25, 0,
getWidth() / 3, 0, mPaint);
} else if (i % 6 == 0) {//中刻度
canvas.drawLine(getWidth() / 3 - 14, 0,
getWidth() / 3, 0, mPaint);
} else {//短刻度
canvas.drawLine(getWidth() / 3 - 9, 0,
getWidth() / 3, 0, mPaint);
}
canvas.rotate(1);
}
canvas.save();
mPaint.setTextSize(25);
mPaint.setStyle(Paint.Style.FILL);
for (int i = 0; i < 12; i++) {
if (i == 0) {
drawNum(canvas, i * 30, 12 + "", mPaint);
} else {
drawNum(canvas, i * 30, i + "", mPaint);
}
}
canvas.restore();
//秒針
canvas.save();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
canvas.rotate(mSecondDegree);
canvas.drawLine(0, 0, 0,
-190, mPaint);
canvas.restore();
//分針
canvas.save();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
canvas.rotate(30);
canvas.drawLine(0, 0, 0,
-130, mPaint);
canvas.restore();
//時針
canvas.save();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(7);
canvas.rotate(90);
canvas.drawLine(0, 0, 0,
-90, mPaint);
canvas.restore();
在代碼中我們看到,我們先創建了一個Timer,又創建了一個定時任務TimerTask,然后重寫里面的run()方法,這個run方法中其實就是我們每隔一秒要處理的事情,這里代碼也很簡單,就是每隔一秒鐘,我們就把秒針的度數加上6度,然后調用postInvalidate();調用這個方法就會執行onDraw方法讓畫布重繪,當然invalidate()也會調用onDraw方法,兩者區別就是,invalidate()要在主線程調用,而postInvalidate()在子線程中調用,我們開啟了一個定時器,相當于開啟了一個子線程,所以這里要用postInvalidate()方法。我們的onDraw方法中代碼基本和上篇文章一樣,而且講的也非常詳細了,這里就不在贅述了 。唯一不同的就是在畫秒針的地方, 我們多了這句代碼: canvas.rotate(mSecondDegree);即在畫秒針之前我們讓畫布旋轉了mSecondDegree度,這里的mSecondDegree就是我們在定時任務中計算得來的。最后我們就可以啟動這個定時器啦,啟動也很簡單,只需要調用定時器Timer的schedule方法,這里我們傳入三個參數,第一個就是我們的定時任務task,第二個表示啟動定時器后多少毫秒開始工作,傳入0代表,一調用schedule這個方法就立即開啟定時任務;第三個參數就是任務的執行間隔,單位也是毫秒,由于是秒針,所以每秒要重繪一次,這里自然就是1000毫秒啦。好了接下來我們在MainActivity中調用start方法來啟動這個定時器就可以了。
public class MainActivity extends AppCompatActivity {
private TimeView time_view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
time_view = (TimeView)findViewById(R.id.time_view);
time_view.start();
}
}
我們來看一下效果圖:
怎么樣,秒針動起來了!!是不是有點小興奮。。雖然這是簡單的一小步驟,但確實我們學習知識的一大步,我們從畫靜態的圖形,過渡到能做出動畫了。好了秒針是動起來,但別忘了還有分針和時針這兩家伙,我們該如何讓它們也動起來呢?相信聰明的你一定學會了舉一反三了。我們在上面算出,每一秒鐘,秒針是要加6度,那么我們只要算出每秒鐘,分針和時針要加多少度,問題就迎刃而解了。那么具體怎么算呢?先來看分針,我們知道,分鐘走一圈是60分鐘吧,而圓的一圈是360度,那么一分鐘,其實就是分針走了360度/60分鐘 = 6度,而一分鐘等于60秒,所以對應一秒鐘就是,6度/60秒 = 0.1度/秒,即每隔一秒鐘就讓分針的度數加0.1度。這樣我們就知道了分針每秒要加的度數。接下里看一下時針,同理,時針走一圈是12小時,那么每小時就走360度/12小時,而每小時等于3600秒,所以每秒鐘也就是360度/(12*3600秒) = 1/120度/秒。這樣,每隔一秒鐘,分針和時針要加相應的度數也都已經求出來啦,代碼相信你也一定會了,和秒針一樣的邏輯,
private TimerTask task = new TimerTask() {
@Override
public void run() {
if (mSecondDegree == 360) {
mSecondDegree = 0;
}
if (mMinDegree == 360) {
mMinDegree = 0;
}
if (mHourDegree == 360) {
mHourDegree = 0;
}
mSecondDegree = mSecondDegree + 6;//秒針
mMinDegree = mMinDegree + 0.1f;//分針
mHourDegree = mHourDegree + 1.0f/240;//時針
postInvalidate();
}
};
在onDraw方法中,畫布旋轉的具體度數就由定時任務中算出來的度數。
//分針
canvas.save();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
canvas.rotate(mMinDegree);//定時任務中算出分針的度數
canvas.drawLine(0, 0, 0,
-130, mPaint);
canvas.restore();
//時針
canvas.save();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(7);
canvas.rotate(mHourDegree);//定時任務中算出時針的度數
canvas.drawLine(0, 0, 0,
-90, mPaint);
canvas.restore();
這樣我們的三個指針就都可以動起來了,當然分針和時針動的不是很明顯,你可以過上一段時間在來看一下。
好了,時鐘總算是動起來了,不過還有個問題,就是我們無法給它設置時間。接下來,我們來看看如何給他設置時間。設置時間說白了就是給每個指針的角度設置一個具體值。首先我們再來理一遍關系:秒針一秒鐘走6度(一圈60秒共走了360度);分針一鐘也是走6度(一圈60分鐘走共走了360度);而時針一小時走30度(一圈12小時共走了360度)。所以我們就可以根據具體的時間來求出各指針的角度。比如我們要設置時間:1點30分30秒,那么根據上述關系求時針的角度為 1*30 = 30度 ;分針的角度為 30*6 = 180度 ;秒針的角度為 30*6=180度 ;
自定義TimeView里的代碼如下:
public void setTime(int hour, int min, int second) {
mMinDegree = min * 6f;
mHourDegree = hour * 30f;
mSecondDegree = second * 6f;
invalidate();//重繪控件
}
然后我們在MainActivity中調用上面的方法
public class MainActivity extends AppCompatActivity {
private TimeView time_view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
time_view = (TimeView)findViewById(R.id.time_view);
time_view.setTime(1,30, 30);
time_view.start();
}
}
運行后看一下效果:
額。。是不是看著特變扭,沒錯細心的你一定看出來了,分針在30分的時候,時鐘卻還是在1點整,秒針都走了30多秒了,分針確還停在30分鐘的位置上。所以我們上面角度計算的還是有點問題,我們知道30分30秒其實就是30.5分鐘,而我們計算時僅僅只算了30分鐘的角度,少了那0.5分鐘。所以我們還是得把傳入的秒轉換為分鐘,即 mMinDegree = (min + second * 1.0f/60f) *6f ;同理時針的角度和分針秒針都有關,我們得把傳入的分和秒也都轉換為小時再計算它的角度,即 mHourDegree = (hour + min * 1.0f/60f + second * 1.0f/3600f)*30f ;
修改后的代碼:
public void setTime(int hour, int min, int second) {
if (hour >= 24 || hour < 0 || min >= 60 || min < 0 || second >= 60 || second < 0) {
Toast.makeText(getContext(), "時間不合法",
Toast.LENGTH_SHORT).show();
return;
}
if (hour >= 12) {//這里我們采用24小時制
mIsNight = true;//添加一個變量,用于記錄是否為下午。
mHourDegree = (hour + min * 1.0f/60f + second * 1.0f/3600f - 12)*30f;
} else {
mIsNight = false;
mHourDegree = (hour + min * 1.0f/60f + second * 1.0f/3600f )*30f;
}
mMinDegree = (min + second * 1.0f/60f) *6f;
mSecondDegree = second * 6f;
invalidate();
}
代碼還是很簡潔的,這里我們采用的是24小時制,給時分秒加了邊界的判斷,然后當傳入的小時大于12時,就讓它減去12小時計算它的角度,并且我們定義一個變量mIsNight,這個變量用于標志是否為下午,當傳入的小時大于12個小時,使他為true,這個變量會在后面獲取時鐘時間時用到。好了我們再重新運行下代碼,效果如下:
這樣三個指針的位置就比較合理了。
好了,知道了如何設置時間后我們再來看看如何獲取當前時間。其實也很簡單,上面我們知道時針走30度時一個小時,也就是3600秒,所以一度就是3600秒/30度 = 120秒。我們就可以根據時針走的度數,來求出一共是多少秒。比如時針正好為90度,也就是整3點鐘的位置,那么我們可求出共有 3 * 3600秒 = 10800秒 。這樣我們定義一個記錄總秒數的變量 mTotalSecond = mHourDegree * 120 。具體代碼如下
public float getTimeTotalSecond() {
if (mIsNight) {//判斷是否為下午,是的話再加12個小時
mTotalSecond = mHourDegree * 120 + 12 * 3600;
return mTotalSecond;
} else {
mTotalSecond = mHourDegree * 120;
return mTotalSecond;
}
}
有了總秒數,時分秒就比較好求了,具體代碼如下:
public int getHour() {//獲取小時
return (int) (getTimeTotalSecond() / 3600);
}
public int getMin() {//獲取分鐘
return (int) ((getTimeTotalSecond() - getHour() * 3600) / 60);
}
public int getSecond() {//獲取秒鐘
return (int) (getTimeTotalSecond() - getHour() * 3600 - getMin() * 60);
}
這樣我們的時鐘就可以進行設置和獲取時間的操作了。有了基本的功能,我們再來看一下樣式方面,我們自定義的控件說到底是拿來用的,不同的人有不同的喜好,比如有的人想將時鐘邊框的顏色設置成黑的,有的人就喜歡紅色。所以接下來我們看看,如何在XML布局文件里自由設置樣式,比如時鐘邊框的顏色。首先,我們在values文件夾下新建一個attrs.xml文件,里面的內容為
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TimeView">
<attr name="borderColor" format="color"/>//自定義屬性
</declare-styleable>
</resources>
我們可以看到,在資源標簽下有一個declare-styleable標簽,名字可以任意取,這里就叫TimeView。然后在這個標簽下有個attr標簽,這個標簽就是我們自定義的屬性,這里就拿邊框顏色為例,名字可以任意起,易讀就可以了,這里就叫 borderColor 由于 我們定義的屬性和顏色相關,這里的format就是color,format還有很多其他格式,如果是布爾型,那么它就是boolean,如果是長度的話就是dimension,當然還有很多其他格式,大家可以查閱官網,這里就不細講了。然后我們在初始自定義控件的時候添加如下代碼
private void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs,
R.styleable.TimeView);
borderColor = ta.getColor(R.styleable.TimeView_borderColor,
Color.BLACK);//獲取布局文中設置的顏色,默認設置為黑色
ta.recycle();
}
獲取顏色后,我們就要在畫邊框之前將畫筆設置成獲取到的顏色代碼如下:
mPaint.setColor(borderColor);//將畫筆顏色設置成獲取到的顏色
mPaint.setStrokeWidth(2);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 3, mPaint);
mPaint.setColor(Color.BLACK);
接著我們就可以在XML文件中設置自己喜歡的顏色了,完整的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:background="#fff"
>
<com.example.administrator.timeviewdemo.TimeView
android:layout_gravity="center_horizontal"
android:id="@+id/time_view"
custom:borderColor="#ff0000"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
我們要在根布局加上了這么一句話 xmlns:custom="http://schemas.android.com/apk/res-auto" ,只有加上這句話,你才能使用自已之前定義的屬性,這里的 custom 可以是任意的,但必須和下面的要保持一致。好了,看一下效果吧:
可以看到,設置的還是挺成功的,當然你還可以添加其他的屬性來豐富你的樣式,這里就不在一一演示了。這里我們的寬高是和父布局一樣大小,如果你覺得太大,可以讓它的寬高變小點,比如我們可以給它的寬高都設置成300dp,我們來看一下效果:
可以看到時鐘變小了,我們再將寬高都改為wrap_content試試吧:
。。額,好像不起作用,明明是wrap_content,怎么還是和match_parent的效果一樣,這究竟是怎么回事呢?
其實這就牽扯到自定義view的測量,我們先來看看一個控件展示在手機屏幕上的幾個過程,或者說是執行哪些方法:
- onMeasure-----------告訴系統這個自定義控件多大
- onLayou -----------告訴系統這個控件放哪。單獨的一個view不需要調用這個方法,主要是針對自定義ViewGroup的,關于自定義ViewGroup,后續會有文章詳細講解,這里就先不講了。
- onDraw -----------告訴系統這個控件展示的內容,這個方法在上一篇 Android自定義控件之圓形時鐘 講的也是比較詳細了,這里就不在贅述了。
所以今天我們就來看看這個onMeasure方法。 我們要想告訴系統這個控件多大,只用在onMeasure方法中調用setMeasuredDimension(int width,int height),將你想要設置的寬高傳入就可以了。比如我們想把寬高都設置為300,代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(300,300);
}
這樣控件的寬高就是300啦,我們來運行下:
不過,如果這樣設置的話,你自己寫的是很爽,但你讓別人怎么活。。。。。這樣設置的話,別人就無法再在XML文件中進行寬高的設置。。因為不管怎么設置,最后都被你指定為300不變。。。要是有人用這個控件的話,估計都會開始懷疑人生了。。。那么該怎么辦呢?這個時候MeasureSpec這個類就閃亮登場了。這個類是一個32位的int值,高兩位是測量模式,低30位是測量尺寸。測量模式一共有三種:
- EXACTLY
當我們在布局文件中指定寬高為具體的值或者指定為match_parent時,系統用的就是這個模式 - AT_MOST
當我們在布局文件中,指定寬高為wrap_content時,就是這個模式。 - UNSPECIFIED
這個模式比較特殊,就是view想要多大就有多大,一般不怎么用
如果你在自定義view時不重寫onMeasure這個方法,那么系統默認只支持EXACTLY這個模式,即你可以在布局文件中,指定控件寬高一個具體的數值,也可以讓它match_parent。但是無法識別wrap_content的,要想讓wrap_content有效,我們就要重寫onMeasure方法,然后給控件指定一個大小。代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
//自己寫的測量寬度的方法
private int measureWidth(int measureSpec) {
int result;
int specSize = MeasureSpec.getSize(measureSpec);
int specMode = MeasureSpec.getMode(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 300;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
可以看到,我們自己寫了一個方法,用于測量控件的寬,由于這個控件是圓形的所以寬高是一樣的,這里就只貼出測量寬的代碼。我們根據系統傳入的MeasureSpec類,來獲取測量尺寸和測量模式,即得到specSize 和specMode ,如果我們在XML文件中指定控件的具體數值大小,那么獲取到的specSize 就等于這個具體的值,如果是match_parent,那么這個值就是父控件的值。然后我們來看一下獲取到的specMode ,如果布局文件指定為match_parent,那么specMode 就等于MeasureSpec.EXACTLY,如果是wrap_content,那么就等于MeasureSpec.AT_MOST。接下來就是判斷獲取的是什么模式,根據不同的模式來返回具體的測量值。如果是EXACTLY那么測量的結果就是specSize ,如果是AT_MOST,那么我們指定一個具體的值,然后和specSize 比較,較小者作為測量的值。這個測量的代碼,其實可以作為一個模板,這樣onMeasure這個方法其實也沒有什么難的地方,以后要重寫onMeasure方法時,套用這個模板就行了,然后在AT_MOST時,指定自己需要的大小。
好了最后我們寫個Demo來演示下我們的時鐘把,首先看一下XML代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:background="#fff"
>
<com.example.administrator.timeviewdemo.TimeView
android:id="@+id/time_view"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center_horizontal"
custom:secondPointerColor="#fff"
custom:borderColor="#f12"
custom:borderWidth="3dp"
custom:maxScaleColor="#fff"
custom:minScaleColor="#fff"
custom:midScaleColor="#fff"
custom:maxScaleLength="14dp"
custom:midScaleLength="10dp"
custom:minScaleLength="7dp"
custom:centerPointRadiu="2dp"
custom:centerPointType="circle"
custom:centerPointColor="#fff"
custom:secondPointerLength="80dp"
custom:minPointerLength="50dp"
custom:hourPointerLength="30dp"
custom:minPointerColor="#fff"
custom:hourPointerColor="#fff"
custom:isSecondGoSmooth="false"
custom:textColor="#fff"
custom:textSize="20sp"
custom:circleBackground="#0af"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/hour"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="時"/>
<EditText
android:id="@+id/min"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="分"/>
<EditText
android:id="@+id/second"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="秒"/>
</LinearLayout>
<Button
android:layout_margin="10dp"
android:id="@+id/set_time"
android:textColor="#fff"
android:background="@color/colorPrimary"
android:layout_gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="設置時間"/>
<Button
android:layout_margin="10dp"
android:id="@+id/get_time"
android:textColor="#fff"
android:background="@color/colorPrimary"
android:layout_gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="獲取時間"/>
</LinearLayout>
布局文件也很簡單,一個是我們的自定義控件,可以看到我添加很多別的樣式。然后下面三個EditText分別可以輸入時分秒,然后下面兩個Button,用來設置和獲取時間。接下來,看一下MainActivity中的代碼:
public class MainActivity extends AppCompatActivity {
private TimeView time_view;
private EditText hour;
private EditText min;
private EditText second;
private Button set_time;
private Button get_time;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
time_view = (TimeView)findViewById(R.id.time_view);
hour = (EditText)findViewById(R.id.hour);
min = (EditText)findViewById(R.id.min);
second = (EditText)findViewById(R.id.second);
set_time = (Button)findViewById(R.id.set_time);
get_time = (Button)findViewById(R.id.get_time);
set_time.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
time_view.setTime(Integer.parseInt(hour.getText().toString()),
Integer.parseInt(min.getText().toString()),
Integer.parseInt(second.getText().toString()));
}
});
get_time.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,
time_view.getHour()+":"+time_view.getMin()+":"+time_view.getSecond()+"",
Toast.LENGTH_SHORT).show();
}
});
time_view.setTime(1,30,30);//設置了默認時間
time_view.start();
}
}
代碼還是很簡單的,分別給兩個按鈕添加點擊事件,然后給時鐘一個初始的時間,并調用start方法,讓它動起來。我們來看一下效果吧!
演示
好了,這個自定義時鐘到此就告一段落了
來自:http://www.jianshu.com/p/c2abd6226897