如何在Glance Widget中使用Timer实现秒级计时并长期保活?
用Glance Widget实现秒级计时的详细方案
核心思路
Glance Widget基于Android AppWidget机制,本身无法直接维持长周期的计时逻辑——因为Widget的Provider是广播接收器,生命周期极短。要实现秒级计时,必须依赖**前台服务(Foreground Service)**承载Timer逻辑,通过定时更新Widget的UI来展示实时计时数据,同时用持久化存储保存计时状态,避免进程重启后数据丢失。
Timer的编写位置
绝对不能将Timer写在Glance Widget的Provider类中,必须放在前台服务里:
- 前台服务拥有独立的生命周期,不会随Widget的更新回调结束而销毁;
- Android 8.0+对后台服务限制严格,前台服务通过显示持续通知获得更高优先级,不易被系统回收。
长期保持运行的关键措施
- 使用Foreground Service:强制显示前台通知,规避后台服务的系统限制;
- 状态持久化:用SharedPreferences保存累计秒数,服务重启后可恢复计时;
- 引导用户开启忽略电池优化:低电量场景下,系统会主动回收非白名单进程,需用户手动授权将应用加入电池优化白名单;
- 避免内存泄漏:服务销毁时必须取消Timer,释放资源。
完整实现步骤
1. 配置Glance Widget基础结构
创建GlanceWidget子类,负责渲染计时UI和处理启动/停止交互:
class TimerGlanceWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { val prefs = context.getSharedPreferences("timer_prefs", Context.MODE_PRIVATE) val elapsedSeconds = prefs.getLong("elapsed_seconds", 0L) content { Column( modifier = GlanceModifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalAlignment = Alignment.CenterVertically ) { Text( text = String.format("%02d:%02d", elapsedSeconds / 60, elapsedSeconds % 60), style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold) ) Row(modifier = GlanceModifier.padding(top = 16.dp)) { Button(text = "启动", onClick = actionStartService<TimerService>()) Button(text = "停止", onClick = actionStopService<TimerService>(), modifier = GlanceModifier.padding(start = 8.dp)) } } } } }
2. 实现前台计时服务
创建承载Timer逻辑的Foreground Service,每秒更新计时数据并同步到Widget:
class TimerService : Service() { private lateinit var timer: Timer private var elapsedSeconds = 0L private val prefs by lazy { getSharedPreferences("timer_prefs", MODE_PRIVATE) } override fun onCreate() { super.onCreate() // 恢复上次计时状态 elapsedSeconds = prefs.getLong("elapsed_seconds", 0L) // 启动前台服务,显示通知 val notification = NotificationCompat.Builder(this, "timer_channel") .setContentTitle("秒级计时器") .setContentText("当前计时: ${String.format("%02d:%02d", elapsedSeconds/60, elapsedSeconds%60)}") .setSmallIcon(R.drawable.ic_timer) .build() startForeground(1, notification) // 初始化Timer,每秒执行一次 timer = Timer() timer.scheduleAtFixedRate(object : TimerTask() { override fun run() { elapsedSeconds++ // 保存计时状态 prefs.edit().putLong("elapsed_seconds", elapsedSeconds).apply() // 更新Widget UI updateWidget() // 更新前台通知内容 updateNotification() } }, 0, 1000) } private fun updateWidget() { val glanceId = GlanceAppWidgetManager(this).getGlanceIds(TimerGlanceWidget::class.java).firstOrNull() glanceId?.let { TimerGlanceWidget().update(this, it) } } private fun updateNotification() { val notification = NotificationCompat.Builder(this, "timer_channel") .setContentTitle("秒级计时器") .setContentText("当前计时: ${String.format("%02d:%02d", elapsedSeconds/60, elapsedSeconds%60)}") .setSmallIcon(R.drawable.ic_timer) .build() (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify(1, notification) } override fun onDestroy() { super.onDestroy() timer.cancel() // 保存最终计时状态 prefs.edit().putLong("elapsed_seconds", elapsedSeconds).apply() } override fun onBind(intent: Intent): IBinder? = null }
3. 配置清单文件
在AndroidManifest.xml中注册Widget和服务,并声明必要权限:
<manifest ...> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- 可选:申请忽略电池优化权限,需用户手动授权 --> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <application ...> <!-- 注册Widget接收器 --> <receiver android:name=".TimerGlanceWidgetReceiver" android:exported="true"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/timer_widget_info" /> </receiver> <!-- 注册计时服务 --> <service android:name=".TimerService" android:foregroundServiceType="dataSync" /> </application> </manifest>
4. Widget配置文件
在res/xml/timer_widget_info.xml中定义Widget的基础属性:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="180dp" android:minHeight="120dp" android:updatePeriodMillis="0" <!-- 禁用系统自动更新,由服务手动控制 --> android:widgetCategory="home_screen" />
注意事项
- 首次启动前需创建通知渠道:Android 8.0+要求前台通知必须关联渠道,可在应用启动时初始化;
- 服务重启处理:若系统因资源紧张杀死服务,可通过
onStartCommand返回START_STICKY尝试自动重启,但无法保证100%生效; - 性能优化:每秒更新Widget会消耗少量系统资源,若无需严格秒级,可适当延长更新间隔。
内容的提问来源于stack exchange,提问作者wzk
相关产品推荐
相关产品推荐

