display-glasses-with-jetpack-compose-glimmer
About display-glasses-with-jetpack-compose-glimmer
A guide for Android developers building display-glasses experiences with Jetpack Compose Glimmer. It covers projected activities, theming, layout, focus, and input controls.
Key features
- 01Configure projected activities and glasses connection
- 02Use Glimmer themes and components
- 03Design for additive displays
- 04Handle touchpad input and focus navigation
- 05Follow text size and contrast guidance
Use cases
- 01Build Android interfaces projected to display glasses
- 02Design readable content for additive displays
- 03Implement glasses touch interactions and focus movement
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ android/ skills --skill display-glasses-with-jetpack-compose-glimmerAfter installing, type/display-glasses-with-jetpack-compose-glimmerin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with 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.mdDetails
From the project READMEGlossary
| Term | Definition |
|---|---|
| Intelligent eyewear | Hands-free eyewear with speakers, camera, and microphone (audio-only or display glasses). |
| Display glasses | Eyewear with a private in-lens display for glanceable visuals that pair with audio. |
| Jetpack Compose Glimmer | Compose UI toolkit providing components, theming, and focus behavior for display glasses. |
| Projected activity | An Activity running on a phone host whose UI and input are projected to connected eyewear. |
| Projected device | An XR eyewear device connected to an Android host phone that projects app content to it. |
| GlimmerTheme | Root styling provider for GlimmerColors, GlimmerTypography, and GlimmerShapes. |
| Additive display | Display where black (#000000) renders as 100% transparent by adding light to the scene. |
| Augmented experiences | Lightweight, additive Android XR experiences that help users while they're on the go. |
| Visual angle | Perceived size in XR; minimum readable text is 0.6 degrees (about 18sp at 1 m). |
Prerequisites
- Target
compileSdk37 or higher (compileSdk = 37orcompileSdk { version = release(37) }). UpdatecompileSdkto 37 if lower. - When adding dependencies, follow the instructions in Create your first
activity for intelligent eyewear:
androidx.xr.glimmer:glimmer(minimum1.0.0-alpha21, or catalog aliaslibs.androidx.glimmer)androidx.xr.glimmer:glimmer-google-fonts(minimum1.0.0-alpha21, or catalog aliaslibs.androidx.glimmer.google.fonts)androidx.xr.projected:projected(minimum1.0.0-alpha13, or catalog aliaslibs.androidx.xr.projectedorlibs.androidx.projected)- Check
gradle/libs.versions.tomlfirst; if catalog entries exist, use them inapp/build.gradle.ktsand verify their versions meet the minimums. Never guess1.0.0-alpha01forglimmer-google-fonts.
Core constraints and limitations
- Don't : Use
MaterialThemeor Material 3 components (androidx.compose.material3.*) inside Projected Activities or Glimmer UI. - Do : Use
GlimmerThemeand Glimmer components (androidx.xr.glimmer.*). - Do : Always pass
createGoogleSansFlexTypography()fromandroidx.xr.glimmer.googlefontsastypographyinGlimmerTheme. - Specific hardware device models or unique capabilities aren't detailed.
- Elevation : Material 3 shadow or elevation modifiers aren't supported. Use
ShadowScopeorDepthtokens from Jetpack Compose Glimmer. - Typography limits: Minimum readable text size is 18sp (0.6°). Avoid "Thin" or "Hairline" weights, which shimmer on additive lenses.
1. Set up dependencies
-
Set up projected activity : When creating a projected activity, follow Create your first activity for intelligent eyewear and projectedcontext-source.md. Set
android:requiredDisplayCategorytoandroid.hardware.display.category.XR_PROJECTEDinAndroidManifest.xml:<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>
-
Mobile app integration : In the host phone app, add a button with
Text("Launch on Glasses"). ObserveProjectedContext.isProjectedDeviceConnected(context, coroutineContext)usingcollectAsStateWithLifecycle(initialValue = false)to set the button'senabledstate so it's disabled when glasses are disconnected. When clicked, launchGlassesMainActivityon the glasses display usingProjectedContext.createProjectedActivityOptions(context).toBundle()as shown in the following snippet:val options = ProjectedContext.createProjectedActivityOptions(context) val intent = Intent(context, ProjectedMainActivity::class.java) context.startActivity(intent, options.toBundle())
-
UI library and font : When declaring the Glimmer, Google Fonts, and Projected artifacts in
app/build.gradle.kts, follow Declaring Jetpack Compose Glimmer Dependencies, and follow glimmersansflextypography-source.md when configuring Google Sans Flex usingcreateGoogleSansFlexTypography. -
Theming, initial focus, and black background : On additive displays, pure black (
Color.BlackorGlimmerTheme.colors.background) renders as 100% transparent. InonCreate(annotated with@OptIn(ExperimentalComposeUiApi::class)), setComposeUiFlags.isInitialFocusOnFocusableAvailable = true(androidx.compose.ui.ComposeUiFlags), wrap content inGlimmerTheme(typography = createGoogleSansFlexTypography()), and applyModifier.background(Color.Black)(orGlimmerTheme.colors.background) to the root container:@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) } } }
-
Hardware capabilities, permissions, and access : When checking hardware capabilities, requesting permissions, or accessing glasses hardware, follow Check device capabilities at runtime, Request hardware permissions, and Use a projected context to access hardware.
2. Minimize and translate the UI
- Build UI with Jetpack Compose Glimmer components and use depth for priority.
- Design from the bottom up and always bottom-align UI (
Alignment.BottomCenter). - One thing at a time : Show one primary piece of information at a time (for example, using
VerticalStack) and avoid multiple simultaneous cards. - Color contrast : When choosing colors, follow material-hct-source.md to maintain at least a 70% tone difference between foreground and background in HCT.
3. Map input controls
- Map tap and swipe interactions to the glasses touchpad. Inputs are one-dimensional; avoid nesting scrolling controls.
- Use System Back to dismiss temporary states or detailed views.
- When implementing focus navigation and hardware input handling, follow Focus in Jetpack Compose Glimmer and Hardware Controls for display glasses. Focus movement is continuous for scrollable containers and discrete for elements like button rows.
4. Build with Jetpack Compose Glimmer
Implementing Glimmer styles
When accessing theme tokens using GlimmerTheme, follow
glimmertheme-source.md.
| Category | Token | Value / Role |
|---|---|---|
| Color | primary / secondary | #9BBFFF / #4C88E9 (Focal colors) |
| Color | positive / negative | #7AD48F (Confirmation) / #F57082 (Alert) |
| Color | background | #000000 (Pure black, renders 100% transparent) |
| Color | surface | #262626 (Use SurfaceDefaults.border for borders) |
| Shape | Standard / Small | RoundedCornerShape(36.dp) / RoundedCornerShape(12.dp) |
Typography scale (Google Sans Flex) and depth levels
Use Google Sans Flex with variable axes ROND = 100f, wdth = 100f, opsz
= 9f, and wght by role:
| Style | Size / Line-Height | Weight |
|---|---|---|
| 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 |
When simulating depth with DepthEffect and DepthEffectLevels, follow
deptheffect-source.md and deptheffectlevels-source.md: use level1
for resting cards and background UI, and level2 for focused or pressed
interactive UI.
Implementing Jetpack Compose Glimmer components
Cards, ActionCards, ImageCards, and ButtonGroups
Cards (androidx.xr.glimmer.Card) group related content into a single focal
point. Never embed a Card within a ListItem. When implementing cards, follow
the instructions in Jetpack Compose Glimmer: Cards, card-source.md,
and card-samples-source.md.
-
Clickable versus non-clickable card : Pass
onClick = { ... }if the entireCardacts as a trigger; omitonClickif informational. Control focus on non-clickable cards withfocusable: Boolean = true(such asCard(focusable = false)). -
Single primary action (
ActionCard) : When a card has a single primary button action anchored to its bottom edge, useandroidx.xr.glimmer.ActionCardinstead of placing aButtoninsideCard. Itsactionbutton takes focus directly: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") }
-
Multiple actions (
CardandButtonGroup)---avoiding focus contention : When pairing a card with multiple action buttons, useandroidx.xr.glimmer.ButtonGroup(with optionalrememberButtonGroupState). Never nestButtonGroupinsideCard { ... }. Place a non-focusableCard(focusable = false)(withoutonClick) aboveButtonGroupas a sibling in aColumnso the card and buttons don't compete for focus: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") } }
-
Image cards : Use
ImageCard,LeadingImageCard, orTrailingImageCardto display prominent imagery alongside card text.
Buttons and ToggleButtons
When implementing buttons or toggle buttons, follow the instructions in Jetpack Compose Glimmer: Buttons, button-source.md, and button-samples-source.md.
-
Standard and icon buttons : Use
Buttonfor text actions (with optionalleadingIcon/trailingIcon) andIconButtonfor icon-only triggers (such as media next-track or previous-track buttons). -
Toggle buttons (
ToggleButtonandIconToggleButton) : UseToggleButtonorIconToggleButtonfor stateful on/off or play/pause controls that morph based onchecked:@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") } }
Title chips
TitleChip (androidx.xr.glimmer.TitleChip) is a non-interactive pill header
(three words or fewer, single line). When implementing title chips, follow the
instructions in Jetpack Compose Glimmer: Title Chips,
titlechip-source.md, and titlechip-samples-source.md.
-
Spatial spacing : When placing a standalone
TitleChipabove aCardorActionCard, center theTitleChiphorizontally and separate it from the card usingTitleChipDefaults.associatedContentSpacing(usingSpacerorArrangement.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") } } }
Icons
When adding icons or icon buttons, follow the instructions in Jetpack Compose Glimmer: Icons, icon-source.md, iconbutton-source.md, and iconsizes-source.md.
- Sizing and interactivity : Size icons with
GlimmerTheme.iconSizes(small,medium,large). UseIconButtonorIconToggleButtonfor triggers andIconfor status indicators. - Contrast and library : Never use pure black (
Color.Black) for icon tints; black renders as transparent. Use Material Symbols (600 weight, Rounded, Unfilled). Ifandroidx.compose.material:material-icons-*isn't inapp/build.gradle.kts, build vectors withImageVector.BuilderorpainterResourceinstead of importingandroidx.compose.material.icons.*.
Lists
Use GlimmerLazyColumn (androidx.xr.glimmer.list.GlimmerLazyColumn) for
homogeneous scrollable items. When building scrollable lists and list items,
follow the API definitions in list-source.md, listitem-source.md,
and liststate-source.md.
-
Always use
GlimmerLazyColumn: Never use foundationLazyColumn. ImportGlimmerLazyColumn,rememberGlimmerLazyListState, anditemsfromandroidx.xr.glimmer.list(itemis a scope member, not imported). For programmatic scrolling to the first item, hoistval listState = rememberGlimmerLazyListState()and calllistState.scrollToItem(0)orlistState.animateScrollToItem(0). -
Integrated title, spacing, and rows : Pass headers using
title = { TitleChip { Text("...") } }directly onGlimmerLazyColumn(not outside it), setverticalArrangement = Arrangement.spacedBy(20.dp), and render rows withandroidx.xr.glimmer.ListItem(onClick = { ... }, supportingLabel = { Text(...) }) { Text(...) }(pass the primary label as the trailingcontentlambda, notheadlineContent, and never nest aCardinsideListItem):@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) } } } }
Stacks
Use VerticalStack (androidx.xr.glimmer.stack.VerticalStack) to display
heterogeneous cards one at a time (such as calendar, transit, and weather
cards). Never use a TitleChip with VerticalStack. When implementing a
VerticalStack, follow the API definitions in stack-source.md,
stackstate-source.md, and stackitemscope-source.md.
-
Bottom alignment, height (+66.dp), and
itemDecoration: AlignVerticalStacktoAlignment.BottomCenter, set its height to66.dptaller than the tallest card (maxCardHeight + 66.dp), and applyModifier.itemDecoration(CardDefaults.shape)to theCardinside everyitem { ... }block so background cards are clipped and masked:@Composable fun VerticalStackSample() { VerticalStack { item { Card { Text("Top Card") } } item { Card { Text("Second Card") } } item { Card { Text("Third Card") } } } }
Horizontal pagers and animated text
Use GlimmerHorizontalPager
(androidx.xr.glimmer.pager.GlimmerHorizontalPager) with
rememberGlimmerPagerState(pageCount = { ... }) for horizontal card
walkthroughs (never foundation HorizontalPager).
-
Smooth text motion : Inside pager pages, configure Glimmer
Text(androidx.xr.glimmer.Text, neverandroidx.compose.material3.Text) withtextMotion = TextMotion.Animated(using.copy(textMotion = TextMotion.Animated)) for smooth rendering during snap transitions:// 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) builds custom interactive
components (such as status badges without Card or Button). When creating
custom surfaces, follow the API definitions in surface-source.md.
-
Shared
MutableInteractionSourceand order : Create a singleremember { MutableInteractionSource() }and pass it to bothModifier.surface(interactionSource = interactionSource, ...)and.clickable(interactionSource = interactionSource, ...)(or.focusable), chaining.surface(...)before.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") } }
Voice input indicator and indirect pointer gestures
-
VoiceInputIndicatorandContainedVoiceInputIndicator: Pass alevellambda returning aFloatnormalized to0.0f..1.0f. When driven bySpeechRecognizer(RecognitionListener.onRmsChanged(rmsdB: Float)), normalize and clamprmsdBwith.coerceIn(0f, 1f):// 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: Handle low-level touchpad tap (onClick), forward swipe (onSwipeForward), and backward swipe (onSwipeBackward) gestures usingandroidx.xr.glimmer.onIndirectPointerGestureon a container made focusable (usingfocusTarget,focusable, orsurface(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. Integrate with system UI
- When handling notifications or starting a projected activity from a notification, follow the instructions in Understand notification behavior for intelligent eyewear and Start a glasses activity on display glasses from a notification.