1. 程式人生 > >時鐘視窗小部件實現

時鐘視窗小部件實現

前言

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
    }
}