您的位置:首页 > 移动开发

时钟窗口小部件实现

2018-02-26 19:55 260 查看

前言

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
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  AppWidget Clock