記得在2013年12月的時(shí)候,有系列文章是介紹怎么開發(fā)一個(gè)智能手表的app,讓用戶可以在足球比賽中記錄停表時(shí)間。隨著android wear的問世,在可穿戴設(shè)備中開發(fā)一款這樣的app確實(shí)是個(gè)很不錯(cuò)的想法,但是按照目前對(duì)于android wear的架構(gòu)了解來說,似乎有些困難。所以本系列文章我們就重寫這個(gè)應(yīng)用,帶領(lǐng)大家進(jìn)入android wear的世界。
本文不會(huì)長(zhǎng)篇大論地講解我們要開發(fā)的這款app的用途,因?yàn)槲覀冊(cè)谥暗南盗形恼乱呀?jīng)深入了解過了。這么說吧,這是一個(gè)計(jì)時(shí)類應(yīng)用,在比賽開始的時(shí)候開始執(zhí)行,在比賽的過程中可以暫停(停表),然后45分鐘過去后會(huì)有震動(dòng)提醒,然后比賽進(jìn)行45分鐘后也會(huì)有提醒。
在開始之前,很有必要先看看我們?yōu)槭裁匆貙戇@個(gè)app而不是直接上代碼。智能手表使用的是一個(gè)修改版的android1.6的系統(tǒng),所以它的架構(gòu)很像一個(gè)運(yùn)行android1.6的手機(jī),所以我們的app基于一個(gè)activity,我們所有的工作都運(yùn)行在這個(gè)activity上。在開始學(xué)習(xí)智能手表開發(fā)之前,我們必須很清楚地知道,我們之前的設(shè)計(jì)在android wear上并不適用,盡管它也是支持activity,但是在android wear上工作方式是不同的。在手機(jī)或者平板上,如果一個(gè)activity從sleep狀態(tài)回到喚醒狀態(tài),activity會(huì)被重新喚醒,但是在wear上卻不是這樣。一段時(shí)間過去后wear設(shè)備會(huì)進(jìn)入sleep,但是在設(shè)備喚醒后,處于sleep狀態(tài)的activity卻不會(huì)再被喚醒了。
首先這個(gè)問題使我非常驚訝,我一直很想知道activity有了這個(gè)限制后,還能開發(fā)實(shí)用的app嗎?后來才發(fā)現(xiàn)這個(gè)問題完全是多慮的,我漸漸地發(fā)現(xiàn),要開發(fā)一個(gè)實(shí)用的app也很簡(jiǎn)單——我們只需要轉(zhuǎn)變我們的軟件設(shè)計(jì)模式,使它更符合android wear的體系結(jié)構(gòu),而不是當(dāng)做一個(gè)手機(jī)來看。
這里我們需要考慮的最基本的問題是,這個(gè)計(jì)時(shí)應(yīng)用程序需要基于一個(gè)一直運(yùn)行的服務(wù)來記錄時(shí)間。但是基于長(zhǎng)運(yùn)行的服務(wù)不是一個(gè)好的方案,因?yàn)樗鼤?huì)耗電。這里我們提到的記錄時(shí)間這個(gè)關(guān)鍵詞,也就是說,我們并不需要真的實(shí)現(xiàn)一個(gè)長(zhǎng)運(yùn)行的服務(wù),只要在用戶需要看的時(shí)候我們可以更新消息顯示就行。在大部分的時(shí)間里,其實(shí)用戶只需要了解大概過去了多長(zhǎng)時(shí)間,只有在比賽暫停或者中場(chǎng)快結(jié)束的時(shí)候才需要顯示更詳細(xì)的信息。所以在大部分的時(shí)間里,我們只需要顯示精確到分鐘即可,然后在用戶需要的時(shí)候才精確到秒。
我們要實(shí)現(xiàn)這個(gè)方法的基本方法就是使用alarmmanager每分鐘觸發(fā)一次更新通知事件,去更新分鐘顯示。這個(gè)通知事件還包括顯示精確到秒的activity,但是只有在用戶滑動(dòng)屏幕的時(shí)候才會(huì)顯示整個(gè)通知。通過這種方式我們可以在必須顯示的時(shí)候才去更新消息,所以對(duì)大部分設(shè)備來說,每分鐘更新一次消息顯示比一直運(yùn)行一個(gè)服務(wù)更加省電。
下圖顯示充分證明了這點(diǎn),首先我們需要打開通知,這樣就可以得到精確到秒的顯示了。
然而,在有信息顯示或者設(shè)備休眠的時(shí)候,我們只需要顯示精確到分鐘就可以了。
有一件事情需要說明一下,就是這個(gè)app的名字已經(jīng)改變了。之前在在i'm watch的版本上叫做“footy timer”,現(xiàn)在改為“match timer”。因?yàn)樵谑褂谜Z音啟動(dòng)app的時(shí)候,google的聲音識(shí)別對(duì)“footy”這個(gè)詞很不敏感,我們用“ok google,start footy timer”這個(gè)命令不能啟動(dòng)應(yīng)用,而使用“ok google,start match timer”就可以使用。
最后,很抱歉這篇文章沒有代碼,但是本系列文章會(huì)稍微有些變動(dòng)。以前本人會(huì)在每篇文章末尾附上文章相關(guān)的代碼段,這個(gè)請(qǐng)放心,之后的文章還是會(huì)這樣的,因?yàn)檫@個(gè)是一個(gè)功能完善的app,而不是系列技術(shù)文章,所以在接下來的文章會(huì)包含一些代碼示例和注釋,在本系列文章完結(jié)的時(shí)候會(huì)附上整個(gè)項(xiàng)目的源碼。
match timer 可以在google play上找到:https://play.google.com/store/apps/details?id=com.stylingandroid.matchtimer
上面我們解釋了為什么要在android wear重寫這個(gè)計(jì)時(shí)器app(因?yàn)橹耙呀?jīng)在“i'm watch”里面開發(fā)過了),下面我們就來看看代碼。
我們以這個(gè)app的一個(gè)核心類開始,這個(gè)類負(fù)責(zé)控制計(jì)時(shí)器的狀態(tài)。這個(gè)類包含了4個(gè)long類型的變量:第一個(gè)代表計(jì)時(shí)器開始的時(shí)間;第二個(gè)代表計(jì)時(shí)器停止的時(shí)間(在運(yùn)行中的話,它就是0);第三個(gè)代表計(jì)時(shí)器停表的時(shí)間(如果當(dāng)前沒有停表,那它也是0),第四個(gè)代表總共停表的時(shí)長(zhǎng)。通過這四個(gè)變量我們就可以維持計(jì)時(shí)器的狀態(tài)了,還可以通過計(jì)算得到我們需要展示的其他信息。這個(gè)類的基本功能就是都是為了操作這些變量,即維持計(jì)時(shí)器的這些狀態(tài)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
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代碼,就不費(fèi)時(shí)間講了。下面的函數(shù)更高級(jí)一些,可以操作計(jì)時(shí)器的狀態(tài)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
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()方法我們還沒有見到,這是在類的最后寫的,這個(gè)函數(shù)才值得的我們講講。
前一篇文章我們討論了關(guān)于喚醒機(jī)制的問題,我們不需要去維持一個(gè)長(zhǎng)連接或者后臺(tái)服務(wù),只需要維持這幾個(gè)計(jì)時(shí)器的狀態(tài)就可以了。我們使用sharedpreference來實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
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 ); } @override public 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中構(gòu)造一個(gè)matchtimer實(shí)例,我們還需要save()方法,可以幫我們把當(dāng)前的計(jì)時(shí)器狀態(tài)保存到sharedpreference中。
最后我們要說明的是,如果某一部分持有matchtimer對(duì)象的引用,但是其他對(duì)象已經(jīng)改變了計(jì)時(shí)器的狀態(tài),就可能會(huì)發(fā)生異常(見下一篇文章)。所以我們還需要提供一些方法去注冊(cè)和注銷matchtimer的實(shí)例,在sharedpreference的值改變時(shí)去接收計(jì)時(shí)器狀態(tài)的變化。
現(xiàn)在我們已經(jīng)定義了一個(gè)基本的計(jì)時(shí)器了,下一篇文章我們會(huì)介紹怎么保持計(jì)時(shí)器的狀態(tài)以及在需要的時(shí)候去喚醒這些狀態(tài)。
match timer 可以在google play上下載:match timer.
在本系列前幾篇文章中,我們介紹了android wear計(jì)時(shí)器app,對(duì)設(shè)計(jì)思路和app的結(jié)構(gòu)進(jìn)行了分析。本文將講解如何定時(shí)喚醒程序提醒用戶。
對(duì)于為什么不用后臺(tái)服務(wù)的方式一直運(yùn)行,我們已經(jīng)進(jìn)行了解釋——這種方式非常耗電。因此,我們必須要有一個(gè)定時(shí)喚醒機(jī)制。我們可以使用alarmmanager來實(shí)現(xiàn)這個(gè)機(jī)制,定時(shí)執(zhí)行一個(gè)intent,然后通知broadcastreceiver。之所以選擇broadcastreceiver而不用intentservice,是因?yàn)槲覀円\(yùn)行的任務(wù)是輕量級(jí)的而且生命周期非常短暫。使用broadcastreceiver可以避免每次執(zhí)行任務(wù)的時(shí)候都經(jīng)歷service的整個(gè)生命周期。因此,對(duì)于我們這種輕量級(jí)的任務(wù)來說非常合適——我們執(zhí)行的任務(wù)都在毫秒級(jí)。
broadcastreceiver的核心在于onreceiver方法,我們需要在這里安排各種事件響應(yīng)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
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); } } } . . . } |
代碼還是非常直觀易于理解的。首先實(shí)例化一個(gè)matchtimer對(duì)象(從sharedpreference中讀取數(shù)據(jù)),然后分別傳給對(duì)應(yīng)的事件處理handler。之后等待動(dòng)作發(fā)生,最后更新notification。
這里會(huì)處理8個(gè)事件動(dòng)作,其中5個(gè)負(fù)責(zé)控制計(jì)時(shí)器的狀態(tài)(start、stop、pause、resume、reset);一個(gè)負(fù)責(zé)更新notification,剩下兩個(gè)負(fù)責(zé)到45分鐘喚醒后震動(dòng)提示。
我們先從這幾個(gè)控制狀態(tài)開始:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
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); } } } . . . } |
這些方法主要有兩個(gè)功能:首先設(shè)置matchtimer的狀態(tài),然后設(shè)置時(shí)間提醒的鬧鈴,改變參數(shù)就可以播放鬧鈴。這個(gè)功能還可以封裝成一個(gè)工具方法,叫setupdate()。這樣外部也可以觸發(fā)計(jì)時(shí)器的更新。
我們使用標(biāo)準(zhǔn)alarmmanager的方法來設(shè)置鬧鈴:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
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()這個(gè)方法。因?yàn)樵趙ear在實(shí)現(xiàn)方式上有點(diǎn)不一樣。我們會(huì)在start事件中每秒鐘觸發(fā)一次鬧鈴更新notification動(dòng)作,所以這里需要記錄具體已經(jīng)過去了多少分鐘。正常來說我們會(huì)每隔60秒觸發(fā)一次這個(gè)動(dòng)作,但是在wear上不能這么做。原因是——當(dāng)設(shè)備在喚醒著的時(shí)候可以這樣做,但是如果設(shè)備進(jìn)入睡眠狀態(tài)就需要重新計(jì)算下一分鐘的邊界值。這就需要異步更新部件,然后設(shè)備只需要每分鐘喚醒一次。一分鐘結(jié)束后在計(jì)時(shí)器需要更新狀態(tài)的時(shí)候觸發(fā)操作。
對(duì)于我們的計(jì)時(shí)器應(yīng)用來說,顯示的分鐘數(shù)會(huì)比實(shí)際時(shí)間少1分鐘。但是顯示分鐘并不要求非常實(shí)時(shí)(但顯示秒數(shù)時(shí)需要非常精確),所以我們可以這樣操作:
完整的alarm handler是這樣使用振動(dòng)服務(wù)的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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 ); } . . . } |
最后,我們通過這個(gè)方法來構(gòu)造notification然后呈現(xiàn)給用戶:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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計(jì)時(shí)器的一個(gè)重要的部分,這里還需要一個(gè)自定義類來構(gòu)造這些notification通知。下一篇文章我們會(huì)講如何在計(jì)時(shí)器app中使用notification。
match timer可以在google play上下載:match timer。