时钟窗口小部件实现

前言

Android自带的时钟AppWidget感觉特别的简单,只有一个圆盘加上时针和分针,看着也没有什么变化,这里就自己来实现一下简单的时钟小部件。

实现效果

时钟窗口小部件实现_第1张图片

时钟控件实现

时钟控件最外层的大圆只可以调用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实现源码,配置文件如下:


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

你可能感兴趣的:(Android学习)