Display Glasses With Jetpack Compose Glimmer
Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)
- Skill
- Tool integration
- Software development
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides design principles, UI patterns, and development workflows for creating projected Android XR applications for display glasses. It focuses on the Jetpack Compose Glimmer toolkit, covering component usage, theming, input handling, and hardware integration for additive display environments.
What it does
- Defines Glimmer design principles and interaction models for display glasses.
- Provides guidance on setting up projected activities and managing device connectivity.
- Outlines UI component usage including cards, buttons, lists, and stacks.
- Details typography, color, and depth token usage for additive displays.
- Explains mapping hardware touchpad and gesture inputs to app actions.
- Offers patterns for voice input indicators and hardware access.
When to use it
- When developing Android XR apps specifically for display glasses.
- When implementing UI using the Jetpack Compose Glimmer toolkit.
- When designing interactions for one-dimensional hardware controls.
- When configuring apps to run as projected activities on connected eyewear.
Advantages
- Ensures consistent UI behavior across display glasses.
- Provides specific guidance for additive display rendering.
- Standardizes interaction patterns for limited input hardware.
- Facilitates proper integration with the Android XR projected context.
Usage examples
- “How do I set up a basic projected activity for display glasses?”
- The skill provides the AndroidManifest configuration and the necessary Activity setup, including the use of ProjectedDisplayController and GlimmerTheme.
- “What are the best practices for card layouts in Glimmer?”
- It explains how to use ActionCard for single actions and ButtonGroup with non-focusable cards to avoid focus contention.
- “How should I handle user input on display glasses?”
- The skill details mapping touchpad gestures like taps and swipes, and explains how to use System Back for navigation.
- “How do I implement a voice input indicator?”
- It provides a code pattern for using the VoiceInputIndicator component driven by SpeechRecognizer RMS changes.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/android/display-glasses-with-jetpack-compose-glimmer/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip display-glasses-with-jetpack-compose-glimmer.zip -d .agents/skills
Documentation by Google LLC
Glossary
| 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><br />
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())<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 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) } } }<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, 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") }<br />
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") } }<br />
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") } }<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, 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") } } }<br />
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) } } } }<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,
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") } } } }<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, 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), ) } }<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.
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") } }<br />
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)<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 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") } }<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 and Start a glasses activity on display glasses from a notification.
Files
- ATTRIBUTION.mdAdded by AutoGineer1.0 KiB
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md25.7 KiB
- references/android/design/ui/ai-glasses/guides/interaction/inputs.md4.6 KiB
- references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md25.9 KiB
- references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md9.1 KiB
- references/button-samples-source.md4.9 KiB
- references/button-source.md9.7 KiB
- references/card-samples-source.md10.3 KiB
- references/card-source.md51.7 KiB
- references/deptheffect-source.md7.8 KiB
- references/deptheffectlevels-source.md4.1 KiB
- references/glimmersansflextypography-source.md11.3 KiB
- references/glimmertheme-source.md7.6 KiB
- references/icon-source.md16.8 KiB
- references/iconbutton-source.md4.8 KiB
- references/iconsizes-source.md2.3 KiB
- references/list-source.md713 B
- references/listitem-source.md11.2 KiB
- references/liststate-source.md17.8 KiB
- references/material-hct-source.md6.3 KiB
- references/projectedcontext-source.md12.9 KiB
- references/stack-source.md15.3 KiB
- references/stackitemscope-source.md16.2 KiB
- references/stackstate-source.md12.9 KiB
- references/surface-source.md32.3 KiB
- references/titlechip-samples-source.md2.9 KiB
- references/titlechip-source.md5.1 KiB
references/card-source.md
When creating a Glimmer Card component, refer to the following source code in
Card.kt:
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.xr.glimmer
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
/**
* Card is a component used to group related information into a single digestible unit. A card can
* adapt to display a wide range of content, from simple text blurbs to more complex summaries with
* multiple elements. A card contains text [content], and may also have any combination of [title],
* [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is placed on top of the
* [subtitle], which is placed on top of the [content]. A card fills the maximum width available by
* default.
*
* This Card is focusable by default - see the other [Card] overload for a clickable Card.
*
* See [ActionCard] for a card with a primary action, and [ImageCard] for a card with an image.
*
* Cards can also be combined with a [TitleChip]. See the documentation for [TitleChip] for more
* information / sample code.
*
* A simple Card with just text:
*
* @sample androidx.xr.glimmer.samples.CardSample
*
* A Card with a trailing icon:
*
* @sample androidx.xr.glimmer.samples.CardWithTrailingIconSample
*
* A Card with a title, subtitle, and a leading icon:
*
* @sample androidx.xr.glimmer.samples.CardWithTitleAndSubtitleAndLeadingIconSample
*
* An example of usage when [focusable] is `false`, with focus handled by a [ButtonGroup] instead:
*
* @sample androidx.xr.glimmer.samples.NonFocusableCardWithButtonGroupSample
* @param modifier the [Modifier] to be applied to this card
* @param focusable whether this card is focusable. When `false`, this card will not receive focus
* or participate in focus navigation
* @param title optional title to be placed above [subtitle] and [content]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param leadingIcon optional leading icon to be placed before [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param trailingIcon optional trailing icon to be placed after [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], [subtitle],
* [leadingIcon], and [trailingIcon].
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / icons inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun Card(
modifier: Modifier = Modifier,
focusable: Boolean = true,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
color: Color = CardDefaults.color,
contentColor: Color = CardDefaults.contentColor(color),
contentPadding: PaddingValues = CardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
CardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.focusable(enabled = focusable, interactionSource = internalInteractionSource),
title = title,
subtitle = subtitle,
image = null,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
action = null,
contentPadding = contentPadding,
content = content,
)
}
/**
* Card is a component used to group related information into a single digestible unit. A card can
* adapt to display a wide range of content, from simple text blurbs to more complex summaries with
* multiple elements. A card contains text [content], and may also have any combination of [title],
* [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is placed on top of the
* [subtitle], which is placed on top of the [content]. A card fills the maximum width available by
* default.
*
* This Card is focusable and clickable - see the other [Card] overload for a non-clickable variant
* with optional focus.
*
* See [ActionCard] for a card with a primary action, and [ImageCard] for a card with an image.
*
* Cards can also be combined with a [TitleChip]. See the documentation for [TitleChip] for more
* information / sample code.
*
* A simple clickable Card with just text:
*
* @sample androidx.xr.glimmer.samples.ClickableCardSample
*
* A clickable Card with a trailing icon:
*
* @sample androidx.xr.glimmer.samples.ClickableCardWithTrailingIconSample
*
* A clickable Card with a title, subtitle, and a leading icon:
*
* @sample androidx.xr.glimmer.samples.ClickableCardWithTitleAndSubtitleAndLeadingIconSample
* @param onClick called when this card item is clicked
* @param modifier the [Modifier] to be applied to this card
* @param title optional title to be placed above [subtitle] and [content]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param leadingIcon optional leading icon to be placed before [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param trailingIcon optional trailing icon to be placed after [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], [subtitle],
* [leadingIcon], and [trailingIcon].
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / icons inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun Card(
onClick: () -> Unit,
modifier: Modifier = Modifier,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
color: Color = CardDefaults.color,
contentColor: Color = CardDefaults.contentColor(color),
contentPadding: PaddingValues = CardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
CardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.clickable(interactionSource = internalInteractionSource, onClick = onClick),
title = title,
subtitle = subtitle,
image = null,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
action = null,
contentPadding = contentPadding,
content = content,
)
}
/**
* ActionCard is a version of a card that contains a primary [action] that is placed inside the card
* along the bottom edge of the card. The action should be a [Button], and represents the action
* that will be performed when this card is interacted with. The main card itself is not focusable -
* the [action] takes the focus instead.
*
* ActionCard is a component used to group related information into a single digestible unit. An
* action card can adapt to display a wide range of content, from simple text blurbs to more complex
* summaries with multiple elements. An action card contains text [content], and may also have any
* combination of [title], [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is
* placed on top of the [subtitle], which is placed on top of the [content]. An action card fills
* the maximum width available by default.
*
* For more documentation and samples of the other cards, see [Card].
*
* @sample androidx.xr.glimmer.samples.ActionCardWithTitleSample
*
* ActionCard contains a single embedded [action]. If you need to have more than one action at a
* time, consider combining an unfocusable [Card] with a [ButtonGroup] instead. Cards can be
* configured as unfocusable by setting `focusable` to `false`.
*
* @sample androidx.xr.glimmer.samples.NonFocusableCardWithButtonGroupSample
* @param action the action for this card. This should be a [Button], and represents the action
* performed when a user interacts with this card. The action is placed inside the card along the
* bottom, and fills up the width of the [ActionCard].
* @param modifier the [Modifier] to be applied to the outer layout containing the card and action
* @param title optional title to be placed above [subtitle] and [content]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param leadingIcon optional leading icon to be placed before [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param trailingIcon optional trailing icon to be placed after [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], [subtitle],
* [leadingIcon], and [trailingIcon].
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / icons inside
* a card, this only affects the outermost content padding.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun ActionCard(
action: @Composable () -> Unit,
modifier: Modifier = Modifier,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = ActionCardDefaults.shape,
color: Color = ActionCardDefaults.color,
contentColor: Color = ActionCardDefaults.contentColor(color),
contentPadding: PaddingValues = ActionCardDefaults.contentPadding,
content: @Composable () -> Unit,
) {
CardImpl(
modifier = modifier.surface(shape = shape, color = color, contentColor = contentColor),
title = title,
subtitle = subtitle,
image = null,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
action = action,
contentPadding = contentPadding,
content = content,
)
}
/**
* ImageCard is a version of a card that contains a primary [image] that is placed at the top of the
* card.
*
* ImageCard is a component used to group related information into a single digestible unit. An
* image card can adapt to display a wide range of content, from simple text blurbs to more complex
* summaries with multiple elements. An image card contains an [image] and text [content], and may
* also have any combination of [title], [subtitle], [leadingIcon], and [trailingIcon]. If
* specified, [title] is placed on top of the [subtitle], which is placed on top of the [content],
* below [image]. An image card fills the maximum width available by default.
*
* This ImageCard is focusable by default - see the other [ImageCard] overload for a clickable
* ImageCard.
*
* For more documentation and samples of the other cards, see [Card].
*
* A simple ImageCard with just text:
*
* @sample androidx.xr.glimmer.samples.ImageCardSample
*
* An ImageCard with a title, subtitle, and a leading icon:
*
* @sample androidx.xr.glimmer.samples.ImageCardWithTitleAndSubtitleAndLeadingIconSample
*
* An example of usage when [focusable] is `false`, with focus handled by a [ButtonGroup] instead:
*
* @sample androidx.xr.glimmer.samples.NonFocusableCardWithButtonGroupSample
* @param image image to be placed at the top of the card. This image should typically fill the max
* width available, for example using [androidx.compose.ui.layout.ContentScale.FillWidth]. Images
* are constrained to a maximum aspect ratio (1.6) to avoid taking up too much vertical space, so
* using a modifier such as [androidx.compose.foundation.layout.fillMaxSize] will result in an
* image that fills the maximum aspect ratio.
* @param modifier the [Modifier] to be applied to this card
* @param focusable whether this card is focusable. When `false`, this card will not receive focus
* or participate in focus navigation
* @param title optional title to be placed above [subtitle] and [content], below [image]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param leadingIcon optional leading icon to be placed before [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param trailingIcon optional trailing icon to be placed after [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], [subtitle],
* [leadingIcon], and [trailingIcon].
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / icons inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun ImageCard(
image: @Composable () -> Unit,
modifier: Modifier = Modifier,
focusable: Boolean = true,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = ImageCardDefaults.shape,
color: Color = ImageCardDefaults.color,
contentColor: Color = ImageCardDefaults.contentColor(color),
contentPadding: PaddingValues = ImageCardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
CardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.focusable(enabled = focusable, interactionSource = internalInteractionSource),
title = title,
subtitle = subtitle,
image = image,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
action = null,
contentPadding = contentPadding,
content = content,
)
}
/**
* ImageCard is a version of a card that contains a primary [image] that is placed at the top of the
* card.
*
* ImageCard is a component used to group related information into a single digestible unit. An
* image card can adapt to display a wide range of content, from simple text blurbs to more complex
* summaries with multiple elements. An image card contains an [image] and text [content], and may
* also have any combination of [title], [subtitle], [leadingIcon], and [trailingIcon]. If
* specified, [title] is placed on top of the [subtitle], which is placed on top of the [content],
* below [image]. An image card fills the maximum width available by default.
*
* This ImageCard is focusable and clickable - see the other [ImageCard] overload for a
* non-clickable variant with optional focus.
*
* For more documentation and samples of the other cards, see [Card].
*
* A simple clickable ImageCard with just text:
*
* @sample androidx.xr.glimmer.samples.ClickableImageCardSample
*
* A clickable ImageCard with a title, subtitle, and a leading icon:
*
* @sample androidx.xr.glimmer.samples.ClickableImageCardWithTitleAndSubtitleAndLeadingIconSample
* @param onClick called when this card item is clicked
* @param image image to be placed at the top of the card. This image should typically fill the max
* width available, for example using [androidx.compose.ui.layout.ContentScale.FillWidth]. Images
* are constrained to a maximum aspect ratio (1.6) to avoid taking up too much vertical space, so
* using a modifier such as [androidx.compose.foundation.layout.fillMaxSize] will result in an
* image that fills the maximum aspect ratio.
* @param modifier the [Modifier] to be applied to this card
* @param title optional title to be placed above [subtitle] and [content], below [image]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param leadingIcon optional leading icon to be placed before [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param trailingIcon optional trailing icon to be placed after [content]. This is typically an
* [Icon] tinted with [contentColor] by default.
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], [subtitle],
* [leadingIcon], and [trailingIcon].
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / icons inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun ImageCard(
onClick: () -> Unit,
image: @Composable () -> Unit,
modifier: Modifier = Modifier,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = ImageCardDefaults.shape,
color: Color = ImageCardDefaults.color,
contentColor: Color = ImageCardDefaults.contentColor(color),
contentPadding: PaddingValues = ImageCardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
CardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.clickable(interactionSource = internalInteractionSource, onClick = onClick),
title = title,
subtitle = subtitle,
image = image,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
action = null,
contentPadding = contentPadding,
content = content,
)
}
/**
* LeadingImageCard is a component used to group related information into a single digestible unit,
* featuring a leading [image]. A leading image card contains text [content], and may also have any
* combination of [title] and [subtitle]. If specified, [title] is placed on top of the [subtitle],
* which is placed on top of the [content]. A leading image card fills the maximum width available
* by default.
*
* This LeadingImageCard is focusable by default - see the other [LeadingImageCard] overload for a
* clickable LeadingImageCard.
*
* A LeadingImageCard with a title and subtitle:
*
* @sample androidx.xr.glimmer.samples.LeadingImageCardSample
*
* An example of usage when [focusable] is `false`, with focus handled by a [ButtonGroup] instead:
*
* @sample androidx.xr.glimmer.samples.NonFocusableCardWithButtonGroupSample
* @param image the image to be placed before the text content. This image is allocated 30% of the
* card width.
* @param modifier the [Modifier] to be applied to this card
* @param focusable whether this card is focusable. When `false`, this card will not receive focus
* or participate in focus navigation
* @param title optional title to be placed above [subtitle] and [content]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], and [subtitle]
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / image inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun LeadingImageCard(
image: @Composable () -> Unit,
modifier: Modifier = Modifier,
focusable: Boolean = true,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
shape: Shape = LeadingImageCardDefaults.shape,
color: Color = LeadingImageCardDefaults.color,
contentColor: Color = LeadingImageCardDefaults.contentColor(color),
contentPadding: PaddingValues = LeadingImageCardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
SmallImageCardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.focusable(enabled = focusable, interactionSource = internalInteractionSource),
image = image,
title = title,
subtitle = subtitle,
contentPadding = contentPadding,
isLeading = true,
content = content,
)
}
/**
* LeadingImageCard is a component used to group related information into a single digestible unit,
* featuring a leading [image]. A leading image card contains text [content], and may also have any
* combination of [title] and [subtitle]. If specified, [title] is placed on top of the [subtitle],
* which is placed on top of the [content]. A leading image card fills the maximum width available
* by default.
*
* This LeadingImageCard is focusable and clickable - see the other [LeadingImageCard] overload for
* a non-clickable variant with optional focus.
*
* @param onClick called when this card item is clicked
* @param image the image to be placed before the text content. This image is allocated 30% of the
* card width.
* @param modifier the [Modifier] to be applied to this card
* @param title optional title to be placed above [subtitle] and [content]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], and [subtitle]
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / image inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun LeadingImageCard(
onClick: () -> Unit,
image: @Composable () -> Unit,
modifier: Modifier = Modifier,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
shape: Shape = LeadingImageCardDefaults.shape,
color: Color = LeadingImageCardDefaults.color,
contentColor: Color = LeadingImageCardDefaults.contentColor(color),
contentPadding: PaddingValues = LeadingImageCardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
SmallImageCardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.clickable(interactionSource = internalInteractionSource, onClick = onClick),
image = image,
title = title,
subtitle = subtitle,
contentPadding = contentPadding,
isLeading = true,
content = content,
)
}
/**
* TrailingImageCard is a component used to group related information into a single digestible unit,
* featuring a trailing [image]. A trailing image card contains text [content], and may also have
* any combination of [title] and [subtitle]. If specified, [title] is placed on top of the
* [subtitle], which is placed on top of the [content]. A trailing image card fills the maximum
* width available by default.
*
* This TrailingImageCard is focusable by default - see the other [TrailingImageCard] overload for a
* clickable TrailingImageCard.
*
* A TrailingImageCard with a title and subtitle:
*
* @sample androidx.xr.glimmer.samples.TrailingImageCardSample
*
* An example of usage when [focusable] is `false`, with focus handled by a [ButtonGroup] instead:
*
* @sample androidx.xr.glimmer.samples.NonFocusableCardWithButtonGroupSample
* @param image the image to be placed after the text content. This image is allocated 30% of the
* card width.
* @param modifier the [Modifier] to be applied to this card
* @param focusable whether this card is focusable. When `false`, this card will not receive focus
* or participate in focus navigation
* @param title optional title to be placed above [subtitle] and [content]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], and [subtitle]
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / image inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun TrailingImageCard(
image: @Composable () -> Unit,
modifier: Modifier = Modifier,
focusable: Boolean = true,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
shape: Shape = TrailingImageCardDefaults.shape,
color: Color = TrailingImageCardDefaults.color,
contentColor: Color = TrailingImageCardDefaults.contentColor(color),
contentPadding: PaddingValues = TrailingImageCardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
SmallImageCardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.focusable(enabled = focusable, interactionSource = internalInteractionSource),
image = image,
title = title,
subtitle = subtitle,
contentPadding = contentPadding,
isLeading = false,
content = content,
)
}
/**
* TrailingImageCard is a component used to group related information into a single digestible unit,
* featuring a trailing [image]. A trailing image card contains text [content], and may also have
* any combination of [title] and [subtitle]. If specified, [title] is placed on top of the
* [subtitle], which is placed on top of the [content]. A trailing image card fills the maximum
* width available by default.
*
* This TrailingImageCard is focusable and clickable - see the other [TrailingImageCard] overload
* for a non-clickable variant with optional focus.
*
* @param onClick called when this card item is clicked
* @param image the image to be placed after the text content. This image is allocated 30% of the
* card width.
* @param modifier the [Modifier] to be applied to this card
* @param title optional title to be placed above [subtitle] and [content]
* @param subtitle optional subtitle to be placed above [content], below [title]
* @param shape the [Shape] used to clip this card, and also used to draw the background and border
* @param color background color of this card
* @param contentColor content color used by components inside [content], [title], and [subtitle]
* @param contentPadding the spacing values to apply internally between the container and the
* content. Note that there is additional padding applied around the content / text / image inside
* a card, this only affects the outermost content padding.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting Interactions for this card. You can use this to change the card's appearance or
* preview the card in different states. Note that if `null` is provided, interactions will still
* happen internally.
* @param content the main content / body text to display inside this card. This is recommended to
* be limited to 10 lines of text.
*/
@Composable
public fun TrailingImageCard(
onClick: () -> Unit,
image: @Composable () -> Unit,
modifier: Modifier = Modifier,
title: @Composable (() -> Unit)? = null,
subtitle: @Composable (() -> Unit)? = null,
shape: Shape = TrailingImageCardDefaults.shape,
color: Color = TrailingImageCardDefaults.color,
contentColor: Color = TrailingImageCardDefaults.contentColor(color),
contentPadding: PaddingValues = TrailingImageCardDefaults.contentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
SmallImageCardImpl(
modifier =
modifier
.surface(
shape = shape,
color = color,
contentColor = contentColor,
interactionSource = internalInteractionSource,
)
.clickable(interactionSource = internalInteractionSource, onClick = onClick),
image = image,
title = title,
subtitle = subtitle,
contentPadding = contentPadding,
isLeading = false,
content = content,
)
}
@Composable
private fun CardImpl(
modifier: Modifier,
title: @Composable (() -> Unit)?,
subtitle: @Composable (() -> Unit)?,
image: @Composable (() -> Unit)?,
leadingIcon: @Composable (() -> Unit)?,
trailingIcon: @Composable (() -> Unit)?,
action: @Composable (() -> Unit)?,
contentPadding: PaddingValues,
content: @Composable () -> Unit,
) {
val iconSize = GlimmerTheme.iconSizes.large
val typography = GlimmerTheme.typography
val componentSpacingValues = GlimmerTheme.componentSpacingValues
val innerPadding = componentSpacingValues.small
val iconSpacing = componentSpacingValues.medium
Column(
modifier = modifier.defaultMinSize(minHeight = MinimumHeight).padding(contentPadding),
verticalArrangement = CardVerticalArrangement,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
image?.let {
Box(
Modifier.constrainHeightToAspectRatio(HeaderImageMaximumAspectRatio)
.clip(ImageShape),
contentAlignment = Alignment.Center,
) {
it()
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(innerPadding),
verticalAlignment = Alignment.CenterVertically,
) {
if (leadingIcon != null) {
Box(
modifier = Modifier.align(Alignment.Top).padding(end = iconSpacing),
contentAlignment = Alignment.TopStart,
) {
CompositionLocalProvider(
LocalIconSize provides iconSize,
content = leadingIcon,
)
}
}
Column(
Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(TextVerticalSpacing),
) {
if (title != null) {
CompositionLocalProvider(
LocalTextStyle provides typography.bodyMedium,
content = title,
)
}
if (subtitle != null) {
CompositionLocalProvider(
LocalTextStyle provides typography.caption,
content = subtitle,
)
}
CompositionLocalProvider(
LocalTextStyle provides typography.bodySmall,
content = content,
)
}
if (trailingIcon != null) {
Box(
modifier = Modifier.align(Alignment.Top).padding(start = iconSpacing),
contentAlignment = Alignment.TopEnd,
) {
CompositionLocalProvider(
LocalIconSize provides iconSize,
content = trailingIcon,
)
}
}
}
}
// b/436852852 - in a list the button won't be focused until it crosses the focus line.
action?.let {
Box(modifier = Modifier.fillMaxWidth(), propagateMinConstraints = true) { it() }
}
}
}
@Composable
private fun SmallImageCardImpl(
modifier: Modifier,
image: @Composable () -> Unit,
title: @Composable (() -> Unit)?,
subtitle: @Composable (() -> Unit)?,
contentPadding: PaddingValues,
isLeading: Boolean,
content: @Composable () -> Unit,
) {
val typography = GlimmerTheme.typography
val componentSpacingValues = GlimmerTheme.componentSpacingValues
val innerPadding = componentSpacingValues.small
val imageSpacing = componentSpacingValues.medium
Row(
modifier =
modifier
.defaultMinSize(minHeight = MinimumHeight)
.fillMaxWidth()
.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically,
) {
if (isLeading) {
Box(
modifier =
Modifier.align(Alignment.Top).weight(ImageWidthFraction).clip(ImageShape),
contentAlignment = Alignment.TopStart,
) {
image()
}
}
Column(
modifier =
Modifier.weight(1f - ImageWidthFraction)
.padding(
start = if (isLeading) imageSpacing else innerPadding,
end = if (isLeading) innerPadding else imageSpacing,
),
verticalArrangement = Arrangement.spacedBy(TextVerticalSpacing),
) {
if (title != null) {
CompositionLocalProvider(
LocalTextStyle provides typography.bodyMedium,
content = title,
)
}
if (subtitle != null) {
CompositionLocalProvider(
LocalTextStyle provides typography.caption,
content = subtitle,
)
}
CompositionLocalProvider(
LocalTextStyle provides typography.bodySmall,
content = content,
)
}
if (!isLeading) {
Box(
modifier =
Modifier.align(Alignment.Top).weight(ImageWidthFraction).clip(ImageShape),
contentAlignment = Alignment.TopStart,
) {
image()
}
}
}
}
/**
* A custom [Arrangement.Vertical] used by [CardImpl] that pins the action to the bottom of the card
* and centers the body content in the remaining space above it.
*
* For regular cards (single child), it vertically centers the body within the card. For action
* cards (two children), it places the action at the bottom of the available space and centers the
* body in the remaining space above the action.
*/
private object CardVerticalArrangement : Arrangement.Vertical {
override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
if (sizes.isEmpty()) return
if (sizes.size == 1) {
outPositions[0] = CenterVertically.align(sizes[0], totalSize)
return
}
val bodyHeight = sizes[0]
val actionHeight = sizes[1]
val availableBodyHeight = totalSize - actionHeight
// Sets outPositions[0] using CenterVertically on the available space
outPositions[0] = CenterVertically.align(size = bodyHeight, space = availableBodyHeight)
outPositions[1] = totalSize - actionHeight
}
}
/**
* Constrains the content's height to a maximum aspect ratio, based on the maximum width.
*
* This modifier is similar to [androidx.compose.foundation.layout.aspectRatio], but it only
* enforces a maximum size, allowing the content to be smaller than the bounds defined by the aspect
* ratio. It also only constrains the height based on the width, it does not constrain the width
* based on the height.
*
* @param widthToHeightRatio the maximum aspect ratio allowed for the height. This is defined as the
* ratio of width / height
*/
private fun Modifier.constrainHeightToAspectRatio(widthToHeightRatio: Float): Modifier {
require(widthToHeightRatio > 0) { "Ratio must be positive" }
return this.layout { measurable, constraints ->
// We only want to constrain height, based on width. If width is unbounded and there is a
// bounded height, we don't want to constrain the width based on height. So do nothing if
// we don't have a constrained width
if (!constraints.hasBoundedWidth) {
val placeable = measurable.measure(constraints)
return@layout layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
val height =
(constraints.maxWidth / widthToHeightRatio)
.roundToInt()
// Handle the case where the width is more than ratio times larger than available
// height
.coerceAtMost(constraints.maxHeight)
val newConstraints =
constraints.copy(
// Relax minimum height to let the content be smaller than constraints.minHeight if
// the aspect ratio results in a height smaller than min height
minHeight = 0,
maxHeight = height,
)
val placeable = measurable.measure(newConstraints)
// We relaxed the constraints earlier, but we still need to respect the incoming constraints
// ourselves.
val layoutHeight = placeable.height.coerceIn(constraints.minHeight, constraints.maxHeight)
layout(placeable.width, layoutHeight) {
// Center the content within the final layout height if needed
val y = (layoutHeight - placeable.height) / 2
placeable.placeRelative(0, y)
}
}
}
/** Default values used for [Card] */
public object CardDefaults {
/**
* Default content padding used for a [Card]
*
* This affects the outermost content padding applied around the content container. Note that
* there is additional padding applied around the content / text / icons inside a card, this
* only represents the outer padding for the entire content.
*/
public val contentPadding: PaddingValues
@Composable get() = PaddingValues(GlimmerTheme.componentSpacingValues.medium)
/** The default shape of [Card], which determines its corner radius. */
public val shape: Shape
@Composable get() = GlimmerTheme.shapes.medium
/** The default background color of [Card]. */
public val color: Color
@Composable get() = GlimmerTheme.colors.surface
/**
* Calculates the default content color for a [Card] based on the provided [color].
*
* @param color the background color of the card
* @return the calculated content color
*/
@Composable
public fun contentColor(color: Color = CardDefaults.color): Color {
return calculateContentColor(color)
}
}
/** Default values used for [ActionCard] */
public object ActionCardDefaults {
/**
* Default content padding used for an [ActionCard]
*
* This affects the outermost content padding applied around the content container. Note that
* there is additional padding applied around the content / text / icons inside an action card,
* this only represents the outer padding for the entire content.
*/
public val contentPadding: PaddingValues
@Composable get() = CardDefaults.contentPadding
/** The default shape of [ActionCard], which determines its corner radius. */
public val shape: Shape
@Composable get() = CardDefaults.shape
/** The default background color of [ActionCard]. */
public val color: Color
@Composable get() = CardDefaults.color
/**
* Calculates the default content color for an [ActionCard] based on the provided [color].
*
* @param color the background color of the action card
* @return the calculated content color
*/
@Composable
public fun contentColor(color: Color = ActionCardDefaults.color): Color {
return CardDefaults.contentColor(color)
}
}
/** Default values used for [ImageCard] */
public object ImageCardDefaults {
/**
* Default content padding used for an [ImageCard]
*
* This affects the outermost content padding applied around images and the content container.
* Note that there is additional padding applied around the content / text / icons inside an
* image card, this only represents the outer padding for the entire content.
*/
public val contentPadding: PaddingValues
@Composable get() = CardDefaults.contentPadding
/** The default shape of [ImageCard], which determines its corner radius. */
public val shape: Shape
@Composable get() = CardDefaults.shape
/** The default background color of [ImageCard]. */
public val color: Color
@Composable get() = CardDefaults.color
/**
* Calculates the default content color for an [ImageCard] based on the provided [color].
*
* @param color the background color of the image card
* @return the calculated content color
*/
@Composable
public fun contentColor(color: Color = ImageCardDefaults.color): Color {
return CardDefaults.contentColor(color)
}
}
/** Default values used for [LeadingImageCard] */
public object LeadingImageCardDefaults {
/**
* Default content padding used for a [LeadingImageCard]
*
* This affects the outermost content padding applied around the image and the content
* container. Note that there is additional padding applied around the content / text / image
* inside a leading image card, this only represents the outer padding for the entire content.
*/
public val contentPadding: PaddingValues
@Composable get() = CardDefaults.contentPadding
/** The default shape of [LeadingImageCard], which determines its corner radius. */
public val shape: Shape
@Composable get() = CardDefaults.shape
/** The default background color of [LeadingImageCard]. */
public val color: Color
@Composable get() = CardDefaults.color
/**
* Calculates the default content color for a [LeadingImageCard] based on the provided [color].
*
* @param color the background color of the leading image card
* @return the calculated content color
*/
@Composable
public fun contentColor(color: Color = LeadingImageCardDefaults.color): Color {
return CardDefaults.contentColor(color)
}
}
/** Default values used for [TrailingImageCard] */
public object TrailingImageCardDefaults {
/**
* Default content padding used for a [TrailingImageCard]
*
* This affects the outermost content padding applied around the image and the content
* container. Note that there is additional padding applied around the content / text / image
* inside a trailing image card, this only represents the outer padding for the entire content.
*/
public val contentPadding: PaddingValues
@Composable get() = CardDefaults.contentPadding
/** The default shape of [TrailingImageCard], which determines its corner radius. */
public val shape: Shape
@Composable get() = CardDefaults.shape
/** The default background color of [TrailingImageCard]. */
public val color: Color
@Composable get() = CardDefaults.color
/**
* Calculates the default content color for a [TrailingImageCard] based on the provided [color].
*
* @param color the background color of the trailing image card
* @return the calculated content color
*/
@Composable
public fun contentColor(color: Color = TrailingImageCardDefaults.color): Color {
return CardDefaults.contentColor(color)
}
}
/** Default minimum height for a [Card] */
private val MinimumHeight = 80.dp
/** Spacing between title / subtitle / body text */
private val TextVerticalSpacing = 3.dp
/** Shape used to clip the image in a card */
private val ImageShape = RoundedCornerShape(24.dp)
/**
* Width / height aspect ratio for the image used in [ImageCard], to prevent it from taking up too
* much vertical space
*/
private const val HeaderImageMaximumAspectRatio = 1.6f
/** Maximum width fraction for leading and trailing images in a card */
private const val ImageWidthFraction = 0.3f<br />
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Google LLC
- Publisher
- Android (Google) (Official)
- Distributed by
- Android (Google)
- Source
- android/skills · xr/display-glasses-with-jetpack-compose-glimmer · commit bf68e2d
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE.txt)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Google LLC or Android (Google).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download display-glasses-with-jetpack-compose-glimmer.zip