Bring one-handed gestures to your Wear OS app

Posted by Chiara Chiappini, Developer Relation Engineer, Android Developer Relations


One-handed gestures offer a convenient and touch-free way for users to interact with their watches, enabling them to perform key actions using only the hand on which the device is worn. 

First introduced on Pixel Watch with Wear OS 6.1, one-handed gestures made quick interactions effortless, such as starting and stopping a timer, accepting calls, and controlling media. 

Now, with Wear OS 7, we're expanding this functionality with a new Gestures framework that allows OEMs to map gestures to primary actions and dismissals, and an API to bring gesture control to the developer community.

Starting with the 1.7 beta release of Compose for Wear OS, you can seamlessly integrate gesture control into your Wear Compose apps. To use this release, upgrade your Wear Compose dependency to:

androidx.wear.compose:compose-material3:1.7.0-beta01

Designing for one-handed interaction

The one-handed gestures framework is designed around two primary interaction patterns that allow users to take action without touching the screen:

  • Primary action, which on Pixel Watch is mapped to a double-pinch gesture: this action should be mapped to the most important task in a given context. For example, users can perform this gesture to take a photo in a camera app, start/stop a timer, or accept an incoming call. 
  • Dismiss action, which on Pixel Watch is mapped to a wrist turn gesture: this action is mapped to system back by default and provides an intuitive way to close interruptive screens or get back to the watch face. It may be overridden for specific use cases, such as silencing an incoming phone call.

These gestures are currently available on Pixel Watch 3 and newer, and the Wear OS gesture framework is available to all Wear OS device manufactures to adopt.

Check out our new design guidance for integrating one-handed gestures into your Wear app.

Integrating gestures with Compose on Wear OS

To provide seamless gesture support in Wear OS 7, we are introducing a new Modifier.oneHandedGesture that you can apply to any existing interactive composable to make it gesture-aware. 

Implementing gestures with Compose on Wear OS requires these steps:

  1. Define the gesture configuration. Start by using rememberOneHandedGestureConfiguration to define the nature of the interaction. This configuration dictates the basic behavior by providing the GestureAction (e.g. tracking a primary pinch or a dismiss wrist flick).
  2. Initialize the indicator state. Depending on your UI component, initialize a specific state object, such as OneHandedGestureClickIndicatorState for buttons or OneHandedGestureScrollIndicatorState for scrollable lists. This state is used to coordinate visual feedback between the gesture detection modifier and the visual UI indicators, seamlessly managing visibility, timing, and animations.
  3. Apply Modifier.oneHandedGesture to your interactive component. You'll pass in your configuration and state, and you’ll provide standard callbacks: onGestureAvailable to activate the visual hint when the system prepares the gesture, and onGesture to execute your action when the gesture happens.

The following sample shows how those three steps translate into code when configuring an IconButton:


val gestureConfig = rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
val indicatorState = remember { OneHandedGestureClickIndicatorState() }
val coroutineScope = rememberCoroutineScope()

OutlinedIconButton(
    onClick = onPlayPauseButtonClicked,
    modifier = Modifier.touchTargetAwareSize(IconButtonDefaults.LargeButtonSize)
        .oneHandedGesture(
            gestureConfiguration = gestureConfig,
            interactionSource = interactionSource,
            onGestureLabel = "play or pause",
            onGestureAvailable = { 
                coroutineScope.launch { indicatorState.showIndicator() } 
            },
            onGesture = onPlayPauseButtonClicked,
        ),
) {
    // button content goes here
    // See "Guided discovery with gesture indicators" section of this post for recommendations on adding a gesture indicator.
}

 

The GestureAction.Primary can also be used to scroll when the content is the end goal of the user journey, or there is a gesture actionable button off screen that the user can scroll to. Some examples include:

  • Scrolling through a notification to view the content and/or initiate a reply (available in TransformingLazyColumn and ScalingLazyColumn).
  • Paging through workout metrics or other content that doesn’t require the user to tap to continue the user journey (available in  HorizontalPager and VerticalPager).  

val scrollGestureConfig = rememberOneHandedGestureConfiguration(action = GestureAction.Primary)
val scrollIndicatorState = remember { OneHandedGestureScrollIndicatorState() }
val coroutineScope = rememberCoroutineScope()

TransformingLazyColumn(
    state = scrollState,
    contentPadding = contentPadding,
    modifier = Modifier
        .fillMaxSize()
        .oneHandedGesture(
            gestureConfiguration = scrollGestureConfig,
            onGestureLabel = "scroll",
            onGestureAvailable = { 
                coroutineScope.launch { scrollIndicatorState.showIndicator() } 
            },
            onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }
        )
) {
    // list content goes here
    // See "Guided discovery with gesture indicators" section of this post for recommendations on adding a gesture indicator.
}

Guided discovery with gesture indicators

To help users learn which gestures are available, gesture indicators work as hints to help discovery about which gestures are available on a screen.

These hints provide animated cues that inform users where they can perform a gesture. The framework manages the cadence and appearance of these hints, ensuring that they are helpful without being intrusive. System settings let users change the cadence to something less frequent if desired.

To integrate with hints, the API provides the following gesture indicator components:

The following example shows how to use the OneHandedGestureClickIndicator for a Button. See another example for using the OneHandedGestureScrollIndicator in our guidance.


val gestureConfig = rememberOneHandedGestureConfiguration(action = GestureAction.Primary)
val indicatorState = remember { OneHandedGestureClickIndicatorState() }
val coroutineScope = rememberCoroutineScope()

OutlinedIconButton(
    onClick = onPlayPauseButtonClicked,
    modifier = Modifier.touchTargetAwareSize(IconButtonDefaults.LargeButtonSize)
        .oneHandedGesture(
            gestureConfiguration = gestureConfig,
            interactionSource = interactionSource,
            onGestureLabel = "play or pause",
            onGestureAvailable = { 
                coroutineScope.launch { indicatorState.showIndicator() } 
            },
            onGesture = onPlayPauseButtonClicked,
        ),
) {
    OneHandedGestureClickIndicator(
        gestureConfiguration = gestureConfig,
        indicatorState = indicatorState,
    ) {
        val icon = if (playerUiModel.playbackState.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow
        Icon(icon, contentDescription = "Play or Pause")
    }
}

Sample app showing gesture hint for media controls

We are already seeing early adoption of these APIs from partners like Spotify, who are using one-handed gestures to make music control more seamless on the go. By adopting the Modifier.oneHandedGesture into their Wear OS app, Spotify allows users to play or pause their music with the primary gesture action, which on Pixel Watch devices is the double-pinch gesture. This action triggers the same behavior as the physical play/pause button, and the user doesn’t  need to touch the screen.

Spotify app with gesture integration

Bring one-handed gestures to your app

You can begin experimenting with one-handed gestures today in the 1.7 beta release of Compose for Wear OS.

Ensure your app is running on Wear OS 7, which provides the underlying platform support for gesture detection. Check out our new one-handed gestures developer guide  to see how you can start building more convenient experiences for your users.

What’s new in the Jetpack Compose August ’26 release

Posted by Nick Butcher, Product Manager, Jetpack Compose


Today, the Jetpack Compose August ‘26 release is stable! This release brings version 1.12 across core Compose modules (see the full BOM mapping), introducing rich visual APIs like Mesh Gradients and Wide Color Gamut (WCG) support, structural layout features like named areas in Grid, seamless integration with Android’s Credential Manager, and significant testing and performance improvements.

To update your project to today’s release, upgrade your Compose BOM version to 2026.08.00:

implementation(platform("androidx.compose:compose-bom:2026.08.00"))

Breaking Changes

AGP & Compile SDK: Compose 1.12 updates compileSdk to API 37, requiring a minimum AGP 9.2.0. As a reminder, Compose will always target the latest compileSdk. Learn more about this change here.

Modifier.onFirstVisible() is deprecated: Migrate to Modifier.onVisibilityChanged(), which provides more precise visibility threshold tracking.

Graphics

Mesh Gradients

Compose 1.12 introduces MeshGradientPainter to help you create multi-point, organic color gradients.



val rows = 1
val columns = 1

val gradientPainter = remember {
    MeshGradientPainter(rows, columns) {
        // Parameters: row, column, position, color
        setVertex(0, 0, Offset(0f, 0f), Color.Red)     // Top-Left
        setVertex(0, 1, Offset(1f, 0f), Color.Blue)    // Top-Right
        setVertex(1, 0, Offset(0f, 1f), Color.Green)   // Bottom-Left
        setVertex(1, 1, Offset(1f, 1f), Color.Yellow)  // Bottom-Right
    }
}

Box(
    modifier = modifier
        .aspectRatio(16/9f)
        .fillMaxWidth()
        .paint(gradientPainter)
)

For more information and examples, see the documentation.

Wide Color Gamut & HDR Support

Modern displays offer extended color fidelity and higher dynamic range. In Compose 1.12, full pipeline support for Wide Color Gamut (P3) and HDR rendering has been enabled across Compose graphics, paint, and shaders. Colors defined in non-sRGB color spaces (such as Display P3) are preserved through to platform rendering without color clamping. Colors will safely fall back to sRGB if they use an unsupported color space (e.g. CieXyz, CieLab, or Oklab), rely on a color space on an unsupported Android version (e.g Bt2020Hlg on Android 13 and below), or if the app is running on Android 9 (API 28) and below.

Other notable changes:

  • LayerOutsets was added to GraphicsLayer & Modifier.graphicsLayer, which you can use to increase the visual bounds of the layer beyond its measured size. Apply LayerOutsets to avoid the implicit clipToBounds behavior when the layer is promoted to an offscreen buffer.

Styles

At Google I/O, we shared our early vision for the Compose Styles API—a unified, performant way to style components. Since then, we have continued building the underlying architecture to guarantee strict type safety and predictable correctness, and to support building custom design systems.

To ensure we get this foundational layer correct, the API will remain experimental, and you can expect breaking changes.

Runtime Optimizations

Keyed SideEffect Overload

SideEffect now supports key arguments, which lets you fire one-shot side effects whenever specific keys change. This can lead to better performance compared to using a LaunchedEffect or DisposableEffect when you don’t need the coroutine or dispose block. SideEffect is up to 90% faster than LaunchedEffect and around 20% faster than DisposableEffect. Note that SideEffect runs its effect before DisposableEffect and LaunchedEffect, so use caution if migrating existing effects to this API, especially for LaunchedEffects that rely on being dispatched to start after the current frame is completed.

@Composable
fun AnalyticsTracker(userId: String, screenName: String) {
    SideEffect(key1 = userId, key2 = screenName) {
        analytics.logScreenView(userId, screenName)
    }
}

Animation

DeferredTargetAnimation has graduated out of experimental status.

Interactive Two-Stage Transitions

New composables: DeferredAnimatedContent and DeferredAnimatedVisibility allow creating delightful two-stage transitions, e.g. for predictive back gesture tracking.

Manual animation control: During a transition's deferred phase, animated properties (like scale or offset) can now be manually manipulated in real-time (e.g., tracking a swipe gesture).

Seamless handoff: Once the deferred phase ends, the transition engine takes over and performs a seamless handoff, including velocity transfer, to the automatic transition.

Shared element support: A new permitTransformDuringDeferredTransition flag in SharedContentConfig controls whether shared elements visually transform along with their parent containers during the deferred transition phase.

val state = remember { DeferredTransitionState(initialScreen) }
val transition = rememberDeferredTransition(state)

if (predictiveBackInProgress) {
    state.defer(targetScreen)
} else {
    state.animateTo(targetScreen)
}

transition.DeferredAnimatedContent(
    targetState = targetScreen,
    mutableTransformSpec = {
       MutableContentTransform {
           // Manually manipulate properties during the deferred phase
           initialContentTransform { scale = swipeProgress }
       }
    }
) { screen ->
    ScreenContent(screen)
}

Below are two demos of use cases where a gesture-driven animation is handed off to a triggered animation:



Text, Input & Platform Integrations

Editable Text Formatting

New APIs offer rich-text formatting for editable text in BasicTextField. You can now programmatically apply and manipulate inline character and paragraph formatting using SpanStyle and ParagraphStyle via the new addStyle() method inside a TextFieldBuffer scope (such as inside textFieldState.edit { ... } or an InputTransformation). Additionally, TextFieldBuffer provides getSpanStyles() and getParagraphStyles() APIs that return TrackedRange objects, allowing you to read, update, or remove applied styles. To complement formatting creation, TextFieldState now exposes a read-only textStyles property for querying active styles across ranges, while TextFieldBuffer provides originalTextStyles to inspect formatting state prior to an edit. Text formatting and custom annotations are persisted across configuration changes.

val state = rememberTextFieldState("Formatted text in Compose 1.12")

// Apply bold and color styles to a range of text
state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold, color = Color.Blue),
        start = 0,
        end = 9
    )
}

// Query active styles from TextFieldState
val currentStyles = state.textStyles

Text Selection

A new SelectionState API provides programmatic control and observability over text selection within a SelectionContainer. Hoisting a SelectionState object via rememberSelectionState() and passing into SelectionContainer exposes selectedTexts as a reactive list of AnnotatedStrings and provides methods like selectAll(), clear(), select(TextRange), and extendSelectionByWord().

Additionally, use getSelectableTexts() to retrieve all selectable text items in layout order and select text across composables in the SelectionContainer using a global range.

@Composable
fun ProgrammaticSelectionExample() {
    val selectionState = rememberSelectionState()

    Column {
        Button(
            onClick = { selectionState.selectAll() },
            modifier = Modifier.disableSelectionClearOnTap()
        ) {
            Text("Select All")
        }

        SelectionContainer(state = selectionState) {
            Text("Text content to be selected programmatically.")
        }
    }
}

Credential Manager Integration

Compose text fields now natively integrate with Android’s Credential Manager (API 34+) via the Autofill framework (below API 34 is handled by androidx.credentialslibrary). By attaching the new credentialRequest semantics property with CredentialRequestData, text inputs can prompt passkeys, saved credentials, or sign-in requests directly within the user input flow.

@Composable
fun LoginField(textFieldState: TextFieldState) {
    val credentialData = remember {
        CredentialRequestData(
            // Specify Credential Manager request options
        )
    }

    BasicTextField(
        state = textFieldState,
        modifier = Modifier.semantics {
            credentialRequest = credentialData
        }
    )
}

Other notable changes:

  • Support for font variation settings in downloadable fonts.
  • Enabled auto-scrolling when dragging text selection beyond the viewport in SelectionContainer.
  • Added support for automatic interaction sounds (clicks and focus navigation) to Compose components, with a new SoundEffectOnInteraction composable to allow opt-out. Note that as a consequence of this change, semantics click listeners must now be called from the main thread, which may affect a small number of test cases.
  • KeyboardType now includes Date, Time, DateTime, and SignedDecimal.
  • BasicSecureTextField now uses TextObfuscationMode.System by default, while RevealLastTyped serves as an absolute override.

Layout Enhancements

Named Areas in Grid Layout

Building complex 2D layouts is now easier with named areas in the @Experimental Grid component. Rather than managing numeric column and row indices across items, you can define semantic regions in your GridConfigurationScope and position composables by area name.

@OptIn(ExperimentalGridApi::class)
@Composable
fun DashboardLayout() {
    Grid(
        config = {
            area("header", row = 0, column = 0, rowSpan = 1, columnSpan = 2)
            area("sidebar", row = 1, column = 0)
            area("content", row = 1, column = 1)
            gap(16.dp)
        }
    ) {
        HeaderSection(modifier = Modifier.gridItem(areaId = "header"))
        NavigationSidebar(modifier = Modifier.gridItem(areaId = "sidebar"))
        MainContentView(modifier = Modifier.gridItem(areaId = "content"))
    }
}

Performance

As with every release, we continue to invest in Compose's performance to ensure that the framework helps you to build beautiful, performant apps. In this release we've focused on improving startup performance and are now seeing Time to Initial Display (the time it takes for an app to produce its first frame) that is comparable to Views in our benchmarks.

Testing & Tooling Upgrades

Test Synchronization

Compose 1.12 introduces new test APIs designed to reduce test execution times and eliminate flakiness during state sampling:

  • hasPendingWork: Passively checks if the UI has pending work without advancing the clock, which is ideal for manual animation loops.

  • runWithoutImplicitWait: Temporarily disables implicit synchronization when stepping through manual clock frames (e.g. animation tests).

  • @Test
    fun testAnimationStateFast() {
    
    composeTestRule.mainClock.autoAdvance = false
        
        while (composeTestRule.hasPendingWork()) {
            composeTestRule.mainClock.advanceTimeByFrame()
            composeTestRule.waitForIdle()
            
            composeTestRule.runOnUiThread {
                composeTestRule.runWithoutImplicitWait {
                    // This is most effective when querying multiple nodes in a single frame. 
                    // It prevents the redundant synchronization overhead that would 
                    // otherwise occur on every individual query.
                    val box1 = composeTestRule.onNodeWithTag("Box1").fetchSemanticsNode()
                    val box2 = composeTestRule.onNodeWithTag("Box2").fetchSemanticsNode()
                    
                    assertThat(box1.boundsInRoot.right).isAtMost(box2.boundsInRoot.left)
                }
            }
        }
    }

    Other notable changes:

    • The captureToImage API now allows you to capture a popup or dialog together with its anchor in a single bitmap.
    • Added onRootWithViewInteraction to scope Compose semantic searches to specific Android Views. This simplifies testing hybrid UIs, such as RecyclerViews, without requiring unique test tags in production code.
    • @PreviewWrapper annotations can now be applied to custom @MultiPreview classes, enabling reusable preview setups (such as custom themes) across multiple components.

    Happy Composing!

    As always, we value your input, so please share your feedback on these changes or what you'd like to see next on our issue tracker. Happy composing!

    Query report data with the Campaign Manager 360 API

    You can now use the reportData.query endpoint in the Campaign Manager 360 API to synchronously query your campaign performance data and retrieve structured JSON data directly in the response.

    The new reportData.query endpoint offers a simplified option for report data retrieval compared to the standard reporting API workflow:

    • Queries run synchronously with a 60-second execution limit, making this endpoint ideal for real-time dashboards and ad-hoc data explorations.
    • Report data is returned as structured JSON directly in the HTTP response, which eliminates file management overhead.
    • You no longer need to create a preconfigured Report resource to fetch data, as you can specify your dimensions, metrics, and filters directly in the request body.

    The existing Reports service is still recommended for retrieval of large datasets, scheduled jobs, or applications that rely on downloadable report files (CSV or Excel). This service requires multiple steps: developers must create and manage Report configurations, trigger a run of the report, poll for status updates, and download the report file.

    Get Started

    If you have any questions or need technical support, please reach out to Campaign Manager 360 API Support.