关于 display-glasses-with-jetpack-compose-glimmer
为 Android 开发者提供面向显示眼镜的 Jetpack Compose Glimmer 实现指南,涵盖投影 Activity、主题、布局、焦点和输入控制。
主要功能
- 01配置投影 Activity 与眼镜连接
- 02使用 Glimmer 主题和组件
- 03按加法显示特性设计界面
- 04处理触控板输入与焦点导航
- 05遵循文本尺寸和对比度要求
适用场景
- 01构建投影到显示眼镜的 Android 界面
- 02为加法显示器设计易读内容
- 03实现眼镜触控操作与焦点移动
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ android/ skills --skill display-glasses-with-jetpack-compose-glimmer安装后在 Claude Code 中输入/display-glasses-with-jetpack-compose-glimmer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/button-samples-source.mdbutton-source.mdcard-samples-source.mdcard-source.mddeptheffect-source.mddeptheffectlevels-source.mdglimmersansflextypography-source.mdglimmertheme-source.mdicon-source.mdiconbutton-source.mdiconsizes-source.mdlist-source.mdlistitem-source.mdliststate-source.mdmaterial-hct-source.mdprojectedcontext-source.mdstack-source.mdstackitemscope-source.mdstackstate-source.mdsurface-source.mdtitlechip-samples-source.mdreferences/android/design/ui/ai-glasses/guides/interaction/inputs.mdreferences/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.mdrequest-hardware-permissions.mdreferences/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.mdcards.mdfocus.mdicons.mdtext.mdtitle-chips.md详细介绍
术语表
| 术语 | 定义 |
|---|---|
| 智能眼镜 | 免提眼镜,配有扬声器、摄像头和麦克风(仅音频眼镜或带显示屏的眼镜)。 |
| 显示眼镜 | 镜片内置私密显示屏的眼镜,可显示一瞥即可查看的视觉内容,并与音频配合使用。 |
| Jetpack Compose Glimmer | Compose UI 工具包,提供适用于显示眼镜的组件、主题和焦点行为。 |
| 投射 Activity | 在手机主机上运行的 Activity,其 UI 和输入会投射到已连接的眼镜上。 |
| 投射设备 | 连接到 Android 主机手机并接收应用内容投射的 XR 眼镜设备。 |
| GlimmerTheme | 为 GlimmerColors、GlimmerTypography 和 GlimmerShapes 提供根样式的提供程序。 |
| 加色显示屏 | 通过向场景添加光线来渲染内容的显示屏,其中黑色(#000000)呈现为 100% 透明。 |
| 增强体验 | 轻量、加色的 Android XR 体验,可在用户外出时提供帮助。 |
| 视觉角度 | XR 中感知到的尺寸;可读文本的最小视觉角度为 0.6 度(距离 1 m 时约为 18sp)。 |
先决条件
- 目标
compileSdk为 37 或更高(compileSdk = 37或compileSdk { version = release(37) })。如果低于 37,请将compileSdk更新为 37。 - 添加依赖项时,请遵循为智能眼镜创建首个
Activity中的说明:
androidx.xr.glimmer:glimmer(最低版本为1.0.0-alpha21,或目录别名libs.androidx.glimmer)androidx.xr.glimmer:glimmer-google-fonts(最低版本为1.0.0-alpha21,或目录别名libs.androidx.glimmer.google.fonts)androidx.xr.projected:projected(最低版本为1.0.0-alpha13,或目录别名libs.androidx.xr.projected或libs.androidx.projected)- 先检查
gradle/libs.versions.toml;如果存在目录条目,请在app/build.gradle.kts中使用这些条目,并确认其版本符合最低要求。切勿猜测glimmer-google-fonts的版本为1.0.0-alpha01。
核心约束和限制
- 不要:在投射 Activity 或 Glimmer UI 中使用
MaterialTheme或 Material 3 组件(androidx.compose.material3.*)。 - 应当:使用
GlimmerTheme和 Glimmer 组件(androidx.xr.glimmer.*)。 - 应当:始终将
androidx.xr.glimmer.googlefonts中的createGoogleSansFlexTypography()作为typography传入GlimmerTheme。 - 未详细说明具体硬件设备型号或独特功能。
- 海拔:不支持 Material 3 阴影或海拔修饰符。请使用 Jetpack Compose Glimmer 中的
ShadowScope或Depth令牌。 - 排版限制:文本的最小可读字号为 18sp(0.6°)。避免使用“Thin”或“Hairline”字重,因为它们在加色镜片上会闪烁。
1. 设置依赖项
-
设置投射活动:创建投射活动时,请遵循为智能眼镜创建你的第一个活动和 projectedcontext-source.md。在
AndroidManifest.xml中将android:requiredDisplayCategory设置为android.hardware.display.category.XR_PROJECTED:<activity android:name="com.example.xr.projected.ProjectedMainActivity" android:exported="true" android:requiredDisplayCategory="android.hardware.display.category.XR_PROJECTED"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.XR_PROJECTED_LAUNCHER"/> </intent-filter> </activity>
-
移动应用集成:在宿主手机应用中,添加一个按钮,并设置
Text("Launch on Glasses")。使用collectAsStateWithLifecycle(initialValue = false)观察ProjectedContext.isProjectedDeviceConnected(context, coroutineContext), 并据此设置按钮的enabled状态,以便眼镜断开连接时禁用按钮。点击按钮后,使用ProjectedContext.createProjectedActivityOptions(context).toBundle()在眼镜显示屏上启动GlassesMainActivity,如下方代码片段所示:val options = ProjectedContext.createProjectedActivityOptions(context) val intent = Intent(context, ProjectedMainActivity::class.java) context.startActivity(intent, options.toBundle())
-
UI 库和字体:在
app/build.gradle.kts中声明 Glimmer、Google Fonts 和 Projected 工件时,请遵循声明 Jetpack Compose Glimmer 依赖项。使用createGoogleSansFlexTypography配置 Google Sans Flex 时,请遵循 glimmersansflextypography-source.md。 -
主题、初始焦点和黑色背景:在加色显示屏上,纯黑色(
Color.Black或GlimmerTheme.colors.background)会渲染为 100% 透明。在onCreate中(使用@OptIn(ExperimentalComposeUiApi::class)注解),设置ComposeUiFlags.isInitialFocusOnFocusableAvailable = true(androidx.compose.ui.ComposeUiFlags),使用GlimmerTheme(typography = createGoogleSansFlexTypography())包裹内容,并对根容器应用Modifier.background(Color.Black)(或GlimmerTheme.colors.background):@OptIn(ExperimentalProjectedApi::class) class ProjectedMainActivity : ComponentActivity() { private var displayController: ProjectedDisplayController? = null private var isVisualUiSupported by mutableStateOf(false) private var areVisualsOn by mutableStateOf(true) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { displayController?.close() displayController = null } }) initializeGlassesFeatures() setContent { GlimmerTheme { HomeScreen( areVisualsOn = areVisualsOn, isVisualUiSupported = isVisualUiSupported, onClose = { finish() } ) } } } private fun initializeGlassesFeatures() { lifecycleScope.launch { // Check device capabilities val projectedDeviceController = ProjectedDeviceController.create(this@ProjectedMainActivity) isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI) val controller = ProjectedDisplayController.create(this@ProjectedMainActivity) displayController = controller val observer = GlassesLifecycleObserver( context = this@ProjectedMainActivity, controller = controller, onVisualsChanged = { visualsOn -> areVisualsOn = visualsOn } ) lifecycle.addObserver(observer) } } }
-
硬件功能、权限和访问:检查硬件功能、请求权限或访问眼镜硬件时,请遵循 在运行时检查设备功能、请求硬件 权限以及使用投射上下文访问硬件。
2. 精简并转换 UI
- 使用 Jetpack Compose Glimmer 组件构建 UI,并通过深度体现优先级。
- 从底部向上设计,并始终将 UI 与底部对齐(
Alignment.BottomCenter)。 - 一次只呈现一项内容:一次只显示一条主要信息(例如使用
VerticalStack),避免同时显示多张卡片。 - 颜色对比度:选择颜色时,请遵循 material-hct-source.md,确保前景与背景在 HCT 中的色调差至少为 70%。
3. 映射输入控件
- 将点按和滑动交互映射到眼镜触控板。输入是一维的;避免嵌套滚动控件。
- 使用系统返回键关闭临时状态或详细视图。
- 实现焦点导航和硬件输入处理时,请遵循 Jetpack Compose Glimmer 中的焦点以及显示 眼镜的硬件控件。对于可滚动容器,焦点会连续移动;对于按钮行等元素,焦点则离散移动。
4. 使用 Jetpack Compose Glimmer 构建
实现 Glimmer 样式
使用 GlimmerTheme 访问主题令牌时,请遵循
glimmertheme-source.md。
| 类别 | 令牌 | 值 / 作用 |
|---|---|---|
| 颜色 | primary / secondary | #9BBFFF / #4C88E9(焦点颜色) |
| 颜色 | positive / negative | #7AD48F(确认) / #F57082(警示) |
| 颜色 | background | #000000(纯黑,渲染为 100% 透明) |
| 颜色 | surface | #262626(边框使用 SurfaceDefaults.border) |
| 形状 | Standard / Small | RoundedCornerShape(36.dp) / RoundedCornerShape(12.dp) |
字体排版比例(Google Sans Flex)和深度级别
使用 Google Sans Flex,可变轴设置为 ROND = 100f、wdth = 100f、opsz
= 9f,并根据角色设置 wght:
| 样式 | 大小 / 行高 | 字重 |
|---|---|---|
| Title Large / Medium / Small | 30/36.sp、24/32.sp、20/28.sp | 725 |
| Body Large / Medium / Small | 30/36.sp、24/32.sp、20/28.sp | 520 |
| Caption | 18.sp / 28.sp | 650 |
使用 DepthEffect 和 DepthEffectLevels 模拟深度时,请遵循
deptheffect-source.md 和 deptheffectlevels-source.md:静置卡片和背景 UI 使用 level1,聚焦或按下的交互式 UI 使用 level2。
实现 Jetpack Compose Glimmer 组件
卡片、ActionCard、ImageCard 和 ButtonGroups
卡片(androidx.xr.glimmer.Card)将相关内容组合成一个焦点。切勿在 ListItem 中嵌入 Card。实现卡片时,请遵循 Jetpack Compose Glimmer:卡片、card-source.md
和 card-samples-source.md 中的说明。
-
可点击与不可点击卡片:如果整个
Card都是触发器,请传入onClick = { ... };如果仅用于展示信息,则省略onClick。使用focusable: Boolean = true控制不可点击卡片的焦点(例如Card(focusable = false))。 -
单个主要操作(
ActionCard):当卡片在底边附带一个主要按钮操作时,请使用androidx.xr.glimmer.ActionCard,而不是在Card中放置Button。其action按钮可直接获得焦点:ActionCard( action = { Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { Text("Send") } } ) { Text("This is a card with a title, leading icon, header image, and action") }
-
多个操作(
Card和ButtonGroup)——避免焦点争用: 将卡片与多个操作按钮配对时,请使用androidx.xr.glimmer.ButtonGroup(可选使用rememberButtonGroupState)。**切勿将ButtonGroup嵌套在Card { ... }中。**在Column中,将不可聚焦的Card(focusable = false)(不带onClick) 放在作为同级元素的ButtonGroup上方,这样卡片和按钮 就不会争抢焦点:ButtonGroup(modifier = Modifier.fillMaxWidth()) { Button(onClick = {}) { Text("Button 1") } Button(onClick = {}) { Text("Button 2") } Button(onClick = {}) { Text("Button 3") } Button(onClick = {}) { Text("Button 4") } Button(onClick = {}) { Text("Button 5") } }
-
图像卡片:使用
ImageCard、LeadingImageCard或TrailingImageCard在卡片文字旁显示醒目的图像。
按钮和 ToggleButtons
实现按钮或切换按钮时,请遵循 Jetpack Compose Glimmer:按钮、button-source.md 和 button-samples-source.md 中的说明。
-
标准按钮和图标按钮:文字操作使用
Button(可选添加leadingIcon/trailingIcon),仅图标触发器使用IconButton(例如媒体播放器的下一曲或上一曲按钮)。 -
切换按钮(
ToggleButton和IconToggleButton):对于会随checked状态变化的开/关或播放/暂停控件,请使用ToggleButton或IconToggleButton:@Composable fun ToggleButtonWithLeadingIconSample() { var checked by remember { mutableStateOf(false) } ToggleButton( checked = checked, leadingIcon = { Icon( if (checked) FavoriteIcon else OutlinedFavoriteIcon, contentDescription = "Toggle favorite" ) }, onCheckedChange = { checked = it } ) { Text(if (checked) "On" else "Off") } }
标题标签
TitleChip(androidx.xr.glimmer.TitleChip)是一种非交互式胶囊形标题(三个词以内,单行显示)。实现标题标签时,请遵循 Jetpack Compose Glimmer:标题标签、titlechip-source.md 和 titlechip-samples-source.md 中的说明。
-
空间间距:将独立的
TitleChip放在Card或ActionCard上方时,请将TitleChip水平居中,并使用TitleChipDefaults.associatedContentSpacing设置其与卡片之间的间距(通过Spacer或Arrangement.spacedBy(TitleChipDefaults.associatedContentSpacing)):@Composable fun TitleChipWithCardSample() { Column(horizontalAlignment = Alignment.CenterHorizontally) { TitleChip { Text("Title Chip") } Spacer(Modifier.height(TitleChipDefaults.associatedContentSpacing)) Card( title = { Text("Title") }, subtitle = { Text("Subtitle") }, leadingIcon = { Icon(FavoriteIcon, "Localized description") }, ) { Text("Card Content") } } }
图标
添加图标或图标按钮时,请遵循 Jetpack Compose Glimmer:图标、icon-source.md、iconbutton-source.md 和 iconsizes-source.md 中的说明。
- 尺寸和交互性:使用
GlimmerTheme.iconSizes(small、medium、large)设置图标尺寸。触发操作时使用IconButton或IconToggleButton,状态指示时使用Icon。 - 对比度和图标库:绝不要使用纯黑色(
Color.Black)作为图标色调;黑色会呈现为透明。请使用 Material Symbols(600 字重、Rounded、未填充)。如果app/build.gradle.kts中没有androidx.compose.material:material-icons-*,请使用ImageVector.Builder或painterResource构建矢量图,而不要导入androidx.compose.material.icons.*。
列表
对同类可滚动条目使用 GlimmerLazyColumn(androidx.xr.glimmer.list.GlimmerLazyColumn)。构建可滚动列表和列表项时,请遵循 list-source.md、listitem-source.md 和 liststate-source.md 中的 API 定义。
-
始终使用
GlimmerLazyColumn:绝不要使用 foundationLazyColumn。从androidx.xr.glimmer.list导入GlimmerLazyColumn、rememberGlimmerLazyListState和items(item是作用域成员,无需导入)。要以编程方式滚动到第一项,请提升val listState = rememberGlimmerLazyListState(),并调用listState.scrollToItem(0)或listState.animateScrollToItem(0)。 -
集成标题、间距和行:直接在
GlimmerLazyColumn上通过title = { TitleChip { Text("...") } }传入标题(不要放在其外部),设置verticalArrangement = Arrangement.spacedBy(20.dp),并使用androidx.xr.glimmer.ListItem(onClick = { ... }, supportingLabel = { Text(...) }) { Text(...) }渲染行(将主标签作为末尾的contentlambda 传入,而不是headlineContent,且绝不要在ListItem内嵌套Card):@Composable fun GlimmerLazyColumnWithTitleChipSample() { val ingredientItems = listOf("Milk", "Flour", "Egg", "Salt", "Apples", "Butter", "Vanilla", "Sugar", "Cinnamon") GlimmerLazyColumn(title = { TitleChip { Text("Ingredients") } }) { items(ingredientItems) { text -> ListItem { Text(text) } } } }
堆叠
使用 VerticalStack(androidx.xr.glimmer.stack.VerticalStack)逐个显示不同类型的卡片(例如日历、交通和天气卡片)。绝不要在 VerticalStack 中使用 TitleChip。实现 VerticalStack 时,请遵循 stack-source.md、stackstate-source.md 和 stackitemscope-source.md 中的 API 定义。
-
底部对齐、尺寸(+66.dp)和
itemDecoration:将VerticalStack对齐到Alignment.BottomCenter,将其高度设置为比最高卡片高66.dp(maxCardHeight + 66.dp),并在每个item { ... }块内的Card上应用Modifier.itemDecoration(CardDefaults.shape),以便裁剪和遮罩背景卡片:@Composable fun VerticalStackSample() { VerticalStack { item { Card { Text("Top Card") } } item { Card { Text("Second Card") } } item { Card { Text("Third Card") } } } }
水平分页器和动画文本
水平卡片引导流程使用 GlimmerHorizontalPager
(androidx.xr.glimmer.pager.GlimmerHorizontalPager)和
rememberGlimmerPagerState(pageCount = { ... })(不要使用 foundation 的 HorizontalPager)。
-
平滑文本动画:在分页器页面中,将 Glimmer
Text(androidx.xr.glimmer.Text,不要使用androidx.compose.material3.Text)配置为textMotion = TextMotion.Animated(使用.copy(textMotion = TextMotion.Animated)),以便在吸附过渡期间实现平滑渲染:// Hoist the pager state, specifying the total page count with a lambda. val pagerState = rememberGlimmerPagerState(pageCount = { 10 }) GlimmerHorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), ) { page -> // Use Glimmer components like Card and Text for optimized glasses styling. Card(modifier = Modifier.fillMaxWidth()) { Text( text = "Page: $page", // Recommended: use TextMotion.Animated for smooth transitions in a pager. style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated), ) } }
Surface
Modifier.surface(androidx.xr.glimmer.surface)用于构建自定义交互组件(例如不使用 Card 或 Button 的状态徽章)。创建自定义 surface 时,请遵循 surface-source.md 中的 API 定义。
-
共享的
MutableInteractionSource和顺序:创建单个remember { MutableInteractionSource() },并将其传递给Modifier.surface(interactionSource = interactionSource, ...)和.clickable(interactionSource = interactionSource, ...)(或.focusable),按顺序先调用.surface(...),再调用.clickable(...):@Composable fun FocusableSurfaceSample() { val interactionSource = remember { MutableInteractionSource() } Box( modifier = Modifier .size(100.dp) .surface(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Focusable") } } @Composable fun ClickableSurfaceSample() { val interactionSource = remember { MutableInteractionSource() } Box( modifier = Modifier .size(100.dp) .surface(interactionSource = interactionSource) .focusable(interactionSource = interactionSource) .clickable( interactionSource = interactionSource, indication = null ) { /* Handle click action */ }, contentAlignment = Alignment.Center ) { Text("Clickable") } }
语音输入指示器和间接指针手势
-
VoiceInputIndicator和ContainedVoiceInputIndicator:传入一个返回Float的levellambda,数值需归一化到0.0f..1.0f。通过SpeechRecognizer(RecognitionListener.onRmsChanged(rmsdB: Float))驱动时, 使用.coerceIn(0f, 1f)对rmsdB进行归一化并限制范围:// Example state variable to hold the normalized level (0.0 to 1.0) // that the VoiceInputIndicator component expects. val audioLevel = MutableStateFlow(0f) // Initialize the Android SpeechRecognizer val speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context) // Listener to capture speech events and audio level changes val listener = object : RecognitionListener { override fun onRmsChanged(rmsdB: Float) { // Normalize raw dB level to a 0.0-1.0 range. // Android SpeechRecognizer's rmsdB typically ranges from 0 to ~10. audioLevel.value = ((rmsdB - 1f) / 9f).coerceIn(0f, 1f) } // ... Implement other required RecognitionListener methods ... override fun onReadyForSpeech(params: Bundle?) {} override fun onBeginningOfSpeech() {} override fun onEndOfSpeech() {} override fun onError(error: Int) {} override fun onResults(results: Bundle?) {} override fun onPartialResults(partialResults: Bundle?) {} override fun onEvent(eventType: Int, params: Bundle?) {} override fun onBufferReceived(buffer: ByteArray?) {} } // Attach the listener to the recognizer speechRecognizer.setRecognitionListener(listener) // Create an intent to specify the recognition model and behavior val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) } // Begin listening for audio input speechRecognizer.startListening(intent)
@Composable fun VoiceInputExample(modifier: Modifier) { // Collect the flow as Compose State so the UI reacts to changes in real-time. val currentLevel by audioLevel.collectAsState() VoiceInputIndicator( // The VoiceInputIndicator component provides a visual "pulse" or indicator // that changes based on the 'level' lambda, which returns a Float between 0.0 and 1.0. level = { currentLevel }, modifier = modifier ) }
-
Modifier.onIndirectPointerGesture:在设置为可聚焦的容器上,使用androidx.xr.glimmer.onIndirectPointerGesture处理低级触控板轻击 (onClick)、向前滑动(onSwipeForward)和向后滑动(onSwipeBackward)手势 (通过focusTarget、focusable或surface(focusable = true)设置为可聚焦):@Composable fun OnIndirectPointerGestureSample() { var lastGesture by remember { mutableStateOf("None") } Box( modifier = Modifier.fillMaxSize() .onIndirectPointerGesture( enabled = true, onSwipeForward = { lastGesture = "Forward" }, onSwipeBackward = { lastGesture = "Backward" }, onClick = { lastGesture = "Click" }, ) .focusTarget(), contentAlignment = Alignment.Center, ) { Text("Last Gesture: $lastGesture") } }
5. 与系统 UI 集成
- 处理通知或从通知启动投影活动时,请遵循了解智能眼镜的通知行为和通过通知在显示眼镜上启动眼镜活动中的说明。