Android Glance API桌面Widget按钮添加触觉反馈方案求助
如何为Glance桌面Widget按钮添加触觉反馈?
我正在制作一个仅含按钮的Android桌面Widget,用于切换两张壁纸,希望为按钮添加触觉反馈。了解到Glance API的Compose功能有限,尝试的代码无法触发触觉反馈,且抛出「CompositionLocal LocalHapticFeedback not present」异常。但我看到Nothing手机的指南针Widget在指向北时会触发触觉反馈,说明该功能可实现。作为Android开发新手,恳请指导如何实现该效果。
尝试的Widget代码
// desktop widget to quickly toggle wallpaper object WallChangeWidget : GlanceAppWidget() { val wallVariant = intPreferencesKey("wallVariant") override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { Button(text = "click", onClick = hapticsAndThenAction()) } } @Composable fun hapticsAndThenAction(): Action { val haptic = LocalHapticFeedback.current haptic.performHapticFeedback(HapticFeedbackType.LongPress) return actionRunCallback(ChangeWallActionCallback::class.java) } }
切换壁纸的ActionCallback代码
object ChangeWallActionCallback : ActionCallback { @RequiresApi(Build.VERSION_CODES.R) override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { prefs -> val currentCount = prefs[WallChangeWidget.wallVariant] val x = WallpaperManager.getInstance(context) if (currentCount != 1) { val bmp: Bitmap = BitmapFactory.decodeFile("/storage/emulated/0/Media/Pictures/WallSwitch/two.png") x.setBitmap(bmp) prefs[WallChangeWidget.wallVariant] = 1 } else { val bmp: Bitmap = BitmapFactory.decodeFile("/storage/emulated/0/Media/Pictures/WallSwitch/one.png") x.setBitmap(bmp) // x.clear() prefs[WallChangeWidget.wallVariant] = 0 } } } }
抛出的异常信息
2024-03-23 21:56:31.511 32147-32183 WM-WorkerWrapper com.sliya.np.ext.wallswitch E Work [ id=0f9eb632-47c1-4e38-bd06-86acbf66c988, tags={ androidx.glance.session.SessionWorker } ] failed because it threw an exception/error java.util.concurrent.ExecutionException: java.lang.IllegalStateException: CompositionLocal LocalHapticFeedback not present at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:516) at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475) at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:311) at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:91) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) at java.lang.Thread.run(Thread.java:1012) Caused by: java.lang.IllegalStateException: CompositionLocal LocalHapticFeedback not present at androidx.compose.ui.platform.CompositionLocalsKt.noLocalProvidedFor(CompositionLocals.kt:220) at androidx.compose.ui.platform.CompositionLocalsKt.access$noLocalProvidedFor(CompositionLocals.kt:1) at androidx.compose.ui.platform.CompositionLocalsKt$LocalHapticFeedback$1.invoke(CompositionLocals.kt:117) at androidx.compose.ui.platform.CompositionLocalsKt$LocalHapticFeedback$1.invoke(CompositionLocals.kt:116) at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) at androidx.compose.runtime.LazyValueHolder.getCurrent(ValueHolders.kt:29) at androidx.compose.runtime.LazyValueHolder.getValue(ValueHolders.kt:31) at androidx.compose.runtime.CompositionLocalMapKt.read(CompositionLocalMap.kt:88) at androidx.compose.runtime.ComposerImpl.consume(Composer.kt:2049) at com.sliya.np.ext.wallswitch.WallChangeWidget.MyButton(WallChangeWidget.kt:90) at com.sliya.np.ext.wallswitch.ComposableSingletons$WallChangeWidgetKt$lambda-1$1.invoke(WallChangeWidget.kt:34) at com.sliya.np.ext.wallswitch.ComposableSingletons$WallChangeWidgetKt$lambda-1$1.invoke(WallChangeWidget.kt:33) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.glance.appwidget.SizeBoxKt$SizeBox$1.invoke(SizeBox.kt:127) at androidx.glance.appwidget.SizeBoxKt$SizeBox$1.invoke(SizeBox.kt:74) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228) at androidx.glance.appwidget.SizeBoxKt.SizeBox-IbIYxLY(SizeBox.kt:74) at androidx.glance.appwidget.SizeBoxKt.ForEachSize-eVKgIn8(SizeBox.kt:114) at androidx.glance.appwidget.AppWidgetSession$provideGlance$1$1.invoke(AppWidgetSession.kt:110) at androidx.glance.appwidget.AppWidgetSession$provideGlance$1$1.invoke(AppWidgetSession.kt:90) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228) at androidx.glance.appwidget.AppWidgetSession$provideGlance$1.invoke(AppWidgetSession.kt:85) at androidx.glance.appwidget.AppWidgetSession$provideGlance$1.invoke(AppWidgetSession.kt:84) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:169) at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2468) at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2737) at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3352) at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:3303) 2024-03-23 21:56:31.517 32147-32183 WM-WorkerWrapper com.sliya.np.ext.wallswitch E at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:781) at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1097) at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:124) at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:569) at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:537) at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42) at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:71) at androidx.glance.session.InteractiveFrameClock.sendFrame(InteractiveFrameClock.kt:127) at androidx.glance.session.InteractiveFrameClock.access$sendFrame(InteractiveFrameClock.kt:39) at androidx.glance.session.InteractiveFrameClock$onNewAwaiters$2.invokeSuspend(InteractiveFrameClock.kt:117) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at android.os.Handler.handleCallback(Handler.java:958) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:232) at android.os.Looper.loop(Looper.java:334) at android.app.ActivityThread.main(ActivityThread.java:8293) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:578) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1053)
解决方案
问题本质
Glance Widget的Compose渲染环境运行在后台Worker中,没有关联的View或Window容器,因此无法获取LocalHapticFeedback实例,这就是你抛出异常的原因。触觉反馈必须在用户交互触发的回调逻辑中,通过系统服务直接调用。
实现步骤
把触觉反馈逻辑移到ActionCallback的onAction方法中,使用系统的Vibrator或HapticFeedbackManager触发震动:
修改后的ActionCallback代码
object ChangeWallActionCallback : ActionCallback { @RequiresApi(Build.VERSION_CODES.R) override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { // 触发触觉反馈 val vibrator = context.getSystemService(Vibrator::class.java) if (vibrator?.hasVibrator() == true) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Android 8.0+ 使用振动效果API,短震100毫秒 val effect = VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE) vibrator.vibrate(effect) } else { // 兼容旧版本 vibrator.vibrate(100) } } // 原有切换壁纸逻辑 updateAppWidgetState(context, glanceId) { prefs -> val currentCount = prefs[WallChangeWidget.wallVariant] ?: 0 val wallpaperManager = WallpaperManager.getInstance(context) val bmp = if (currentCount != 1) { BitmapFactory.decodeFile("/storage/emulated/0/Media/Pictures/WallSwitch/two.png") } else { BitmapFactory.decodeFile("/storage/emulated/0/Media/Pictures/WallSwitch/one.png") } bmp?.let { wallpaperManager.setBitmap(it) } prefs[WallChangeWidget.wallVariant] = if (currentCount != 1) 1 else 0 } } }
简化Widget代码(移除错误的Composable触觉调用)
object WallChangeWidget : GlanceAppWidget() { val wallVariant = intPreferencesKey("wallVariant") override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { Button(text = "click", onClick = actionRunCallback(ChangeWallActionCallback::class.java)) } } }
必要配置
在AndroidManifest.xml中添加振动权限:
<uses-permission android:name="android.permission.VIBRATE" />
可选优化:使用HapticFeedbackManager
如果需要更贴近系统按钮的触觉反馈(比如轻触震动),可以替换为HapticFeedbackManager:
val hapticManager = context.getSystemService(HapticFeedbackManager::class.java) hapticManager?.performHapticFeedback(HapticFeedbackType.CONTEXT_CLICK)
内容的提问来源于stack exchange,提问作者Boby
相关产品推荐
相关产品推荐

