Android Wear計(jì)時(shí)器開發(fā)
記得在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ì)長篇大論地講解我們要開發(fā)的這款A(yù)pp的用途,因?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也很簡單——我們只需要轉(zhuǎn)變我們的軟件設(shè)計(jì)模式,使它更符合Android Wear的體系結(jié)構(gòu),而不是當(dāng)做一個(gè)手機(jī)來看。
這里我們需要考慮的最基本的問題是,這個(gè)計(jì)時(shí)應(yīng)用程序需要基于一個(gè)一直運(yùn)行的服務(wù)來記錄時(shí)間。但是基于長運(yùn)行的服務(wù)不是一個(gè)好的方案,因?yàn)樗鼤?huì)耗電。這里我們提到的記錄時(shí)間這個(gè)關(guān)鍵詞,也就是說,我們并不需要真的實(shí)現(xiàn)一個(gè)長運(yùn)行的服務(wù),只要在用戶需要看的時(shí)候我們可以更新消息顯示就行。在大部分的時(shí)間里,其實(shí)用戶只需要了解大概過去了多長時(shí)間,只有在比賽暫?;蛘咧袌隹旖Y(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í)長。通過這四個(gè)變量我們就可以維持計(jì)時(shí)器的狀態(tài)了,還可以通過計(jì)算得到我們需要展示的其他信息。這個(gè)類的基本功能就是都是為了操作這些變量,即維持計(jì)時(shí)器的這些狀態(tài)。
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)。
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è)長連接或者后臺(tái)服務(wù),只需要維持這幾個(gè)計(jì)時(shí)器的狀態(tài)就可以了。我們使用SharedPreference來實(shí)現(xiàn):
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)。
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)開始:
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è)置鬧鈴:
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ù)的:
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)給用戶:
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。
- android之計(jì)時(shí)器(Chronometer)的使用以及常用的方法
- android計(jì)時(shí)器,時(shí)間計(jì)算器的實(shí)現(xiàn)方法
- android開發(fā)教程之間隔執(zhí)行程序(android計(jì)時(shí)器)
- Android 編程下的計(jì)時(shí)器代碼
- Android時(shí)分秒計(jì)時(shí)器的兩種實(shí)現(xiàn)方法
- Android編程之簡單計(jì)時(shí)器實(shí)現(xiàn)方法
- Android中CountDownTimer倒計(jì)時(shí)器用法實(shí)例
- Android實(shí)現(xiàn)的秒表計(jì)時(shí)器示例
- 學(xué)習(xí)使用Android Chronometer計(jì)時(shí)器
- Android利用SurfaceView實(shí)現(xiàn)簡單計(jì)時(shí)器
相關(guān)文章
Android開發(fā)實(shí)現(xiàn)生成excel的方法詳解
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)生成excel的方法,結(jié)合實(shí)例形式詳細(xì)分析了Android生成Excel的具體步驟與存儲(chǔ)、導(dǎo)入、添加等相關(guān)操作技巧,需要的朋友可以參考下2017-10-10
android教程之使用popupwindow創(chuàng)建菜單示例
這篇文章主要介紹了android使用popupwindow創(chuàng)建菜單的示例,需要的朋友可以參考下2014-02-02
Android自定義帶有圓形進(jìn)度條的可長按控件功能
這篇文章主要介紹了Android自定義帶有圓形進(jìn)度條的可長按控件,思路很簡單,使用簡單的畫筆工具就可以完成這個(gè)控件,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-06-06
android 調(diào)用JNI SO動(dòng)態(tài)庫的方法
android 調(diào)用JNI 分為靜態(tài)調(diào)用與動(dòng)態(tài)調(diào)用,接下來通過本文給大家介紹android 調(diào)用JNI SO動(dòng)態(tài)庫的方法,感興趣的朋友一起看看吧2021-11-11
Android 處理 View 重復(fù)點(diǎn)擊的多種方法
這篇文章主要介紹了Android 處理 View 重復(fù)點(diǎn)擊的多種方法,幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-03-03
詳解Flutter網(wǎng)絡(luò)圖片本地緩存的實(shí)現(xiàn)
這篇文章主要為大家介紹了詳解Flutter網(wǎng)絡(luò)圖片本地緩存的實(shí)現(xiàn)示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
Android自定義Drawable之在Drawable中部指定透明區(qū)域方法示例
對(duì)于不同的屏幕密度、不同的設(shè)備方向,不同的語言和區(qū)域,都會(huì)涉及到備選 drawable 資源,下面這篇文章主要給你大家介紹了關(guān)于Android自定義Drawable之在Drawable中部指定透明區(qū)域的相關(guān)資料,需要的朋友可以參考下2018-07-07
Android獲取觸摸手勢(shì)實(shí)現(xiàn)左右滑動(dòng)
這篇文章主要為大家詳細(xì)介紹了Android獲取觸摸手勢(shì)實(shí)現(xiàn)左右滑動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05

