/community/skills/android skills/display glasses with jetpack compose glimmer
innernet
docsskillscommunitycompanydeveloperssecurityprivacytermscontact

a skill by android, brought here by SD

display glasses with jetpack compose glimmer

paste this link into your ai. it will know what to do.

/skills/android-display-glasses-with-jetpack-compose-glimmer

Provides guidelines for developing projected Android XR apps for display

kindskill
last tended2026-11-05
written byandroid
sourcegithub.com/android/skills
kept currentwith its source · last read 2026-11-15
in the folderandroid skills · 2 of 2 · ‹ camerax
brought here bySD
reuseComplete terms in LICENSE.txt
yours?it stays under your name. to change it or take it down, write to connect@innernet.live
reading12 min

Glossary

TermDefinition
Intelligent eyewearHands-free eyewear with speakers, camera, and microphone (audio-only or display glasses).
Display glassesEyewear with a private in-lens display for glanceable visuals that pair with audio.
Jetpack Compose GlimmerCompose UI toolkit providing components, theming, and focus behavior for display glasses.
Projected activityAn Activity running on a phone host whose UI and input are projected to connected eyewear.
Projected deviceAn XR eyewear device connected to an Android host phone that projects app content to it.
GlimmerThemeRoot styling provider for GlimmerColors, GlimmerTypography, and GlimmerShapes.
Additive displayDisplay where black (#000000) renders as 100% transparent by adding light to the scene.
Augmented experiencesLightweight, additive Android XR experiences that help users while they're on the go.
Visual anglePerceived size in XR; minimum readable text is 0.6 degrees (about 18sp at 1 m).

Prerequisites

  • Target compileSdk 37 or higher (compileSdk = 37 or `compileSdk { version

= release(37) }). Update compileSdk` to 37 if lower.

  • When adding dependencies, follow the instructions in [Create your first

activity for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity):

  • androidx.xr.glimmer:glimmer (minimum 1.0.0-alpha21, or catalog alias libs.androidx.glimmer)
  • androidx.xr.glimmer:glimmer-google-fonts (minimum 1.0.0-alpha21, or catalog alias libs.androidx.glimmer.google.fonts)
  • androidx.xr.projected:projected (minimum 1.0.0-alpha13, or catalog alias libs.androidx.xr.projected or libs.androidx.projected)
  • Check gradle/libs.versions.toml first; if catalog entries exist, use them in app/build.gradle.kts and verify their versions meet the minimums. Never guess 1.0.0-alpha01 for glimmer-google-fonts.

Core constraints and limitations

  • Don't : Use MaterialTheme or Material 3 components (androidx.compose.material3.*) inside Projected Activities or Glimmer UI.
  • Do : Use GlimmerTheme and Glimmer components (androidx.xr.glimmer.*).
  • Do : Always pass createGoogleSansFlexTypography() from androidx.xr.glimmer.googlefonts as typography in GlimmerTheme.
  • Specific hardware device models or unique capabilities aren't detailed.
  • Elevation : Material 3 shadow or elevation modifiers aren't supported. Use ShadowScope or Depth tokens 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](references/projectedcontext-source.md). Set android:requiredDisplayCategory to android.hardware.display.category.XR_PROJECTED in AndroidManifest.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>

<br />

  • Mobile app integration : In the host phone app, add a button with

Text("Launch on Glasses"). Observe ProjectedContext.isProjectedDeviceConnected(context, coroutineContext) using collectAsStateWithLifecycle(initialValue = false) to set the button's enabled state so it's disabled when glasses are disconnected. When clicked, launch GlassesMainActivity on the glasses display using ProjectedContext.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())

<br />

  • 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](references/glimmersansflextypography-source.md) when configuring Google Sans Flex using createGoogleSansFlexTypography.

  • Theming, initial focus, and black background : On additive displays, pure

black (Color.Black or GlimmerTheme.colors.background) renders as 100% transparent. In onCreate (annotated with @OptIn(ExperimentalComposeUiApi::class)), set ComposeUiFlags.isInitialFocusOnFocusableAvailable = true (androidx.compose.ui.ComposeUiFlags), wrap content in GlimmerTheme(typography = createGoogleSansFlexTypography()), and apply Modifier.background(Color.Black) (or GlimmerTheme.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)
          }
      }
  }

<br />

  • Hardware capabilities, permissions, and access : When checking hardware

capabilities, requesting permissions, or accessing glasses hardware, follow Check device capabilities at runtime, [Request hardware permissions](references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md), and [Use a projected context to access hardware](references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md).

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](references/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](references/android/design/ui/ai-glasses/guides/interaction/inputs.md). 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](references/glimmertheme-source.md).

CategoryTokenValue / Role
Colorprimary / secondary#9BBFFF / #4C88E9 (Focal colors)
Colorpositive / negative#7AD48F (Confirmation) / #F57082 (Alert)
Colorbackground#000000 (Pure black, renders 100% transparent)
Colorsurface#262626 (Use SurfaceDefaults.border for borders)
ShapeStandard / SmallRoundedCornerShape(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:

StyleSize / Line-HeightWeight
Title Large / Medium / Small30/36.sp, 24/32.sp, 20/28.sp725
Body Large / Medium / Small30/36.sp, 24/32.sp, 20/28.sp520
Caption18.sp / 28.sp650

When simulating depth with DepthEffect and DepthEffectLevels, follow [deptheffect-source.md](references/deptheffect-source.md) and [deptheffectlevels-source.md](references/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](references/card-source.md), and [card-samples-source.md](references/card-samples-source.md).

  • Clickable versus non-clickable card : Pass onClick = { ... } if the entire Card acts as a trigger; omit onClick if informational. Control focus on non-clickable cards with focusable: Boolean = true (such as Card(focusable = false)).
  • Single primary action (ActionCard) : When a card has a single primary

button action anchored to its bottom edge, use androidx.xr.glimmer.ActionCard instead of placing a Button inside Card. Its action button 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")
  }

<br />

  • Multiple actions (Card and ButtonGroup)---avoiding focus contention :

When pairing a card with multiple action buttons, use androidx.xr.glimmer.ButtonGroup (with optional rememberButtonGroupState). Never nest ButtonGroup inside Card { ... }. Place a non-focusable Card(focusable = false) (without onClick) above ButtonGroup as a sibling in a Column so 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") }
  }

<br />

  • Image cards : Use ImageCard, LeadingImageCard, or TrailingImageCard

to 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](references/button-source.md), and [button-samples-source.md](references/button-samples-source.md).

  • Standard and icon buttons : Use Button for text actions (with optional leadingIcon/trailingIcon) and IconButton for icon-only triggers (such as media next-track or previous-track buttons).
  • Toggle buttons (ToggleButton and IconToggleButton) : Use

ToggleButton or IconToggleButton for stateful on/off or play/pause controls that morph based on checked:

  @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")
      }
  }

<br />

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](references/titlechip-source.md), and [titlechip-samples-source.md](references/titlechip-samples-source.md).

  • Spatial spacing : When placing a standalone TitleChip above a Card or

ActionCard, center the TitleChip horizontally and separate it from the card using TitleChipDefaults.associatedContentSpacing (using Spacer or 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")
          }
      }
  }

<br />

Icons

When adding icons or icon buttons, follow the instructions in Jetpack Compose Glimmer: Icons, [icon-source.md](references/icon-source.md), [iconbutton-source.md](references/iconbutton-source.md), and [iconsizes-source.md](references/iconsizes-source.md).

  • Sizing and interactivity : Size icons with GlimmerTheme.iconSizes (small, medium, large). Use IconButton or IconToggleButton for triggers and Icon for 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). If androidx.compose.material:material-icons-* isn't in app/build.gradle.kts, build vectors with ImageVector.Builder or painterResource instead of importing androidx.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](references/list-source.md), [listitem-source.md](references/listitem-source.md), and [liststate-source.md](references/liststate-source.md).

  • Always use GlimmerLazyColumn : Never use foundation LazyColumn. Import GlimmerLazyColumn, rememberGlimmerLazyListState, and items from androidx.xr.glimmer.list (item is a scope member, not imported). For programmatic scrolling to the first item, hoist `val listState =

rememberGlimmerLazyListState() and call listState.scrollToItem(0) or listState.animateScrollToItem(0)`.

  • Integrated title, spacing, and rows : Pass headers using `title = {

TitleChip { Text("...") } } directly on GlimmerLazyColumn (not outside it), set verticalArrangement = Arrangement.spacedBy(20.dp), and render rows with androidx.xr.glimmer.ListItem(onClick = { ... }, supportingLabel = { Text(...) }) { Text(...) } (pass the primary label as the trailing content lambda, not headlineContent, and never nest a Card inside ListItem`):

  @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) } }
      }
  }

<br />

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](references/stack-source.md), [stackstate-source.md](references/stackstate-source.md), and [stackitemscope-source.md](references/stackitemscope-source.md).

  • Bottom alignment, height (+66.dp), and itemDecoration : Align

VerticalStack to Alignment.BottomCenter, set its height to 66.dp taller than the tallest card (maxCardHeight + 66.dp), and apply Modifier.itemDecoration(CardDefaults.shape) to the Card inside every item { ... } 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") }
          }
      }
  }

<br />

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, never androidx.compose.material3.Text) with textMotion = 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),
          )
      }
  }

<br />

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](references/surface-source.md).

  • Shared MutableInteractionSource and order : Create a single `remember {

MutableInteractionSource() } and pass it to both Modifier.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")
      }
  }

<br />

Voice input indicator and indirect pointer gestures

  • VoiceInputIndicator and ContainedVoiceInputIndicator : Pass a level

lambda returning a Float normalized to 0.0f..1.0f. When driven by SpeechRecognizer (RecognitionListener.onRmsChanged(rmsdB: Float)), normalize and clamp rmsdB with .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)

<br />

  @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
      )
  }

<br />

  • Modifier.onIndirectPointerGesture : Handle low-level touchpad tap

(onClick), forward swipe (onSwipeForward), and backward swipe (onSwipeBackward) gestures using androidx.xr.glimmer.onIndirectPointerGesture on a container made focusable (using focusTarget, focusable, or 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")
      }
  }

<br />

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](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior) and Start a glasses activity on display glasses from a notification.


keep it where your ai can reach it.

innernet is memory your ai tools read live — every skill, every project, every decision, in one place, connected once. save this skill to yours, or publish one of your own as a link like this.

save it to your innernetsee every skill