時鐘視窗小部件實現
前言
Android自帶的時鐘AppWidget感覺特別的簡單,只有一個圓盤加上時針和分針,看著也沒有什麼變化,這裡就自己來實現一下簡單的時鐘小部件。
實現效果
時鐘控制元件實現
時鐘控制元件最外層的大圓只可以呼叫Canvas.drawCircle實現,這個很簡單,事實上canvas物件除了那些直接畫線、畫圓等畫圖操作外,它還能夠像真正的畫布那樣做各種移位、旋轉等動作,為了保證這些操作不會影響其他元素的繪製,通常會先儲存畫布內容在做移位旋轉等操作,最後在還原畫布內容。瞭解了這些之後就可以輕鬆的繪製內部的刻度線了,圓形最上方的12點鐘刻度線位置很容易確定,如果直接繪製12點前後的刻度需要用三角函式計算它們的位置,這樣太麻煩了,可以旋轉畫布360 / (12 * 60)也就是每個最小刻度之間的角度值,然後再繪製,不停的旋轉直到所有的刻度都畫完,時鐘的時間文案也是用同樣的方式繪製。
private void drawClock(Canvas canvas) {
paint.setStrokeWidth(CommonUtils.dp2px(1));
paint.setStyle(Paint.Style.STROKE);
paint.setTextSize(CommonUtils.dp2px(13));
int centerX = width / 2;
int centerY = height / 2;
canvas.save();
canvas.drawCircle(centerX, centerY, centerX - CommonUtils.dp2px(5 ), paint);
for (int i = 0; i < MINUTES_COUNT; i++) {
if (i % 5 == 0) { // 畫時
canvas.drawLine(centerX, CommonUtils.dp2px(5), centerX, LONG_TICK, paint);
} else { // 畫分鐘
canvas.drawLine(centerX, CommonUtils.dp2px(5), centerX, SHORT_TICK, paint);
}
// 旋轉畫布,避免用三角函式計算位置
canvas.rotate(360 / MINUTES_COUNT, centerX, centerY);
}
canvas.restore();
// 畫時刻
canvas.save();
paint.setStyle(Paint.Style.FILL);
for (int i = 0; i < HOURS_COUNT; i++) {
int textWidth = (int) paint.measureText(HOURS[i]);
canvas.drawText(HOURS[i], centerX - textWidth / 2, CommonUtils.dp2px(5 + 13) + LONG_TICK, paint);
canvas.rotate(360 / HOURS_COUNT, centerX, centerY);
}
canvas.restore();
...
}
畫完刻度和文字之後就需要畫時針,分針和秒針,實現方式類似前面的刻度畫法,需要確定當前針與12點鐘方向的角度值,旋轉畫布,畫一條直線,然後還原轉向的畫布,繼續下一隻針的繪製。
// 拿到當前時分秒
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
// 繪製時針
canvas.save();
float degree = 360 / HOURS_COUNT * (hour + (float) minute / 60);
canvas.rotate(degree, centerX, centerY);
paint.setStrokeWidth(CommonUtils.dp2px(5));
canvas.drawLine(centerX, centerY, centerX, centerY - CommonUtils.dp2px(30), paint);
// 繪製分針
canvas.rotate(-degree, centerX, centerY);
canvas.rotate(360 / MINUTES_COUNT * minute, centerX, centerY);
paint.setStrokeWidth(CommonUtils.dp2px(3));
canvas.drawLine(centerX, centerY, centerX, centerY - CommonUtils.dp2px(40), paint);
// 繪製秒針
canvas.rotate(-360 / MINUTES_COUNT * minute, centerX, centerY);
canvas.rotate(360 / MINUTES_COUNT * second, centerX, centerY);
paint.setStrokeWidth(CommonUtils.dp2px(1));
canvas.drawLine(centerX, centerY, centerX, centerY - CommonUtils.dp2px(50), paint);
canvas.restore();
這樣的時鐘佈局只是當前時間秒針不會運動,為了讓秒針每秒移動一小格,需要不停的傳送移動要求。
private Runnable runnable = new Runnable() {
@Override
public void run() {
if (autoUpdate) {
calendar.setTimeInMillis(System.currentTimeMillis());
invalidate();
postDelayed(runnable, 1000);
}
}
};
if (autoUpdate) {
postDelayed(runnable, 1000);
}
由於時分秒針都是根據當前的時間獲取的角度值繪製,隨著時間的流逝每根針的角度都會有所變化。
視窗小部件實現
視窗小部件AppWidget物件繼承自BraodcaseReceiver,從本質上來說它們都是廣播接受者物件。AppWidget中一個重要的概念就是RemoteViews,表明這些View實在其他的程序中展示的,要想操作它們必須通過AppWidgetManager來作用,而且系統實際支援的佈局都是系統提供的佈局,並不支援使用者自定義生成的控制元件。這隻能通過將使用者佈局做成Bitmap物件然後設定到ImageView物件上去,不停的更新Bitmap物件到ImageView實現指標的動態效果。
Androi Studio本身集成了新增AppWidget的功能,這裡主要看配置檔案和AppWidget實現原始碼,配置檔案如下:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/clock_widget"
android:initialLayout="@layout/clock_widget"
android:minHeight="110dp"
android:minWidth="110dp"
android:previewImage="@drawable/tower"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="1000"
android:widgetCategory="home_screen">
</appwidget-provider>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
android:padding="@dimen/widget_margin">
<ImageView
android:id="@+id/clock_view"
android:scaleType="centerInside"
android:layout_centerInParent="true"
android:layout_width="300dp"
android:layout_height="300dp" />
</RelativeLayout>
上面的android:minHeight=”110dp”和android:minWidth=”110dp”就是用來配置視窗小部件在螢幕上的展示大小,需要注意如果之前新增過這個小部件之後又更改了這個大小,之前新增的那個小部件不會受影響,只有修改之後新增的小部件才會受到影響。android:updatePeriodMillis這個配置只要配置小於30分鐘都會被重置為30分鐘,所有視窗小部件的按時更新只能由使用者內部實現,不要依靠這個屬性的更新廣播。
在視窗小部件佈局裡只需要定義一個ClockView和自定義的Canvas物件,通過ClockView把當前時間的時鐘圖片畫到Canvas上,也就是Canvas載入的Bitmap上,呼叫RemoteViews的設定遠端ImageView的setImageBitmap將最新的時鐘圖片展示到遠端程序中。定時更新則是通過ScheduledExecutorService的定時器功能實現,每隔1秒重新畫一幅最新的時鐘圖片並且更新到遠端IamgeView上。
public class ClockWidget extends AppWidgetProvider {
private static final String TAG = "ClockWidget";
// 更新廣播動作
private static final String ACTION_UPDATE_TIME = "action_update_time";
// 時鐘控制元件
private static ClockView clockView;
// 繪製的Bitmap
private static Bitmap bitmap;
private static Canvas canvas;
// 定時執行緒池
private static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
private static Set<Integer> widgetIds = new HashSet<>();
private static Paint paint;
private static PorterDuffXfermode clear;
private static PorterDuffXfermode src;
static void updateAppWidget(final Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
Log.d(TAG, "updateAppWidget");
if (clockView == null) {
// 初始化視窗小部件的內部資料
clockView = new ClockView(context);
clockView.setAutoUpdate(false);
bitmap = Bitmap.createBitmap(clockView.getRealWidth(), clockView.getRealHeight(), Bitmap.Config.ARGB_4444);
canvas = new Canvas(bitmap);
paint = new Paint();
paint.setColor(Color.TRANSPARENT);
clear = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
src = new PorterDuffXfermode(PorterDuff.Mode.SRC);
// 開始定時傳送更新廣播
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Intent intent =new Intent(ACTION_UPDATE_TIME);
context.sendBroadcast(intent);
}
}, 0,1000L, TimeUnit.MILLISECONDS);
}
// 繪製最新的時鐘圖片到Bitmap
paint.setXfermode(clear);
canvas.drawPaint(paint);
paint.setXfermode(src);
clockView.draw(canvas);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.clock_widget);
// 更新遠端ImageView的時鐘圖片
views.setBitmap(R.id.clock_view, "setImageBitmap", bitmap);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
widgetIds.add(appWidgetId);
}
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
// 接收到更新廣播,開始更新操作
if (intent.getAction().equalsIgnoreCase(ACTION_UPDATE_TIME)) {
int[] widgets = new int[widgetIds.size()];
int index = 0;
for (Integer widgetId : widgetIds) {
widgets[index++] = widgetId;
}
onUpdate(context, AppWidgetManager.getInstance(context), widgets);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
}