记得在2013年12月的时候,有系列文章是介绍怎么开发一个智能手表的App,让用户可以在足球比赛中记录停表时间。随着Android Wear的问世,在可穿戴设备中开发一款这样的App确实是个很不错的想法,但是按照目前对于Android Wear的架构了解来说,似乎有些困难。所以本系列文章我们就重写这个应用,带领大家进入Android Wear的世界。

本文不会长篇大论地讲解我们要开发的这款App的用途,因为我们在之前的系列文章已经深入了解过了。这么说吧,这是一个计时类应用,在比赛开始的时候开始执行,在比赛的过程中可以暂停(停表),然后45分钟过去后会有震动提醒,然后比赛进行45分钟后也会有提醒。

在开始之前,很有必要先看看我们为什么要重写这个App而不是直接上代码。智能手表使用的是一个修改版的Android1.6的系统,所以它的架构很像一个运行Android1.6的手机,所以我们的App基于一个Activity,我们所有的工作都运行在这个Activity上。在开始学习智能手表开发之前,我们必须很清楚地知道,我们之前的设计在Android Wear上并不适用,尽管它也是支持Activity,但是在Android Wear上工作方式是不同的。在手机或者平板上,如果一个Activity从sleep状态回到唤醒状态,Activity会被重新唤醒,但是在Wear上却不是这样。一段时间过去后Wear设备会进入sleep,但是在设备唤醒后,处于sleep状态的Activity却不会再被唤醒了。

首先这个问题使我非常惊讶,我一直很想知道Activity有了这个限制后,还能开发实用的App吗?后来才发现这个问题完全是多虑的,我渐渐地发现,要开发一个实用的App也很简单——我们只需要转变我们的软件设计模式,使它更符合Android Wear的体系结构,而不是当做一个手机来看。

这里我们需要考虑的最基本的问题是,这个计时应用程序需要基于一个一直运行的服务来记录时间。但是基于长运行的服务不是一个好的方案,因为它会耗电。这里我们提到的记录时间这个关键词,也就是说,我们并不需要真的实现一个长运行的服务,只要在用户需要看的时候我们可以更新消息显示就行。在大部分的时间里,其实用户只需要了解大概过去了多长时间,只有在比赛暂停或者中场快结束的时候才需要显示更详细的信息。所以在大部分的时间里,我们只需要显示精确到分钟即可,然后在用户需要的时候才精确到秒。

我们要实现这个方法的基本方法就是使用AlarmManager每分钟触发一次更新通知事件,去更新分钟显示。这个通知事件还包括显示精确到秒的Activity,但是只有在用户滑动屏幕的时候才会显示整个通知。通过这种方式我们可以在必须显示的时候才去更新消息,所以对大部分设备来说,每分钟更新一次消息显示比一直运行一个服务更加省电。

下图显示充分证明了这点,首先我们需要打开通知,这样就可以得到精确到秒的显示了。

然而,在有信息显示或者设备休眠的时候,我们只需要显示精确到分钟就可以了。

有一件事情需要说明一下,就是这个App的名字已经改变了。之前在在I'm Watch的版本上叫做“Footy Timer”,现在改为“Match Timer”。因为在使用语音启动App的时候,Google的声音识别对“Footy”这个词很不敏感,我们用“ok Google,start Footy Timer”这个命令不能启动应用,而使用“ok Google,start Match Timer”就可以使用。

最后,很抱歉这篇文章没有代码,但是本系列文章会稍微有些变动。以前本人会在每篇文章末尾附上文章相关的代码段,这个请放心,之后的文章还是会这样的,因为这个是一个功能完善的App,而不是系列技术文章,所以在接下来的文章会包含一些代码示例和注释,在本系列文章完结的时候会附上整个项目的源码。

Match Timer 可以在Google Play上找到:https://play.google.com/store/apps/details?id=com.stylingandroid.matchtimer

上面我们解释了为什么要在Android Wear重写这个计时器app(因为之前已经在“I'm Watch”里面开发过了),下面我们就来看看代码。

我们以这个app的一个核心类开始,这个类负责控制计时器的状态。这个类包含了4个long类型的变量:第一个代表计时器开始的时间;第二个代表计时器停止的时间(在运行中的话,它就是0);第三个代表计时器停表的时间(如果当前没有停表,那它也是0),第四个代表总共停表的时长。通过这四个变量我们就可以维持计时器的状态了,还可以通过计算得到我们需要展示的其他信息。这个类的基本功能就是都是为了操作这些变量,即维持计时器的这些状态。

public final class MatchTimer {
.
.
.
public static final int MINUTE_MILLIS = 60000;private long start;
private long currentStoppage;
private long totalStoppages;
private long end;
.
.
.
public long getElapsed() {if (isRunning()) {return System.currentTimeMillis() - start;}if (end > 0) {return end - start;}return 0;
}public boolean isRunning() {return start > 0 && end == 0;
}public boolean isPaused() {return currentStoppage > 0;
}public int getElapsedMinutes() {return (int) ((System.currentTimeMillis() - start) / MINUTE_MILLIS);
}public long getTotalStoppages() {long now = System.currentTimeMillis();if (isPaused()) {return totalStoppages + (now - currentStoppage);}return totalStoppages;
}public long getPlayed() {return getElapsed() - getTotalStoppages();
}public long getStartTime() {return start;
}
.
.
.
}

这些都是基本的java代码,就不费时间讲了。下面的函数更高级一些,可以操作计时器的状态。

public final class MatchTimer {
.
.
.
public void start() {if (end > 0) {start = System.currentTimeMillis() - (end - start);end = 0;} else {start = System.currentTimeMillis();}save();
}public void stop() {if (isPaused()) {resume();}end = System.currentTimeMillis();save();
}public void pause() {currentStoppage = System.currentTimeMillis();save();
}public void resume() {totalStoppages += System.currentTimeMillis() - currentStoppage;currentStoppage = 0L;save();
}public void reset() {resetWithoutSave();save();
}private void resetWithoutSave() {start = 0L;currentStoppage = 0L;totalStoppages = 0L;end = 0L;
}
}

这些还是基本的Java代码,也可以不用讲了。只有save()方法我们还没有见到,这是在类的最后写的,这个函数才值得的我们讲讲。

前一篇文章我们讨论了关于唤醒机制的问题,我们不需要去维持一个长连接或者后台服务,只需要维持这几个计时器的状态就可以了。我们使用SharedPreference来实现:

 public final class MatchTimer implements SharedPreferences.OnSharedPreferenceChangeListener {private static final String KEY_START = "com.stylingandroid.matchtimer.KEY_START";private static final String KEY_CURRENT_STOPPAGE = "com.stylingandroid.matchtimer.KEY_CURRENT_STOPPAGE";private static final String KEY_TOTAL_STOPPAGES = "com.stylingandroid.matchtimer.KEY_TOTAL_STOPPAGES";private static final String KEY_END = "com.stylingandroid.matchtimer.KEY_END";private static final String PREFERENCES = "MatchTimer";private final SharedPreferences preferences;public static MatchTimer newInstance(Context context) {SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);long start = preferences.getLong(KEY_START, 0);long currentStoppage = preferences.getLong(KEY_CURRENT_STOPPAGE, 0);long totalStoppages = preferences.getLong(KEY_TOTAL_STOPPAGES, 0);long end = preferences.getLong(KEY_END, 0);return new MatchTimer(preferences, start, currentStoppage, totalStoppages, end);}private MatchTimer(SharedPreferences preferences, long start, long currentStoppage, long totalStoppages, long end) {this.preferences = preferences;this.start = start;this.currentStoppage = currentStoppage;this.totalStoppages = totalStoppages;this.end = end;}public void save() {preferences.edit().putLong(KEY_START, start).putLong(KEY_CURRENT_STOPPAGE, currentStoppage).putLong(KEY_TOTAL_STOPPAGES, totalStoppages).putLong(KEY_END, end).apply();}public void registerForUpdates() {preferences.registerOnSharedPreferenceChangeListener(this);}public void unregisterForUpdates() {preferences.unregisterOnSharedPreferenceChangeListener(this);}@Overridepublic void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {long value = sharedPreferences.getLong(key, 0L);if (key.equals(KEY_START)) {start = value;} else if (key.equals(KEY_END)) {end = value;} else if (key.equals(KEY_CURRENT_STOPPAGE)) {currentStoppage = value;} else if (key.equals(KEY_TOTAL_STOPPAGES)) {totalStoppages = value;}}...
}

我们需要的就是newInstance()方法从SharedPreference中构造一个MatchTimer实例,我们还需要save()方法,可以帮我们把当前的计时器状态保存到SharedPreference中。

最后我们要说明的是,如果某一部分持有MatchTimer对象的引用,但是其他对象已经改变了计时器的状态,就可能会发生异常(见下一篇文章)。所以我们还需要提供一些方法去注册和注销MatchTImer的实例,在Sharedpreference的值改变时去接收计时器状态的变化。

现在我们已经定义了一个基本的计时器了,下一篇文章我们会介绍怎么保持计时器的状态以及在需要的时候去唤醒这些状态。

Match Timer 可以在Google Play上下载:Match Timer.

在本系列前几篇文章中,我们介绍了Android Wear计时器app,对设计思路和app的结构进行了分析。本文将讲解如何定时唤醒程序提醒用户。

对于为什么不用后台服务的方式一直运行,我们已经进行了解释——这种方式非常耗电。因此,我们必须要有一个定时唤醒机制。我们可以使用AlarmManager来实现这个机制,定时执行一个Intent,然后通知BroadcastReceiver。之所以选择BroadcastReceiver而不用IntentService,是因为我们要运行的任务是轻量级的而且生命周期非常短暂。使用BroadcastReceiver可以避免每次执行任务的时候都经历Service的整个生命周期。因此,对于我们这种轻量级的任务来说非常合适——我们执行的任务都在毫秒级。

BroadcastReceiver的核心在于onReceiver方法,我们需要在这里安排各种事件响应。

public class MatchTimerReceiver extends BroadcastReceiver {
public static final int MINUTE_MILLIS = 60000;
private static final long DURATION = 45 * MINUTE_MILLIS;private static final Intent UPDATE_INTENT = new Intent(ACTION_UPDATE);
private static final Intent ELAPSED_ALARM = new Intent(ACTION_ELAPSED_ALARM);
private static final Intent FULL_TIME_ALARM = new Intent(ACTION_FULL_TIME_ALARM);private static final int REQUEST_UPDATE = 1;
private static final int REQUEST_ELAPSED = 2;
private static final int REQUEST_FULL_TIME = 3;public static void setUpdate(Context context) {context.sendBroadcast(UPDATE_INTENT);
}
.
.
.
private void reset(MatchTimer timer) {timer.reset();
}private void resume(Context context, MatchTimer timer) {timer.resume();long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION;if (playedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd);}
}private void pause(Context context, MatchTimer timer) {timer.pause();cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM);long elapsedEnd = timer.getStartTime() + DURATION;if (!isAlarmSet(context, REQUEST_ELAPSED, ELAPSED_ALARM) && elapsedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd);}
}private void stop(Context context, MatchTimer timer) {timer.stop();cancelAlarm(context, REQUEST_UPDATE, UPDATE_INTENT);cancelAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM);cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM);
}private void start(Context context, MatchTimer timer) {timer.start();long elapsedEnd = timer.getStartTime() + DURATION;setRepeatingAlarm(context, REQUEST_UPDATE, UPDATE_INTENT);if (timer.getTotalStoppages() > 0 && !timer.isPaused()) {long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION;if (playedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd);}if (elapsedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd);}} else {if (elapsedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, elapsedEnd);}}
}
.
.
.
}

代码还是非常直观易于理解的。首先实例化一个MatchTimer对象(从SharedPreference中读取数据),然后分别传给对应的事件处理Handler。之后等待动作发生,最后更新Notification。

这里会处理8个事件动作,其中5个负责控制计时器的状态(START、STOP、PAUSE、RESUME、RESET);一个负责更新Notification,剩下两个负责到45分钟唤醒后震动提示。

我们先从这几个控制状态开始:

public class MatchTimerReceiver extends BroadcastReceiver {
public static final int MINUTE_MILLIS = 60000;
private static final long DURATION = 45 * MINUTE_MILLIS;private static final Intent UPDATE_INTENT = new Intent(ACTION_UPDATE);
private static final Intent ELAPSED_ALARM = new Intent(ACTION_ELAPSED_ALARM);
private static final Intent FULL_TIME_ALARM = new Intent(ACTION_FULL_TIME_ALARM);private static final int REQUEST_UPDATE = 1;
private static final int REQUEST_ELAPSED = 2;
private static final int REQUEST_FULL_TIME = 3;public static void setUpdate(Context context) {context.sendBroadcast(UPDATE_INTENT);
}
.
.
.
private void reset(MatchTimer timer) {timer.reset();
}private void resume(Context context, MatchTimer timer) {timer.resume();long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION;if (playedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd);}
}private void pause(Context context, MatchTimer timer) {timer.pause();cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM);long elapsedEnd = timer.getStartTime() + DURATION;if (!isAlarmSet(context, REQUEST_ELAPSED, ELAPSED_ALARM) && elapsedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd);}
}private void stop(Context context, MatchTimer timer) {timer.stop();cancelAlarm(context, REQUEST_UPDATE, UPDATE_INTENT);cancelAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM);cancelAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM);
}private void start(Context context, MatchTimer timer) {timer.start();long elapsedEnd = timer.getStartTime() + DURATION;setRepeatingAlarm(context, REQUEST_UPDATE, UPDATE_INTENT);if (timer.getTotalStoppages() > 0 && !timer.isPaused()) {long playedEnd = timer.getStartTime() + timer.getTotalStoppages() + DURATION;if (playedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, playedEnd);}if (elapsedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_ELAPSED, ELAPSED_ALARM, elapsedEnd);}} else {if (elapsedEnd > System.currentTimeMillis()) {setAlarm(context, REQUEST_FULL_TIME, FULL_TIME_ALARM, elapsedEnd);}}
}
.
.
.
}

这些方法主要有两个功能:首先设置MatchTimer的状态,然后设置时间提醒的闹铃,改变参数就可以播放闹铃。这个功能还可以封装成一个工具方法,叫setUpdate()。这样外部也可以触发计时器的更新。

我们使用标准AlarmManager的方法来设置闹铃:

public class MatchTimerReceiver extends BroadcastReceiver {
.
.
.
public static final int MINUTE_MILLIS = 60000;
.
.
.private void setRepeatingAlarm(Context context, int requestCode, Intent intent) {AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), MINUTE_MILLIS, pendingIntent);
}private boolean isAlarmSet(Context context, int requestCode, Intent intent) {return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_NO_CREATE) != null;
}private void setAlarm(Context context, int requestCode, Intent intent, long time) {AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}private void cancelAlarm(Context context, int requestCode, Intent intent) {PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_NO_CREATE);if (pendingIntent != null) {AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);alarmManager.cancel(pendingIntent);pendingIntent.cancel();}
}
.
.
.
}

这里值得讨论的是setRepeatingAlarm()这个方法。因为在Wear在实现方式上有点不一样。我们会在Start事件中每秒钟触发一次闹铃更新Notification动作,所以这里需要记录具体已经过去了多少分钟。正常来说我们会每隔60秒触发一次这个动作,但是在Wear上不能这么做。原因是——当设备在唤醒着的时候可以这样做,但是如果设备进入睡眠状态就需要重新计算下一分钟的边界值。这就需要异步更新部件,然后设备只需要每分钟唤醒一次。一分钟结束后在计时器需要更新状态的时候触发操作。

对于我们的计时器应用来说,显示的分钟数会比实际时间少1分钟。但是显示分钟并不要求非常实时(但显示秒数时需要非常精确),所以我们可以这样操作:

完整的alarm Handler是这样使用振动服务的:

public class MatchTimerReceiver extends BroadcastReceiver {
.
.
.
private static final long[] ELAPSED_PATTERN = {0, 500, 250, 500, 250, 500};
private static final long[] FULL_TIME_PATTERN = {0, 1000, 500, 1000, 500, 1000};private void elapsedAlarm(Context context) {Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);vibrator.vibrate(ELAPSED_PATTERN, -1);
}private void fullTimeAlarm(Context context) {Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);vibrator.vibrate(FULL_TIME_PATTERN, -1);
}
.
.
.
}

最后,我们通过这个方法来构造Notification然后呈现给用户:

public class MatchTimerReceiver extends BroadcastReceiver {
public static final int NOTIFICATION_ID = 1;
.
.
.
private void updateNotification(Context context, MatchTimer timer) {NotificationBuilder builder = new NotificationBuilder(context, timer);Notification notification = builder.buildNotification();NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);notificationManager.notify(NOTIFICATION_ID, notification);
}
}

Notification是Wear计时器的一个重要的部分,这里还需要一个自定义类来构造这些Notification通知。下一篇文章我们会讲如何在计时器app中使用Notification。

Match Timer可以在Google Play上下载:Match Timer。

Android Wear计时器开发相关推荐

  1. android wear手表6,Android Wear(手表)开发

    Android Wear开发 - 学习指南 http://www.cnblogs.com/benhero/p/4273800.html 前言 本篇文章是本人对这这一阶段Android Wear的开发总 ...

  2. android wear gear2,开发大神实现三星Gear 2上启动Android Wear

    原标题:开发大神实现三星Gear 2上启动Android Wear 安珀网(公众号:ianpocom) 三星希望打造自己的Tizen OS智能手表生态系统.在三星的智能手表中,运行Android We ...

  3. android wear打开卡片,Android Wear入门开发之卡片CardFragment

    手持设备上面有一个CardView的东西(如果你不知道自行度娘,这不是我们重点),卡片类的东西,这是android5.0+ Api的控件,可以做出很漂亮的卡片的风格,之前我写过手持设备CardView ...

  4. android+wear独立应用,谷歌更新Android Wear政策:鼓励开发可独立运行的手表应用

    IT之家9月1日消息 日前,谷歌更新了其Android Wear平台的政策,鼓励开发者构建可以在Android Wear智能手表上独立工作而不依赖手机的应用程序. ▲图片来源:NeoWin 该政策变化 ...

  5. 苹果android wear教程,Android Wear/Apple Watch/WatchKit智能手表开发入门教程

    Android Wear/DuWear/TicWear/TOS/YunOS For Wear Apple Watch/Watch OS/WatchKit 智能手表开发者服务平台OpenWatch SD ...

  6. Android Wear 开发 (一),移动应用开发技术

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YDRiKjYy-1637431867561)(https://blog.csdn.net/)] 创建Phone端的页面样 ...

  7. Android Wear开发浅析(一)

    Android Wear究竟是什么? Android Wear和手机是完全不同的设计理念:手机大多数情况下,都是用户去操作,去触发里面的应用:而手表则是"用户被动"的,用户越少感觉 ...

  8. Android Wear 开发 (一),如何化身BAT面试收割机

    3.   自定义卡片:保留官方提供的卡片流样式,个性化修改卡片内容 4.   完全自定义界面:自己实现界面,需要处理多分辨率问题,自由度高 5.   手表表盘:类似手机动态壁纸的机制 开发平台的搭建 ...

  9. Android Wear开发步骤

    创建Android Wear 项目,运行 下载Android Wear SDK 下载穿戴设备SDK需要使用SDK Manager来进行下载,打开Android Studio后,点击工具栏上的 图标,打 ...

最新文章

  1. comsol稀物质传递_印刷指南丨印刷油墨传递的影响因素?
  2. JSP 标准标签库(JSTL)
  3. eclipse+spket+Extjs4.2.1开发环境搭建
  4. android 绘制按钮,Android:使用xml定义创建一个三角形的按钮(可绘制)
  5. 简练软考知识点整理-互联网+
  6. goldengate for mysql_GoldenGate for mysql to mysql:单向同步
  7. 关于Swift中的forEach(_:)和for-in loop
  8. hdu 2670 01背包变形
  9. C/C++编程语言中“crosses initialization”编译错误分析
  10. bootstrap居中
  11. 用IntelliJ IDEA自带的代码对比
  12. [基于Python的微信公众号后台开发:1]配置对接阿里云服务器
  13. 有一份好看的简历,面试都要加分,推荐好看的简历来这里下载
  14. 世界各主要国家及组织的大数据战略
  15. java POST接口报错417
  16. plt 固定X轴、Y轴的范围 ax设置横纵坐标的范围 ax.set_ylim(ymin = 0, ymax = 130)ax.set_xlim(xmin = -5, xmax = 5)
  17. HDU 2000 ASII 排序
  18. No matching provisioning profile found: Your build settings specify a provis...
  19. 埃里克贝里奇_未来公司客服的标配?苹果Watch智能手表开启新时代
  20. 京东主图如何优化,一键自动替换长图

热门文章

  1. 神经网络训练中的Tricks之高效BP(反向传播算法)
  2. OpenCV3学习(8.3)模板匹配函数matchTemplate详解
  3. python对文件的相关操作
  4. 条件编译指令:#if,#endif
  5. java 二维数组奇数金字塔_调整数组顺序使得奇数在前偶数在后(Java)
  6. idea隐藏 .iml 和 .idea 等自动生成文件
  7. idea部署tomcat并实现简单的web项目
  8. Matlab编写一个脚本计算e,Matlab编程基础I脚本.PPT
  9. Linux服务跟随启动,Linux—添加开机启动(服务/脚本)
  10. 函数 -- 1.模块导入 2.ATM架构 # 14