diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index b3846d6..13ae856 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -17,6 +17,7 @@
"./build-system/agp/agp-9-upgrade",
"./camera/camerax",
"./device-ai/appfunctions",
+ "./device-ai/ml-kit-genai-prompt-api",
"./devtools/android-cli",
"./identity/restore-credentials",
"./identity/verified-email",
@@ -25,6 +26,7 @@
"./jetpack-compose/theming/styles",
"./media/media3-cast-integration",
"./navigation/navigation-3",
+ "./navigation/navigation-event",
"./performance/r8-analyzer",
"./play/engage-sdk-integration",
"./play/play-billing-library-version-upgrade",
diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json
index 8911950..bf5f0ae 100644
--- a/.codex-plugin/plugin.json
+++ b/.codex-plugin/plugin.json
@@ -30,6 +30,11 @@
"path": "./device-ai/appfunctions"
}
},
+ {
+ "source": {
+ "path": "./device-ai/ml-kit-genai-prompt-api"
+ }
+ },
{
"source": {
"path": "./devtools/android-cli"
@@ -70,6 +75,11 @@
"path": "./navigation/navigation-3"
}
},
+ {
+ "source": {
+ "path": "./navigation/navigation-event"
+ }
+ },
{
"source": {
"path": "./performance/r8-analyzer"
diff --git a/device-ai/ml-kit-genai-prompt-api/SKILL.md b/device-ai/ml-kit-genai-prompt-api/SKILL.md
new file mode 100644
index 0000000..9ac0ca4
--- /dev/null
+++ b/device-ai/ml-kit-genai-prompt-api/SKILL.md
@@ -0,0 +1,121 @@
+---
+name: ml-kit-genai-prompt-api
+description: Analyzes Android codebases to implement ML Kit GenAI Prompt API. Use
+ this skill to send natural language requests on-device to Gemini Nano, use structured
+ output with Prompt API, implement prefix caching, optimize the current prompt, or
+ apply best practices."
+license: Complete terms in LICENSE.txt
+metadata:
+ author: Google LLC
+ last-updated: '2026-09-03'
+ keywords:
+ - ML Kit
+ - Prompt API
+ - Structured Output
+ - Prefix Caching
+ - Gemini Nano
+---
+
+This skill provides step-by-step guidance for integrating and optimizing the ML
+Kit GenAI Prompt API in Android apps.
+
+## Prerequisites
+
+- Android API level must be 26 or higher. If `minSdk` is below 26, update it to 26.
+- Add the ML Kit GenAI Prompt API dependency (`com.google.mlkit:genai-prompt`) to the app-level `build.gradle` file, with version at least `1.0.0-beta4`.
+- If `com.google.mlkit:genai-schema-compiler` dependency is used and KSP plugin version is below 2.3.6, update it to 2.3.6.
+
+## Detailed steps
+
+### 1. Prompt optimization
+
+To optimize prompts for use with the ML Kit Prompt API, follow the
+[prompt optimization guide](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prompt-optimization).
+
+### 2. Prefix caching optimization
+
+If the prompt is more than 200 words, implement the [prefix caching API](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching).
+
+### 3. Lifecycle and best practices
+
+- The model must be fully downloaded and available before calling the first inference. Follow the guide on [implementing a generative model](https://developer.android.com/agents/skills/device-ai/prompt-api/references/get-started) to check that the `FeatureStatus` of a model is `AVAILABLE` before making an inference.
+- Release ML Kit instances by calling `close()` when an `Activity`,
+ `Fragment`, or `ViewModel` is destroyed. Example:
+
+
+ ```kotlin
+ // Instantiating model in activity, fragment, or ViewModel
+ val generativeModel = Generation.getClient()
+
+ // When activity, fragment, or ViewModel is destroyed
+ generativeModel.close()
+ ```
+
+
+
+### 4. Structured output
+
+When implementing or refactoring a prompt to use structured output, follow
+these rules:
+
+1. **Check for API availability:** Verify Structured Output feature is available on the device with `isStructuredOutputFeatureAvailable()` before using it. Refer to the [Structured Output API guide](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output) for full instructions.
+2. **Return type:** Return the `@Generable` typed object from the function
+ signature instead of a `String` or JSON string.
+
+ For example:
+
+ fun parseEmail(email: String): String {
+ ...
+ }
+
+ should be refactored to:
+
+ fun parseEmail(email: String): ParsedEmail? {
+ ...
+ }
+
+3. **Example:**
+
+ This is the example code before refactoring:
+
+
+ ```kotlin
+ suspend fun parseEmail(email: String): String {
+ val parseEmailPrompt = "Parse this email and return the sender, title, and short summary of the email less than 10 words: "
+
+ val parsedEmail = generativeModel.generateContent(parseEmailPrompt + email)
+
+ return parsedEmail.candidates[0].text
+ }
+ ```
+
+
+
+ This is the example code after using Structured Output API:
+
+
+ ```kotlin
+ @Generable
+ data class ParsedEmail(
+ @Guide(description = "Sender of the email")
+ var sender: String = "",
+
+ @Guide(description = "Title of the email")
+ var title: String = "",
+
+ @Guide(description = "Summary of the email less than 10 words")
+ var summary: String = ""
+ )
+
+ suspend fun parseEmail(email: String): ParsedEmail? {
+ val parseEmailPrompt =
+ "Parse this email: $email"
+
+ val baseRequest = GenerateContentRequest.Builder(TextPart(parseEmailPrompt)).build()
+ val typedRequest = generateTypedContentRequest(baseRequest, ParsedEmail::class)
+ val typedResponse = generativeModel.generateContent(typedRequest)
+ return typedResponse.candidates[0].response
+ }
+ ```
+
+
diff --git a/device-ai/ml-kit-genai-prompt-api/references/get-started.md b/device-ai/ml-kit-genai-prompt-api/references/get-started.md
new file mode 100644
index 0000000..ba6598a
--- /dev/null
+++ b/device-ai/ml-kit-genai-prompt-api/references/get-started.md
@@ -0,0 +1,272 @@
+This page describes how to do the following:
+
+- Configure your project to use Prompt API
+- Provide text-only input and receive a response
+- Provide an image input with related text input and receive a response
+
+For more details about the Prompt API, see the
+reference documentation for Kotlin ([com.google.mlkit.genai.prompt](https://developer.android.com/android/reference/kotlin/com/google/mlkit/genai/prompt/package-summary)) and
+Java ([com.google.mlkit.genai.prompt.java](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/java/package-summary),
+[com.google.mlkit.genai.prompt](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/package-summary)).
+
+## Configure project
+
+> [!NOTE]
+> **Note:** This API requires Android API level 26 or higher.
+
+Add the ML Kit Prompt API as a dependency in your `build.gradle` configuration:
+
+ implementation("com.google.mlkit:genai-prompt:1.0.0-beta2")
+
+If you need your responses in a certain format using the Structured Output API,
+you need to configure KSP and add additional dependencies. For details, see
+[Generate structured output](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output).
+
+## Implement generative model
+
+To implement the code in your project, follow these steps:
+
+- Create a `generativeModel` object:
+
+ ### Kotlin
+
+ // Get a GenerativeModel instance
+ val generativeModel = Generation.getClient()
+
+ ### Java
+
+ // Get a GenerativeModel instance
+ GenerativeModelFutures generativeModelFutures = GenerativeModelFutures
+ .from(Generation.INSTANCE.getClient());
+
+- Check if Gemini Nano is `AVAILABLE,` `DOWNLOADABLE`, or `UNAVAILABLE`. Then,
+ download the feature if it is downloadable:
+
+ ### Kotlin
+
+ val status = generativeModel.checkStatus()
+ when (status) {
+ FeatureStatus.UNAVAILABLE -> {
+ // Gemini Nano not supported on this device or device hasn't fetched the latest configuration to support it
+ }
+
+ FeatureStatus.DOWNLOADABLE -> {
+ // Gemini Nano can be downloaded on this device, but is not currently downloaded
+ generativeModel.download().collect { status ->
+ when (status) {
+ is DownloadStatus.DownloadStarted ->
+ Log.d(TAG, "starting download for Gemini Nano")
+
+ is DownloadStatus.DownloadProgress ->
+ Log.d(TAG, "Nano ${status.totalBytesDownloaded} bytes downloaded")
+
+ DownloadStatus.DownloadCompleted -> {
+ Log.d(TAG, "Gemini Nano download complete")
+ modelDownloaded = true
+ }
+
+ is DownloadStatus.DownloadFailed -> {
+ Log.e(TAG, "Nano download failed ${status.e.message}")
+ }
+ }
+ }
+ }
+
+ FeatureStatus.DOWNLOADING -> {
+ // Gemini Nano currently being downloaded
+ }
+
+ FeatureStatus.AVAILABLE -> {
+ // Gemini Nano currently downloaded and available to use on this device
+ }
+ }
+
+ ### Java
+
+ ListenableFuture status = generativeModelFutures.checkStatus();
+ Futures.addCallback(generativeModelFutures.checkStatus(), new FutureCallback<>() {
+ @Override
+ public void onSuccess(Integer featureStatus) {
+ switch (featureStatus) {
+ case FeatureStatus.AVAILABLE -> {
+ // Gemini Nano currently downloaded and available to use on this device
+ }
+ case FeatureStatus.UNAVAILABLE -> {
+ // Gemini Nano not supported on this device or device hasn't fetched the latest configuration to support it
+ }
+ case FeatureStatus.DOWNLOADING -> {
+ // Gemini Nano currently being downloaded
+ }
+ case FeatureStatus.DOWNLOADABLE -> {
+ generativeModelFutures.download(new DownloadCallback() {
+ @Override
+ public void onDownloadStarted(long l) {
+ Log.d(TAG, "starting download for Gemini Nano");
+ }
+ @Override
+ public void onDownloadProgress(long l) {
+ Log.d(TAG, "Nano " + l + " bytes downloaded");
+ }
+ @Override
+ public void onDownloadCompleted() {
+ Log.d(TAG, "Gemini Nano download complete");
+ }
+ @Override
+ public void onDownloadFailed(@NonNull GenAiException e) {
+ Log.e(TAG, "Nano download failed: " + e.getMessage());
+ }
+ });
+ }
+ }
+ }
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ // Failed to check status
+ }
+ }, ContextCompat.getMainExecutor(context));
+
+## Provide text-only input
+
+### Kotlin
+
+ val response = generativeModel.generateContent("Write a 3 sentence story about a magical dog.")
+
+### Java
+
+ GenerateContentResponse response = generativeModelFutures.generateContent(
+ new GenerateContentRequest.Builder(
+ new TextPart("Write a 3 sentence story about a magical dog."))
+ .build())
+ .get();
+
+Alternatively, add optional parameters:
+
+### Kotlin
+
+ val response = generativeModel.generateContent(
+ generateContentRequest(
+ TextPart("Write a 3 sentence story about a magical dog."),
+ ) {
+ // Optional parameters
+ temperature = 0.2f
+ topK = 10
+ candidateCount = 3
+ },
+ )
+
+### Java
+
+ GenerateContentRequest.Builder requestBuilder =
+ new GenerateContentRequest.Builder(
+ new TextPart("Write a 3 sentence story about a magical dog."));
+ requestBuilder.setTemperature(.2f);
+ requestBuilder.setTopK(10);
+ requestBuilder.setCandidateCount(3);
+
+ GenerateContentResponse response =
+ generativeModelFutures.generateContent(requestBuilder.build()).get();
+
+For more information about the optional parameters, see [Optional
+configurations](https://developer.android.com/agents/skills/device-ai/ml-kit-genai-prompt-api/references/get-started#optional-configurations).
+
+## Provide multimodal (image and text) input
+
+Bundle an image and a text input together in the `generateContentRequest()`
+function, with the text prompt being a question or command related to the
+image. You can bundle multiple images and text together in the same request.
+
+### Kotlin
+
+ val response = generativeModel.generateContent(
+ generateContentRequest(ImagePart(bitmap), TextPart(textPrompt)) {
+ // optional parameters
+ ...
+ },
+ )
+
+### Java
+
+ GenerateContentResponse response = generativeModelFutures.generateContent(
+ new GenerateContentRequest.Builder(
+ new ImagePart(bitmap),
+ new TextPart("textPrompt"))
+ // optional parameters
+ .build())
+ .get();
+
+## Process inference result
+
+- Run the inference and retrieve the result. You can choose to either wait for
+ the full result or stream the response as it's generated for both text-only
+ and multimodal prompts.
+
+ - This uses non-streaming inference, which retrieves the entire result from
+ the AI model before returning the result:
+
+ ### Kotlin
+
+ // Call the AI model to generate content and store the complete
+ // in a new variable named 'response' once it's finished
+ val response = generativeModel.generateContent("Write a 3 sentence story about a magical dog")
+
+ ### Java
+
+ GenerateContentResponse response = generativeModelFutures.generateContent(
+ new GenerateContentRequest.Builder(
+ new TextPart("Write a 3 sentence story about a magical dog."))
+ .build())
+ .get();
+
+ - The following snippets are examples of using streaming inference, which
+ retrieves the result in chunks as it's being generated:
+
+ ### Kotlin
+
+ // Streaming inference
+ var fullResponse = ""
+ generativeModel.generateContentStream("Write a 3 sentence story about a magical dog").collect { chunk ->
+ val newChunkReceived = chunk.candidates[0].text
+ print(newChunkReceived)
+ fullResponse += newChunkReceived
+ }
+
+ ### Java
+
+ // Streaming inference
+ StringBuilder fullResponse = new StringBuilder();
+ generativeModelFutures.generateContent(new GenerateContentRequest.Builder(
+ (new TextPart("Write a 3 sentence story about a magical dog"))).build(),
+ chunk -> {
+ Log.d(TAG, chunk);
+ fullResponse.append(chunk);
+ });
+
+For more information about streaming and non-streaming inference, see [Streaming
+versus non-streaming](https://developer.android.com/ml-kit/genai#streaming-vs-non).
+
+## Latency optimization
+
+To optimize for the first inference call, your application may optionally call
+`warmup()`. This loads Gemini Nano into memory and initializes runtime
+components.
+
+## Optional configurations
+
+As part of each `GenerateContentRequest`, you can set the following optional
+parameters:
+
+- `temperature` : Controls the degree of randomness in token selection.
+- `seed` : Enables generating stable and deterministic results.
+- `topK` : Controls randomness and diversity in results.
+- `candidateCount` : Requests the number of unique responses returned. Note that the exact number of responses may not be the same as `candidateCount` because duplicate responses are automatically removed.
+- `maxOutputTokens` : Defines the maximum number of tokens that can be generated in the response.
+
+For more guidance on setting optional configurations, see
+[`GenerateContentRequest`](https://developer.android.com/android/reference/kotlin/com/google/mlkit/genai/prompt/GenerateContentRequest).
+
+## Supported features and limitations
+
+- Input must be under 4000 tokens (or approximately 3000 English words). For more information, see the [`countTokens`](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/GenerativeModel#countTokens(com.google.mlkit.genai.prompt.GenerateContentRequest)) reference.
+- Use cases that require long output (more than 4K tokens) should be avoided.
+- AICore enforces an inference quota per app. For more information, see [Quota
+ per application](https://developer.android.com/ml-kit/genai#quota-per).
\ No newline at end of file
diff --git a/device-ai/ml-kit-genai-prompt-api/references/prefix-caching.md b/device-ai/ml-kit-genai-prompt-api/references/prefix-caching.md
new file mode 100644
index 0000000..13eb571
--- /dev/null
+++ b/device-ai/ml-kit-genai-prompt-api/references/prefix-caching.md
@@ -0,0 +1,146 @@
+> [!NOTE]
+> **Note:** Prefix caching is experimental and may change in the future. This feature is only available on a subset of the supported devices for Prompt API, with support for more devices coming soon. We encourage you to experiment with this API on a Pixel device to understand how it can improve inference latency speeds for your specific use case.
+
+*Prefix caching* is a feature that reduces inference time by storing and reusing
+the intermediate LLM state of processing a shared and recurring prompt prefix
+part. To enable prefix caching, you only have to separate the static prefix from
+the dynamic suffix in your API request.
+
+Prefix caching currently only supports text-only input, so you shouldn't use
+this feature if you're providing an image in your prompt.
+
+There are two approaches to implement prefix caching: implicit or explicit:
+
+- [Implicit (automatic) prefix caching](https://developer.android.com/agents/skills/device-ai/ml-kit-genai-prompt-api/references/prefix-caching#implicit) is a lightweight approach where the application only needs to define a shared portion of the prompt.
+- [Explicit (manual) prefix caching](https://developer.android.com/agents/skills/device-ai/ml-kit-genai-prompt-api/references/prefix-caching#explicit) allows applications to have more control over caches, including cache creation, querying, and deletion.
+
+## Use prefix caching implicitly
+
+To enable prefix caching, add the shared portion of the prompt to the
+`promptPrefix` field, as shown in the following code snippets:
+
+### Kotlin
+
+ val promptPrefix = "Reverse the given sentence: "
+ val dynamicSuffix = "Hello World"
+
+ val result = generativeModel.generateContent(
+ generateContentRequest(TextPart(dynamicSuffix)) {
+ promptPrefix = PromptPrefix(promptPrefix)
+ }
+ )
+
+### Java
+
+ String promptPrefix = "Reverse the given sentence: ";
+ String dynamicSuffix = "Hello World";
+
+ GenerateContentResponse response = generativeModelFutures.generateContent(
+ new GenerateContentRequest.Builder(new TextPart(dynamicSuffix))
+ .setPromptPrefix(new PromptPrefix(promptPrefix))
+ .build())
+ .get();
+
+In the preceding snippet, the `dynamicSuffix` is passed as the main content, and
+the `promptPrefix` is provided separately.
+
+### Estimated performance gains
+
+|---|---|---|
+| | **Without prefix caching** | **With prefix cache-hit** (Prefix cache-miss may occur when prefix is used for the first time) |
+| Pixel 9 with 300-token fixed prefix and a 50-token dynamic suffix prompt | 0.82 seconds | 0.45 seconds |
+| Pixel 9 with a 1,000-token fixed prefix and a 100-token dynamic suffix prompt | 2.11 seconds | 0.5 seconds |
+
+### Storage considerations
+
+With implicit prefix caching, cache files are saved on the client application's
+private storage, which increases your app's storage usage. Encrypted cache files
+and their associated metadata, including original prefix text, are stored. Keep
+the following storage considerations in mind:
+
+- The number of caches is managed by an LRU (Least Recently Used) mechanism. Least used caches are deleted automatically when exceeding the max total cache amount.
+- Prompt cache sizes are dependent on the length of the prefix.
+- To clear all caches created from prefix caching, use the
+ [`generativeMode.clearImplicitCaches()`](https://developer.android.com/android/reference/kotlin/com/google/mlkit/genai/prompt/GenerativeModel#clearCaches%28%29) method.
+
+ > [!NOTE]
+ > **Note:** The `clearImplicitCaches()` method is experimental and may change in the future.
+
+## Use explicit cache management
+
+The Prompt API includes explicit cache management methods to give developers
+more precise control over how caches are created, searched, used, and removed.
+These manual operations run independently of the system's automated cache
+handling.
+
+This example illustrates how to initialize explicit cache management and
+perform inference:
+
+### Kotlin
+
+ val cacheName = "my_cache"
+ val promptPrefix = "Reverse the given sentence: "
+ val dynamicSuffix = "Hello World"
+
+ // Create a cache
+ val cacheRequest = createCachedContextRequest(cacheName, PromptPrefix(promptPrefix))
+ val cache = generativeModel.caches.create(cacheRequest)
+
+ // Run inference with the cache
+ val response = generativeModel.generateContent(
+ generateContentRequest(TextPart(dynamicSuffix)) {
+ cachedContextName = cache.name
+ }
+ )
+
+### Java
+
+ String cacheName = "my_cache";
+ String promptPrefix = "Reverse the given sentence: ";
+ String dynamicSuffix = "Hello World";
+
+ // Create a cache
+ CachedContext cache = cachesFutures.create(
+ new CreateCachedContextRequest.Builder(cacheName, new PromptPrefix(promptPrefix))
+ .build())
+ .get();
+
+ // Run inference with the cache
+ GenerateContentResponse response = generativeModelFutures.generateContent(
+ new GenerateContentRequest.Builder(new TextPart(dynamicSuffix))
+ .setCachedContextName(cache.getName())
+ .build())
+ .get();
+
+This example demonstrates how to query, retrieve, and delete explicitly managed
+caches using `generativeModel.caches`:
+
+### Kotlin
+
+ val cacheName = "my_cache"
+
+ // Query pre-created caches
+ for (cache in generativeModel.caches.list()) {
+ // Do something with cache
+ }
+
+ // Get specific cache
+ val cache = generativeModel.caches.get(cacheName)
+
+ // Delete a pre-created cache
+ generativeModel.caches.delete(cacheName)
+
+### Java
+
+ String cacheName = "my_cache";
+
+ // Query pre-created caches
+ for (PrefixCache cache : cachesFutures.list().get()) {
+ // Do something with cache
+ }
+
+ // Get specific cache
+ PrefixCache cache = cachesFutures.get(cacheName).get();
+
+ // Delete a pre-created cache
+ cachesFutures.delete(cacheName);
\ No newline at end of file
diff --git a/device-ai/ml-kit-genai-prompt-api/references/prompt-design.md b/device-ai/ml-kit-genai-prompt-api/references/prompt-design.md
new file mode 100644
index 0000000..bba16ab
--- /dev/null
+++ b/device-ai/ml-kit-genai-prompt-api/references/prompt-design.md
@@ -0,0 +1,55 @@
+When using Prompt API, there are specific strategies you can use to tailor your
+prompts and receive optimal results. This page describes best practices for
+formatting prompts for Gemini Nano.
+
+For more general prompt engineering guidance, see [Prompt Engineering
+whitepaper](https://www.kaggle.com/whitepaper-prompt-engineering), [Prompt Engineering for Generative
+AI](https://developer.android.com/machine-learning/resources/prompt-eng), and [Prompt design strategies](https://ai.google.dev/gemini-api/docs/prompting-strategies).
+
+Alternatively, to automatically refine and improve prompts, you can use the
+[zero-shot optimizer](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/zero-shot-optimizer#optimizing_for_smaller_models), which can target on-device models such as
+`gemma-3n-e4b-it`.
+
+## Prompt design best practices
+
+When designing prompts for Prompt API, use the following techniques:
+
+- **Provide examples for in-context learning**. Add well-distributed examples to
+ your prompt to show Gemini Nano the kind of result you expect.
+
+ Consider using the [prefix caching](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching) feature when you use in-context
+ learning, as providing examples makes the prompt longer and increases
+ inference time.
+- **Be concise** . Verbose preambles with repeated instructions can produce
+ suboptimal results. Keep your prompt focused and to-the-point. If you need to
+ repeat a short directive that guides the model's behavior, consider using
+ [system instructions](https://developer.android.com/agents/skills/device-ai/prompt-api/references/system-instructions).
+
+- **Structure prompts** to generate more effective responses, such as this
+ [sample prompt template](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/prompt-design-strategies#sample-prompt-template) that clearly defines instructions,
+ constraints, and examples.
+
+- **Keep output short** . LLM inference speeds are heavily dependent on the
+ output length. Carefully consider how you can generate the shortest possible
+ output for your use case and do manual post-processing to structure the output
+ in the chosen format. To help ensure that the response output is in your
+ preferred format, use the [Structured Output API](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output).
+
+- **Add delimiters** . Use delimiters like ``,
+ ``, and `##` to create separation between different parts of your
+ prompt. Using `##` between components is particularly critical for Gemini
+ Nano, as it significantly reduces the chances of the model failing to
+ correctly interpret each component.
+
+- **Prefer simple logic and a more focused task** . If you find it challenging
+ to achieve good results with a prompt requiring multi-step reasoning (for
+ example, *do X first, if the result of X is A, do M; otherwise do N;
+ then do Y...* ), consider breaking the task up and let each Gemini Nano call
+ handle a more focused task, while using code to chain multiple calls together.
+ If you do need to tackle a complex, reasoning-intensive task all at once,
+ consider using [thinking mode](https://developer.android.com/ml-kit/genai/prompt/android/thinking-mode).
+
+- **Use lower temperature values for deterministic tasks** . For tasks such as
+ entity extraction or translation that don't rely on creativity, consider
+ starting with a `temperature` value of `0.2`, and tune this value based on
+ your testing.
\ No newline at end of file
diff --git a/device-ai/ml-kit-genai-prompt-api/references/prompt-optimization.md b/device-ai/ml-kit-genai-prompt-api/references/prompt-optimization.md
new file mode 100644
index 0000000..05afba0
--- /dev/null
+++ b/device-ai/ml-kit-genai-prompt-api/references/prompt-optimization.md
@@ -0,0 +1,74 @@
+This skill provides guidance on how to optimize prompts for use with the ML Kit
+Prompt API.
+
+## Guidelines \& Rules
+
+The best practices for prompt design for Prompt API are at
+[Prompt Design](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prompt-design). Apply these core rules:
+
+- Include examples for in-context learning with examples of the output. This is an example prompt that includes in-context learning because it has examples of the desired model output: Analyze the message and return whether the customer sentiment is positive, negative, or neutral. Example 1: Input - "This product doesn't work"; Output - "Negative" Example 2: Input - "I liked this product"; Output - "Positive"
+- Make prompts concise. Remove duplicate or repeated instructions. Do not include conversational filler such as: hello, please, do this, help me with, etc. For example, a verbose prompt such as: "Hello! I want you to act as an expert translator for me today. I am going to give you a short paragraph of text in English, and I would really like it if you could translate it into formal, high-level Business French. Please make sure that you do not include any intro like 'Here is your translation' or any closing remarks at all. Just output the translation itself." can be improved with this concise prompt: "Translate to formal Business French. Return ONLY the translation"
+- Use paired XML/HTML delimiters to denote dynamic inputs (for example `[content]`) to clearly isolate user data from prompt instructions. Mention that the delimiters represent placeholder text that should be replaced by code in the actual implementation.
+- Use the [Structured Output API](https://developer.android.com/agents/skills/device-ai/prompt-api/references/structured-output) if the output requires parsing responses into certain formats. Don't include the Structured Output API implementation in the prompt itself, but mention it after presenting the optimized prompt to the user so the user knows how to parse the model output. Prompts that use Structured Output API must return the `@Generable` typed object from the function signature instead of a string or JSON string.
+- Keep output short by adding output constraints such as word count, character count, or number of bullets or sentences. For example, add constraints such as "The summarization must be 10 words or fewer."
+- Use [system instructions](https://developer.android.com/agents/skills/device-ai/prompt-api/references/system-instructions) for short instructions that define how a model should behave, and use [prefix caching](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching) for prompts that are over 200 words.
+
+## Examples
+
+Here are examples of prompts that the user might ask you to optimize, and the
+improved, optimized versions.
+
+### Example 1
+
+Unoptimized prompt:
+
+ Process this customer email and return the order ID, what was bought, the phone
+ number, and what they want. Format it as JSON so my app can read it.
+
+Optimized prompt:
+
+ ## Task
+
+ Extract key details from the customer email below.
+
+ ## Customer Email
+
+
+ [email_content]
+
+
+Explanation: Because the information that the user wants to extract using the
+prompt is well suited for the Structured Output API, the optimized prompt
+implicitly uses the Structured Output API and doesn't need to explicitly define
+the schema for the extracted data. In your response to the user that explains
+the optimizations made to the prompt, mention that the optimized prompt is
+designed for use with the Structured Output API. Also, mention that the
+"" delimiters signify placeholder text that should be replaced with code in the user's actual implementation.
+
+### Example 2
+
+Unoptimized prompt:
+
+ Given this itinerary for a trip: [Flight to Paris (CDG), Check-in: Hotel Le
+ Meurice, Visit to the Louvre, Dinner at Le Jules Verne, Eiffel Tower Visit,
+ Versailles Palace Tour, Montmartre Walk, Seine River Cruise, Pastry and Macaron
+ Tasting, Flight Out], generate the following: overall vibe, tips on how to
+ prepare for this trip, and common short phrases to learn for the trip.
+
+Optimized prompt:
+
+ ## Task
+
+ Analyze the trip itinerary below and extract details to build a concise travel
+ guide.
+
+ ## Rules
+ - Overall vibe: Limit to 1 short sentence (under 15 words).
+ - Preparation tips: Provide exactly 3 short tips (maximum 10 words per tip).
+ - Useful phrases: Provide exactly 3 phrases (under 15 words each).
+
+ ## Itinerary
+
+
+ [itinerary_content]
+
\ No newline at end of file
diff --git a/device-ai/ml-kit-genai-prompt-api/references/structured-output.md b/device-ai/ml-kit-genai-prompt-api/references/structured-output.md
new file mode 100644
index 0000000..2740974
--- /dev/null
+++ b/device-ai/ml-kit-genai-prompt-api/references/structured-output.md
@@ -0,0 +1,219 @@
+If you need to parse the responses from the Prompt API into certain formats,
+such as JSON, for further processing, use the Structured Output API.
+
+With the Structured Output API, you define the target output structure using
+Kotlin classes and annotations. The Prompt API then returns a response in the
+form of your Kotlin object.
+
+Generating structured output is particularly useful for tasks like the
+following:
+
+- **Entity extraction**: Extracting structured fields (for example, event name, date, location) from unstructured text.
+- **Classification**: Categorizing input text into predefined categories.
+- **Data serialization**: Converting unstructured user input into a format suitable for database storage or API calls.
+
+## Prerequisites
+
+To verify that the Structured Output API is available on the device, use the
+`isStructuredOutputFeatureAvailable()` API. The API returns `true` if the
+Structured Output API is available on the device, and `false` otherwise.
+
+ suspend fun isStructuredOutputFeatureAvailable(): Boolean
+
+The Structured Output API also has the following requirements:
+
+- Android API level 26 or higher (`minSdk` 26)
+- KSP plugin version 2.3.6 or higher
+
+## Limitations
+
+The Structured Output API has the following limitations:
+
+- Works in Kotlin only.
+- ProGuard might interfere with the parsing of your annotated class. Add your annotated class to your [keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview) to exclude them from ProGuard if you get errors parsing, for example:
+
+ # Keep classes used by structured output for deserialization for release builds.
+ -keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }
+
+## Configure project
+
+To get started with the Structured Output API, follow these steps:
+
+1. [Add the ML Kit Prompt API as a dependency](https://developer.android.com/agents/skills/device-ai/prompt-api/references/get-started#configure-project) in your
+ app-level `build.gradle.kts` (or `build.gradle`) file, if you haven't
+ already.
+
+2. Add the KSP plugin to your project-level `build.gradle.kts` file. Use a
+ KSP plugin version that is compatible with your Kotlin version; we
+ recommend KSP version 2.3.6 or higher.
+
+ dependencies {
+ ...
+ classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.6"
+ }
+
+3. Add the structured compiler dependencies to your app-level
+ `build.gradle.kts` file:
+
+ dependencies {
+ ...
+ ksp("com.google.mlkit:genai-schema-compiler:1.0.0-alpha1")
+ }
+
+## Define the output structure
+
+Define the structure of the data you want the model to return using Kotlin
+data classes. There are two main annotations for defining the output
+structure:
+
+- Use the `@Generable` annotation to define the class as a target for structured output.
+- Use the `@Guide` annotations on the class properties to provide descriptions and constraints that guide the model's output.
+
+The following example defines a structure for extracting plant information:
+
+ import com.google.mlkit.genai.schema.annotations.Generable
+ import com.google.mlkit.genai.schema.annotations.Guide
+
+ @Generable
+ data class PlantList(
+ @Guide(description = "The list of plants found", minItems = 1, maxItems = 5)
+ val plants: List
+ )
+
+ @Generable("Information about a plant species")
+ data class Plant(
+ @Guide(description = "The common name of the plant")
+ val commonName: String,
+
+ @Guide(description = "The full latin scientific name of the plant")
+ val scientificName: String,
+
+ @Guide(
+ description = "The maximum height of the plant in centimeters.",
+ minimum = 1.0,
+ maximum = 10000.0
+ )
+ val maxHeightCm: Int,
+
+ @Guide(description = "Whether the plant is poisonous or not")
+ val isPoisonous: Boolean?,
+
+ @Guide(
+ description = "The primary continent where this plant is native to",
+ enumValues = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"]
+ )
+ val nativeContinent: String
+ )
+
+### Supported types and constraints
+
+The following types are supported within a `@Generable` annotated class,
+along with their respective `@Guide` constraints:
+
+| Type | Description | Supported `@Guide` constraints |
+|---|---|---|
+| `String` | For text. | `description`, `enumValues` |
+| `Double` / `Float` | For floating-point numbers. | `description`, `minimum`, `maximum` |
+| `Int` / `Long` | For whole numbers. | `description`, `minimum`, `maximum` |
+| `Boolean` | For true/false values. | `description` |
+| `List` | For lists of supported types or nested `@Generable` classes. | `description`, `minItems`, `maxItems` |
+| `List` | For lists of `String` values. | `description`, `enumValues`, `minItems`, `maxItems` > [!NOTE] > **Note:** Setting the `enumValues` parameter defines the values allowed for the individual list items. |
+| `@Generable` class | For nested structured objects. | `description` |
+
+> [!NOTE]
+> **Note:** Circular dependencies between nested `@Generable` classes are not supported (for example, a class referencing itself, or Class A referencing Class B which in turn references Class A).
+
+## Generate structured content
+
+To request structured output, use the `generateTypedContentRequest` helper
+function to wrap your standard prompt and specify the target output class.
+
+ // 1. Initialize your GenerativeModel as usual
+ val generativeModel = Generation.getClient()
+
+ // 2. Prepare the prompt text
+ val promptText = "List some common plants found in California."
+ val baseRequest = GenerateContentRequest.Builder(TextPart(promptText)).build()
+
+ // 3. Create the typed request, specifying the target class (e.g., PlantList)
+ val typedRequest = generateTypedContentRequest(
+ generateContentRequest = baseRequest,
+ outputClass = PlantList::class
+ )
+
+ // 4. Run the inference
+ try {
+ val typedResponse = generativeModel.generateContent(typedRequest)
+
+ // 5. Access the parsed object
+ // The response candidates contain the parsed object of type T (PlantList in this case)
+ val plantList: PlantList? = typedResponse.candidates.firstOrNull()?.response
+
+ if (plantList != null) {
+ // Process the structured data
+ for (plant in plantList.plants) {
+ Log.d("StructuredOutput", "Found plant: ${plant.commonName} (${plant.scientificName})")
+ }
+ } else {
+ Log.e("StructuredOutput", "Failed to parse response into the desired structure.")
+
+ // Inspect finish reason for details
+ val finishReason = typedResponse.candidates.firstOrNull()?.finishReason
+ Log.d("StructuredOutput", "Finish reason: $finishReason")
+ }
+ } catch (e: GenAiException) {
+ // Handle API errors
+ when (e.errorCode) {
+ GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS -> {
+ Log.e("StructuredOutput", "The class structure is not supported.")
+ }
+ GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE -> {
+ Log.e("StructuredOutput", "The model generated values that violate the schema constraints.")
+ }
+ else -> {
+ Log.e("StructuredOutput", "API error: ${e.message}")
+ }
+ }
+ }
+
+## Handle finish reasons and errors
+
+When using the Structured Output API, you should handle potential exceptions
+thrown by the API and inspect the `finishReason` property in the response
+candidates if the parsed response is null.
+
+### finishReason values
+
+The `finishReason` property can take one of the following values:
+
+- `TypedFinishReason.STOP`: The model finished generating successfully and the output matches the schema.
+- `TypedFinishReason.MAX_TOKENS`: The model stopped because it reached the token limit. The output might be incomplete.
+- `TypedFinishReason.PARSE_CLASS_ERROR`: The model completed generation, but the resulting JSON couldn't be parsed into the target Kotlin class.
+- `TypedFinishReason.STRUCTURE_NOT_ANNOTATED`: The target class or its nested classes are missing the required `@Generable` annotation.
+- `TypedFinishReason.STRUCTURE_VALUES_INVALID`: The generated values violated the constraints defined in the `@Guide` annotations (for example value out of range, list size out of bounds).
+- `TypedFinishReason.OTHER`: Generation stopped due to other reasons.
+
+### Exceptions
+
+The Structured Output API might throw `GenAiException` with the following
+error codes:
+
+- `GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS` (-104): The structure of the annotated class is invalid or contains unsupported types. This is typically a development-time configuration error. Review your `@Generable` data class definition to check that all property types are supported and that there aren't any circular dependencies.
+- `GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE` (-105): The values generated by the model are invalid or fail constraints verification. This is a runtime error. If you encounter this error frequently, consider the following solutions:
+ - Refining your prompt instructions to guide the model more strictly.
+ - Relaxing the constraints (like minimum, maximum, or list size limits) in your `@Guide` annotations if they are too restrictive for the model's capabilities.
+ - Implementing a fallback strategy in your app, such as retrying the request or displaying a default state.
+
+## Count tokens
+
+To check if your structured prompt is within the input token limit, calculate
+the token count using the [`countTokens()`](https://developer.android.com/android/reference/com/google/mlkit/genai/prompt/GenerativeModel#countTokens(com.google.mlkit.genai.prompt.GenerateContentRequest)) method.
+
+Because structured output requests need to instruct the model on the schema
+structure, counting tokens on just the raw prompt text (using a
+`GenerateContentRequest` instance) isn't accurate. To get an accurate token
+count, you must pass the complete `GenerateTypedContentRequest` instance, which
+includes your target class and schema configurations, to the `countTokens()`
+method:
+
+ suspend fun countTokens(request: GenerateTypedContentRequest): CountTokensResponse
\ No newline at end of file
diff --git a/device-ai/ml-kit-genai-prompt-api/references/system-instructions.md b/device-ai/ml-kit-genai-prompt-api/references/system-instructions.md
new file mode 100644
index 0000000..844131c
--- /dev/null
+++ b/device-ai/ml-kit-genai-prompt-api/references/system-instructions.md
@@ -0,0 +1,71 @@
+System instructions let you give the model a persona, set the tone of its
+responses, or provide specific rules it must follow. These instructions are
+separate from the user's prompt and are treated with higher priority by the
+model to ensure it behaves as expected.
+
+Common use cases include:
+
+- **Setting a persona:** For example, "You are a helpful math tutor."
+- **Enforcing output format:** For example, "Always respond in bullet points."
+- **Setting constraints:** For example, "Do not answer questions about politics."
+
+## Prerequisites
+
+System instructions work on devices running Gemini Nano V3 and higher. For a
+list of supported devices, see [Prompt API device support](https://developer.android.com/ml-kit/genai#prompt-device).
+
+## Limitations
+
+We don't recommend using system instructions with
+[prefix caching](https://developer.android.com/agents/skills/device-ai/prompt-api/references/prefix-caching). In general, use system instructions for
+short instructions that define how the model should behave; use prefix
+caching if you need to repeat a large part of your prompt across queries and
+need to optimize performance.
+
+## How to use system instructions
+
+To provide system instructions, create a `SystemInstruction` object and pass it
+to the `GenerateContentRequest` builder:
+
+ import com.google.mlkit.genai.prompt.SystemInstruction
+ import com.google.mlkit.genai.prompt.TextPart
+ import com.google.mlkit.genai.prompt.generateContentRequest
+
+ // 1. Define the system instruction
+ val systemInstruction =
+ SystemInstruction("You are a concise assistant. Answer in 2 sentences or less.")
+
+ // 2. Create the request
+ val request = generateContentRequest(TextPart("How does photosynthesis work?")) {
+ this.systemInstruction = systemInstruction
+ }
+
+ // 3. Run inference
+ try {
+ val response = generativeModel.generateContent(request)
+ println(response.candidates.firstOrNull()?.text)
+ } catch (e: GenAiException) {
+ // Handle SDK-specific exceptions
+ }
+
+You can further simplify the code by passing the system instructions directly
+into the `generateContentRequest` request builder:
+
+ val request = generateContentRequest(
+ SystemInstruction("You are a pirate. Speak like one."),
+ TextPart("What is the weather like today?")
+ ) {
+ // Optional configurations like temperature
+ temperature = 0.7f
+ }
+
+## Best practices
+
+Here are some best practices when using system instructions:
+
+- **Be clear and direct:** The model follows clear, direct instructions better than ambiguous ones. Here are some examples:
+ - Vague (avoid): "Don't write too much. Try to be helpful and friendly, and format the output nicely."
+ - Clear (preferred): "You are a friendly customer support assistant. Limit your responses to a maximum of 3 sentences. Format any lists using bullet points."
+- **Be concise:** While system instructions are powerful, very long instructions can consume the model's limited context window.
+- **Factor in token counts:** Make sure that your token counting logic includes the system instructions to avoid underestimating request size. We recommend keeping your system instructions to under 150 words (100-200 tokens).
+- **Test and iterate:** Model behavior can vary based on phrasing. Test with various user inputs to ensure the model maintains its persona consistently.
\ No newline at end of file
diff --git a/identity/restore-credentials/SKILL.md b/identity/restore-credentials/SKILL.md
index dcb8e40..35dbd2c 100644
--- a/identity/restore-credentials/SKILL.md
+++ b/identity/restore-credentials/SKILL.md
@@ -9,7 +9,7 @@ description: Provides knowledge and workflows to implement Android's Restore Cre
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-21'
+ last-updated: '2026-09-01'
keywords:
- Credential Manager
- Restore Credentials
@@ -33,7 +33,7 @@ federated sign-in) and requires no UI changes to existing sign-in flows.
**Crucial:** This skill focuses exclusively on the Android client-side
integration. It does **not** implement the server-side cryptographic
validation logic. The developer must be reminded of this and the
-[Points to inform the developer about](skill.md) after implementation is done.
+[Points to inform the developer about](#backend-guidelines) after implementation is done.
## Implementation Guidelines
@@ -41,8 +41,8 @@ When instructed to implement Restore Credentials on a developer's application,
remember the following:
1. Before the implementation, you **MUST** read and understand the [Two-Tier
- Restoration Architecture](skill.md) and review the [DOs and DON'Ts](skill.md).
-2. After the implementation, you **MUST** present the developer with the [Backend Guidelines](skill.md) as a reminder for their backend setup. It is important that you remind the developer that they still have to implement the backend.
+ Restoration Architecture](#two-tier-restoration-architecture) and review the [DOs and DON'Ts](#dos-and-donts).
+2. After the implementation, you **MUST** present the developer with the [Backend Guidelines](#backend-guidelines) as a reminder for their backend setup. It is important that you remind the developer that they still have to implement the backend.
## Two-Tier Restoration Architecture
@@ -56,6 +56,10 @@ If `allowBackup` in the manifest is set to true, implement both. Otherwise, only
implement tier 2 (Foreground Restoration). Do **NOT** change the value of
`allowBackup` in the manifest.
+If you added a `BackupAgent` to the app, you **MUST** also set
+`android:fullBackupOnly="true"` in the manifest. Do **NOT** do this if there
+already existed a `BackupAgent` in the app before your implementation.
+
## DOs and DON'Ts
**DO:**
@@ -63,6 +67,7 @@ implement tier 2 (Foreground Restoration). Do **NOT** change the value of
- Do check `AndroidManifest.xml` for the value of allowBackup to determine what you have to implement.
- Do implement a fallback for createCredential: always try calling it first with `isCloudBackupEnabled` set to true. If an `E2eeUnavailableException` is thrown, catch it and retry the call with `isCloudBackupEnabled` set to `false`.
- Do implement a `BackupAgent` (subclass of `android.app.backup.BackupAgent`) if `allowBackup` is true in the manifest.
+- Do set `android:fullBackupOnly="true"` in the manifest if you added a `BackupAgent` to the app.
- Call `clearCredentialState()` when the user signs out. This is a mandatory security measure to log the user out fully.
- Do attempt to get the restore key on the first launch of the app on a new device and also within the `BackupAgent.onRestoreFinished()` callback if your app uses it.
- Do ensure that a restore credential is created even if the user is already logged in.
@@ -70,7 +75,7 @@ implement tier 2 (Foreground Restoration). Do **NOT** change the value of
- Do restore notifications in the `BackupAgent` if your app uses them. (For example capture and send FCM token to backend)
- Do ensure that if you implement mock network requests or stubs, you replace all placeholders with valid, properly formatted JSON payloads for the credential requests.
- Do encapsulate credential creation and retrieval into their own dedicated functions. Because credential creation must be called in multiple places (sign-up, sign-in) and retrieval across multiple tiers (`BackupAgent` and Launcher `Activity`), this prevents code duplication.
-- Do remind the developer of the [critical guidelines](skill.md) for implementing the backend once you're done with the implementation.
+- Do remind the developer of the [critical guidelines](#implementation-guide) for implementing the backend once you're done with the implementation.
- Do generate a separate restore key for each application if the organization has multiple apps with different package names, as a restore key is tied to a unique application package name.
**DON'T:**
@@ -86,18 +91,18 @@ implement tier 2 (Foreground Restoration). Do **NOT** change the value of
Implement the Android client-side code by using the following guide. Follow it
**step-by-step** and don't implement any backend functionality, only remind
-the user of the [Backend Guidelines](skill.md) once you're done.
+the user of the [Backend Guidelines](#backend-guidelines) once you're done.
## Version compatibility
-Credential Manager's Restore Credentials works on devices running Android 9 and
-higher, Google Play services (GMS) core version 24220000 or higher, and version
-1.5.0 or higher of the `androidx.credentials` library.
+Credential Manager's Restore Credentials works on devices running Android 9 (API
+level 28) and higher, Google Play services (GMS) core version 24220000 or
+higher, and version 1.5.0 or higher of the `androidx.credentials` library.
## Prerequisites
-Set up a [relying party server](skill.md) similar to the server for [passkeys](skill.md). If
-you already have a [server](skill.md) set up to handle authentication with passkeys,
+Set up a [relying party server](#backend-guidelines) similar to the server for [passkeys](#dos-and-donts). If
+you already have a [server](#two-tier-restoration-architecture) set up to handle authentication with passkeys,
use the same server-side implementation for restore keys.
> [!NOTE]
@@ -130,20 +135,20 @@ androidx.credentials library. However, it's recommended to use the latest stable
versions of the dependencies where possible.
> [!NOTE]
-> **Note:** The Restore Credentials feature works regardless of whether [`allowBackup`](references/android/guide/topics/manifest/application-element.md) is set in the `manifest`.
+> **Note:** The Restore Credentials feature works regardless of whether [`allowBackup`](references/android/guide/topics/manifest/application-element.md) is set in the manifest.
## Overview
-1. [**Create a restore key**](skill.md): To create a restore key, complete the following steps:
- 1. [**Instantiate Credential Manager**](skill.md): Create a `CredentialManager` object.
+1. [**Create a restore key**](#create-restore-key): To create a restore key, complete the following steps:
+ 1. [**Instantiate Credential Manager**](#implementation-guide): Create a `CredentialManager` object.
2. [**Get credential creation options from the app server**](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API): Send the client app the details required to create the restore key from your app server.
3. [**Create the restore key**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson): Create a restore key for the user's account if the user is signed in to your app.
4. [**Handle the credential creation response**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson): Send the credentials from your client app to your app server for processing, and handle any exceptions.
-2. [**Sign in with a restore key**](skill.md): To sign in with a restore key, complete the following steps:
- 1. [**Get credential retrieval options from the app server**](skill.md): Send the client app the details required to retrieve the restore key from your app server.
- 2. [**Get the restore key**](skill.md): Request the restore key from Credential Manager when the user sets up a new device. This lets the user sign in without additional input.
- 3. [**Handle the credential retrieval response**](skill.md): Send the restore key from the client app to the app server to sign in the user.
-3. [**Delete a restore key**](skill.md).
+2. [**Sign in with a restore key**](#sign-restore): To sign in with a restore key, complete the following steps:
+ 1. [**Get credential retrieval options from the app server**](#get-credential-retrieval): Send the client app the details required to retrieve the restore key from your app server.
+ 2. [**Get the restore key**](#get-restore): Request the restore key from Credential Manager when the user sets up a new device. This lets the user sign in without additional input.
+ 3. [**Handle the credential retrieval response**](#handle-sign-in): Send the restore key from the client app to the app server to sign in the user.
+3. [**Delete a restore key**](#delete-restore).
## Create a restore key
@@ -200,7 +205,7 @@ restore key by wrapping these options in a
- `false`: This value saves the key locally and not in the cloud. The key is not available on the new device if the user chooses to restore from the cloud.
> [!CAUTION]
- > **Caution:** It is recommended to set `isCloudBackupEnabled` to `true`. If cloud backup is disabled and the user restores from a cloud backup, the call to retrieve the restore key fails. Users who restore your app with a cloud backup don't receive the restore key and are not automatically signed in.
+ > **Caution:** It's recommended to set `isCloudBackupEnabled` to `true`. If cloud backup is disabled and the user restores from a cloud backup, the call to retrieve the restore key fails. Users who restore your app with a cloud backup don't receive the restore key and are not automatically signed in.
### Handle the credential creation response
@@ -217,9 +222,9 @@ guidance for passkeys](references/android/identity/passkeys/create-passkeys.md).
During the restore key creation process, handle these exceptions:
- [`CreateRestoreCredentialDomException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/CreateRestoreCredentialDomException): This exception occurs if `requestJson` is invalid and does not follow the WebAuthn format for [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
-- [`E2eeUnavailableException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/E2eeUnavailableException): This exception occurs if `isCloudBackupEnabled` is `true`, but the user's device does not have data backup or end-to-end encryption, such as a screen lock.
+- [`E2eeUnavailableException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/E2eeUnavailableException): This exception occurs if `isCloudBackupEnabled` is `true`, but the user's device doesn't have data backup or end-to-end encryption, such as a screen lock.
To ensure that Restore Credentials are created in all cases, you must handle the `E2eeUnavailableException` explicitly by calling `createCredential` with `isCloudBackupEnabled` set to `true`. If `E2eeUnavailableException` is thrown, catch and call `createCredential` again with `isCloudBackupEnabled` set to `false`.
-- `IllegalArgumentException`: This exception occurs if `createRestoreRequest` is empty or not valid JSON, or if it does not have a valid `user.id` that conforms to the WebAuthn [specifications](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+- `IllegalArgumentException`: This exception occurs if `createRestoreRequest` is empty or not valid JSON, or if it doesn't have a valid `user.id` that conforms to the WebAuthn [specifications](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
## Sign in with a restore key
@@ -238,11 +243,16 @@ authentication guide](https://developers.google.com/identity/passkeys/developer-
To get the restore key on the new device, call the `getCredential()` method on
the `CredentialManager` object.
-It is recommended to fetch the restore key in both of the following scenarios:
+It's recommended to fetch the restore key in both of the following scenarios:
- On the first launch of the app on the device. Credential restoration in this scenario is independent of restoration of the app data.
- If app data backup and restore is enabled, get the restore key immediately after the app data is restored. Use [`BackupAgent`](https://developer.android.com/reference/android/app/backup/BackupAgent) to configure your app's backup and ensure you complete the `getCredential` functionality within the [`onRestoreFinished`](https://developer.android.com/reference/android/app/backup/BackupAgent#onRestoreFinished()) callback. Don't use the `onRestore` method, as it is only called for key-value backups, whereas `onRestoreFinished` is reliably called for any kind of backup restore. This avoids potential delays when users open their new device for the first time and lets users interact with the app without waiting for them to open your app. For example, this lets your app send the user notifications before they open the app for the first time on the new device, which is particularly relevant for messaging or communications apps.
+If you newly create a `BackupAgent` and previously had backup enabled with
+`allowBackup="true"`, set the boolean value `android:fullBackupOnly="true"`in
+your app's manifest. This ensures that your app's backup and restore behavior is
+maintained.
+
> [!IMPORTANT]
> **Important:** Notifications aren't automatically restored after the restore credentials are retrieved. If you use Firebase to handle notifications, you must fetch and send the Firebase Cloud Messaging (FCM) token to the backend to successfully resume background messaging and notifications.
@@ -275,7 +285,7 @@ the server-side implementation for passkeys, see [Sign in with a passkey](refere
## Delete the restore key
-Credential Manager is stateless and unaware of user activity, so it does not
+Credential Manager is stateless and unaware of user activity, so it doesn't
automatically delete restore keys after use. To delete a restore key, call the
`clearCredentialState()` method. For security, delete the key whenever a user
signs out. This ensures that the next time the user opens the app on the same
@@ -311,7 +321,7 @@ developer as a reminder after the client-side implementation is complete.**
1. **Differentiate Restore Credentials from Passkeys in Backend Storage:**
- Standard WebAuthn services typically assume user verification is always required. Restore credentials are hidden from the user and not managed by them.
- - **Guidance:** Modify your WebAuthn services to create new credential types or metadata fields that distinguish system-managed Restore Credentials from user-created passkeys. Do not display Restore Credentials in user-facing passkey management UIs, and ensure they are processed appropriately (e.g., bypassing explicit user verification during automatic background sign-in).
+ - **Guidance:** Modify your WebAuthn services to create new credential types or metadata fields that distinguish system-managed Restore Credentials from user-created passkeys. Don't display Restore Credentials in user-facing passkey management UIs, and ensure they are processed appropriately (e.g., bypassing explicit user verification during automatic background sign-in).
2. **Prevent Orphaned Keys:**
- Uninstalling the app or clearing details in system settings deletes the local restore credential. Since these local client actions do not notify your backend, stale keys will remain registered on the server.
- **Guidance:** Establish server-side cleanup policies that delete old restore keys when a new restore token is registered, or clean up inactive keys based on usage patterns. You could, for example, enforce a limit of one key per user per device.
diff --git a/identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md b/identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md
index 3f36518..a6786be 100644
--- a/identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md
+++ b/identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md
@@ -108,6 +108,9 @@ with the cached data.
Call the `getCredential()` method to show the user the account selector. Use the
following code snippet as a reference for how to launch the sign-in flow:
+> [!NOTE]
+> **Note:** To avoid a potential memory leak, pass in a `MutableContextWrapper` of your foreground `Activity` to the `getCredential()` call. Credential Manager handles updating the context during `Activity` reconstruction.
+
// Use an activity-based context to avoid undefined system UI
// launching behavior.
val context = MutableContextWrapper(activityContext)
diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md
index c16a661..39a537c 100644
--- a/identity/verified-email/SKILL.md
+++ b/identity/verified-email/SKILL.md
@@ -8,7 +8,7 @@ description: Provides a complete workflow for implementing verified email retrie
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-19'
+ last-updated: '2026-09-04'
keywords:
- implementation
- Android
@@ -263,6 +263,9 @@ Present the user with the request, using the Credential Manager built-in UI.
## Parse the response on the client
+> [!WARNING]
+> **Warning:** From August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to strictly match the W3C standards. It contains `data` and `protocol` keys, with the OpenID4VP `vp_token` nested in `data`, while legacy formats hold the `vp_token` directly. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
+
After receiving the response, you can perform a preliminary parse on the client.
This is useful for immediately updating the UI, for example, by showing the
user's name.
@@ -275,7 +278,8 @@ The following code extracts the raw [Selective Disclosure JWT
// 1. Parse the outer JSON wrapper to get the `vp_token`
val responseData = JSONObject(responseJsonString)
- val vpToken = responseData.getJSONObject("vp_token")
+ val dataObject = responseData.getJSONObject("data")
+ val vpToken = dataObject.getJSONObject("vp_token")
// 2. Extract the raw SD-JWT string
val credentialId = vpToken.keys().next()
@@ -374,6 +378,9 @@ By combining these steps, your server can validate both the authenticity of the
data and the identity of the presenter, ensuring the credential wasn't
intercepted or spoofed before provisioning the new account.
+> [!WARNING]
+> **Warning:** As mentioned in [Parse the response on the client](#parse-response), from August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to match W3C standards. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
+
try {
// Send the raw credential response and the original nonce to your server.
// Your server must validate the response. createAccountWithVerifiedCredentials
diff --git a/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md b/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md
index 63e45b0..a0dc0ad 100644
--- a/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md
+++ b/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md
@@ -6,7 +6,7 @@
Use an [Android skill](https://developer.android.com/tools/agents/android-skills) to integrate a secure, OTP-less email verification flow into your app. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill verified-email
+ android skills add verified-email
@@ -156,6 +156,9 @@ Present the user with the request, using the Credential Manager built-in UI.
## Parse the response on the client
+> [!WARNING]
+> **Warning:** From August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to strictly match the W3C standards. It contains `data` and `protocol` keys, with the OpenID4VP `vp_token` nested in `data`, while legacy formats hold the `vp_token` directly. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
+
After receiving the response, you can perform a preliminary parse on the client.
This is useful for immediately updating the UI, for example, by showing the
user's name.
@@ -168,7 +171,8 @@ The following code extracts the raw [Selective Disclosure JWT
// 1. Parse the outer JSON wrapper to get the `vp_token`
val responseData = JSONObject(responseJsonString)
- val vpToken = responseData.getJSONObject("vp_token")
+ val dataObject = responseData.getJSONObject("data")
+ val vpToken = dataObject.getJSONObject("vp_token")
// 2. Extract the raw SD-JWT string
val credentialId = vpToken.keys().next()
@@ -267,6 +271,9 @@ By combining these steps, your server can validate both the authenticity of the
data and the identity of the presenter, ensuring the credential wasn't
intercepted or spoofed before provisioning the new account.
+> [!WARNING]
+> **Warning:** As mentioned in [Parse the response on the client](https://developer.android.com/identity/digital-credentials/email-verification-implementation#parse-response), from August 2026, the [response JSON](https://developer.android.com/reference/androidx/credentials/DigitalCredential#getCredentialJson()) format has been updated to match W3C standards. Ensure your client-side parsing and server-side validation handle both formats during the transition period, while the older implementation is phased out. Apps that begin to integrate the email verification flow after August 2026 need to use the new format only.
+
try {
// Send the raw credential response and the original nonce to your server.
// Your server must validate the response. createAccountWithVerifiedCredentials
diff --git a/identity/verified-email/references/android/identity/digital-credentials/email-verification.md b/identity/verified-email/references/android/identity/digital-credentials/email-verification.md
index 2134577..106c2ac 100644
--- a/identity/verified-email/references/android/identity/digital-credentials/email-verification.md
+++ b/identity/verified-email/references/android/identity/digital-credentials/email-verification.md
@@ -17,6 +17,19 @@ This guide assumes you are familiar with the following concepts:
- [Digital Credentials](https://developer.android.com/identity/digital-credentials)
- [Verifiable Credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials)
+
+## Android skills
+
+[View on GitHub](https://github.com/android/skills/tree/main/identity/verified-email)
+
+### Retrieve verified email
+
+Use an [Android skill](https://developer.android.com/tools/agents/android-skills) to integrate a secure, OTP-less email verification flow into your app. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
+
+ android skills add verified-email
+
+
+
## Android compatibility
This feature is supported on mobiles, tablets, and foldable devices running
@@ -45,14 +58,14 @@ The user experience for sharing a verified email is as follows:
message.
> [!NOTE]
- > **Note:** If the verified email you receive does not match what you expect, inform the user about the mismatch and either ask them to try again with a different credential or provide an alternate verification method, such as through OTPs.
+ > **Note:** If the verified email you receive doesn't match what you expect, inform the user about the mismatch and either ask them to try again with a different credential or provide an alternate verification method, such as through OTPs.
-4. (Optional, recommended) If the user is signing up for your service, you
- should prompt the user to [create](https://developer.android.com/identity/passkeys/create-passkeys) a [passkey](https://developer.android.com/identity/passkeys) to make it easier for
- them to sign in subsequently.
+4. Optional: If the user is signing up for your service, prompt the user to
+ [create](https://developer.android.com/identity/passkeys/create-passkeys) a [passkey](https://developer.android.com/identity/passkeys) to make it easier for them to sign in
+ subsequently.
> [!NOTE]
- > **Note:** The email verification process doesn't automatically trigger passkey creation. However, it is highly recommended to include the steps for passkey creation. Passkeys help users by making it easier and more secure for them to sign in, and remove the need for the conventional username and password interaction.
+ > **Note:** The email verification process doesn't automatically trigger passkey creation. However, it's highly recommended to include the steps for passkey creation. Passkeys help users by making it easier and more secure for them to sign in, and remove the need for the conventional username and password interaction.
### Include primary and fallback flows
@@ -92,7 +105,7 @@ details, by requiring a quick reauthentication step.
Email verification through Credential Manager only supports verification of
consumer Google Accounts. [Workspace accounts](https://knowledge.workspace.google.com/admin/getting-started/set-up-google-workspace-for-your-organization) and [supervised
-accounts](https://support.google.com/families/answer/9499054) are not supported.
+accounts](https://support.google.com/families/answer/9499054) aren't supported.
A consumer Google Account can be created with an email address from any
provider, not necessarily @gmail.com. However, Google verifies these accounts
@@ -126,7 +139,7 @@ providing an expired VC or a VC for an inactive Google Account.
### Email deliverability
-While the process confirms the account's legitimacy, it does not guarantee inbox
+While the process confirms the account's legitimacy, it doesn't guarantee inbox
delivery (for instance, the email might be diverted to spam). An OTP remains the
definitive method for confirming email deliverability.
@@ -136,9 +149,9 @@ While both Digital Credentials and [Sign in with Google](https://developer.andro
verified email, the user flows and use cases are different:
- **Use cases**: The Credential Manager email verification flow is not exclusively used in sign up or sign in use cases, but rather can be used in any use case involving the retrieval of verified email. This could include account recovery as well.
-- **Registration**: The Credential Manager flow does not require Google registration, unlike Sign in with Google.
+- **Registration**: The Credential Manager flow doesn't require Google registration, unlike Sign in with Google.
- **Platform support**: The Credential Manager flow is an Android-only solution.
-- **Scopes** : Unlike Sign in with Google, which can use OAuth 2.0 to request access to user data (such as Calendar or Drive through scopes), the Digital Credentials API is strictly for retrieving verified identity attributes. It cannot be used to request additional [authorization scopes](https://developers.google.com/identity/protocols/oauth2/scopes).
+- **Scopes** : Unlike Sign in with Google, which can use OAuth 2.0 to request access to user data (such as Calendar or Drive through scopes), the Digital Credentials API is strictly for retrieving verified identity attributes. It can't be used to request additional [authorization scopes](https://developers.google.com/identity/protocols/oauth2/scopes).
## Next steps
diff --git a/identity/verified-email/references/android/identity/digital-credentials/index.md b/identity/verified-email/references/android/identity/digital-credentials/index.md
index 5f9ca67..83b6156 100644
--- a/identity/verified-email/references/android/identity/digital-credentials/index.md
+++ b/identity/verified-email/references/android/identity/digital-credentials/index.md
@@ -1,96 +1,112 @@
-Digital credentials are cryptographically verifiable documents that can be used
-to authenticate, authorize, or otherwise provide information about a user. These
-are typically things such as mobile driver's licenses, digital passports,
-boarding passes, etc. They reside in virtual containers called digital wallets,
-and are part of a W3C standard that specifies how to access and retrieve them.
-This standard is implemented for web use cases with the [W3C Credential
-Management API](https://www.w3.org/TR/credential-management-1/) and on Android, with Credential Manager's
-[DigitalCredential API](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential).
+Digital credentials are cryptographically verifiable digital documents that your
+users can use to provide information about themselves, and use to authenticate
+or authorize themselves. Digital credentials are based on the [open W3C Digital
+Credentials API industry standard](https://www.w3.org/TR/digital-credentials/). On Android, this is
+implemented through [Credential Manager's](https://developer.android.com/identity/credential-manager) [Digital Credentials API](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential).
+ **Figure 1.** Using a digital credential in a sample app.
-## Understand digital credentials
+## Benefits of digital credentials
-In the physical world, a person might keep their identity in their wallet, and
-present it to a requesting party when asked:
- **Figure 1.** The process of fulfilling a physical-world credential request. The requestor asks the user for a specific credential. Then, the user selects and retrieves it from their physical wallet. Finally, the user provides the credential to the requestor.
+Digital credentials offer several advantages over physical credentials and
+non-standardized digital documents:
-In this case, a user generally has a single wallet, and retrieves the requested
-credentials from the wallet to present to the requestor. Wallets are mostly
-interchangeable, and can generally store the same things.
+- **Improved security and trust**: Digital credentials are encrypted, ensuring data integrity and authenticity. This asserts the surety that the credential was issued by a verifiable source and was not tampered with.
+- **Enhanced privacy**: Many digital credential formats support selective disclosure, which lets users share only necessary information. For example, a user could share proof of driving qualification without revealing their birth date.
+- **Consolidated storage**: Users can store various credentials from different issuers in digital holders all on their device, reducing the need to carry physical cards.
+- **Interoperability**: By following open standards, digital credentials can work across different operating systems, devices, and platforms.
-Digital credentials have the following differences from credentials in the
-physical world:
+## Use cases
-1. Users are expected to have multiple wallets - also known as **holders** - which can contain various different credentials. Wallets determine which credentials may be stored inside of them.
-2. The app or service asking for the credential to grant access or verify an identity is called the **verifier**.
-3. The entity that creates the credential and asserts claims about the subject (such as, a university, a government, or a tech company) is referred to as the **issuer**.
-4. The credential presentation happens in software, which means an API surface retrieves and presents the credentials - in Android, this is Credential Manager.
+The Digital Credentials API can be used across a broad set of use cases such as
+the following:
-As such, Credential Manager takes on several roles that were formerly handled by
-the user:
+- **Accept government-issued IDs**: Apps can request and use official government document attributes, for flows including age verification, account recovery, Know-Your-Customer (KYC) process.
+- **Verify phone numbers** : Apps can use the API for phone number verification, by exchanging digital credentials derived from the phone's SIM card directly with the user's carrier. This removes the need for one-time passwords (OTPs), improves security, and reduces transmission costs. For more information, see the [phone number verification guide](https://developer.android.com/identity/digital-credentials/phone-number-verification).
+- **Verify email addresses** : The API lets your app retrieve verified emails directly from the user's device, removing the need for OTPs, for frictionless sign-up, sign-in, and account recovery. For more information, see the [email verification guide](https://developer.android.com/identity/digital-credentials/email-verification).
+- **Custom credentials**: The extensibility of the API lets any app begin issuing its own custom digital credentials, which corresponding verifiers can request.
+- **Confirm payment credentials and transactions**: The API lets you secure payment authorizations and Digital Payment Credentials (DPC) with native, cryptographically bound wallet confirmation.
-1. On Android, wallets must register their credentials metadata with Credential Manager to be listed in the Credential Manager UI.
-2. Credential Manager matches credentials across wallets based on the request and presents a list for the user to select.
-3. When the user selects a credential in the list, Credential Manager then invokes the wallet, which will handle the remainder of the transaction (showing UIs, etc.) and return the credential to the application.
+## How digital credentials work
-This flow is shown here:
- **Figure 2.** Interaction model for digital credential verification. Credential Manager uses pre-registered credentials metadata across user wallet(s) to match a verifier's request and prompts the user to select a credential. Credential Manager then directs the activity flow to the corresponding wallet which handles the remainder of the transaction and returns the credential to the verifier. Note: The verifier needs to handle and verify the credential response once it is returned.
+The digital credential ecosystem involves three primary categories of apps:
-## Verifiable credentials
+- **Issuers** : Issuers are apps that securely create and [issue](https://developer.android.com/identity/digital-credentials/credential-issuer/issue-credentials) credentials.
+- **Holders (wallets)** : Holders are apps on a user's device that store credentials. They should be able to share these credentials with requesting apps through a [presentation](https://developer.android.com/identity/digital-credentials/credential-holder/credential-holder) process.
+- **Verifiers**: Verifiers are apps that verify and use digital credentials.
-Verifiable credentials are a subset of digital credentials governed by strict
-standards (like the W3C Verifiable Credentials Data Model). These credentials
-contain claims that are cryptographically secured, making them tamper-evident
-and proving exactly who issued them.
+Credential Manager's Digital Credentials API orchestrates the interaction
+between the issuers, holders, and verifier apps.
-Not all digital credentials are verifiable credentials, but all verifiable
-credentials are digital credentials.
+When a verifier makes a request for a digital credential, it sends a request to
+the Android system through the API. Credential Manager then displays eligible
+digital credentials from various holders within a trusted system UI. Once the
+user agrees to proceed, Credential Manager invokes the corresponding holder to
+generate the response.
-## What it means for a claim to be verified
+> [!NOTE]
+> **Note:** Issuers and holders don't have to be separate---an app can be both an issuer and holder. For example, if you use the Digital Credentials API for email verification, Google is the issuer and holder for the Gmail email address.
-When a credential arrives through the Android Credential Manager API and a claim
-within it is marked as "verified," it implies that the issuer is asserting that
-they performed a check on that specific piece of data. However, it does not mean
-the data is an absolute, universal truth. "Verified" is an assertion of process,
-not an automatic guarantee of trust.
+## User experience
-The core philosophy of this ecosystem is that trust is always resolved at the
-verifier. When the verifier (your app) receives the cryptographically secure
-data, and sees that the issuer marked it as "verified," it must determine
-whether it trusts the issuer to have verified the claim to its standards.
+Similarly to how Credential Manager has built-in user interfaces for
+authentication flows, such as with passkeys, passwords, and Sign in with Google,
+there are also standardized interfaces for the various use cases of the Digital
+Credentials API.
-### User experience
+There are UI variants tailored to specific use cases. When using the API, the
+Android system automatically renders context-aware bottom sheets, such as for
+the following scenarios:
-As shown in the Android flow, the user only needs to interact once with the
-Credential Manager UI to select the appropriate credential. Here is an example
-of how the selector looks:
- **Figure 3.** The digital credentials UI.
+- **Email verification**: The interface displays an account picker featuring the verified email address and identity provider.
+- **Phone number verification**: The interface includes a verification card with network carriers represented.
+- **Multi-credential**: This view stacks multiple cards, and allows bundling related items into a single tap.
+- **Digital payments credentials**: This checkout sheet displays payment card details, merchant info, and total transaction amount for immediate confirmation.
-### Standards
+ **Figure 2.** The UX variants for digital credentials for email verification, phone verification, and multi-credential scenarios.
-Digital credentials requests are created using the [OpenID4VP
-standard](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-introduction). You can see example requests at the [Digital
-Credentials Demo site](https://digital-credentials.dev/).
+## Industry standards
-Digital credential responses are typically returned in a standardized credential
-format. These are maintained by different standards bodies, and include [W3C
-Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/), [sd-jwt](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/), and
-[mdoc](https://www.iso.org/standard/69084.html).
+Digital credentials rely on industry standards to ensure cross-platform
+compatibility. The commonly used open standards are:
-Custom protocols are also feasible, though we recommend using one of the
-standard protocols in your application.
+- **Sharing** : Digital credentials are requested using the [OpenID4VP
+ standard](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-introduction).
+- **Issuance** : Digital credentials are issued using the [OpenID4VCI
+ standard](https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-1_1-wg-draft.html).
+- **Credential storage format** : Digital credentials are represented in standardized formats maintained by different standards bodies, primarily the following:
+ - IETF [selective disclosure of JSON web tokens (sd-jwt)](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) VC
+ - ISO [MDoc](https://www.iso.org/standard/69084.html)
-### Try it out
+These standards are used widely on multiple platforms and operating systems,
+enabling seamless operation across web browsers, mobile devices, and other form
+factors. This allows developers to exchange digital credentials with other apps
+regardless of the platform they're on.
-You can test out the digital credentials flow across platforms with an Android
-wallet and web-based verifier:
+## Try it out
-1. Install the [CMWallet public sample](https://github.com/digitalcredentialsdev/CMWallet) on your Android phone. You can do this by pulling from the repository and installing directly from Android Studio or navigating to and selecting the latest build to access the latest `app-debug.apk` file.
-2. Open the CMWallet to register the metadata with Credential Manager. Make sure Bluetooth is enabled to allow your devices to connect to each other.
-3. Navigate to and select `Request Credentials (OpenID4VP)`.
-4. Accept the warning prompts and scan the QR Code with your phone, then select "Use passkey" and tap through the confirmation to show the available credentials.
-5. Select the credential from CMWallet to return to the browser. The browser should show the returned credential.
+You can test the cross-platform digital credentials flow by installing the
+sample holder and verifier apps on an Android-powered device.
-### See also
+To test the flow, complete the following steps:
-- To learn more about using Credential Manager to request digital credentials in your app, read the [Credential Manager - Verifier API](https://developer.android.com/identity/digital-credentials/credential-verifier) page.
-- To learn more about building a digital wallet using Credential Manager, read the [Credential Manager - Holder API](https://developer.android.com/identity/digital-credentials/credential-holder) page.
\ No newline at end of file
+1. **Install the sample apps on the Android-powered device** : Use one of the following methods to install the apps on your first device:
+ - **Use the prebuilt APKs** :
+ - **Download the holder app** : Sign in to your GitHub account, navigate to the [holder app GitHub Actions page](https://github.com/digitalcredentialsdev/CMWallet/actions), select the latest successful build, and download the `app-debug.apk` file from the **Artifacts** section. This app is called **CMWallet**.
+ - **Download the verifier app** : For the sample verifier app, get the APK from the [Identity samples repository](https://github.com/android/identity-samples/actions). Then, install the APK on your Android-powered device. This app is called **Digital
+ Credentials Demo**.
+ - **Build from source**: Clone the repositories mentioned and install the app using Android Studio.
+2. **Open the sample holder app**: Open the sample holder app. This registers the holder credentials with Credential Manager.
+3. **Open the sample verifier app** : Open the sample verifier app. Then, select the option to request digital credentials from wallets. A bottomsheet displaying available credentials from **CMWallet** should appear.
+4. **Select a credential**: Select a credential to send back to the verifier app. The verifier app should now display fields from the returned credential.
+
+> [!NOTE]
+> **Note:** You can also initiate the request from a web browser on another device by navigating to and selecting **Request Credentials (OpenID4VP 1.0)**.
+
+## Resources
+
+- For more information about issuing digital credentials, see the [issuer
+ guide](https://developer.android.com/identity/digital-credentials/credential-issuer/issue-credentials).
+- For more information about holder apps, see the [holder guide](https://developer.android.com/identity/digital-credentials/credential-holder/credential-holder).
+- For more information about verifying users based on digital credentials, see the [verifier guide](https://developer.android.com/identity/digital-credentials/credential-verifier).
+- For more information about digital credentials on the web, see [digital
+ credentials on the web](https://developer.chrome.com/blog/digital-credentials-api-shipped).
\ No newline at end of file
diff --git a/jetpack-compose/adaptive/SKILL.md b/jetpack-compose/adaptive/SKILL.md
index 5ffdf6d..b8c73c0 100644
--- a/jetpack-compose/adaptive/SKILL.md
+++ b/jetpack-compose/adaptive/SKILL.md
@@ -10,7 +10,7 @@ description: Instructions to make or update an app's UI so that it adapts to dif
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-20'
+ last-updated: '2026-08-27'
keywords:
- android
- ui
diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md
index cf860ca..eae40cc 100644
--- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md
+++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md
@@ -6,7 +6,7 @@ This page describes how to implement basic `FlexBox` layouts.
`lib.versions.toml`.
[versions]
- compose = "1.13.0-alpha01"
+ compose = "1.13.0-alpha02"
[libraries]
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" }
diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md
index 0d28f7b..88f2701 100644
--- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md
+++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md
@@ -6,7 +6,7 @@ This page describes how to implement basic [`Grid`](https://developer.android.co
`lib.versions.toml`.
[versions]
- compose = "1.13.0-alpha01"
+ compose = "1.13.0-alpha02"
[libraries]
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" }
diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md
index e2790b6..cc6cc95 100644
--- a/jetpack-compose/theming/styles/SKILL.md
+++ b/jetpack-compose/theming/styles/SKILL.md
@@ -8,7 +8,7 @@ description: Use this skill to integrate the Jetpack Compose Styles API into an
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-14'
+ last-updated: '2026-09-03'
keywords:
- Jetpack Compose
- Styles
diff --git a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md
index ab0d26d..73d4a9e 100644
--- a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md
+++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md
@@ -42,8 +42,7 @@ BaseButton(
onClick = { },
style = { }
) {
- BaseText("Click me")
-}
+ BaseText("Click mnippets.kt
```
@@ -57,8 +56,7 @@ BaseButton(
onClick = { },
style = { background(Color.Blue) }
) {
- BaseText("Click me")
-}
+ BaseText("Click mnippets.kt
```
@@ -77,8 +75,7 @@ own custom components.
Row(
modifier = Modifier.styleable { }
) {
- BaseText("Content")
-}
+ BaseText("Contennippets.kt
```
@@ -93,8 +90,7 @@ Row(
background(Color.Blue)
}
) {
- BaseText("Content")
-}
+ BaseText("Contennippets.kt
```
@@ -145,8 +141,8 @@ val styleState = remember { MutableStyleState(null) }
Column(
Modifier.styleable(styleState, style)
) {
- BaseText("Column content")
-}
+ BaseText("Col)
+}StylesSnippets.kt
```
@@ -172,10 +168,8 @@ Column(
}
val rowStyleState = remember { MutableStyleState(null) }
Row(
- Modifier.styleable(rowStyleState, style)
-) {
- BaseText("Row")
-}
+ Modifier.styleable(rowStyleState, stText("Row")
+}StylesSnippets.kt
```
@@ -194,8 +188,7 @@ BaseButton(
contentPaddingStart(16.dp)
}
) {
- BaseText("Button")
-}
+ BaseText("Buttonippets.kt
```
@@ -225,8 +218,7 @@ BaseButton(
//
}
) {
- BaseText("Click me!")
-}
+ BaseText("Click menippets.kt
```
@@ -250,8 +242,7 @@ BaseButton(
},
) {
- BaseText("Click me!")
-}
+ BaseText("Click menippets.kt
```
@@ -283,8 +274,7 @@ BaseButton(
},
) {
- BaseText("Click me!")
-}
+ BaseText("Click menippets.kt
```
@@ -334,8 +324,8 @@ Column(
) {
BaseText("Children inherit", style = { width(60.dp) })
BaseText("certain properties")
- BaseText("from their parents")
-}
+ BaseText(&quents")
+}StylesSnippets.kt
```
@@ -365,8 +355,8 @@ Column(
contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue)))
})
BaseText("override properties")
- BaseText("set by their parents")
-}
+ BaseText("ents")
+}StylesSnippets.kt
```
diff --git a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md
index c8a373b..6b551d0 100644
--- a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md
+++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md
@@ -9,6 +9,19 @@ depends on where your app sits in relation to its adoption of Material Design:
2. Using Material Design
- **Recommendation**: Await Material adoption to integrate with Styles. Use styles on your own components where possible.
+
+## Android skills
+
+[View on GitHub](https://github.com/android/skills/blob/main/jetpack-compose/theming/styles)
+
+### Use the Jetpack Compose Styles API
+
+Use the `styles` [Android skill](https://developer.android.com/tools/agents/android-skills) to create and customize components with the Styles API. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
+
+ android skills add styles
+
+
+
## The Style layer
In the traditional Compose model, customization often relies heavily on
@@ -75,9 +88,8 @@ val interactiveShadowAtomic = Style {
#### Composition using "then"
-One of the powerful features of the new Styles API is the `then` operator, which
-lets you merge multiple `Style` objects. This lets you build a component using
-atomic utility classes.
+The `then` operator in the Styles API lets you merge multiple `Style` objects.
+This lets you build a component using atomic utility classes.
**Traditional (non-atomic)**:
@@ -250,8 +262,8 @@ access your base styles from anywhere in your project.
Beyond global theme adoption, there are alternative strategies for incorporating
-`Styles` into your apps. You can leverage `Styles` inline for specific call
-sites or use static definitions when full theming capabilities are unnecessary.
+`Styles` into your apps. You can use `Styles` inline for specific call sites or
+use static definitions when full theming capabilities aren't necessary.
`Styles` shouldn't be swapped conditionally unless the whole style is
-fundamentally different. You should prefer accessing dynamic tokens inside a
-visual definition rather than switching between distinct style objects.
\ No newline at end of file
+fundamentally different. Prefer accessing dynamic tokens inside a visual
+definition rather than switching between distinct style objects.
\ No newline at end of file
diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md
index a19f8ba..e40f8d0 100644
--- a/navigation/navigation-3/SKILL.md
+++ b/navigation/navigation-3/SKILL.md
@@ -8,7 +8,7 @@ description: Learn how to install and migrate to Jetpack Navigation 3, and how t
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-14'
+ last-updated: '2026-09-01'
keywords:
- recipe
- Android
diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md
index d7df2d8..d567f73 100644
--- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md
+++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md
@@ -6,7 +6,7 @@
Use an Android skill to help you build using Jetpack Navigation 3. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill navigation-3
+ android skills add navigation-3
diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md
index 23a5907..5104177 100644
--- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md
+++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md
@@ -6,7 +6,7 @@
Use an Android skill to help you build and migrate to Jetpack Navigation 3. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill navigation-3
+ android skills add navigation-3
@@ -97,7 +97,7 @@ project. The core dependencies are provided for you to copy.
**lib.versions.toml**
[versions]
- nav3Core = "1.1.6"
+ nav3Core = "1.1.7"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"
diff --git a/navigation/navigation-event/SKILL.md b/navigation/navigation-event/SKILL.md
new file mode 100644
index 0000000..dda0b69
--- /dev/null
+++ b/navigation/navigation-event/SKILL.md
@@ -0,0 +1,309 @@
+---
+name: navigation-event
+description: Intercept back gestures and run Predictive Back animations using the
+ NavigationEvent (androidx.navigationevent) library in Compose Android. Handles Activity
+ setup, parent-child dispatcher scoping in `ViewPagers` or tabs, Compose `NavigationBackHandler`,
+ and migration from legacy `BackHandler` on SDK 36+.
+license: Complete terms in LICENSE.txt
+metadata:
+ author: Google LLC
+ last-updated: '2026-09-01'
+ keywords:
+ - Android
+ - Navigation Event
+ - Jetpack Compose
+ - Back Navigation
+ - Dispatcher
+ - Guidelines
+ - Troubleshooting
+ - ComponentActivity
+ - Dialog
+ - ViewPager
+---
+
+## Common guidelines
+
+- **For architecture concepts** : To understand the foundational architecture, continuous gesture event lifecycles, or class definitions of the Navigation Event library, read [Navigation Event overview](references/android/guide/navigation/navigation-event/index.md).
+- **For Android target** : If compile SDK is lower than 36, set it to `36` or higher in `build.gradle.kts`.
+- **For Compose Android target**: The project must use Jetpack Compose for Compose-specific APIs. This skill is scoped exclusively to Compose Android (Android Views and non-Compose implementations are excluded).
+- **For activity dispatchers** : `ComponentActivity` automatically implements `NavigationEventDispatcherOwner` out-of-the-box. You must use the built-in `navigationEventDispatcher` without creating anonymous delegate owners or overriding member properties.
+- **For dialog scoping** : Floating windows (Compose `Dialog`, `ModalBottomSheet`, `ComponentDialog`) automatically provide a `NavigationEventDispatcherOwner`. You don't need manual `CompositionLocalProvider` propagation for dialogs.
+- **For parent-child dispatcher hierarchies** : When scoping navigation handling to `ViewPagers`, tabbed interfaces, or nested navigation containers in Compose, use `rememberNavigationEventDispatcherOwner()` to create a child owner linked to the parent. Disabling the owner (`enabled = false`) automatically cascades to disable all child handlers.
+- **For Compose handlers** : A one-to-one relationship between `NavigationEventState` and handlers is strictly enforced. Never bind the same `NavigationEventState` to multiple active `NavigationBackHandler` instances (`IllegalArgumentException`).
+
+## Step 1: Plan
+
+To complete this step, you **MUST** ensure the following:
+
+1. **Identify the target platform** : Verify the app is targeting Compose Android. If `compileSdk` is lower than 36, set it to `36` or higher in `build.gradle.kts`.
+2. **Navigation check**: Check if Navigation 3 is in use. If it is in use, use Navigation 3's built-in back navigation support rather than manually implementing low-level dispatchers from this skill.
+3. **Hierarchy check** : Identify host Activities, `ViewPagers`, tabbed interfaces, or nested navigation hosts that require back gesture interception or parent-child dispatcher linking.
+4. **Migration check** : Check if the project is migrating from back handling (`OnBackPressedCallback`, `BackHandler`, `onBackPresser`) to `NavigationEvent` and `NavigationBackHandler`.
+5. **Input interception** : Detect where the app is intercepting navigation events from gestures or hardware button presses requiring translation to `NavigationEvent`.
+
+## Step 2: Set up dependencies
+
+To complete this step, you **MUST** ensure the following:
+
+- For setting up compile SDKs, declaring catalog versions, and adding dependencies, follow [setup guide](references/android/guide/navigation/navigation-event/setup.md).
+
+## Step 3: Configure dispatcher and inputs
+
+To complete this step, you **MUST** ensure the following:
+
+- To configure your dispatcher, leverage automatic `ComponentActivity` or `ComponentDialog` owner resolution.
+- Link parent-child dispatchers in Compose following [dispatcher guide](references/android/guide/navigation/navigation-event/dispatcher.md).
+
+## Step 4: Handle back navigation and UI transitions
+
+To complete this step, you **MUST** ensure the following:
+
+- To create navigation event handlers, integrate back gesture interception in Compose, animate UI components during swipes, and migrate from legacy back handlers, follow [handle back guide](references/android/guide/navigation/navigation-event/handle-back.md).
+
+## Step 5: Clean up resources
+
+> [!WARNING]
+> **Warning:** Compose APIs perform teardown automatically. When using Compose APIs such as `NavigationBackHandler` and `rememberNavigationEventDispatcherOwner()`, handler removal and dispatcher disposal occur automatically when the composable leaves the composition.
+
+You **MUST** perform explicit manual cleanup only when managing custom
+dispatchers or non-Compose handlers:
+
+- Call `remove()` on active handlers during teardown.
+- Call `isEnabled = false` to temporarily disable navigation subtrees.
+- Call `dispose()` on dispatcher instances when hosting components are destroyed. Disposing a parent dispatcher automatically cascades to all child dispatchers.
+
+## Core troubleshooting guidelines
+
+### 1. Activity dispatcher setup (StackOverflowError recursion)
+
+`ComponentActivity` implements `NavigationEventDispatcherOwner` automatically
+out-of-the-box. Don't override `navigationEventDispatcher` or wrap it in an
+anonymous delegate owner.
+
+#### RIGHT
+
+**Why this is RIGHT** : Compose apps use `ComponentActivity` as the host.
+`LocalNavigationEventDispatcherOwner.current` automatically resolves the
+Activity's built-in dispatcher.
+
+
+```kotlin
+// RIGHT
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MyApplicationContent()
+ }
+ }
+}
+```
+
+
+
+#### WRONG
+
+**Why this is WRONG** : Implementing `NavigationEventDispatcherOwner` directly on
+`MainActivity` and overriding `navigationEventDispatcher` with a new instance
+shadows the library's extension property, causing a recursive infinite loop
+crash on launch (`StackOverflowError`). Creating redundant anonymous delegate
+owners (`object : NavigationEventDispatcherOwner`) is unnecessary.
+
+
+```kotlin
+// WRONG
+class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
+ override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MyApplicationContent()
+ }
+ }
+}
+```
+
+
+
+### 2. Floating window and dialog scoping (automatic ComponentDialog owner)
+
+Floating windows (Compose `Dialog`, `ModalBottomSheet`, and any window backed by
+`ComponentDialog`) automatically provide a `NavigationEventDispatcherOwner`.
+Don't manually re-provide `LocalNavigationEventDispatcherOwner` using
+`CompositionLocalProvider` inside dialogs.
+
+#### RIGHT
+
+**Why this is RIGHT** : `ComponentDialog` handles navigation dispatchers
+automatically. Compose `Dialog` components resolve their dispatcher owner
+out-of-the-box without manual propagation.
+
+
+```kotlin
+// RIGHT
+@Composable
+fun MyDialog(onDismiss: () -> Unit) {
+ Dialog(onDismissRequest = onDismiss) {
+ val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
+ NavigationBackHandler(
+ state = navigationState,
+ onBackCompleted = onDismiss
+ )
+ }
+}
+```
+
+
+
+#### WRONG
+
+**Why this is WRONG** : Wrapping dialog content in a manual
+`CompositionLocalProvider` creates redundant boilerplate and obscures the
+automatic dispatcher resolution provided by `ComponentDialog`.
+
+
+```kotlin
+// WRONG
+@Composable
+fun MyDialog(onDismiss: () -> Unit) {
+ val dispatcherOwner = LocalNavigationEventDispatcherOwner.current!!
+ Dialog(onDismissRequest = onDismiss) {
+ // Redundant: ComponentDialog provides NavigationEventDispatcherOwner automatically
+ CompositionLocalProvider( LocalNavigationEventDispatcherOwner provides dispatcherOwner) {
+ val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
+ NavigationBackHandler(
+ state = navigationState,
+ onBackCompleted = onDismiss
+ )
+ }
+ }
+}
+```
+
+
+
+### 3. Parent-child dispatcher hierarchy (`ViewPagers` and nested navigation)
+
+When managing nested UI hierarchies such as `ViewPagers`, tabbed
+interfaces, or custom navigation containers in Compose, use
+`rememberNavigationEventDispatcherOwner()` to create a child owner linked to the
+composition hierarchy. Setting `enabled = false` on the child owner
+automatically disables its dispatcher and all registered child handlers.
+
+#### RIGHT
+
+**Why this is RIGHT** : Using `rememberNavigationEventDispatcherOwner(enabled =
+isSelected)` creates a scoped child dispatcher linked to the parent from
+`LocalNavigationEventDispatcherOwner.current`. Providing it using
+`CompositionLocalProvider` ensures non-visible tabs or pages automatically stop
+intercepting back gestures without leaking handlers.
+
+
+```kotlin
+// RIGHT: Scoping child navigation in a ViewPager or Tab interface
+@Composable
+fun TabPage(isSelected: Boolean) {
+ val childOwner = rememberNavigationEventDispatcherOwner(enabled = isSelected)
+ CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides childOwner) {
+ val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
+ NavigationBackHandler(
+ state = navigationState,
+ onBackCompleted = { /* Handle page back navigation */ }
+ )
+ // Page content
+ }
+}
+```
+
+
+
+#### WRONG
+
+**Why this is WRONG** : Creating unlinked standalone dispatchers, instantiating
+raw dispatchers without remembering them across recompositions, or attempting to
+use non-existent methods like `.addChild()` breaks hierarchy routing and leaves
+child handlers active even when the page is inactive.
+
+
+```kotlin
+// WRONG
+@Composable
+fun TabPage(isSelected: Boolean) {
+ val parentDispatcher = LocalNavigationEventDispatcherOwner.current?.navigationEventDispatcher
+ val childDispatcher = NavigationEventDispatcher() // Unlinked and not remembered across recompositions
+ // WRONG: Method does not exist
+ parentDispatcher?.addChild(childDispatcher)
+}
+```
+
+
+
+### 4. Compose multi-handler registration (IllegalArgumentException)
+
+You must not bind the same `NavigationEventState` to multiple active
+`NavigationBackHandler` instances, as this throws an `IllegalArgumentException`
+at runtime. To handle conditional workflows (such as checking for unsaved
+changes versus navigating back immediately), you must register a single unified
+handler and branch logic inside `onBackCompleted`.
+
+#### RIGHT
+
+**Why this is RIGHT** : Using a single `NavigationBackHandler` with internal
+branching logic inside `onBackCompleted` maintains a strict 1:1 mapping between
+`NavigationEventState` and the handler, preventing state collisions.
+
+
+```kotlin
+// RIGHT
+val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
+NavigationBackHandler(
+ state = navigationState,
+ isBackEnabled = true,
+ onBackCompleted = {
+ if (hasUnsavedChanges) {
+ showDiscardDialog()
+ } else {
+ onNavigateUp()
+ }
+ }
+)
+```
+
+
+
+#### WRONG
+
+**Why this is WRONG** : Attaching multiple `NavigationBackHandler` composables to
+the same `navigationState` instance attempts to bind duplicate handlers to a
+single state object, which throws an `IllegalArgumentException` at runtime.
+
+
+```kotlin
+// WRONG
+val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
+NavigationBackHandler(
+ state = navigationState,
+ isBackEnabled = hasUnsavedChanges,
+ onBackCompleted = { /* Discard changes */ }
+)
+NavigationBackHandler(
+ state = navigationState,
+ isBackEnabled = !hasUnsavedChanges,
+ onBackCompleted = { /* Navigate up */ }
+)
+```
+
+
+
+## Checklist
+
+**For Compose Android targets:**
+
+- \[ \] Is compile SDK set to `36` or higher? (If compile SDK is lower than 36, set it to `36` or higher in `build.gradle.kts`).
+- \[ \] Is `android:enableOnBackInvokedCallback` NOT explicitly set to `"false"` in `AndroidManifest.xml`? (On API 36+, it defaults to `"true"`; on API 33--35, ensure it is set to `"true"`).
+- \[ \] Does the Activity rely on the built-in `ComponentActivity` dispatcher owner without redundant anonymous delegate wrapping?
+- \[ \] Do dialogs or sheets rely on automatic `ComponentDialog` dispatcher resolution without redundant `CompositionLocalProvider` wrapping?
+- \[ \] Are parent-child dispatcher relationships in Compose scoped using `rememberNavigationEventDispatcherOwner()` when managing nested hierarchies?
+- \[ \] Is conditional back logic handled within a single unified `NavigationBackHandler` to avoid duplicate registration (`IllegalArgumentException`)?
+- \[ \] Are legacy `BackHandler` usages migrated to `NavigationBackHandler` with predictive progress support?
+- \[ \] Does the project build and pass tests successfully?
diff --git a/navigation/navigation-event/references/android/guide/navigation/navigation-event/dispatcher.md b/navigation/navigation-event/references/android/guide/navigation/navigation-event/dispatcher.md
new file mode 100644
index 0000000..27b6a96
--- /dev/null
+++ b/navigation/navigation-event/references/android/guide/navigation/navigation-event/dispatcher.md
@@ -0,0 +1,134 @@
+To implement a robust navigation system, your app needs a centralized way to
+handle back gestures and other navigation signals. This page describes how to
+use [`NavigationEventDispatcher`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher) to coordinate and distribute these
+navigation events across your application.
+
+## Declare a `NavigationEventDispatcher`
+
+The `NavigationEventDispatcher` is the central component of the
+`NavigationEvent` library. It acts as an event hub that dispatches
+navigation-related events, such as back gestures and navigation transitions, to
+registered listeners within your app. Components can subscribe to these events
+to react to navigation changes or other system-driven navigation actions.
+
+You should provide `NavigationEventDispatcher` instances through a
+[`NavigationEventDispatcherOwner`](https://developer.android.com/reference/androidx/navigationevent/NavigationEventDispatcherOwner). This ensures that different parts of your
+app can access the same dispatcher and observe navigation events in a consistent
+and coordinated way.
+
+
+```kotlin
+class MyComponent: NavigationEventDispatcherOwner {
+ override val navigationEventDispatcher: NavigationEventDispatcher =
+ NavigationEventDispatcher()
+}
+```
+
+
+
+If you are inside of a `ComponentActivity`, instead of implementing your own
+dispatcher, you can retrieve the one provided for you.
+
+
+```kotlin
+class MyCustomActivity : ComponentActivity() {
+ fun addMyHandler() {
+ // navigationEventDispatcher provided by the ComponentActivity
+ navigationEventDispatcher.addHandler(myNavigationEventHandler)
+ }
+}
+```
+
+
+
+## Add a `NavigationEventInput`
+
+Now that you've registered the handler, you are set up to receive events.
+However, you need to provide a source from which the events are generated with
+`NavigationEventInput`.
+
+`NavigationEventInput` is the platform-specific component that receives
+raw system input and translates it into a standard `NavigationEvent` to be sent
+to the `NavigationEventDispatcher`.
+
+The following example is a custom implementation of a `NavigationEventInput`:
+
+
+```kotlin
+public class MyInput : NavigationEventInput() {
+ @MainThread
+ public fun backStarted(event: NavigationEvent) {
+ dispatchOnBackStarted(event)
+ }
+
+ @MainThread
+ public fun backProgressed(event: NavigationEvent) {
+ dispatchOnBackProgressed(event)
+ }
+
+ @MainThread
+ public fun backCancelled() {
+ dispatchOnBackCancelled()
+ }
+
+ @MainThread
+ public fun backCompleted() {
+ dispatchOnBackCompleted()
+ }
+}
+```
+
+
+
+Next, provide that input to your dispatcher:
+
+
+```kotlin
+navigationEventDispatcher.addInput(MyInput())
+```
+
+
+
+> [!NOTE]
+> **Note:** To provide a simple input, use the [`DirectNavigationEventInput`](https://developer.android.com/reference/androidx/navigationevent/DirectNavigationEventInput) class.
+
+## Clean up resources with `dispose()`
+
+To prevent memory leaks in a dynamic UI, every created
+`NavigationEventDispatcher` instance must be explicitly removed from the
+hierarchy using the `dispose()` method when the component it is tied to is
+destroyed:
+
+
+```kotlin
+navigationEventDispatcher.dispose()
+```
+
+
+
+The `dispose()` method ensures a *cascading cleanup* by iteratively removing
+the dispatcher and all of its descendants (children and grandchildren),
+guaranteeing that all associated handlers are unregistered from the shared
+system.
+
+### Dispatcher hierarchy and control
+
+The `NavigationEventDispatcher` supports a parent-child hierarchy, enabling
+components nested deep within a UI (such as nested `NavHost`s or dialogs) to
+participate in navigation event handling.
+
+#### Create a child dispatcher
+
+A child dispatcher is created by passing a reference to its parent dispatcher
+during construction. All dispatchers in a hierarchy share the same
+`NavigationEventProcessor` to maintain a global **Last-In, First-Out (LIFO)**
+event ordering based on priority.
+
+#### Hierarchical enabling
+
+The dispatcher includes an `isEnabled` property that allows developers to enable
+or disable an entire subtree of handlers at once.
+
+When a parent dispatcher is disabled (`isEnabled = false`), all handlers
+associated with that parent and any of its children will be ignored, regardless
+of their individual enabled state.
\ No newline at end of file
diff --git a/navigation/navigation-event/references/android/guide/navigation/navigation-event/handle-back.md b/navigation/navigation-event/references/android/guide/navigation/navigation-event/handle-back.md
new file mode 100644
index 0000000..685cc69
--- /dev/null
+++ b/navigation/navigation-event/references/android/guide/navigation/navigation-event/handle-back.md
@@ -0,0 +1,217 @@
+You can extend the abstract class `NavigationEventHandler` to handle navigation
+events across platforms. This class provides methods corresponding to the
+lifecycle of a navigation gesture.
+
+
+```kotlin
+val myHandler = object: NavigationEventHandler(
+ initialInfo = NavigationEventInfo.None,
+ isBackEnabled = true
+) {
+ override fun onBackStarted(event: NavigationEvent) {
+ // Prepare for the back event
+ }
+
+ override fun onBackProgressed(event: NavigationEvent) {
+ // Use event.progress for predictive animations
+ }
+
+ // This is the required method for final event handling
+ override fun onBackCompleted() {
+ // Complete the back event
+ }
+
+ override fun onBackCancelled() {
+ // Cancel the back event
+ }
+}
+```
+
+
+
+The `addHandler` function connects the handler to the dispatcher:
+
+
+```kotlin
+navigationEventDispatcher.addHandler(myHandler)
+```
+
+
+
+Call `myHandler.remove()` to remove the handler from the dispatcher:
+
+
+```kotlin
+myHandler.remove()
+```
+
+
+
+Handlers are invoked based on priority, and then by recency. All
+[`PRIORITY_OVERLAY`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher#PRIORITY_OVERLAY()) handlers are called before any [`PRIORITY_DEFAULT`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher#PRIORITY_DEFAULT())
+handlers. Within each priority group, handlers are invoked in a Last-In,
+First-Out (LIFO) order --- the most recently added handler is called first.
+
+## Intercept back with Jetpack Compose
+
+For Jetpack Compose, the library provides a utility composable to manage the
+dispatcher hierarchy.
+
+The `NavigationBackHandler` composable creates a `NavigationEventHandler` for
+its content and links it to the `LocalNavigationEventDispatcherOwner`. It uses
+Compose's `DisposableEffect` to automatically call the dispatcher's `dispose()`
+method when the composable leaves the screen, safely managing resources.
+
+
+```kotlin
+@Composable
+public fun NavigationBackHandler(
+ state: NavigationEventState,
+ isBackEnabled: Boolean = true,
+ onBackCancelled: () -> Unit = {},
+ onBackCompleted: () -> Unit,
+){
+
+}
+```
+
+
+
+This function lets you control event handling precisely within localized UI
+subtrees.
+
+
+```kotlin
+@Composable
+fun HandlingBackWithTransitionState(
+ onNavigateUp: () -> Unit
+) {
+ val navigationState = rememberNavigationEventState(
+ currentInfo = NavigationEventInfo.None
+ )
+ val transitionState = navigationState.transitionState
+ // React to predictive back transition updates
+ when (transitionState) {
+ is NavigationEventTransitionState.InProgress -> {
+ val progress = transitionState.latestEvent.progress
+ // Use progress (0f..1f) to update UI during the gesture
+ }
+ is NavigationEventTransitionState.Idle -> {
+ // Reset any temporary UI state if the gesture is cancelled
+ }
+ }
+ NavigationBackHandler(
+ state = navigationState,
+ onBackCancelled = {
+ // Called if the back gesture is cancelled
+ },
+ onBackCompleted = {
+ // Called when the back gesture fully completes
+ onNavigateUp()
+ }
+ )
+}
+```
+
+
+
+This example shows how to observe predictive back gesture updates using
+[`NavigationEventTransitionState`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventTransitionState). The `progress` value can be used to
+update UI elements in response to the back gesture, while handling completion
+and cancellation through `NavigationBackHandler`.
+
+### Access the back gesture or swipe edge in Compose
+
+> [!NOTE]
+> **Note:** For Android, if you're already using a navigation library with built-in Predictive Back support, like [Navigation 3](https://developer.android.com/guide/navigation/navigation-3/animate-destinations), use that instead of implementing the guidance here. The following section shows how to create a Predictive Back animation using only `NavigationEvent` and Compose.
+
+**Figure 1** . A predictive back animation built with `NavigationEvent` and Compose.
+
+To animate the screen while the user swipes back, you'll need to (a) check if
+the `NavigationEventTransitionState` is `InProgress`, and (b) observe the
+progress and swipe edge state with `rememberNavigationEventState`:
+
+- `progress`: A Float from `0.0` to `1.0` indicating how far the user has swiped.
+- `swipeEdge`: An integer constant (`EDGE_LEFT` or `EDGE_RIGHT`) indicating where the gesture started.
+
+The following snippet is a simplified example of how to implement a scale and
+shift animation:
+
+
+```kotlin
+object Routes {
+ const val SCREEN_A = "Screen A"
+ const val SCREEN_B = "Screen B"
+}
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ var state by remember { mutableStateOf(Routes.SCREEN_A) }
+ val backEventState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
+ when (state) {
+ Routes.SCREEN_A -> {
+ ScreenA(onNavigate = { state = Routes.SCREEN_B })
+ }
+ else -> {
+ if (backEventState.transitionState is NavigationEventTransitionState.InProgress) {
+ ScreenA(onNavigate = { })
+ }
+ ScreenB(
+ backEventState = backEventState,
+ onBackCompleted = { state = Routes.SCREEN_A }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ScreenB(
+ backEventState: NavigationEventState,
+ onBackCompleted: () -> Unit = {},
+) {
+ val transitionState = backEventState.transitionState
+ val latestEvent =
+ (transitionState as? NavigationEventTransitionState.InProgress)
+ ?.latestEvent
+ val backProgress = latestEvent?.progress ?: 0f
+ val swipeEdge = latestEvent?.swipeEdge ?: NavigationEvent.EDGE_LEFT
+ if (transitionState is NavigationEventTransitionState.InProgress) {
+ Log.d("BackGesture", "Progress: ${transitionState.latestEvent.progress}")
+ } else if (transitionState is NavigationEventTransitionState.Idle) {
+ Log.d("BackGesture", "Idle")
+ }
+ val animatedScale by animateFloatAsState(
+ targetValue = 1f - (backProgress * 0.1f),
+ label = "ScaleAnimation"
+ )
+ val windowInfo = LocalWindowInfo.current
+ val density = LocalDensity.current
+ val maxShift = remember(windowInfo, density) {
+ val widthDp = with(density) { windowInfo.containerSize.width.toDp() }
+ (widthDp.value / 20f) - 8
+ }
+ val offsetX = when (swipeEdge) {
+ NavigationEvent.EDGE_LEFT -> (backProgress * maxShift).dp
+ NavigationEvent.EDGE_RIGHT -> (-backProgress * maxShift).dp
+ else -> 0.dp
+ }
+ NavigationBackHandler(
+ state = backEventState,
+ onBackCompleted = onBackCompleted,
+ isBackEnabled = true
+ )
+ Box(
+ modifier = Modifier
+ .offset(x = offsetX)
+ .scale(animatedScale)
+ ){
+ // Rest of UI
+ }
+}
+```
+
+
\ No newline at end of file
diff --git a/navigation/navigation-event/references/android/guide/navigation/navigation-event/index.md b/navigation/navigation-event/references/android/guide/navigation/navigation-event/index.md
new file mode 100644
index 0000000..59d8ad4
--- /dev/null
+++ b/navigation/navigation-event/references/android/guide/navigation/navigation-event/index.md
@@ -0,0 +1,83 @@
+Navigation Event is a library that provides a Kotlin Multiplatform (KMP)
+solution for integrating system-level navigation events into your application.
+It is designed to be the foundational layer for handling navigation directions
+across various [supported platforms](https://developer.android.com/kotlin/multiplatform#kotlin-multiplatform-and-jetpack-libraries).
+
+## Key concepts
+
+The Navigation Event system is built around a centralized dispatcher-handler
+model, often used in a parent-child hierarchy to map to complex UI structures,
+such as those found in Jetpack Compose.
+
+### `NavigationEventDispatcher`
+
+The [`NavigationEventDispatcher`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventDispatcher) is the central class responsible for
+managing all registered navigation event consumers
+([`NavigationEventHandler`](https://developer.android.com/reference/kotlin/androidx/navigationevent/NavigationEventHandler))) and orchestrating the flow of events.
+
+In a hierarchical setup, all dispatchers within the same chain share a single
+`NavigationEventProcessor`, which manages the global state and ensures a single,
+unified dispatching order across the entire tree.
+
+### `NavigationEventHandler`
+
+`NavigationEventHandler` is an abstract class that receives and handles
+navigation events dispatched by a `NavigationEventDispatcher`. It defines
+callback methods that correspond to different stages of a navigation gesture
+lifecycle, such as when a gesture starts, progresses, completes, or is
+cancelled.
+
+Handlers can respond to these events to update UI or application state in
+response to user navigation actions. Multiple handlers can be registered with a
+dispatcher and are invoked based on priority and registration order.
+
+### `NavigationEvent`
+
+[`NavigationEvent`](https://developer.android.com/reference/androidx/navigationevent/NavigationEvent) is a data class that carries the details of the
+navigation gesture.
+
+### `NavigationEventInfo`
+
+[`NavigationEventInfo`](https://developer.android.com/reference/androidx/navigationevent/NavigationEventInfo) is an abstract class that provides contextual
+information about a navigation state.
+
+### `NavigationEventInput`
+
+[`NavigationEventInput`](https://developer.android.com/reference/androidx/navigationevent/NavigationEventInput) is an abstract class for components that generate
+and dispatch navigation events. It acts as the "input" side of the navigation
+system, translating platform-specific events (like system back gestures or
+button clicks) into standardized events that can be sent to a
+`NavigationEventDispatcher`.
+
+## Supported navigation directions and triggers
+
+The Navigation Event system is designed to encompass more than just the system
+back button, with designs supporting multiple navigation directions and input
+methods across platforms.
+
+### Supported directions
+
+Different platforms support varying navigation directions:
+
+|---|---|---|---|---|
+| **Platform** | **Back** | **Up** | **Forward** | **Home** |
+| **Android phone** | ✅ | ✅ | 🚫 | ✅ |
+| **Android tablet** | ✅ | ✅ | 🚫 | ✅ |
+| **Web (Browser)** | ✅ | ✅ | ✅ | 🚫 |
+| **iOS (iPhone/iPad)** | ✅ | 🚫 | ✅ | ✅ |
+
+### Supported triggers
+
+Input handling is achieved through various mechanisms on each platform:
+
+|---|---|---|---|
+| **Trigger** | **Android Phone** | **Web (Browser)** | **iOS (iPhone/iPad)** |
+| **Keyboard back button** | ✅ Back | ❓ | ✅ Back |
+| **Software back button** | 🚫 | ✅ Back | ✅ Back |
+| **Software up button** | ✅ Up | 🚫 | 🚫 |
+| **Gesture from left** | ✅ Back | ❓ | ✅ Back |
+| **Gesture from right** | ✅ Back | ❓ | ✅ Forward |
+| **Gesture from bottom** | ✅ Home | 🚫 | ✅ Home |
+
+> [!NOTE]
+> **Note:** The **Web** platform has unique navigation handling, where the browser controls the back stack state. This requires synchronization between the browser window and the application's navigation stack. The question mark represents behavior that is inconsistent because web browsers don't have a single, unified "back" button or gesture.
\ No newline at end of file
diff --git a/navigation/navigation-event/references/android/guide/navigation/navigation-event/setup.md b/navigation/navigation-event/references/android/guide/navigation/navigation-event/setup.md
new file mode 100644
index 0000000..7981cf1
--- /dev/null
+++ b/navigation/navigation-event/references/android/guide/navigation/navigation-event/setup.md
@@ -0,0 +1,32 @@
+To set up your development environment for `NavigationEvent`, follow these
+steps.
+
+## Declare dependencies
+
+1. Add the `navigationevent` artifact to your project. This is the core library
+ containing the shared `NavigationEventDispatcher` and `NavigationEventHandler`
+ classes.
+
+ For Jetpack Compose integration, you also need to add the corresponding
+ Compose artifact:
+
+ [versions]
+ navigationevent = "1.0.0"
+
+ [libraries]
+ # NavigationEvent libraries
+ androidx-navigationevent = { module = "androidx.navigationevent:navigationevent", version.ref = "navigationevent" }
+ androidx-navigationevent-compose = { module = "androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent" }
+
+2. Update your compile SDK to 36 or above:
+
+ [versions]
+ compileSdk = "36"
+
+3. Add the following to your app build file, `app/build.gradle.kts`:
+
+ dependencies {
+ ...
+ implementation(libs.androidx.navigationevent)
+ implementation(libs.androidx.navigationevent.compose)
+ }
\ No newline at end of file
diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md
index e72b03a..f696836 100644
--- a/performance/r8-analyzer/SKILL.md
+++ b/performance/r8-analyzer/SKILL.md
@@ -7,7 +7,7 @@ description: Analyzes Android build files and R8 keep rules to identify redundan
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-15'
+ last-updated: '2026-08-28'
keywords:
- R8
- proguard
diff --git a/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md b/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md
index b5ddc30..560e2ec 100644
--- a/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md
+++ b/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md
@@ -6,7 +6,7 @@
Use the R8 Analyzer [Android skill](https://developer.android.com/tools/agents/android-skills) to analyze build files and identify redundant, broad, or subsumed [keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview). To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill r8-analyzer
+ android skills add r8-analyzer
@@ -247,7 +247,6 @@ apps, you might use an alternative build system like [Bazel](https://bazel.build
you're using Bazel, you can integrate R8 into your build pipeline to shrink,
obfuscate, and optimize your app.
-For information about building an Android app using Bazel, see the [Android
-Bazel tutorial](https://bazel.build/start/android-app) and the official [rules_android
-repository](https://github.com/bazelbuild/rules_android). Note that Bazel isn't [officially supported](https://developer.android.com/build#other-build-systems)
-for Android app development.
\ No newline at end of file
+For information about optimizing an Android app using Bazel, see Bazel's
+[`rules_android` documentation](https://github.com/bazelbuild/rules_android/tree/main/docs/r8-optimization.md). Note that Bazel isn't
+[officially supported](https://developer.android.com/build#other-build-systems) for Android app development.
\ No newline at end of file
diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md
index 9eddd7d..6dadbed 100644
--- a/play/engage-sdk-integration/SKILL.md
+++ b/play/engage-sdk-integration/SKILL.md
@@ -6,7 +6,7 @@ description: Helps developers integrate, debug, and resolve Play Engage SDK impl
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-19'
+ last-updated: '2026-09-01'
keywords:
- android
- engage
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md
index d683a52..5c6f54c 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md
@@ -305,7 +305,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md
index c9034e1..417d333 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md
@@ -354,7 +354,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md
index 8acb9c0..7aeb887 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md
@@ -374,7 +374,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md
index b9a6d61..75195ab 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md
@@ -592,7 +592,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md
index ab0650a..70b0701 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md
@@ -236,7 +236,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md
index c59de5f..de0a90f 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md
@@ -274,7 +274,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md
index b8de98f..0b2d1c4 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md
@@ -208,7 +208,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md
index 1ac8584..55bb090 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md
@@ -622,7 +622,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md
index 42c9296..2770f93 100644
--- a/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md
+++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md
@@ -380,7 +380,7 @@ whether the content can be presented on the device.
To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill engage-sdk-integration
+ android skills add engage-sdk-integration
If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant:
diff --git a/play/play-billing-library-version-upgrade/SKILL.md b/play/play-billing-library-version-upgrade/SKILL.md
index 3174a11..0795e6a 100644
--- a/play/play-billing-library-version-upgrade/SKILL.md
+++ b/play/play-billing-library-version-upgrade/SKILL.md
@@ -6,7 +6,7 @@ description: Use this skill when upgrading or migrating an Android project from
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-14'
+ last-updated: '2026-09-01'
keywords:
- android
- play billing
diff --git a/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md b/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md
index ae3ff26..9fc90a8 100644
--- a/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md
+++ b/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md
@@ -9,7 +9,7 @@ This document contains release notes for the Google Play Billing Library.
Use the Play Billing Library [Android skill](https://developer.android.com/tools/agents/android-skills) to automate your upgrade to the latest version. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
- android skills add --skill play-billing-library-version-upgrade
+ android skills add play-billing-library-version-upgrade
To activate the skill, try the following prompt:
diff --git a/system/edge-to-edge/SKILL.md b/system/edge-to-edge/SKILL.md
index c77ef1b..77cc0d6 100644
--- a/system/edge-to-edge/SKILL.md
+++ b/system/edge-to-edge/SKILL.md
@@ -7,7 +7,7 @@ description: Use this skill to migrate your Jetpack Compose app to add adaptive
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-14'
+ last-updated: '2026-08-24'
keywords:
- android
- compose
@@ -185,7 +185,7 @@ applied twice, once with innerPadding, which contains IME insets from the passed
```kotlin
// WRONG
-Scaffold( contentWindowInsets = WindowInsets.safeDrawing ) { innerPadding ->
+Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
diff --git a/testing/testing-setup/SKILL.md b/testing/testing-setup/SKILL.md
index 99a4368..a4becc4 100644
--- a/testing/testing-setup/SKILL.md
+++ b/testing/testing-setup/SKILL.md
@@ -6,7 +6,7 @@ description: Analyze and create a testing strategy for native Android apps - ins
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-14'
+ last-updated: '2026-09-03'
keywords:
- android
- testing
diff --git a/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md b/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md
index b1f7876..ad83abc 100644
--- a/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md
+++ b/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md
@@ -7,6 +7,19 @@ features of [composable previews](https://developer.android.com/develop/ui/compo
gains of running host-side screenshot tests. Compose Preview Screenshot Testing
is designed to be as straightforward to use as composable previews.
+
+## Android skills
+
+[View on GitHub](https://github.com/android/skills/blob/main/testing/testing-setup)
+
+### Create screenshot tests
+
+Use the `testing-setup` [Android skill](https://developer.android.com/tools/agents/android-skills) to develop a testing strategy and create screenshot tests. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run:
+
+ android skills add testing-setup
+
+
+
A screenshot test is an automated test that takes a screenshot of a piece of UI
and then compares it against a previously approved reference image. If the
images don't match, the test fails and produces an HTML report to help you
@@ -158,6 +171,21 @@ make code changes. To generate reference images for your composable preview
screenshot tests, follow the instructions in this section for the IDE
integration or for the Gradle tasks.
+
+## Android CLI
+
+[Download the Android CLI](https://developer.android.com/tools/agents)
+
+### Try Android CLI to render a Compose preview
+
+Try [Android CLI](https://developer.android.com/tools/agents) if you're not using Android Studio or prefer to do things from the command line.
+
+For example, use the [`android studio render-compose-preview`](https://developer.android.com/tools/agents/android-cli#studio-render-compose-preview) command when you need to render a Compose preview for visual testing.
+
+ android studio render-compose-preview
+
+
+
### In the IDE
Click the gutter icon next to a `@PreviewTest` function and select **Add/Update
diff --git a/tv/leanback-to-compose-tv-migration/SKILL.md b/tv/leanback-to-compose-tv-migration/SKILL.md
index c5d709d..773a23e 100644
--- a/tv/leanback-to-compose-tv-migration/SKILL.md
+++ b/tv/leanback-to-compose-tv-migration/SKILL.md
@@ -13,7 +13,7 @@ description: Provides instructions and architectural patterns for migrating Andr
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-21'
+ last-updated: '2026-09-03'
keywords:
- Android TV
- Jetpack Compose
diff --git a/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md b/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md
index 57815a2..ad5bf93 100644
--- a/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md
+++ b/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md
@@ -2,17 +2,18 @@ TV devices provide a limited set of navigation controls for apps. Creating an
effective navigation scheme for your TV app depends on understanding these
limited controls as well as users' limitations while operating your app.
As you build your Android app for TV, pay special attention to how the user
-navigates when using remote control buttons instead of a touch screen.
+navigates when using remote control buttons instead of a touchscreen.
## Principles
-The goal is for navigation to feel natural and familiar without dominating the user
-interface or diverting attention from content. The following principles help set
-a baseline for a consistent and intuitive user experience across TV apps.
+The goal is for navigation to feel natural and familiar without dominating the
+user interface or diverting attention from content. The following principles
+help set a baseline for a consistent and intuitive user experience across TV
+apps.
**Efficient**
-Make it fast and easy to get to content. Users want to access content
+Simplify getting to content. Users want to access content
quickly, using a minimal number of clicks. Organize your information in
a way that requires the fewest screens.
**Predictable**
@@ -24,15 +25,16 @@ unnecessarily, as this leads to confusion and unpredictability.
**Intuitive**
-Make navigation simple enough to seamlessly support widely adopted user
-behaviors. Don't over-complicate by adding unnecessary layers of navigation.
+Simplify navigation enough to seamlessly support widely adopted user
+behaviors. Don't over-complicate by adding unnecessary layers of
+navigation.
## Controllers
Controllers come in a variety of styles, from a minimalist remote control to
complex game controllers. All controllers include a directional pad (D-pad) plus
select, home, and back buttons. Other buttons vary by model.
- **Figure 1.** Example of a TV remote.
+ **Figure 1.** Example of a TV remote.
**D-pad**
@@ -60,8 +62,7 @@ Invokes either Google Assistant or voice input.
> [!NOTE]
-> **Note:** The remote control shown in figure 1 is for reference only. There are many layouts and styles of remotes and controllers, though all of them have the basic functionality described here. For more information, see [Manage
-> TV controllers](https://developer.android.com/training/tv/get-started/controllers).
+> **Note:** The remote control shown in figure 1 is for reference only. There are many layouts and styles of remotes and controllers, though all of them have the basic functionality described here. For more information, see [Manage TV controllers](https://developer.android.com/training/tv/get-started/controllers).
## D-pad navigation
@@ -71,12 +72,12 @@ build a great TV-optimized app, you must provide a navigation scheme where the
user can quickly learn how to navigate your app using these limited controls.
The Android framework handles directional navigation between layout elements
-automatically, so you typically do not need to do anything extra for your app.
+automatically, so you typically don't need to do anything extra for your app.
However, you should thoroughly test navigation with a D-pad controller to
discover any navigation problems.
-Follow these guidelines to test that your
-app's navigation system works well with a D-pad on a TV device:
+Follow these guidelines to test that your app's navigation system works well
+with a D-pad on a TV device:
- Ensure that a user with a D-pad controller can navigate to all visible controls on the screen.
- For scrolling lists with focus, make sure that the D-pad up and down buttons scroll the list and that the select button selects an item in the list. Verify that users can select an element in the list and that the list still scrolls when an element is selected.
@@ -85,9 +86,9 @@ app's navigation system works well with a D-pad on a TV device:
### Modify directional navigation
The Android framework automatically applies a directional navigation scheme
-based on the relative position of focusable elements in your layouts. Test
-the generated navigation scheme in your app using a D-pad controller. After
-testing, if you decide that you want users to move through your layouts in a specific
+based on the relative position of focusable elements in your layouts. Test the
+generated navigation scheme in your app using a D-pad controller. After testing,
+if you decide that you want users to move through your layouts in a specific
way, you can set up explicit directional navigation for your controls.
> [!NOTE]
@@ -115,15 +116,16 @@ first one.
### Provide clear focus and selection
-The success of an app's navigation scheme on TV devices depends on how easy it
-is for a user to determine what user interface element is in focus. If
-you do not provide a clear indication of the focused item, and therefore what item a
-user can take action on, they can quickly become frustrated and exit your app.
-For the same reason, it is important to always have an item in focus that a user
-can take action on immediately after your app starts or any time it is idle.
+The success of an app's navigation scheme on TV devices depends on whether
+a user can quickly determine what user interface element is in focus. If
+you don't provide a clear indication of the focused item, and therefore what
+item a user can take action on, they can quickly become frustrated and exit your
+app. For the same reason, it is important to always have an item in focus that a
+user can take action on immediately after your app starts or any time it is
+idle.
In your app layout and implementation, use color, size, animation, or a
-combination of these attributes to help users easily determine what actions they
+combination of these attributes to help users determine what actions they
can take next. Use a uniform scheme for indicating focus across your
application.
@@ -166,9 +168,9 @@ button follows these guidelines.
### Use predictable back button behavior
-To create an easy and predictable navigation experience, when the user presses
-the remote's back button, take them to the previous destination.
- **Figure 2.** Flow using top navigation.  **Figure 3.** Flow using side navigation.
+To create a straightforward and predictable navigation experience, when the user
+presses the remote's back button, take them to the previous destination.
+ **Figure 2.** Flow using top navigation.  **Figure 3.** Flow using side navigation.
If the user navigates from a menu item to a card on the middle of the
page and then presses the back button, the result depends on whether the app
@@ -179,7 +181,7 @@ uses top navigation or left navigation:
Ensure that the back button isn't gated by confirmation screens or part of an
infinite loop.
- **Figure 4.** Exit gating
+ **Figure 4.** Exit gating
Don't.
Avoid exit gating. Let users exit out of the app without
@@ -187,7 +189,7 @@ confirmation.
- **Figure 5.** Navigation loop
+ **Figure 5.** Navigation loop
Don't.
Never enter the infinite loop of closing and opening the
@@ -198,8 +200,9 @@ as a kids profile.
### Don't display an up or back button
Unlike on handheld devices, the back button on the remote is used to navigate
-backward on a TV. It's not necessary to show a virtual back button on the screen:
- **Figure 6.** Soft back button
+backward on a TV. It's not necessary to show a virtual back button on the
+screen:
+ **Figure 6.** Soft back button
Don't.
@@ -212,7 +215,7 @@ Don't.
If the only visible actions are confirming, destructive, or purchase actions,
it's good practice to have a **Cancel** button that returns to the previous
destination:
- **Figure 7.** Soft cancel button.
+ **Figure 7.** Soft cancel button.
Do.
@@ -221,10 +224,10 @@ Do.
### Implement back navigation
The Android framework generally handles back
-navigation well, as it does for the D-pad. If you use the [Navigation component](https://developer.android.com/guide/navigation),
-you can support a variety of navigation graphs. Occasionally, you might need
-to implement some custom behavior, such as having the back button reset the focus
-to the beginning of a long list.
+navigation well, as it does for the D-pad. If you use the
+[Navigation component](https://developer.android.com/guide/navigation), you can support a variety of navigation graphs.
+Occasionally, you might need to implement some custom behavior, such as having
+the back button reset the focus to the beginning of a long list.
[`ComponentActivity`](https://developer.android.com/reference/androidx/activity/ComponentActivity),
the base class for [`FragmentActivity`](https://developer.android.com/reference/androidx/fragment/app/FragmentActivity)
@@ -254,23 +257,14 @@ playback and direct-back requirements, as described in the following sections.
#### Frictionless playback
-Frictionless playback applies to in-app behavior following any Live/Linear
+Frictionless playback applies to in-app behavior following any Live or Linear
channel deep link from Google TV and Android TV.
-Users who click a Live/Linear channel deep link from Google TV and Android
-TV must be led directly to channel playback, without any blocking or delaying
-screens from the target app. Sign-in flows, sign-up flows, branding videos,
-and other delays are *not* permitted.
+When a user clicks a deep link, the following rules apply:
-However, if the deep link initiates the target app loading from a cold
-boot, this boot-up delay before playback *is* permitted. An app boot-up
-branding video or animation is also permitted in this case. Such a cold boot
-experience is unlikely to occur more than once per session.
-
-Also, if tuning into the deep-linked channel takes a few seconds, displaying
-channel and/or service branding *is* permitted. However, its
-duration should only be as long as it takes to load the channel (and similar
-to average channel load times within the app).
+- **No delays:** Users must be led directly to channel playback without blocking or delaying screens, such as sign-in or sign-up flows or branding videos.
+- **Cold boot exception:** If the deep link initiates the target app loading from a cold boot, the boot-up delay is permitted. App boot-up branding videos or animations are also allowed in this case.
+- **Loading delay exception:** If tuning into the channel takes a few seconds, displaying branding is permitted. The duration must only be as long as it takes to load the channel.
If the user is signed out or isn't subscribed, you can block playback for a
paid channel to complete a sign-in or sign-up flow.
@@ -307,6 +301,10 @@ section.
## Navigation architecture
+A well-defined navigation architecture helps users understand where they are in
+your app and how to access different content. Consider the following aspects of
+navigation design.
+
### Fixed start destination
The first screen the user sees when they launch the app from the launcher
@@ -333,40 +331,40 @@ the Moviestar app.
> [!NOTE]
-> **Note:** This does not apply when launching from the Live Tab for direct playback. See the [Live Tab navigation](https://developer.android.com/training/tv/get-started/navigation#live_tab_navigation) section for details.
+> **Note:** This does not apply when launching from the Live Tab for direct playback. See the [Live Tab navigation](https://developer.android.com/training/tv/get-started/navigation#live-tab-navigation) section for details.
### Clear path to all focusable elements
Let users navigate your UI with clear direction. If there isn't a
straight path to get to a control, consider relocating it.
- **Figure 9.** Control focusability.
+ **Figure 9.** Control focusability.
Do.
Place controls, like the search action shown here, in locations that don't overlap
with other clickable elements.
- **Figure 10.** Control focusability.
+ **Figure 10.** Control focusability.
Don't.
Avoid layouts that contain controls in hard-to-reach places. Reaching
-the search action shown here is not easy to manage with the D-pad.
+the search action shown here might be difficult with the D-pad.
### Axes
Design your layout to take advantage of both horizontal and vertical axes.
Give each direction a specific function, making it fast to navigate large
hierarchies.
- **Figure 11.** Traversal.
+ **Figure 11.** Traversal.
Do.
Categories can be traversed on the vertical axis, and items within each
category can be browsed on the horizontal axis.
- **Figure 12.** Traversal.
+ **Figure 12.** Traversal.
Don't.
diff --git a/tv/leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/index.md b/tv/leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/index.md
index c665e0b..0844d91 100644
--- a/tv/leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/index.md
+++ b/tv/leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/index.md
@@ -1,23 +1,22 @@
[Video](https://www.youtube.com/watch?v=_X4tswgV67Y)
-Compose for TV is the modern approach for building Android TV
-user interfaces. Compose for TV unlocks all the benefits of Android's Jetpack Compose for
-your TV apps, making building beautiful and functional UIs for your app much
-easier.
+Compose for TV is the modern approach for building Android TV user
+interfaces. Compose for TV unlocks all the benefits of Android's
+Jetpack Compose for your TV apps, making building beautiful and functional UIs
+for your app much easier.
-Some specific benefits of using Compose for TV include the following:
+Some specific benefits of using Compose for TV include the
+following:
-- **Flexibility**: Compose can be used to create any type of UI, from simple layouts to complex animations. Components work out of the box but can also be customized and styled to fit your app's needs.
+- **Flexibility**: Compose can be used to create any type of UI, from basic layouts to complex animations. Components work out of the box but can also be customized and styled to fit your app's needs.
- **Simplified \& Accelerated Development**: Compose is compatible with existing code and enables developers to more efficiently build apps with less code.
- **Intuitive**: Compose uses a declarative syntax that lets you to make changes to your UI, debug, understand and review your code.
If you are unfamiliar with using the Jetpack Compose toolkit, check out the
-[Compose pathway](https://developer.android.com/courses/pathways/compose). Many
-of the development principles for mobile Compose apply to TV as well. See [Why
-Compose](https://developer.android.com/jetpack/compose/why-adopt) for more
-information about the general advantages of a declarative UI framework. To learn
-more, also see [the Compose for
-TV samples repository on GitHub](https://github.com/android/tv-samples/).
+[Compose pathway](https://developer.android.com/courses/pathways/compose). Many of the development principles for mobile Compose
+apply to TV as well. See [Why Compose](https://developer.android.com/jetpack/compose/why-adopt) for more information about the general
+advantages of a declarative UI framework. To learn more, also see [the Compose
+for TV samples repository on GitHub](https://github.com/android/tv-samples/).
@@ -35,7 +34,8 @@ This prompt asks for guidance around adding Android TV support to your app using
- `Use Jetpack Compose for TV as part of the response instead of Leanback.`
+ `Use Jetpack Compose for TV as part of the response instead of
+ Leanback.`
### Using AI prompts
@@ -48,10 +48,9 @@ Learn more about Gemini in Studio here: [https://developer.android.com/studio/ge
## Compatibility
-Compose for TV works on Android TVs with Android 5.0 (API level 21) or higher.
-Using version 1.0 of Compose for TV requires version 1.3.0 of
-[androidx.compose](https://developer.android.com/jetpack/androidx/releases/compose) libraries
-and Kotlin 1.7.10.
+Compose for TV works on Android TVs with Android 5.0 (API level 21)
+or higher. Using version 1.0 of Compose for TV requires version
+1.3.0 of [androidx.compose](https://developer.android.com/jetpack/androidx/releases/compose) libraries and Kotlin 1.7.10.
## Setup
@@ -59,51 +58,45 @@ Using Jetpack Compose on Android TV is similar to using Jetpack Compose for any
other Android project. The main difference is that Compose for TV
adds libraries that offer TV-optimized components and make it easier to create
user interfaces tailored to TV. In some cases those components share the same
-name as their non-TV counterparts, such as
-[`androidx.tv.material3.Button`](https://developer.android.com/reference/kotlin/androidx/tv/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.tv.material3.ButtonScale,androidx.tv.material3.ButtonGlow,androidx.tv.material3.ButtonShape,androidx.tv.material3.ButtonColors,androidx.compose.ui.unit.Dp,androidx.tv.material3.ButtonBorder,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1))
-and
-[`androidx.compose.material3.Button`](https://developer.android.com/reference/kotlin/androidx/compose/material3/Button.composable#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.compose.material3.ButtonColors,androidx.compose.material3.ButtonElevation,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)).
+name as their non-TV counterparts, such as [`androidx.tv.material3.Button`](https://developer.android.com/reference/kotlin/androidx/tv/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.tv.material3.ButtonScale,androidx.tv.material3.ButtonGlow,androidx.tv.material3.ButtonShape,androidx.tv.material3.ButtonColors,androidx.compose.ui.unit.Dp,androidx.tv.material3.ButtonBorder,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1))
+and [`androidx.compose.material3.Button`](https://developer.android.com/reference/kotlin/androidx/compose/material3/Button.composable#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.compose.material3.ButtonColors,androidx.compose.material3.ButtonElevation,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)).
## Jetpack Compose toolkit dependencies
-To use Compose for TV, include Jetpack Compose toolkit
-dependencies in your app's `build.gradle` file as follows:
+To use Compose for TV, include Jetpack Compose toolkit dependencies
+in your app's `build.gradle` file as follows:
-### Kotlin
+### Kotlin (build.gradle.kts)
-```kotlin
-dependencies {
- val composeBom = platform("androidx.compose:compose-bom:2026.08.00")
- implementation(composeBom)
+ dependencies {
+ val composeBom = platform("androidx.compose:compose-bom:2026.08.00")
+ implementation(composeBom)
- // General compose dependencies.
- implementation("androidx.activity:activity-compose:1.13.0")
+ // General compose dependencies.
+ implementation("androidx.activity:activity-compose:1.13.0")
- implementation("androidx.compose.ui:ui-tooling-preview")
- debugImplementation("androidx.compose.ui:ui-tooling")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
- // Compose for TV dependencies.
- implementation("androidx.tv:tv-material:1.0.0")
-}
-```
+ // Compose for TV dependencies.
+ implementation("androidx.tv:tv-material:1.0.0")
+ }
-### Groovy
+### Groovy (build.gradle)
-```groovy
-dependencies {
- def composeBom = platform('androidx.compose:compose-bom:2026.08.00')
- implementation composeBom
+ dependencies {
+ def composeBom = platform('androidx.compose:compose-bom:2026.08.00')
+ implementation composeBom
- // General compose dependencies.
- implementation 'androidx.activity:activity-compose:1.13.0'
+ // General compose dependencies.
+ implementation 'androidx.activity:activity-compose:1.13.0'
- implementation 'androidx.compose.ui:ui-tooling-preview'
- debugImplementation 'androidx.compose.ui:ui-tooling'
+ implementation 'androidx.compose.ui:ui-tooling-preview'
+ debugImplementation 'androidx.compose.ui:ui-tooling'
- // Compose for TV dependencies.
- implementation 'androidx.tv:tv-material:1.0.0'
-}
-```
+ // Compose for TV dependencies.
+ implementation 'androidx.tv:tv-material:1.0.0'
+ }
## What's different
diff --git a/wear/wear-compose-m3/SKILL.md b/wear/wear-compose-m3/SKILL.md
index 69449f4..1a71c4d 100644
--- a/wear/wear-compose-m3/SKILL.md
+++ b/wear/wear-compose-m3/SKILL.md
@@ -9,7 +9,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-22'
+ last-updated: '2026-09-03'
keywords:
- Wear OS
- Compose
@@ -32,7 +32,6 @@ metadata:
- If Kotlin version is **2.0.0+** , the project must use the `org.jetbrains.kotlin.plugin.compose` Gradle plugin.
- If Kotlin version is **\< 2.0.0** , the project must use `kotlinCompilerExtensionVersion` in `composeOptions`, matching the [Compose to Kotlin Compatibility Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin).
6. **Min SDK:** Ensure `minSdk` is at least **25**.
-7. **Sample extraction mandate**: Wear Compose libraries ship with an additional JAR file which contains individual samples for each and every component. You mustn't propose code changes, other than previews or basic changes such as color changes, until the samples in Capability 3 are extracted to the local cache. Library source files are incomplete and NOT a substitute for these samples; bypassing extraction is an environment setup failure.
## Gotchas
@@ -83,7 +82,7 @@ from the table to ensure you know how to correctly use it.
| `AppScaffold` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
| `ArcProgressIndicator`, `ArcProgressIndicatorDefaults`, `CircularProgressIndicator`, `CircularProgressIndicatorDefaults`, `ProgressIndicatorDefaults`, `SegmentedCircularProgressIndicator`, `drawCircularProgressIndicator` | [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt) |
| `Button` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [AnimatedTextSample](references/material3/AnimatedTextSample.kt.md.txt), [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [FadingExpandingLabelSample](references/material3/FadingExpandingLabelSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [PickerSample](references/material3/PickerSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [StepperSample](references/material3/StepperSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](references/material3/SwipeToRevealSample.kt.md.txt), [TimePickerSample](references/material3/TimePickerSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
-| `ButtonDefaults` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
+| `ButtonDefaults` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
| `ButtonGroup` | [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) |
| `Card` | [CardSample](references/material3/CardSample.kt.md.txt), [SwipeToRevealSample](references/material3/SwipeToRevealSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
| `CardDefaults` | [CardSample](references/material3/CardSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](references/material3/SwipeToRevealSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
@@ -99,8 +98,6 @@ from the table to ensure you know how to correctly use it.
| `FadingExpandingLabel` | [FadingExpandingLabelSample](references/material3/FadingExpandingLabelSample.kt.md.txt) |
| `FilledIconButton`, `FilledTonalIconButton`, `IconButtonColors`, `IconButtonShapes`, `OutlinedIconButton` | [IconButtonSample](references/material3/IconButtonSample.kt.md.txt) |
| `FilledTonalButton` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) |
-| `GestureAction`, `OneHandedGestureClickIndicator`, `OneHandedGestureClickIndicatorState`, `oneHandedGesture`, `rememberOneHandedGestureConfiguration` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) |
-| `GesturePriority`, `LocalOneHandedGestureEnabled`, `OneHandedGestureDefaults`, `OneHandedGestureHorizontalPageIndicator`, `OneHandedGesturePageIndicatorState`, `OneHandedGestureScrollIndicator`, `OneHandedGestureScrollIndicatorState`, `OneHandedGestureVerticalPageIndicator` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) |
| `HeadphoneIcon`, `Stepper`, `StepperLevelIndicator`, `rangeSemantics` | [StepperSample](references/material3/StepperSample.kt.md.txt) |
| `HorizontalPageIndicator`, `VerticalPageIndicator` | [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt) |
| `Icon` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [IconButtonSample](references/material3/IconButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt), [RadioButtonSample](references/material3/RadioButtonSample.kt.md.txt), [SwipeToRevealSample](references/material3/SwipeToRevealSample.kt.md.txt), [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt), [TimePickerSample](references/material3/TimePickerSample.kt.md.txt) |
@@ -112,7 +109,9 @@ from the table to ensure you know how to correctly use it.
| `ListHeader` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
| `ListHeaderDefaults` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
| `ListSubHeader` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt) |
+| `LocalOneHandedGestureEnabled`, `OneHandedGestureDefaults`, `OneHandedGestureHorizontalPageIndicator`, `OneHandedGesturePageIndicatorState`, `OneHandedGesturePriority`, `OneHandedGestureScrollIndicator`, `OneHandedGestureScrollIndicatorState`, `OneHandedGestureVerticalPageIndicator` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) |
| `MaterialTheme` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [LinearProgressIndicatorSample](references/material3/LinearProgressIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt) |
+| `OneHandedGestureAction`, `OneHandedGestureClickIndicator`, `OneHandedGestureClickIndicatorState`, `oneHandedGesture`, `rememberOneHandedGestureConfiguration` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) |
| `OpenOnPhoneDialog`, `OpenOnPhoneDialogDefaults`, `openOnPhoneDialogCurvedText` | [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt) |
| `OutlinedCard` | [CardSample](references/material3/CardSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) |
| `PagerScaffoldDefaults` | [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) |
@@ -151,7 +150,8 @@ from the table to ensure you know how to correctly use it.
| Component / Symbol | Reference Samples |
|---|---|
-| `AmbientMode`, `AmbientTickEffect`, `LocalAmbientModeManager`, `rememberAmbientModeManager` | [AmbientModeSample](references/foundation/AmbientModeSample.kt.md.txt) |
+| `AmbientMode`, `LocalAmbientModeManager`, `rememberAmbientModeManager` | [AmbientModeSample](references/foundation/AmbientModeSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) |
+| `AmbientTickEffect` | [AmbientModeSample](references/foundation/AmbientModeSample.kt.md.txt) |
| `AutoCenteringParams`, `ScalingLazyColumnDefaults`, `ScalingLazyListAnchorType` | [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt) |
| `BasicSwipeToDismissBox` | [SwipeToDismissBoxSample](references/foundation/SwipeToDismissBoxSample.kt.md.txt) |
| `CurvedAlignment`, `CurvedTextStyle`, `angularGradientBackground`, `angularSize`, `basicCurvedText`, `clearAndSetSemantics`, `curvedColumn`, `padding`, `radialGradientBackground`, `radialSize`, `semantics`, `size` | [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt) |
diff --git a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md
index 29d2824..2771e0a 100644
--- a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md
+++ b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md
@@ -27,19 +27,19 @@ M3 has a separate package and version to M2.5:
### M3
- implementation("androidx.wear.compose:compose-material3:1.7.0-beta01")
+ implementation("androidx.wear.compose:compose-material3:1.7.0-beta02")
See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3).
-Wear Compose Foundation library version 1.7.0-beta01 introduced
+Wear Compose Foundation library version 1.7.0-beta02 introduced
some new components that are designed to work with Material 3 components.
Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an
updated animation when running on Wear OS 6 (API level 36) or higher. When
updating to Wear Compose Material 3 version, we suggest to also update the Wear
Compose Foundation and Navigation libraries:
- implementation("androidx.wear.compose:compose-foundation:1.7.0-beta01")
- implementation("androidx.wear.compose:compose-navigation:1.7.0-beta01")
+ implementation("androidx.wear.compose:compose-foundation:1.7.0-beta02")
+ implementation("androidx.wear.compose:compose-navigation:1.7.0-beta02")
## Theme
@@ -329,7 +329,7 @@ Here is a full list of all the Material 3 components:
And finally a list of some relevant components from Wear Compose Foundation
library:
-| Wear Compose Foundation 1.7.0-beta01 | |
+| Wear Compose Foundation 1.7.0-beta02 | |
|---|---|
| [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. |
| [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. |
@@ -413,7 +413,7 @@ Scaffold in M3 is different from M2.5. In M3, `AppScaffold` and the new
the `ScrollIndicator` and `TimeText` components.
`AppScaffold` allows static screen elements such as `TimeText` to remain visible
-during in-app transitions such as swipe-to-dismiss. It provides a slot for the
+during in-app transitions such as swipe-to-dismiss. It provides a slot for the
main application content, which will usually be supplied by a navigation
component such as `SwipeDismissableNavHost`
diff --git a/wear/wear-compose-m3/references/foundation/AmbientModeSample.kt.md b/wear/wear-compose-m3/references/foundation/AmbientModeSample.kt.md
new file mode 100644
index 0000000..af0ce41
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/AmbientModeSample.kt.md
@@ -0,0 +1,123 @@
+```
+/*
+ * 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.wear.compose.foundation.AmbientMode
+import androidx.wear.compose.foundation.AmbientTickEffect
+import androidx.wear.compose.foundation.LocalAmbientModeManager
+import androidx.wear.compose.foundation.rememberAmbientModeManager
+import androidx.wear.compose.material.Text
+import kotlinx.coroutines.delay
+
+@Sampled
+@Composable
+fun AmbientModeBasicSample() {
+ // **Best Practice Note:** In a production application, the AmbientModeManager should be
+ // instantiated and provided at the highest level of the Compose hierarchy (typically in
+ // the host Activity's setContent block) using a CompositionLocalProvider. This ensures
+ // proper lifecycle management and broad accessibility.
+
+ // For this self-contained demo, AmbientModeManager is created and provided locally:
+ val activityAmbientModeManager = rememberAmbientModeManager()
+ CompositionLocalProvider(LocalAmbientModeManager provides activityAmbientModeManager) {
+ val ambientModeManager = LocalAmbientModeManager.current
+ val ambientMode = ambientModeManager?.currentAmbientMode
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ val ambientModeName =
+ when (ambientMode) {
+ is AmbientMode.Interactive -> "Interactive"
+ is AmbientMode.Ambient -> "Ambient"
+ else -> "Unknown"
+ }
+
+ val color = if (ambientMode is AmbientMode.Ambient) Color.Gray else Color.Yellow
+ Text(text = "$ambientModeName Mode", color = color)
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun AmbientModeWithAmbientTickSample() {
+ // **Best Practice Note:** In a production application, the AmbientModeManager should be
+ // instantiated and provided at the highest level of the Compose hierarchy (typically in
+ // the host Activity's setContent block) using a CompositionLocalProvider. This ensures
+ // proper lifecycle management and broad accessibility.
+
+ // For this self-contained demo, AmbientModeManager is created and provided locally:
+ val activityAmbientModeManager = rememberAmbientModeManager()
+ CompositionLocalProvider(LocalAmbientModeManager provides activityAmbientModeManager) {
+ var counter by remember { mutableIntStateOf(0) }
+
+ val ambientModeManager = LocalAmbientModeManager.current
+ ambientModeManager?.AmbientTickEffect {
+ // While device is in ambient mode, update counter in onAmbientTick approx. every minute
+ counter++
+ }
+
+ val ambientMode = ambientModeManager?.currentAmbientMode
+ if (ambientMode is AmbientMode.Interactive) {
+ // While device is not in ambient mode, update counter approx. every second
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(1000L)
+ counter++
+ }
+ }
+ }
+
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ val ambientModeName =
+ when (ambientMode) {
+ is AmbientMode.Interactive -> "Interactive"
+ is AmbientMode.Ambient -> "Ambient"
+ else -> "Unknown"
+ }
+
+ val updateInterval = if (ambientMode is AmbientMode.Ambient) "minute" else "second"
+ val color = if (ambientMode is AmbientMode.Ambient) Color.Gray else Color.Yellow
+
+ Text(text = "$ambientModeName Mode", color = color)
+ Text(text = "Updates every $updateInterval")
+ Text(text = "$counter")
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md b/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md
new file mode 100644
index 0000000..efe4b1e
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md
@@ -0,0 +1,430 @@
+```
+/*
+ * Copyright 2021 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.traversalIndex
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.CurvedAlignment
+import androidx.wear.compose.foundation.CurvedDirection
+import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.angularGradientBackground
+import androidx.wear.compose.foundation.angularSize
+import androidx.wear.compose.foundation.angularSizeDp
+import androidx.wear.compose.foundation.background
+import androidx.wear.compose.foundation.basicCurvedText
+import androidx.wear.compose.foundation.clearAndSetSemantics
+import androidx.wear.compose.foundation.curvedBox
+import androidx.wear.compose.foundation.curvedColumn
+import androidx.wear.compose.foundation.curvedComposable
+import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.foundation.padding
+import androidx.wear.compose.foundation.radialGradientBackground
+import androidx.wear.compose.foundation.radialSize
+import androidx.wear.compose.foundation.semantics
+import androidx.wear.compose.foundation.size
+import androidx.wear.compose.foundation.weight
+import androidx.wear.compose.material.Text
+
+@Sampled
+@Composable
+fun SimpleCurvedWorld() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ curvedComposable {
+ BasicText(
+ "Simple",
+ Modifier.background(Color.White).padding(2.dp),
+ TextStyle(color = Color.Black, fontSize = 16.sp),
+ )
+ }
+ curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Gray)) }
+ curvedComposable {
+ BasicText(
+ "CurvedWorld",
+ Modifier.background(Color.White).padding(2.dp),
+ TextStyle(color = Color.Black, fontSize = 16.sp),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedRowAndColumn() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Red)) }
+ curvedColumn(angularAlignment = CurvedAlignment.Angular.End) {
+ repeat(3) {
+ curvedRow {
+ curvedComposable {
+ BasicText(
+ "Row #$it",
+ Modifier.background(Color.White).padding(2.dp),
+ TextStyle(color = Color.Black, fontSize = 14.sp),
+ )
+ }
+ curvedComposable {
+ Box(modifier = Modifier.size(10.dp).background(Color.Green))
+ }
+ curvedComposable {
+ BasicText(
+ "More",
+ Modifier.background(Color.Yellow).padding(2.dp),
+ TextStyle(color = Color.Black, fontSize = 14.sp),
+ )
+ }
+ }
+ }
+ }
+ curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Red)) }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedAndNormalText() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ basicCurvedText(
+ "Curved Text",
+ CurvedModifier.padding(10.dp),
+ style = {
+ CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White)
+ },
+ )
+ curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Gray)) }
+ curvedComposable {
+ BasicText(
+ "Normal Text",
+ Modifier.padding(5.dp),
+ TextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedFixedSize() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ basicCurvedText(
+ "45 deg",
+ style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) },
+ modifier =
+ CurvedModifier.background(Color.White).size(sweepDegrees = 45f, thickness = 40.dp),
+ )
+ basicCurvedText(
+ "40 dp",
+ style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) },
+ modifier =
+ CurvedModifier.background(Color.Yellow).radialSize(40.dp).angularSizeDp(40.dp),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedBackground() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ basicCurvedText(
+ "Radial",
+ style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) },
+ modifier =
+ CurvedModifier.radialGradientBackground(0f to Color.White, 1f to Color.Black)
+ .padding(5.dp),
+ )
+ basicCurvedText(
+ "Angular",
+ style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) },
+ modifier =
+ CurvedModifier.angularGradientBackground(0f to Color.White, 1f to Color.Black)
+ .padding(5.dp),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedWeight() {
+ CurvedLayout(modifier = Modifier.fillMaxSize().background(Color.White)) {
+ // Evenly spread A, B & C in a 90 degree angle.
+ curvedRow(modifier = CurvedModifier.angularSize(90f)) {
+ basicCurvedText("A")
+ curvedRow(modifier = CurvedModifier.weight(1f)) {}
+ basicCurvedText("B")
+ curvedRow(modifier = CurvedModifier.weight(1f)) {}
+ basicCurvedText("C")
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedBottomLayout() {
+ CurvedLayout(
+ modifier = Modifier.fillMaxSize(),
+ anchor = 90f,
+ angularDirection = CurvedDirection.Angular.Reversed,
+ ) {
+ basicCurvedText(
+ "Bottom",
+ style = {
+ CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White)
+ },
+ )
+ curvedComposable { Spacer(modifier = Modifier.size(5.dp)) }
+ basicCurvedText(
+ "text",
+ style = {
+ CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White)
+ },
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedBoxSample() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ curvedBox(
+ modifier = CurvedModifier.background(Color.Red),
+ radialAlignment = CurvedAlignment.Radial.Inner,
+ angularAlignment = CurvedAlignment.Angular.End,
+ ) {
+ curvedComposable {
+ Box(modifier = Modifier.width(40.dp).height(80.dp).background(Color.Green))
+ }
+ curvedComposable {
+ Box(modifier = Modifier.size(30.dp).clip(CircleShape).background(Color.White))
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedLetterSpacingSample() {
+ val style =
+ CurvedTextStyle(
+ letterSpacing = 0.6.sp,
+ letterSpacingCounterClockwise = 1.4.sp,
+ color = Color.White,
+ )
+ Box {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ basicCurvedText("Clockwise", style = style)
+ }
+ CurvedLayout(
+ modifier = Modifier.fillMaxSize(),
+ angularDirection = CurvedDirection.Angular.CounterClockwise,
+ anchor = 90f,
+ ) {
+ basicCurvedText("Counter Clockwise", style = style)
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedWarpingSample() {
+ val style =
+ CurvedTextStyle(
+ letterSpacing = 0.6.sp,
+ letterSpacingCounterClockwise = 1.4.sp,
+ color = Color.White,
+ )
+ Box {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ basicCurvedText(
+ "No Warping",
+ style = style.copy(warpOffset = CurvedTextStyle.WarpOffset.None),
+ )
+ }
+ CurvedLayout(
+ modifier = Modifier.fillMaxSize(),
+ angularDirection = CurvedDirection.Angular.CounterClockwise,
+ anchor = 90f,
+ ) {
+ basicCurvedText(
+ "Standard Warping",
+ style.copy(warpOffset = CurvedTextStyle.WarpOffset.HalfOpticalHeight),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedSemanticsSample() {
+ val style =
+ CurvedTextStyle(
+ letterSpacing = 0.6.sp,
+ letterSpacingCounterClockwise = 1.4.sp,
+ color = Color.White,
+ )
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ basicCurvedText(
+ "2 - Left",
+ style,
+ CurvedModifier.semantics {
+ contentDescription = "Left"
+ traversalIndex = 2f
+ },
+ )
+ curvedComposable { Box(Modifier.padding(5.dp).background(Color.Red).size(5.dp)) }
+ basicCurvedText(
+ "3 - Right",
+ style,
+ CurvedModifier.semantics {
+ contentDescription = "Right"
+ traversalIndex = 3f
+ },
+ )
+ }
+ Row {
+ Text("Text 1", Modifier.semantics { traversalIndex = 1f })
+ Spacer(Modifier.size(10.dp))
+ Text("Text 4", Modifier.semantics { traversalIndex = 4f })
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedClearSemanticsSample() {
+ val style =
+ CurvedTextStyle(
+ letterSpacing = 0.6.sp,
+ letterSpacingCounterClockwise = 1.4.sp,
+ color = Color.White,
+ )
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ basicCurvedText("This is not announced", style, CurvedModifier.clearAndSetSemantics {})
+ }
+ Row { Text("This is announced", Modifier.semantics { traversalIndex = -1f }) }
+ }
+}
+
+@Composable
+fun CurvedFontWeight() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ (100..900 step 100).forEach {
+ basicCurvedText(
+ "W$it",
+ style = CurvedTextStyle(color = Color.White, fontWeight = FontWeight(it)),
+ modifier = CurvedModifier.padding(5.dp),
+ )
+ }
+ }
+}
+
+@Composable
+fun CurvedFontHeight() {
+ Box(
+ modifier =
+ Modifier.aspectRatio(1f)
+ .fillMaxSize()
+ .padding(2.dp)
+ .border(2.dp, Color.White, CircleShape)
+ ) {
+ CurvedLayout() {
+ basicCurvedText("9⎪:⎪0", style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp))
+ }
+ CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.CounterClockwise) {
+ basicCurvedText("9⎪:⎪0", style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp))
+ }
+ }
+}
+
+@Composable
+fun CurvedFonts() {
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ listOf(
+ "Serif" to FontFamily.Serif,
+ "SansSerif" to FontFamily.SansSerif,
+ "Monospace" to FontFamily.Monospace,
+ "Cursive" to FontFamily.Cursive,
+ )
+ .forEach { (name, ff) ->
+ basicCurvedText(
+ name,
+ style = CurvedTextStyle(color = Color.White, fontFamily = ff),
+ modifier = CurvedModifier.padding(5.dp),
+ )
+ }
+ }
+}
+
+@Composable
+fun OversizeComposable() {
+ val modBase = CurvedModifier.size(sweepDegrees = 30f, thickness = 20.dp)
+ CurvedLayout(modifier = Modifier.fillMaxSize()) {
+ curvedComposable(modifier = modBase.background(Color.Red)) {}
+ curvedComposable(modifier = modBase.background(Color.Green)) {
+ Box(Modifier.size(80.dp, 30.dp).background(Color.White))
+ }
+ curvedComposable(modifier = modBase.background(Color.Blue)) {}
+ }
+ CurvedLayout(modifier = Modifier.fillMaxSize(), anchor = 90f) {
+ curvedComposable(modifier = CurvedModifier.background(Color.Green)) {
+ Box(Modifier.size(80.dp, 30.dp).background(Color.White))
+ }
+ }
+}
+
+@Composable
+fun CurvedLineHeight() {
+ val baseStyle = CurvedTextStyle(color = Color.White, background = Color.Gray, fontSize = 16.sp)
+ CurvedLayout(
+ modifier = Modifier.fillMaxSize(),
+ radialAlignment = CurvedAlignment.Radial.Center,
+ ) {
+ basicCurvedText("Line Height 10.sp", style = baseStyle.copy(lineHeight = 10.sp))
+ curvedBox(CurvedModifier.angularSizeDp(1.dp)) {}
+ basicCurvedText("Base", style = baseStyle)
+ curvedBox(CurvedModifier.angularSizeDp(1.dp)) {}
+ basicCurvedText("Line Height 24.sp", style = baseStyle.copy(lineHeight = 24.sp))
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt
index e19960b..efe4b1e 100644
--- a/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt
+++ b/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt
@@ -388,7 +388,7 @@ fun CurvedFonts() {
)
.forEach { (name, ff) ->
basicCurvedText(
- "$name",
+ name,
style = CurvedTextStyle(color = Color.White, fontFamily = ff),
modifier = CurvedModifier.padding(5.dp),
)
diff --git a/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md b/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md
new file mode 100644
index 0000000..84fe733
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md
@@ -0,0 +1,99 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.expandableButton
+import androidx.wear.compose.foundation.expandableItem
+import androidx.wear.compose.foundation.expandableItems
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.rememberExpandableState
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.OutlinedCompactChip
+import androidx.wear.compose.material.Text
+
+@Sampled
+@Composable
+fun ExpandableWithItemsSample() {
+ val expandableState = rememberExpandableState()
+
+ val sampleItem: @Composable (String) -> Unit = { label ->
+ Chip(label = { Text(label) }, onClick = {}, secondaryLabel = { Text("line 2 - Secondary") })
+ }
+
+ val items = List(10) { "Item $it" }
+ val top = items.take(3)
+ val rest = items.drop(3)
+
+ ScalingLazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(top.size) { sampleItem(top[it]) }
+ expandableItems(expandableState, rest.size) { sampleItem(rest[it]) }
+ expandableButton(expandableState) {
+ OutlinedCompactChip(
+ label = {
+ Text("Show More")
+ Spacer(Modifier.size(6.dp))
+ Icon(painterResource(R.drawable.ic_expand_more_24), "Expand")
+ },
+ onClick = { expandableState.expanded = true },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun ExpandableTextSample() {
+ val expandableState = rememberExpandableState()
+
+ ScalingLazyColumn(modifier = Modifier.fillMaxSize()) {
+ expandableItem(expandableState) { expanded ->
+ Text(
+ "Account Alert: you have made a large purchase.\n" +
+ "We have noticed that a large purchase was charged to " +
+ "your credit card account. " +
+ "Please contact us if you did not perform this purchase. " +
+ "Our Customer Service team is available 24 hours a day, " +
+ "7 days a week to answer your account or product support question.",
+ maxLines = if (expanded) 20 else 3,
+ modifier = Modifier.padding(horizontal = 10.dp),
+ )
+ }
+
+ expandableButton(expandableState) {
+ OutlinedCompactChip(
+ label = {
+ Text("Show More")
+ Spacer(Modifier.size(6.dp))
+ Icon(painterResource(R.drawable.ic_expand_more_24), "Expand")
+ },
+ onClick = { expandableState.expanded = true },
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md b/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md
new file mode 100644
index 0000000..1df8a80
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md
@@ -0,0 +1,141 @@
+```
+/*
+ * Copyright 2022 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.hierarchicalFocusGroup
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.requestFocusOnHierarchyActive
+
+@Sampled
+@Composable
+fun HierarchicalFocusSample() {
+ var selected by remember { mutableIntStateOf(0) }
+
+ Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
+ repeat(5) { colIx ->
+ Box(
+ Modifier.hierarchicalFocusGroup(active = selected == colIx)
+ .weight(1f)
+ .clickable { selected = colIx }
+ .then(
+ if (selected == colIx) {
+ Modifier.border(BorderStroke(2.dp, Color.Red))
+ } else {
+ Modifier
+ }
+ )
+ ) {
+ // This is used a Gray background to the currently focused item, as seen by the
+ // focus system.
+ var focused by remember { mutableStateOf(false) }
+
+ BasicText(
+ "$colIx",
+ style =
+ TextStyle(
+ color = Color.White,
+ fontSize = 20.sp,
+ textAlign = TextAlign.Center,
+ ),
+ modifier =
+ Modifier.fillMaxWidth()
+ .requestFocusOnHierarchyActive()
+ .onFocusChanged { focused = it.isFocused }
+ .focusable()
+ .then(
+ if (focused) {
+ Modifier.background(Color.Gray)
+ } else {
+ Modifier
+ }
+ ),
+ )
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun HierarchicalFocus2Levels() {
+ Column(Modifier.fillMaxSize()) {
+ var selectedRow by remember { mutableIntStateOf(0) }
+ repeat(2) { rowIx ->
+ Row(
+ Modifier.weight(1f)
+ .fillMaxWidth()
+ .hierarchicalFocusGroup(active = selectedRow == rowIx)
+ ) {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ repeat(2) { itemIx ->
+ Box(
+ Modifier.weight(1f).hierarchicalFocusGroup(active = selectedItem == itemIx)
+ ) {
+ // ScalingLazyColumn uses requestFocusOnHierarchyActive internally
+ ScalingLazyColumn(
+ Modifier.fillMaxWidth().clickable {
+ selectedRow = rowIx
+ selectedItem = itemIx
+ }
+ ) {
+ val prefix = (rowIx * 2 + itemIx + 'A'.code).toChar()
+ items(20) {
+ BasicText(
+ "$prefix $it",
+ style =
+ TextStyle(
+ color = Color.White,
+ fontSize = 20.sp,
+ textAlign = TextAlign.Center,
+ ),
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md b/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md
new file mode 100644
index 0000000..bae406c
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md
@@ -0,0 +1,56 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.wear.compose.foundation.pager.HorizontalPager
+import androidx.wear.compose.foundation.pager.VerticalPager
+import androidx.wear.compose.foundation.pager.rememberPagerState
+
+@Sampled
+@Composable
+fun SimpleHorizontalPagerSample() {
+ // Creates a horizontal pager with 10 elements
+ val state = rememberPagerState { 10 }
+ HorizontalPager(modifier = Modifier.fillMaxSize(), state = state) { page ->
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ BasicText(text = "Page $page", style = TextStyle(color = Color.White))
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun SimpleVerticalPagerSample() {
+ // Creates a vertical pager with 10 elements
+ val state = rememberPagerState { 10 }
+ VerticalPager(modifier = Modifier.fillMaxSize(), state = state) { page ->
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ BasicText(text = "Page $page", style = TextStyle(color = Color.White))
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md b/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md
new file mode 100644
index 0000000..d15792b
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md
@@ -0,0 +1,153 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.overscroll
+import androidx.compose.foundation.rememberOverscrollEffect
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastSumBy
+import androidx.wear.compose.foundation.requestFocusOnHierarchyActive
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.foundation.rotary.RotarySnapLayoutInfoProvider
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
+import androidx.wear.compose.material.Text
+
+@Sampled
+@Composable
+fun RotaryScrollSample() {
+ val scrollableState = rememberLazyListState()
+ val focusRequester = remember { FocusRequester() }
+ LazyColumn(
+ modifier =
+ Modifier.fillMaxSize()
+ .requestFocusOnHierarchyActive()
+ .rotaryScrollable(
+ behavior = RotaryScrollableDefaults.behavior(scrollableState),
+ focusRequester = focusRequester,
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ state = scrollableState,
+ ) {
+ items(300) {
+ BasicText(
+ text = "item $it",
+ modifier = Modifier.background(Color.Gray),
+ style = TextStyle.Default.copy(),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun RotaryScrollWithOverscrollSample() {
+ val scrollableState = rememberScrollState()
+ val focusRequester = remember { FocusRequester() }
+ val overscrollEffect = rememberOverscrollEffect()
+
+ val screenHeightDp = LocalConfiguration.current.screenHeightDp.dp
+
+ Column(
+ Modifier.fillMaxSize()
+ .requestFocusOnHierarchyActive()
+ .rotaryScrollable(
+ behavior = RotaryScrollableDefaults.behavior(scrollableState),
+ focusRequester = focusRequester,
+ overscrollEffect = overscrollEffect,
+ )
+ .verticalScroll(scrollableState, overscrollEffect)
+ .overscroll(overscrollEffect),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text("Top")
+ Spacer(modifier = Modifier.height(screenHeightDp / 2))
+ Text("Scroll this list up and down with rotary input", textAlign = TextAlign.Center)
+ Spacer(modifier = Modifier.height(screenHeightDp / 2))
+ Text("Bottom")
+ }
+}
+
+@Sampled
+@Composable
+fun RotarySnapSample() {
+ val scrollableState = rememberLazyListState()
+ val focusRequester = remember { FocusRequester() }
+ LazyColumn(
+ modifier =
+ Modifier.fillMaxSize()
+ .requestFocusOnHierarchyActive()
+ .rotaryScrollable(
+ behavior =
+ RotaryScrollableDefaults.snapBehavior(
+ scrollableState,
+ // This sample has a custom implementation of
+ // RotarySnapLayoutInfoProvider which is required for snapping behavior.
+ // ScalingLazyColumn has it built-in, so it's not required there.
+ remember(scrollableState) {
+ object : RotarySnapLayoutInfoProvider {
+
+ override val averageItemSize: Float
+ get() {
+ val items = scrollableState.layoutInfo.visibleItemsInfo
+ return (items.fastSumBy { it.size } / items.size)
+ .toFloat()
+ }
+
+ override val currentItemIndex: Int
+ get() = scrollableState.firstVisibleItemIndex
+
+ override val currentItemOffset: Float
+ get() =
+ scrollableState.firstVisibleItemScrollOffset.toFloat()
+
+ override val totalItemCount: Int
+ get() = scrollableState.layoutInfo.totalItemsCount
+ }
+ },
+ ),
+ focusRequester = focusRequester,
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ state = scrollableState,
+ ) {
+ items(300) {
+ BasicText(text = "item $it", modifier = Modifier.background(Color.Gray).height(50.dp))
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md b/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md
new file mode 100644
index 0000000..8b1c765
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md
@@ -0,0 +1,131 @@
+```
+/*
+ * Copyright 2021 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.Text
+import kotlinx.coroutines.launch
+
+@Sampled
+@Composable
+fun SimpleScalingLazyColumn() {
+ ScalingLazyColumn(modifier = Modifier.fillMaxWidth()) {
+ item { ListHeader { Text(text = "List Header") } }
+ items(20) {
+ Chip(
+ onClick = {},
+ label = { Text("List item $it") },
+ colors = ChipDefaults.secondaryChipColors(),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun SimpleScalingLazyColumnWithSnap() {
+ val state = rememberScalingLazyListState()
+ ScalingLazyColumn(
+ rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(scrollableState = state),
+ flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state),
+ modifier = Modifier.fillMaxWidth(),
+ state = state,
+ ) {
+ item { ListHeader { Text(text = "List Header") } }
+ items(20) {
+ Chip(
+ onClick = {},
+ label = { Text("List item $it") },
+ colors = ChipDefaults.secondaryChipColors(),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo() {
+ val coroutineScope = rememberCoroutineScope()
+ val itemSpacing = 6.dp
+ // Line up the gap between the items on the center-line
+ val scrollOffset = with(LocalDensity.current) { -(itemSpacing / 2).roundToPx() }
+ val state =
+ rememberScalingLazyListState(
+ initialCenterItemIndex = 1,
+ initialCenterItemScrollOffset = scrollOffset,
+ )
+
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ anchorType = ScalingLazyListAnchorType.ItemStart,
+ verticalArrangement = Arrangement.spacedBy(itemSpacing),
+ state = state,
+ autoCentering = AutoCenteringParams(itemOffset = scrollOffset),
+ ) {
+ item { ListHeader { Text(text = "List Header") } }
+ items(20) {
+ Chip(
+ onClick = {
+ coroutineScope.launch {
+ // Add +1 to allow for the ListHeader
+ state.animateScrollToItem(it + 1, scrollOffset)
+ }
+ },
+ label = { Text("List item $it") },
+ colors = ChipDefaults.secondaryChipColors(),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun SimpleScalingLazyColumnWithContentPadding() {
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(top = 20.dp, bottom = 20.dp),
+ autoCentering = null,
+ ) {
+ item { ListHeader { Text(text = "List Header") } }
+ items(20) {
+ Chip(
+ onClick = {},
+ label = { Text("List item $it") },
+ colors = ChipDefaults.secondaryChipColors(),
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md b/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md
new file mode 100644
index 0000000..46ecc1b
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md
@@ -0,0 +1,168 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.BasicSwipeToDismissBox
+import androidx.wear.compose.foundation.SwipeToDismissValue
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.SplitToggleChip
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.ToggleChipDefaults
+
+@Sampled
+@Composable
+fun SimpleSwipeToDismissBox(navigateBack: () -> Unit) {
+ val state = rememberSwipeToDismissBoxState()
+ BasicSwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground ->
+ if (isBackground) {
+ Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.secondaryVariant))
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary)
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun StatefulSwipeToDismissBox() {
+ // State for managing a 2-level navigation hierarchy between
+ // MainScreen and ItemScreen composables.
+ // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation.
+ var showMainScreen by remember { mutableStateOf(true) }
+ val saveableStateHolder = rememberSaveableStateHolder()
+
+ // Swipe gesture dismisses ItemScreen to return to MainScreen.
+ val state = rememberSwipeToDismissBoxState()
+ LaunchedEffect(state.currentValue) {
+ if (state.currentValue == SwipeToDismissValue.Dismissed) {
+ state.snapTo(SwipeToDismissValue.Default)
+ showMainScreen = !showMainScreen
+ }
+ }
+
+ // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind
+ // the ItemScreen, otherwise there's no background to show.
+ BasicSwipeToDismissBox(
+ state = state,
+ userSwipeEnabled = !showMainScreen,
+ backgroundKey = if (!showMainScreen) "MainKey" else "Background",
+ contentKey = if (showMainScreen) "MainKey" else "ItemKey",
+ ) { isBackground ->
+ if (isBackground || showMainScreen) {
+ // Best practice would be to use State Hoisting and leave this composable stateless.
+ // Here, we want to support MainScreen being shown from different destinations
+ // (either in the foreground or in the background during swiping) - that can be achieved
+ // using SaveableStateHolder and rememberSaveable as shown below.
+ saveableStateHolder.SaveableStateProvider(
+ key = "MainKey",
+ content = {
+ // Composable that maintains its own state
+ // and can be shown in foreground or background.
+ val checked = rememberSaveable { mutableStateOf(true) }
+ Column(
+ modifier =
+ Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
+ ) {
+ SplitToggleChip(
+ checked = checked.value,
+ label = { Text("Item details") },
+ modifier = Modifier.height(40.dp),
+ onCheckedChange = { v -> checked.value = v },
+ onClick = { showMainScreen = false },
+ toggleControl = {
+ Icon(
+ imageVector =
+ ToggleChipDefaults.checkboxIcon(checked = checked.value),
+ contentDescription = null,
+ )
+ },
+ )
+ }
+ },
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text("Show details here...", color = MaterialTheme.colors.onPrimary)
+ Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary)
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun EdgeSwipeForSwipeToDismiss(navigateBack: () -> Unit) {
+ val state = rememberSwipeToDismissBoxState()
+
+ // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the
+ // modifier applies exists within a SwipeToDismissBox which shares the same state.
+ BasicSwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground ->
+ val horizontalScrollState = rememberScrollState(0)
+ if (isBackground) {
+ Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.secondaryVariant))
+ } else {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ modifier =
+ Modifier.align(Alignment.Center)
+ .edgeSwipeToDismiss(state)
+ .horizontalScroll(horizontalScrollState),
+ text =
+ "This text can be scrolled horizontally - to dismiss, swipe " +
+ "right from the left edge of the screen (called Edge Swiping)",
+ )
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md b/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md
new file mode 100644
index 0000000..aa0e085
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md
@@ -0,0 +1,282 @@
+```
+/*
+ * Copyright 2023 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.
+ */
+
+@file:OptIn(ExperimentalWearFoundationApi::class)
+@file:Suppress("DEPRECATION")
+
+package androidx.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
+import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeToReveal
+import androidx.wear.compose.foundation.expandableItem
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.rememberExpandableState
+import androidx.wear.compose.foundation.rememberRevealState
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.Text
+import kotlin.math.abs
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@Sampled
+@Composable
+fun SwipeToRevealSample() {
+ val state = rememberRevealState()
+ val coroutineScope = rememberCoroutineScope()
+ SwipeToReveal(
+ state = state,
+ primaryAction = {
+ Box(
+ modifier =
+ Modifier.fillMaxSize().clickable {
+ /* Add the primary action */
+ coroutineScope.launch { state.animateTo(RevealValue.RightRevealed) }
+ },
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Delete")
+ }
+ },
+ undoAction = {
+ Chip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ /* Add the undo action */
+ coroutineScope.launch { state.animateTo(RevealValue.Covered) }
+ },
+ colors = ChipDefaults.secondaryChipColors(),
+ label = { Text(text = "Undo") },
+ )
+ },
+ ) {
+ Chip(
+ modifier =
+ Modifier.fillMaxWidth().semantics {
+ // Use custom actions to make the primary and secondary actions accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ /* Add the primary action click handler */
+ true
+ }
+ )
+ },
+ onClick = { /* the click action associated with chip */ },
+ colors = ChipDefaults.secondaryChipColors(),
+ label = { Text(text = "Swipe Me") },
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun SwipeToRevealWithDelayedText() {
+ val state = rememberRevealState()
+ val coroutineScope = rememberCoroutineScope()
+ SwipeToReveal(
+ state = state,
+ primaryAction = {
+ Row(
+ modifier =
+ Modifier.fillMaxSize().clickable {
+ /* Add the primary action */
+ coroutineScope.launch { state.animateTo(RevealValue.RightRevealed) }
+ },
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Delete")
+ if (abs(state.offset) > state.revealThreshold) {
+ // Delay the text appearance so that it has enough space to be displayed
+ val textAlpha =
+ animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = tween(durationMillis = 250, delayMillis = 250),
+ label = "PrimaryActionTextAlpha",
+ )
+ Box(modifier = Modifier.graphicsLayer { alpha = textAlpha.value }) {
+ Spacer(Modifier.size(5.dp))
+ Text("Clear")
+ }
+ }
+ }
+ },
+ undoAction = {
+ Chip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ /* Add the undo action */
+ coroutineScope.launch { state.animateTo(RevealValue.Covered) }
+ },
+ colors = ChipDefaults.secondaryChipColors(),
+ label = { Text(text = "Undo") },
+ )
+ },
+ ) {
+ Chip(
+ modifier =
+ Modifier.fillMaxWidth().semantics {
+ // Use custom actions to make the primary and secondary actions accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ /* Add the primary action click handler */
+ true
+ }
+ )
+ },
+ onClick = { /* the click action associated with chip */ },
+ colors = ChipDefaults.secondaryChipColors(),
+ label = { Text(text = "Swipe Me") },
+ )
+ }
+}
+
+/**
+ * A sample on how to use Swipe To Reveal within a list of items, preferably [ScalingLazyColumn].
+ */
+@Sampled
+@Composable
+fun SwipeToRevealWithExpandables() {
+ // Shape of actions should match with the overlay content. For example, Chips
+ // should use RoundedCornerShape(CornerSize(percent = 50)), Cards should use
+ // RoundedCornerShape with appropriate radius, based on the theme.
+ val actionShape = RoundedCornerShape(corner = CornerSize(percent = 50))
+ val itemCount = 10
+ val coroutineScope = rememberCoroutineScope()
+ val expandableStates = List(itemCount) { rememberExpandableState(initiallyExpanded = true) }
+ ScalingLazyColumn(modifier = Modifier.fillMaxSize()) {
+ item { ListHeader { Text("Scaling Lazy Column") } }
+ repeat(itemCount) { current ->
+ expandableItem(state = expandableStates[current]) { isExpanded ->
+ val revealState = rememberRevealState()
+ if (isExpanded) {
+ SwipeToReveal(
+ state = revealState,
+ primaryAction = {
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ .background(Color.Red, actionShape)
+ .clickable {
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.RightRevealed)
+ }
+ },
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Delete,
+ contentDescription = "Delete",
+ )
+ }
+ },
+ secondaryAction = {
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ .background(Color.Gray, actionShape)
+ .clickable { /* trigger the optional action */ },
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.MoreVert,
+ contentDescription = "More Options",
+ )
+ }
+ },
+ undoAction = {
+ Chip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.Covered)
+ }
+ },
+ colors = ChipDefaults.secondaryChipColors(),
+ label = { Text(text = "Undo") },
+ )
+ },
+ onFullSwipe = {
+ coroutineScope.launch {
+ delay(1000)
+ expandableStates[current].expanded = false
+ }
+ },
+ ) {
+ Chip(
+ modifier =
+ Modifier.fillMaxWidth().semantics {
+ // Use custom actions to make the primary and secondary actions
+ // accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ /* Add the primary action click handler */
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.RightRevealed)
+ }
+ true
+ },
+ CustomAccessibilityAction("More Options") {
+ /* Add the secondary action click handler */
+ true
+ },
+ )
+ },
+ onClick = { /* the click action associated with chip */ },
+ colors = ChipDefaults.secondaryChipColors(),
+ label = { Text(text = "Swipe Me") },
+ )
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md b/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md
new file mode 100644
index 0000000..87e92be
--- /dev/null
+++ b/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md
@@ -0,0 +1,335 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider.ItemEdge
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.TitleCard
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+import kotlin.math.abs
+import kotlinx.coroutines.launch
+
+@Sampled
+@Preview
+@Composable
+fun SimpleTransformingLazyColumnSample() {
+ val transformationSpec = rememberTransformationSpec()
+ TransformingLazyColumn(contentPadding = PaddingValues(20.dp)) {
+ items(count = 10) { index ->
+ Button(
+ modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ ) {
+ Text(text = "Item $index")
+ }
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnWithSnapSample() {
+ val transformationSpec = rememberTransformationSpec()
+ val state = rememberTransformingLazyColumnState()
+ TransformingLazyColumn(
+ rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(scrollableState = state),
+ flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(state = state),
+ modifier = Modifier.fillMaxWidth(),
+ state = state,
+ contentPadding = PaddingValues(20.dp),
+ ) {
+ items(count = 20) { index ->
+ Button(
+ modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ ) {
+ Text(text = "Item $index")
+ }
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnAnimateItemSample() {
+ val state = rememberTransformingLazyColumnState()
+
+ var list by remember { mutableStateOf(listOf("1", "2", "3")) }
+
+ var next by remember { mutableIntStateOf(4) }
+
+ Box(Modifier.fillMaxSize()) {
+ TransformingLazyColumn(
+ state = state,
+ contentPadding = PaddingValues(5.dp),
+ modifier = Modifier.background(Color.Black).fillMaxSize(),
+ ) {
+ items(list.size, key = { list[it] }) {
+ Text(
+ "Item ${list[it]}",
+ Modifier.animateItem().clickable {
+ list = list.filter { elem -> elem != list[it] }
+ },
+ )
+ }
+ }
+ Text(
+ "+",
+ Modifier.align(Alignment.CenterStart).padding(horizontal = 5.dp).clickable {
+ if (list.size < 25) list = list + "${next++}"
+ },
+ )
+ Text(
+ "S",
+ Modifier.align(Alignment.CenterEnd).padding(horizontal = 5.dp).clickable {
+ list = list.shuffled()
+ },
+ )
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnScrollToItemSample() {
+ val state =
+ rememberTransformingLazyColumnState(
+ // Customize initial scroll position of the TransformingLazyColumn.
+ initialAnchorItemIndex = 10
+ )
+ val coroutineScope = rememberCoroutineScope()
+
+ TransformingLazyColumn(
+ modifier = Modifier.background(Color.Black),
+ state = state,
+ contentPadding = PaddingValues(vertical = 20.dp),
+ ) {
+ items(count = 20) {
+ Text(
+ "Item $it",
+ modifier =
+ Modifier.drawBehind {
+ val isCentered =
+ it == state.anchorItemIndex &&
+ abs(state.anchorItemScrollOffset) < size.height
+ drawRect(if (isCentered) Color.Green else Color.DarkGray)
+ }
+ .padding(5.dp)
+ .clickable { coroutineScope.launch { state.scrollToItem(it) } },
+ )
+ }
+
+ item {
+ Text(
+ "Scroll to top",
+ modifier =
+ Modifier.clickable { coroutineScope.launch { state.animateScrollToItem(0) } },
+ )
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ snapshotFlow { state.anchorItemIndex }.collect { println("Anchor item index: $it") }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnMinimumVerticalContentPaddingSample() {
+ val transformationSpec = rememberTransformationSpec()
+
+ TransformingLazyColumn(contentPadding = PaddingValues(horizontal = 20.dp)) {
+ items(count = 20) { index ->
+ Button(
+ modifier =
+ Modifier.fillMaxWidth()
+ .transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ ),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ ) {
+ Text(text = "Item $index")
+ }
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnFirstLayoutItemProviderSample() {
+ val state = rememberTransformingLazyColumnState()
+ val transformationSpec = rememberTransformationSpec()
+ var expandedItemIndex by remember { mutableIntStateOf(-1) }
+
+ // This sample demonstrates how to use the provider API to control the direction of content
+ // shifting. By default, TransformingLazyColumn uses the center item as the layout reference.
+ // This means that if an item above the center expands, it pushes content upwards;
+ // if below, it pushes downwards.
+ //
+ // Here, we fix the Bottom/End edge of the clicked item regardless of its position on screen,
+ // so that when its animated content appears, the card predictably expands *upwards* every time.
+ val upwardExpandingItemProvider =
+ remember(state) {
+ TransformingLazyColumnFirstLayoutItemProvider { centerItem ->
+ val item = expandedItemIndex
+
+ // Yield to the standard layout behavior during active scrolls.
+ // This avoids custom layout overhead and ensures the [TransformingLazyColumn]
+ // tracks the user's scroll gesture using its default center layout reference.
+ if (item == -1 || state.isScrollInProgress) {
+ return@TransformingLazyColumnFirstLayoutItemProvider centerItem
+ }
+
+ // Look up the item's offset from state.layoutInfo (which holds the details
+ // from the previous measure pass) to maintain its visual position in the current
+ // pass.
+ state.layoutInfo.visibleItems
+ .fastFirstOrNull { visibleItem -> visibleItem.index == item }
+ ?.let { visibleItem ->
+ TransformingLazyColumnFirstLayoutItemProvider.ItemInfo(
+ key = visibleItem.key,
+ index = visibleItem.index,
+ // Pin the bottom edge of the item
+ itemEdge = ItemEdge.End,
+ // Calculate the exact bottom offset from the previous pass
+ offset = visibleItem.offset + visibleItem.transformedHeight,
+ )
+ } ?: centerItem
+ }
+ }
+
+ TransformingLazyColumn(
+ state = state,
+ contentPadding = PaddingValues(horizontal = 20.dp),
+ firstLayoutItemProvider = upwardExpandingItemProvider,
+ ) {
+ items(count = 10, key = { it }) { cardIndex ->
+ val isExpanded = expandedItemIndex == cardIndex
+ TitleCard(
+ onClick = { expandedItemIndex = cardIndex },
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ )
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ title = { Text("Card $cardIndex") },
+ subtitle = {
+ AnimatedVisibility(isExpanded) { Text("Expanded content is available here") }
+ },
+ content = { Text("Tap to expand") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnPinnableContainerSample() {
+ val state = rememberTransformingLazyColumnState()
+ val transformationSpec = rememberTransformationSpec()
+
+ TransformingLazyColumn(state = state, contentPadding = PaddingValues(horizontal = 20.dp)) {
+ items(count = 20) { index ->
+ var isPinned by remember { mutableStateOf(false) }
+ var pinHandle by remember { mutableStateOf(null) }
+ val pinnableContainer = LocalPinnableContainer.current
+
+ // This state will be reset if the item is scrolled out and not pinned
+ var counter by remember { mutableIntStateOf(0) }
+
+ TitleCard(
+ onClick = { counter++ },
+ modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ title = { Text("Item $index") },
+ subtitle = { Text("Count: $counter (Tap card to +)") },
+ content = {
+ // This button toggles the pin state
+ Button(
+ onClick = {
+ if (isPinned) {
+ pinHandle?.release().also { pinHandle = null }
+ isPinned = false
+ } else {
+ pinHandle = pinnableContainer?.pin()
+ isPinned = true
+ }
+ },
+ colors =
+ if (isPinned) {
+ ButtonDefaults.buttonColors()
+ } else {
+ ButtonDefaults.filledTonalButtonColors()
+ },
+ ) {
+ Text(if (isPinned) "Unpin" else "Pin Off-screen")
+ }
+ },
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md b/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md
new file mode 100644
index 0000000..79036c1
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md
@@ -0,0 +1,377 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.AccountCircle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.AlertDialog
+import androidx.wear.compose.material3.AlertDialogDefaults
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.SwitchButton
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+
+@Sampled
+@Composable
+@Preview
+fun AlertDialogWithConfirmAndDismissSample() {
+ var showDialog by remember { mutableStateOf(false) }
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showDialog = true },
+ label = { Text("Show Dialog") },
+ )
+ }
+ AlertDialog(
+ visible = showDialog,
+ onDismissRequest = { showDialog = false },
+ icon = {
+ Icon(
+ Icons.Rounded.AccountCircle,
+ modifier = Modifier.size(32.dp),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ },
+ title = { Text("Enable Battery Saver Mode?") },
+ text = { Text("Your battery is low. Turn on battery saver.") },
+ confirmButton = {
+ AlertDialogDefaults.ConfirmButton(
+ onClick = {
+ // Perform confirm action here
+ showDialog = false
+ }
+ )
+ },
+ dismissButton = {
+ AlertDialogDefaults.DismissButton(
+ onClick = {
+ // Perform dismiss action here
+ showDialog = false
+ }
+ )
+ },
+ ) {
+ item { Text(text = "You can configure battery saver mode in the setting menu.") }
+ item { Button(onClick = {}) { Text(text = "Go to settings") } }
+ }
+}
+
+@Preview
+@Sampled
+@Composable
+fun AlertDialogWithConfirmAndDismissTransformingContentSample() {
+ var showDialog by remember { mutableStateOf(false) }
+ val transformationSpec = rememberTransformationSpec()
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showDialog = true },
+ label = { Text("Show Dialog") },
+ )
+ }
+ AlertDialog(
+ visible = showDialog,
+ onDismissRequest = { showDialog = false },
+ icon = {
+ Icon(
+ Icons.Rounded.AccountCircle,
+ modifier = Modifier.size(32.dp),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ },
+ title = { Text("Enable Battery Saver Mode?") },
+ text = { Text("Your battery is low. Turn on battery saver.") },
+ transformationSpec = transformationSpec,
+ confirmButton = {
+ AlertDialogDefaults.ConfirmButton(
+ onClick = {
+ // Perform confirm action here
+ showDialog = false
+ }
+ )
+ },
+ dismissButton = {
+ AlertDialogDefaults.DismissButton(
+ onClick = {
+ // Perform dismiss action here
+ showDialog = false
+ }
+ )
+ },
+ ) {
+ item {
+ Text(
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec).graphicsLayer {
+ with(SurfaceTransformation(transformationSpec)) {
+ applyContentTransformation()
+ applyContainerTransformation()
+ }
+ },
+ text = "You can configure battery saver mode in the setting menu.",
+ )
+ }
+ item {
+ Button(
+ modifier = Modifier.transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ ) {
+ Text(text = "Go to settings")
+ }
+ }
+ }
+}
+
+@Preview
+@Sampled
+@Composable
+fun AlertDialogWithEdgeButtonSample() {
+ var showDialog by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showDialog = true },
+ label = { Text("Show Dialog") },
+ )
+ }
+
+ AlertDialog(
+ visible = showDialog,
+ onDismissRequest = { showDialog = false },
+ icon = {
+ Icon(
+ Icons.Rounded.AccountCircle,
+ modifier = Modifier.size(32.dp),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ },
+ title = { Text("Mobile network is not currently available") },
+ text = { Text("Please try again later or check your network settings.") },
+ edgeButton = {
+ AlertDialogDefaults.EdgeButton(
+ onClick = {
+ // Perform confirm action here
+ showDialog = false
+ }
+ )
+ },
+ )
+}
+
+@Preview
+@Sampled
+@Composable
+fun AlertDialogWithEdgeButtonTransformingContentSample() {
+ var showDialog by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showDialog = true },
+ label = { Text("Show Dialog") },
+ )
+ }
+
+ AlertDialog(
+ visible = showDialog,
+ onDismissRequest = { showDialog = false },
+ icon = {
+ Icon(
+ Icons.Rounded.AccountCircle,
+ modifier = Modifier.size(32.dp),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ },
+ title = { Text("Mobile network is not currently available") },
+ text = { Text("Please try again later or check your network settings.") },
+ transformationSpec = rememberTransformationSpec(),
+ edgeButton = {
+ AlertDialogDefaults.EdgeButton(
+ onClick = {
+ // Perform confirm action here
+ showDialog = false
+ }
+ )
+ },
+ )
+}
+
+@Preview
+@Sampled
+@Composable
+fun AlertDialogWithContentGroupsSample() {
+ var showDialog by remember { mutableStateOf(false) }
+ var weatherEnabled by remember { mutableStateOf(false) }
+ var calendarEnabled by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showDialog = true },
+ label = { Text("Show Dialog") },
+ )
+ }
+ AlertDialog(
+ visible = showDialog,
+ onDismissRequest = { showDialog = false },
+ title = { Text("Share your location") },
+ text = { Text(" The following apps have asked you to share your location") },
+ edgeButton = {
+ AlertDialogDefaults.EdgeButton(
+ onClick = {
+ // Perform confirm action here
+ showDialog = false
+ }
+ ) {
+ Text("Share once")
+ }
+ },
+ ) {
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth(),
+ checked = weatherEnabled,
+ onCheckedChange = { weatherEnabled = it },
+ label = { Text("Weather") },
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth(),
+ checked = calendarEnabled,
+ onCheckedChange = { calendarEnabled = it },
+ label = { Text("Calendar") },
+ )
+ }
+ item { AlertDialogDefaults.GroupSeparator() }
+ item {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {},
+ label = { Text(modifier = Modifier.fillMaxWidth(), text = "Never share") },
+ )
+ }
+ item {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {},
+ label = { Text(modifier = Modifier.fillMaxWidth(), text = "Share always") },
+ )
+ }
+ }
+}
+
+@Preview
+@Sampled
+@Composable
+fun AlertDialogWithContentGroupsTransformingContentSample() {
+ var showDialog by remember { mutableStateOf(false) }
+ var weatherEnabled by remember { mutableStateOf(false) }
+ var calendarEnabled by remember { mutableStateOf(false) }
+ val transformationSpec = rememberTransformationSpec()
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showDialog = true },
+ label = { Text("Show Dialog") },
+ )
+ }
+ AlertDialog(
+ visible = showDialog,
+ onDismissRequest = { showDialog = false },
+ title = { Text("Share your location") },
+ text = { Text(" The following apps have asked you to share your location") },
+ transformationSpec = transformationSpec,
+ edgeButton = {
+ AlertDialogDefaults.EdgeButton(
+ onClick = {
+ // Perform confirm action here
+ showDialog = false
+ }
+ ) {
+ Text("Share once")
+ }
+ },
+ ) {
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
+ checked = weatherEnabled,
+ onCheckedChange = { weatherEnabled = it },
+ label = { Text("Weather") },
+ transformation = SurfaceTransformation(transformationSpec),
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
+ checked = calendarEnabled,
+ onCheckedChange = { calendarEnabled = it },
+ label = { Text("Calendar") },
+ transformation = SurfaceTransformation(transformationSpec),
+ )
+ }
+ item { AlertDialogDefaults.GroupSeparator() }
+ item {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
+ onClick = {},
+ label = { Text(modifier = Modifier.fillMaxWidth(), text = "Never share") },
+ transformation = SurfaceTransformation(transformationSpec),
+ )
+ }
+ item {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
+ onClick = {},
+ label = { Text(modifier = Modifier.fillMaxWidth(), text = "Share always") },
+ transformation = SurfaceTransformation(transformationSpec),
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md b/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md
new file mode 100644
index 0000000..4cd8d4a
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md
@@ -0,0 +1,165 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontVariation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.material3.AnimatedText
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.rememberAnimatedTextFontRegistry
+import kotlinx.coroutines.launch
+
+@Sampled
+@Composable
+fun AnimatedTextSample() {
+ val scope = rememberCoroutineScope()
+ val animatable = remember { Animatable(0f) }
+ val animate = {
+ scope.launch {
+ // Animate from 0 to 1 and then back to 0.
+ animatable.animateTo(1f)
+ animatable.animateTo(0f)
+ }
+ }
+ val animatedTextFontRegistry =
+ rememberAnimatedTextFontRegistry(
+ // Variation axes at the start of the animation, width 10, weight 200
+ startFontVariationSettings =
+ FontVariation.Settings(FontVariation.width(10f), FontVariation.weight(200)),
+ // Variation axes at the end of the animation, width 100, weight 500
+ endFontVariationSettings =
+ FontVariation.Settings(FontVariation.width(100f), FontVariation.weight(500)),
+ startFontSize = 30.sp,
+ endFontSize = 40.sp,
+ )
+ AnimatedText(
+ text = "Hello!",
+ fontRegistry = animatedTextFontRegistry,
+ // Content alignment anchors the animation at the vertical center, expanding horizontally
+ contentAlignment = Alignment.CenterStart,
+ progressFraction = { animatable.value },
+ modifier = Modifier.clickable(onClick = { animate() }),
+ )
+ LaunchedEffect(Unit) { animate() }
+}
+
+@Sampled
+@Composable
+fun AnimatedTextSampleButtonResponse() {
+ val scope = rememberCoroutineScope()
+ val animatedTextFontRegistry =
+ rememberAnimatedTextFontRegistry(
+ // Variation axes at the start of the animation, width 10, weight 200
+ startFontVariationSettings =
+ FontVariation.Settings(FontVariation.width(10f), FontVariation.weight(200)),
+ // Variation axes at the end of the animation, width 100, weight 500
+ endFontVariationSettings =
+ FontVariation.Settings(FontVariation.width(100f), FontVariation.weight(500)),
+ startFontSize = 30.sp,
+ endFontSize = 30.sp,
+ )
+ val number = remember { mutableIntStateOf(0) }
+ val textAnimatable = remember { Animatable(0f) }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Button(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ onClick = {
+ number.value -= 1
+ scope.launch {
+ textAnimatable.animateTo(1f)
+ textAnimatable.animateTo(0f)
+ }
+ },
+ label = {
+ Text(modifier = Modifier.semantics { contentDescription = "Decrease" }, text = "-")
+ },
+ )
+ AnimatedText(
+ text = "${number.value}",
+ fontRegistry = animatedTextFontRegistry,
+ progressFraction = { textAnimatable.value },
+ )
+ Button(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ onClick = {
+ number.value += 1
+ scope.launch {
+ textAnimatable.animateTo(1f)
+ textAnimatable.animateTo(0f)
+ }
+ },
+ label = {
+ Text(modifier = Modifier.semantics { contentDescription = "Increase" }, text = "+")
+ },
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun AnimatedTextSampleSharedFontRegistry() {
+ val animatedTextFontRegistry =
+ rememberAnimatedTextFontRegistry(
+ // Variation axes at the start of the animation, width 50, weight 300
+ startFontVariationSettings =
+ FontVariation.Settings(FontVariation.width(50f), FontVariation.weight(300)),
+ // Variation axes at the end of the animation are the same as the start axes
+ endFontVariationSettings =
+ FontVariation.Settings(FontVariation.width(50f), FontVariation.weight(300)),
+ startFontSize = 15.sp,
+ endFontSize = 25.sp,
+ )
+ val firstAnimatable = remember { Animatable(0f) }
+ val secondAnimatable = remember { Animatable(0f) }
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ AnimatedText(
+ text = "Top Text",
+ fontRegistry = animatedTextFontRegistry,
+ progressFraction = { firstAnimatable.value },
+ )
+ AnimatedText(
+ text = "Bottom Text",
+ fontRegistry = animatedTextFontRegistry,
+ progressFraction = { secondAnimatable.value },
+ )
+ }
+ LaunchedEffect(Unit) {
+ firstAnimatable.animateTo(1f)
+ firstAnimatable.animateTo(0f)
+ secondAnimatable.animateTo(1f)
+ secondAnimatable.animateTo(0f)
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md b/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md
new file mode 100644
index 0000000..b29826a
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md
@@ -0,0 +1,91 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonGroup
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Composable
+fun ButtonGroupSample() {
+ val interactionSource1 = remember { MutableInteractionSource() }
+ val interactionSource2 = remember { MutableInteractionSource() }
+
+ Box(contentAlignment = Alignment.Center) {
+ ButtonGroup(Modifier.fillMaxWidth()) {
+ Button(
+ onClick = {},
+ modifier = Modifier.animateWidth(interactionSource1),
+ interactionSource = interactionSource1,
+ ) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("L") }
+ }
+ Button(
+ onClick = {},
+ modifier = Modifier.animateWidth(interactionSource2),
+ interactionSource = interactionSource2,
+ ) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("R") }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun ButtonGroupThreeButtonsSample() {
+ val interactionSource1 = remember { MutableInteractionSource() }
+ val interactionSource2 = remember { MutableInteractionSource() }
+ val interactionSource3 = remember { MutableInteractionSource() }
+
+ Box(contentAlignment = Alignment.Center) {
+ ButtonGroup(Modifier.fillMaxWidth()) {
+ Button(
+ onClick = {},
+ modifier = Modifier.animateWidth(interactionSource1),
+ interactionSource = interactionSource1,
+ ) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("A") }
+ }
+ Button(
+ onClick = {},
+ modifier = Modifier.weight(1.5f).animateWidth(interactionSource2),
+ interactionSource = interactionSource2,
+ ) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("B") }
+ }
+ Button(
+ onClick = {},
+ modifier = Modifier.animateWidth(interactionSource3),
+ interactionSource = interactionSource3,
+ ) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("C") }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ButtonSample.kt.md b/wear/wear-compose-m3/references/material3/ButtonSample.kt.md
new file mode 100644
index 0000000..5a3c305
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ButtonSample.kt.md
@@ -0,0 +1,496 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.ChildButton
+import androidx.wear.compose.material3.CompactButton
+import androidx.wear.compose.material3.CompactButtonDefaults
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.OutlinedButton
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState
+import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture
+import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration
+import kotlinx.coroutines.launch
+
+@Sampled
+@Composable
+fun SimpleButtonSample(modifier: Modifier = Modifier) {
+ Button(onClick = { /* Do something */ }, label = { Text("Simple Button") }, modifier = modifier)
+}
+
+@Sampled
+@Composable
+fun ButtonSample() {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun ButtonLargeIconSample(enabled: Boolean = true) {
+ // When customising the icon size, it is recommended to also specify
+ // the associated content padding
+ Button(
+ onClick = { /* Do something */ },
+ enabled = enabled,
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.LargeIconSize),
+ )
+ },
+ contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding,
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun ButtonExtraLargeIconSample(enabled: Boolean = true) {
+ // When customising the icon size, it is recommended to also specify
+ // the associated content padding
+ Button(
+ onClick = { /* Do something */ },
+ enabled = enabled,
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.ExtraLargeIconSize),
+ )
+ },
+ contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding,
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun ButtonWithImageSample(enabled: Boolean = true) {
+ Button(
+ onClick = { /* Do something */ },
+ containerPainter =
+ ButtonDefaults.containerPainter(
+ image = painterResource(id = R.drawable.backgroundimage)
+ ),
+ enabled = enabled,
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun SimpleFilledTonalButtonSample() {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun FilledTonalButtonSample() {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun SimpleFilledVariantButtonSample() {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun FilledVariantButtonSample() {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun SimpleOutlinedButtonSample() {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun OutlinedButtonSample() {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun SimpleChildButtonSample() {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = {
+ Text("Child Button", textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun ChildButtonSample() {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Sampled
+@Composable
+fun CompactButtonSample(modifier: Modifier = Modifier) {
+ CompactButton(
+ onClick = { /* Do something */ },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize),
+ )
+ },
+ modifier = modifier,
+ label = { Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ )
+}
+
+@Sampled
+@Composable
+fun CompactButtonWithContentSample(modifier: Modifier = Modifier) {
+ CompactButton(
+ onClick = { /* Do something */ },
+ modifier = modifier,
+ content = {
+ Box(
+ Modifier.size(CompactButtonDefaults.ExtraSmallIconSize)
+ .clip(CircleShape)
+ .background(Color.Green)
+ )
+ Spacer(Modifier.width(ButtonDefaults.IconSpacing))
+ Text(
+ "Custom content",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ fontWeight = FontWeight.Bold,
+ fontStyle = FontStyle.Italic,
+ letterSpacing = 0.5.sp,
+ )
+ },
+ )
+}
+
+@Sampled
+@Composable
+fun CompactButtonWithOnLongClickSample(
+ onClickHandler: () -> Unit,
+ onLongClickHandler: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ CompactButton(
+ onClick = onClickHandler,
+ onLongClick = onLongClickHandler,
+ onLongClickLabel = "Long click",
+ label = { Text("Long clickable") },
+ modifier =
+ modifier.semantics {
+ // Also override the 'click label' to say 'Double tap to press' instead of
+ // the usual 'Double tap to activate'.
+ onClick("press") { false }
+ },
+ )
+}
+
+@Sampled
+@Composable
+fun FilledTonalCompactButtonSample(modifier: Modifier = Modifier) {
+ CompactButton(
+ onClick = { /* Do something */ },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize),
+ )
+ },
+ colors = ButtonDefaults.filledTonalButtonColors(),
+ modifier = modifier,
+ label = {
+ Text("Filled Tonal Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ },
+ )
+}
+
+@Sampled
+@Composable
+fun OutlinedCompactButtonSample(modifier: Modifier = Modifier) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ var expanded by remember { mutableStateOf(false) }
+ if (expanded) {
+ Text("A multiline string showing two lines")
+ } else {
+ Text("One line text")
+ }
+ Spacer(Modifier.height(ButtonDefaults.IconSpacing))
+ CompactButton(
+ onClick = { expanded = !expanded },
+ colors = ButtonDefaults.outlinedButtonColors(),
+ border = ButtonDefaults.outlinedButtonBorder(enabled = true),
+ modifier = modifier,
+ content = {
+ if (expanded) {
+ Text("Show Less", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ } else {
+ Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ }
+ Spacer(Modifier.width(ButtonDefaults.IconSpacing))
+ if (expanded) {
+ Icon(
+ Icons.Filled.KeyboardArrowUp,
+ contentDescription = "Collapse",
+ modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize),
+ )
+ } else {
+ Icon(
+ Icons.Filled.KeyboardArrowDown,
+ contentDescription = "Expand",
+ modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize),
+ )
+ }
+ },
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun ButtonContentWithOneHandedGestureSample() {
+ var label by remember { mutableStateOf("Filled Button") }
+ val onClick = remember { { label = "Gestured" } }
+ val interactionSource = remember { MutableInteractionSource() }
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGestureClickIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Button(
+ onClick = onClick,
+ interactionSource = interactionSource,
+ modifier =
+ Modifier.oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ onGestureLabel = "click",
+ interactionSource = interactionSource,
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ onGesture = onClick,
+ ),
+ content = {
+ OneHandedGestureClickIndicator(
+ gestureConfiguration = gestureConfig,
+ state = indicatorState,
+ gestureIndicatorTint = MaterialTheme.colorScheme.onPrimary,
+ ) {
+ ButtonDefaults.Content(
+ secondaryLabel = { Text("Secondary Label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ colors = ButtonDefaults.buttonColors(),
+ label = { Text(label) },
+ )
+ }
+ },
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun CompactButtonContentWithOneHandedGestureSample() {
+ var label by remember { mutableStateOf("Compact Button") }
+ val onClick = remember { { label = "Gestured" } }
+ val interactionSource = remember { MutableInteractionSource() }
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGestureClickIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CompactButton(
+ onClick = onClick,
+ interactionSource = interactionSource,
+ modifier =
+ Modifier.oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ onGestureLabel = "click",
+ interactionSource = interactionSource,
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ onGesture = onClick,
+ ),
+ content = {
+ OneHandedGestureClickIndicator(
+ gestureConfiguration = gestureConfig,
+ state = indicatorState,
+ gestureIndicatorTint = MaterialTheme.colorScheme.onPrimary,
+ ) {
+ CompactButtonDefaults.Content(
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize),
+ )
+ },
+ colors = ButtonDefaults.buttonColors(),
+ label = { Text(label) },
+ )
+ }
+ },
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/CardSample.kt.md b/wear/wear-compose-m3/references/material3/CardSample.kt.md
new file mode 100644
index 0000000..bcf5d47
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/CardSample.kt.md
@@ -0,0 +1,470 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.AppCard
+import androidx.wear.compose.material3.Card
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.OutlinedCard
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TitleCard
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState
+import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture
+import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration
+import kotlinx.coroutines.launch
+
+@Preview
+@Sampled
+@Composable
+fun CardSample() {
+ Card(onClick = { /* Do something */ }) { Text("Card") }
+}
+
+@Sampled
+@Composable
+fun NonClickableCardSample() {
+ Card { Text("Non Clickable Card") }
+}
+
+@Sampled
+@Composable
+fun CardWithOnLongClickSample(onLongClickHandler: () -> Unit) {
+ Card(
+ onClick = { /* Do something */ },
+ onLongClick = onLongClickHandler,
+ onLongClickLabel = "Long click",
+ ) {
+ Text("Card with long click")
+ }
+}
+
+@Sampled
+@Composable
+fun AppCardSample() {
+ AppCard(
+ onClick = { /* Do something */ },
+ appName = { Text("App name") },
+ title = { Text("Card title") },
+ time = { Text("Now") },
+ ) {
+ Text("Card content")
+ }
+}
+
+@Sampled
+@Composable
+fun NonClickableAppCardSample() {
+ AppCard(
+ appName = { Text("App name") },
+ title = { Text("Card title") },
+ time = { Text("Now") },
+ ) {
+ Text("Non clickable card content")
+ }
+}
+
+@Sampled
+@Composable
+fun AppCardWithIconSample() {
+ AppCard(
+ onClick = { /* Do something */ },
+ appName = { Text("App name") },
+ appImage = {
+ Icon(
+ painter = painterResource(id = android.R.drawable.star_big_off),
+ contentDescription = "Star icon",
+ modifier =
+ Modifier.size(CardDefaults.AppImageSize)
+ .wrapContentSize(align = Alignment.Center),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ },
+ title = { Text("Card title") },
+ time = { Text("Now") },
+ ) {
+ Text("Card content")
+ }
+}
+
+@Sampled
+@Composable
+fun AppCardWithImageSample() {
+ val configuration = LocalConfiguration.current
+ // Add padding to the end of the image in order to maintain the correct proportions
+ // between the image and the card.
+ val imageEndPaddingDp = (0.15f * configuration.screenWidthDp).dp
+ AppCard(
+ onClick = { /* Do something */ },
+ appName = { Text("App name") },
+ appImage = {
+ Icon(
+ painter = painterResource(id = android.R.drawable.star_big_off),
+ contentDescription = "Star icon",
+ modifier =
+ Modifier.size(CardDefaults.AppImageSize)
+ .wrapContentSize(align = Alignment.Center),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ },
+ title = { Text("With image") },
+ time = { Text("Now") },
+ ) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Row(modifier = Modifier.fillMaxWidth()) {
+ Image(
+ modifier =
+ Modifier.weight(1f).aspectRatio(16f / 9f).clip(RoundedCornerShape(16.dp)),
+ painter = painterResource(id = R.drawable.card_content_image),
+ contentScale = ContentScale.Crop,
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(imageEndPaddingDp))
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TitleCardSample() {
+ TitleCard(
+ onClick = { /* Do something */ },
+ title = { Text("Title card") },
+ time = { Text("Now") },
+ ) {
+ Text("Card content")
+ }
+}
+
+@Sampled
+@Composable
+fun NonClickableTitleCardSample() {
+ TitleCard(title = { Text("Title card") }, time = { Text("Now") }) {
+ Text("Non clickable Card content")
+ }
+}
+
+@Sampled
+@Composable
+fun TitleCardWithSubtitleAndTimeSample() {
+ TitleCard(
+ onClick = { /* Do something */ },
+ time = { Text("Now") },
+ title = { Text("Title card") },
+ subtitle = { Text("Subtitle") },
+ )
+}
+
+@Preview
+@Sampled
+@Composable
+fun TitleCardWithMultipleImagesSample() {
+ TitleCard(
+ onClick = { /* Do something */ },
+ title = { Text("Title card") },
+ time = { Text("Now") },
+ modifier = Modifier.semantics { contentDescription = "Background image" },
+ ) {
+ Spacer(Modifier.height(4.dp))
+ Row(modifier = Modifier.fillMaxWidth()) {
+ Image(
+ modifier =
+ Modifier.weight(2f)
+ .height(68.dp)
+ .align(Alignment.CenterVertically)
+ .clip(RoundedCornerShape(16.dp)),
+ painter = painterResource(id = R.drawable.card_content_image),
+ contentScale = ContentScale.Crop,
+ contentDescription = null,
+ )
+ Spacer(Modifier.width(4.dp))
+ Image(
+ modifier =
+ Modifier.weight(1f)
+ .height(68.dp)
+ .align(Alignment.CenterVertically)
+ .clip(RoundedCornerShape(16.dp)),
+ painter = painterResource(id = R.drawable.card_content_image),
+ contentScale = ContentScale.Crop,
+ contentDescription = null,
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TitleCardWithImageWithTimeAndTitleSample() {
+ TitleCard(
+ onClick = { /* Do something */ },
+ containerPainter =
+ CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)),
+ title = { Text("Card title") },
+ subtitle = { Text("Subtitle") },
+ time = { Text("Now") },
+ contentPadding = CardDefaults.CardWithContainerPainterContentPadding,
+ modifier = Modifier.semantics { contentDescription = "Background image" },
+ ) {
+ Text("Card content")
+ }
+}
+
+@Sampled
+@Composable
+fun NonClickableTitleCardWithImageWithTimeAndTitleSample() {
+ TitleCard(
+ containerPainter =
+ CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)),
+ title = { Text("Card title") },
+ subtitle = { Text("Subtitle") },
+ time = { Text("Now") },
+ contentPadding = CardDefaults.CardWithContainerPainterContentPadding,
+ modifier = Modifier.semantics { contentDescription = "Background image" },
+ ) {
+ Text("Card content")
+ }
+}
+
+@Sampled
+@Composable
+fun OutlinedCardSample() {
+ OutlinedCard(onClick = { /* Do something */ }) { Text("Outlined card") }
+}
+
+@Sampled
+@Composable
+fun NonClickableOutlinedCardSample() {
+ OutlinedCard { Text("Non-clickable outlined card") }
+}
+
+@Sampled
+@Composable
+fun ImageCardSample() {
+ Card(
+ onClick = { /* Do something */ },
+ containerPainter =
+ CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)),
+ ) {
+ Text("Image card")
+ }
+}
+
+@Sampled
+@Composable
+fun NonClickableImageCardSample() {
+ Card(
+ containerPainter =
+ CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage))
+ ) {
+ Text("Non clickable image card")
+ }
+}
+
+@Sampled
+@Composable
+fun OutlinedAppCardSample() {
+ AppCard(
+ onClick = { /* Do something */ },
+ appName = { Text("App name") },
+ appImage = {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(CardDefaults.AppImageSize),
+ )
+ },
+ title = { Text("App card") },
+ time = { Text("Now") },
+ colors = CardDefaults.outlinedCardColors(),
+ border = CardDefaults.outlinedCardBorder(),
+ ) {
+ Text("Card content")
+ }
+}
+
+@Sampled
+@Composable
+fun OutlinedTitleCardSample() {
+ TitleCard(
+ onClick = { /* Do something */ },
+ title = { Text("Title card") },
+ time = { Text("Now") },
+ colors = CardDefaults.outlinedCardColors(),
+ border = CardDefaults.outlinedCardBorder(),
+ ) {
+ Text("Card content")
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun CardFillContentSample() {
+ Card(
+ onClick = { /* Do something */ },
+ // Constrains the card to fill background up to the intrinsic height.
+ modifier = Modifier.height(IntrinsicSize.Min),
+ ) {
+ Text(
+ "Filled Content",
+ color = MaterialTheme.colorScheme.onPrimary,
+ modifier =
+ Modifier.fillMaxHeight()
+ .clip(RoundedCornerShape(8.dp))
+ .background(MaterialTheme.colorScheme.primary)
+ .wrapContentSize(Alignment.Center),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun AppCardContentWithOneHandedGestureSample() {
+ var label by remember { mutableStateOf("App Card") }
+ val onClick = remember { { label = "Gestured" } }
+ val interactionSource = remember { MutableInteractionSource() }
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGestureClickIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Card(
+ onClick = onClick,
+ interactionSource = interactionSource,
+ modifier =
+ Modifier.padding(horizontal = 12.dp)
+ .fillMaxWidth()
+ .oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ onGestureLabel = "click",
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ interactionSource = interactionSource,
+ onGesture = onClick,
+ ),
+ ) {
+ OneHandedGestureClickIndicator(
+ gestureConfiguration = gestureConfig,
+ state = indicatorState,
+ gestureIndicatorTint = MaterialTheme.colorScheme.onSurface,
+ ) {
+ CardDefaults.AppCardContent(
+ appName = { Text("App Name") },
+ title = { Text(label) },
+ appImage = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(CardDefaults.AppImageSize),
+ )
+ },
+ time = { Text("now") },
+ ) {
+ Text("Card body")
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TitleCardContentWithOneHandedGestureSample() {
+ var label by remember { mutableStateOf("Title Card") }
+ val onClick = remember { { label = "Gestured" } }
+ val interactionSource = remember { MutableInteractionSource() }
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGestureClickIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Card(
+ onClick = onClick,
+ interactionSource = interactionSource,
+ modifier =
+ Modifier.padding(horizontal = 12.dp)
+ .fillMaxWidth()
+ .oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ onGestureLabel = "click",
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ interactionSource = interactionSource,
+ onGesture = onClick,
+ ),
+ ) {
+ OneHandedGestureClickIndicator(
+ gestureConfiguration = gestureConfig,
+ state = indicatorState,
+ gestureIndicatorTint = MaterialTheme.colorScheme.onSurface,
+ ) {
+ CardDefaults.TitleCardContent(
+ title = { Text(label) },
+ time = { Text("now") },
+ subtitle = { Text("Subtitle") },
+ ) {
+ Text("Card body")
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt b/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt
index fb9312b..bcf5d47 100644
--- a/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt
+++ b/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt
@@ -83,7 +83,7 @@ fun NonClickableCardSample() {
@Sampled
@Composable
-fun CardWithOnLongClickS>ample(onLongClickHandler: () - Unit) {
+fun CardWithOnLongClickSample(onLongClickHandler: () -> Unit) {
Card(
onClick = { /* Do something */ },
onLongClick = onLongClickHandler,
diff --git a/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md b/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md
new file mode 100644
index 0000000..3ae73bd
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md
@@ -0,0 +1,69 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.wear.compose.material3.CheckboxButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.SplitCheckboxButton
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Preview
+@Composable
+fun CheckboxButtonSample() {
+ var checked by remember { mutableStateOf(true) }
+ CheckboxButton(
+ label = { Text("Checkbox Button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ secondaryLabel = {
+ Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis)
+ },
+ checked = checked,
+ onCheckedChange = { checked = it },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") },
+ enabled = true,
+ )
+}
+
+@Sampled
+@Preview
+@Composable
+fun SplitCheckboxButtonSample() {
+ var checked by remember { mutableStateOf(true) }
+ SplitCheckboxButton(
+ label = { Text("Split Checkbox Button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ checked = checked,
+ onCheckedChange = { checked = it },
+ toggleContentDescription = "Split Checkbox Button Sample",
+ onContainerClick = {
+ /* Do something */
+ },
+ containerClickLabel = "click",
+ enabled = true,
+ )
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md b/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md
new file mode 100644
index 0000000..1f31892
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md
@@ -0,0 +1,162 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.ConfirmationDialog
+import androidx.wear.compose.material3.ConfirmationDialogDefaults
+import androidx.wear.compose.material3.FailureConfirmationDialog
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.SuccessConfirmationDialog
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.confirmationDialogCurvedText
+import androidx.wear.compose.material3.samples.icons.FavoriteIcon
+
+@Sampled
+@Composable
+fun ConfirmationDialogSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") },
+ )
+ }
+
+ // Has an icon and a short curved text content, which will be displayed along the bottom edge of
+ // the screen.
+ val curvedTextStyle = ConfirmationDialogDefaults.curvedTextStyle
+ ConfirmationDialog(
+ visible = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = { confirmationDialogCurvedText("Confirmed", curvedTextStyle) },
+ ) {
+ FavoriteIcon(ConfirmationDialogDefaults.IconSize)
+ }
+}
+
+@Sampled
+@Composable
+fun LongTextConfirmationDialogSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") },
+ )
+ }
+
+ // Has an icon and a text content. Text will be displayed in the center of the screen below the
+ // icon.
+ ConfirmationDialog(
+ visible = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ text = { Text(text = "Your message has been sent") },
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Send,
+ contentDescription = null,
+ modifier = Modifier.size(ConfirmationDialogDefaults.SmallIconSize),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun FailureConfirmationDialogSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") },
+ )
+ }
+
+ val text = "Failure"
+ val style = ConfirmationDialogDefaults.curvedTextStyle
+ FailureConfirmationDialog(
+ visible = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = { confirmationDialogCurvedText(text, style) },
+ )
+}
+
+@Sampled
+@Composable
+fun FailureConfirmationDialogWithGenericFailureIconSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") },
+ )
+ }
+
+ val text = "Failure"
+ val style = ConfirmationDialogDefaults.curvedTextStyle
+ FailureConfirmationDialog(
+ visible = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = { confirmationDialogCurvedText(text, style) },
+ content = { ConfirmationDialogDefaults.GenericFailureIcon() },
+ )
+}
+
+@Sampled
+@Composable
+fun SuccessConfirmationDialogSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") },
+ )
+ }
+
+ val text = "Success"
+ val style = ConfirmationDialogDefaults.curvedTextStyle
+ SuccessConfirmationDialog(
+ visible = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = { confirmationDialogCurvedText(text, style) },
+ )
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md b/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md
new file mode 100644
index 0000000..4e1a9ae
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md
@@ -0,0 +1,78 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.StrokeCap.Companion.Round
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.CurvedDirection
+import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.angularSizeDp
+import androidx.wear.compose.foundation.background
+import androidx.wear.compose.foundation.curvedBox
+import androidx.wear.compose.foundation.curvedComposable
+import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.CurvedTextDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.curvedText
+
+@Sampled
+@Composable
+fun CurvedTextTop() {
+ val backgroundColor = MaterialTheme.colorScheme.onPrimary
+ val customColor = MaterialTheme.colorScheme.tertiaryDim
+ CurvedLayout {
+ curvedRow(CurvedModifier.background(backgroundColor, Round)) {
+ curvedText("Calling", color = customColor)
+ curvedBox(CurvedModifier.angularSizeDp(5.dp)) {}
+ curvedText("Camilia Garcia")
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedTextBottom() {
+ val backgroundColor = MaterialTheme.colorScheme.onPrimary
+ CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.Reversed) {
+ curvedRow(CurvedModifier.background(backgroundColor, Round)) {
+ curvedComposable {
+ Icon(
+ Icons.Filled.Warning,
+ contentDescription = "Warning",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ }
+ curvedText(
+ "Network lost",
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md b/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md
new file mode 100644
index 0000000..261e5d7
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md
@@ -0,0 +1,131 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.DatePicker
+import androidx.wear.compose.material3.DatePickerType
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.Text
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+@Sampled
+@Composable
+fun DatePickerSample() {
+ var showDatePicker by remember { mutableStateOf(true) }
+ var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
+ val formatter =
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+ .withLocale(LocalConfiguration.current.locales[0])
+ if (showDatePicker) {
+ DatePicker(
+ initialDate = datePickerDate, // Initialize with last picked date on reopen
+ onDatePicked = {
+ datePickerDate = it
+ showDatePicker = false
+ },
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Button(
+ onClick = { showDatePicker = true },
+ label = { Text("Selected Date") },
+ secondaryLabel = { Text(datePickerDate.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun DatePickerYearMonthDaySample() {
+ var showDatePicker by remember { mutableStateOf(true) }
+ var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
+
+ val formatter =
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+ .withLocale(LocalConfiguration.current.locales[0])
+
+ if (showDatePicker) {
+ DatePicker(
+ initialDate = datePickerDate, // Initialize with last picked date on reopen
+ onDatePicked = {
+ datePickerDate = it
+ showDatePicker = false
+ },
+ datePickerType = DatePickerType.YearMonthDay,
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Button(
+ onClick = { showDatePicker = true },
+ label = { Text("Selected Date") },
+ secondaryLabel = { Text(datePickerDate.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun DatePickerFutureOnlySample() {
+ val currentDate = LocalDate.now()
+ var showDatePicker by remember { mutableStateOf(true) }
+ var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
+ val formatter =
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+ .withLocale(LocalConfiguration.current.locales[0])
+ if (showDatePicker) {
+ DatePicker(
+ initialDate = datePickerDate, // Initialize with last picked date on reopen
+ onDatePicked = {
+ datePickerDate = it
+ showDatePicker = false
+ },
+ datePickerType = DatePickerType.YearMonthDay,
+ minValidDate = currentDate,
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Button(
+ onClick = { showDatePicker = true },
+ label = { Text("Selected Date") },
+ secondaryLabel = { Text(datePickerDate.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md b/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md
new file mode 100644
index 0000000..45d4651
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md
@@ -0,0 +1,74 @@
+```
+/*
+ * Copyright 2026 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.ColorScheme
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.dynamicColorScheme
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+
+@Sampled
+@Composable
+fun DynamicColorSchemeSample() {
+ val dynamicColorScheme = dynamicColorScheme(LocalContext.current)
+ val transformationSpec = rememberTransformationSpec()
+
+ // Fallback to the default color scheme if dynamic colors are unavailable
+ MaterialTheme(colorScheme = dynamicColorScheme ?: ColorScheme()) {
+ val hasDynamicColors = dynamicColorScheme != null
+ TransformingLazyColumn(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ contentPadding = PaddingValues(20.dp),
+ ) {
+ if (!hasDynamicColors) {
+ item { Text("Dynamic color is not available.") }
+ }
+ items(5) { index ->
+ // The button's defaults will pick up the dynamic primary color from the
+ // MaterialTheme
+ Button(
+ label = { Text("Primary Button ${index + 1}") },
+ modifier =
+ Modifier.fillMaxWidth()
+ .transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ ),
+ onClick = {},
+ transformation = SurfaceTransformation(transformationSpec),
+ )
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt b/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt
index 1d5a441..45d4651 100644
--- a/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt
+++ b/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt
@@ -53,7 +53,7 @@ fun DynamicColorSchemeSample() {
if (!hasDynamicColors) {
item { Text("Dynamic color is not available.") }
}
- items(5)> { index -
+ items(5) { index ->
// The button's defaults will pick up the dynamic primary color from the
// MaterialTheme
Button(
diff --git a/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md b/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md
new file mode 100644
index 0000000..4a01bc8
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md
@@ -0,0 +1,63 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.EdgeButtonSize
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Composable
+fun EdgeButtonSample() {
+ Column(
+ Modifier.fillMaxSize().padding(horizontal = 20.dp).padding(top = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
+ Text(
+ "EdgeButton hugs the bottom of the curved screen",
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+ }
+ EdgeButton(onClick = { /* Do something */ }, buttonSize = EdgeButtonSize.Medium) {
+ Icon(
+ Icons.Filled.Check,
+ contentDescription = "Check icon",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md b/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md
new file mode 100644
index 0000000..4605ac0
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md
@@ -0,0 +1,60 @@
+```
+/*
+ * 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.FadingExpandingLabel
+
+@Sampled
+@Composable
+fun FadingExpandingLabelButtonSample() {
+ var text by remember { mutableStateOf("Line of Text One.") }
+ var lines by remember { mutableIntStateOf(1) }
+
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Button(
+ onClick = {
+ lines = lines % 3 + 1
+ text =
+ (1..lines).joinToString(separator = "\n") {
+ when (it) {
+ 1 -> "Line of Text One."
+ 2 -> "Line of Text Two."
+ else -> "Line of Text Three."
+ }
+ }
+ },
+ modifier = Modifier.fillMaxWidth(),
+ label = { FadingExpandingLabel(text = text, textAlign = TextAlign.Left) },
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md b/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md
new file mode 100644
index 0000000..7ecc104
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md
@@ -0,0 +1,125 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.Image
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.material3.FilledIconButton
+import androidx.wear.compose.material3.FilledTonalIconButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButton
+import androidx.wear.compose.material3.IconButtonColors
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.IconButtonShapes
+import androidx.wear.compose.material3.OutlinedIconButton
+
+@Composable
+@Sampled
+fun IconButtonSample() {
+ IconButton(onClick = { /* Do something */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Composable
+@Sampled
+fun FilledIconButtonSample() {
+ FilledIconButton(onClick = { /* Do something */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Composable
+@Sampled
+fun FilledVariantIconButtonSample() {
+ FilledIconButton(
+ onClick = { /* Do something */ },
+ colors = IconButtonDefaults.filledVariantIconButtonColors(),
+ ) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Composable
+@Sampled
+fun FilledTonalIconButtonSample() {
+ FilledTonalIconButton(onClick = { /* Do something */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Composable
+@Sampled
+fun OutlinedIconButtonSample() {
+ OutlinedIconButton(onClick = { /* Do something */ }) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Sampled
+@Composable
+fun IconButtonWithOnLongClickSample(onLongClick: () -> Unit) {
+ IconButton(
+ onClick = { /* Do something for onClick*/ },
+ onLongClick = onLongClick,
+ onLongClickLabel = "Long click",
+ ) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Composable
+@Sampled
+fun IconButtonWithCornerAnimationSample(
+ colors: IconButtonColors = IconButtonDefaults.filledIconButtonColors()
+) {
+ FilledIconButton(
+ onClick = { /* Do something */ },
+ shapes = IconButtonDefaults.animatedShapes(),
+ colors = colors,
+ ) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Composable
+@Sampled
+fun IconButtonWithImageSample(
+ painter: Painter = painterResource(R.drawable.card_content_image),
+ enabled: Boolean = true,
+ shapes: IconButtonShapes = IconButtonDefaults.shapes(),
+) {
+ IconButton(onClick = { /* Do something */ }, shapes = shapes, enabled = enabled) {
+ Image(
+ painter = painter,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ if (enabled) Modifier else Modifier.alpha(IconButtonDefaults.DisabledImageOpacity),
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md b/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md
new file mode 100644
index 0000000..23c16da
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md
@@ -0,0 +1,116 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.IconToggleButton
+import androidx.wear.compose.material3.IconToggleButtonDefaults
+import androidx.wear.compose.material3.samples.icons.WifiOffIcon
+import androidx.wear.compose.material3.samples.icons.WifiOnIcon
+
+@Sampled
+@Composable
+fun IconToggleButtonSample() {
+ var firstChecked by remember { mutableStateOf(true) }
+ var secondChecked by remember { mutableStateOf(false) }
+
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconToggleButton(
+ checked = firstChecked,
+ onCheckedChange = { firstChecked = !firstChecked },
+ shapes = IconToggleButtonDefaults.animatedShapes(),
+ ) {
+ if (firstChecked) {
+ WifiOnIcon()
+ } else {
+ WifiOffIcon()
+ }
+ }
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ IconToggleButton(
+ checked = secondChecked,
+ onCheckedChange = { secondChecked = !secondChecked },
+ shapes = IconToggleButtonDefaults.animatedShapes(),
+ ) {
+ if (secondChecked) {
+ WifiOnIcon()
+ } else {
+ WifiOffIcon()
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun IconToggleButtonVariantSample() {
+ var firstChecked by remember { mutableStateOf(true) }
+ var secondChecked by remember { mutableStateOf(false) }
+
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconToggleButton(
+ checked = firstChecked,
+ onCheckedChange = { firstChecked = !firstChecked },
+ shapes = IconToggleButtonDefaults.variantAnimatedShapes(),
+ ) {
+ if (firstChecked) {
+ WifiOnIcon()
+ } else {
+ WifiOffIcon()
+ }
+ }
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ IconToggleButton(
+ checked = secondChecked,
+ onCheckedChange = { secondChecked = !secondChecked },
+ shapes = IconToggleButtonDefaults.variantAnimatedShapes(),
+ ) {
+ if (secondChecked) {
+ WifiOnIcon()
+ } else {
+ WifiOffIcon()
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md b/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md
new file mode 100644
index 0000000..7f02be2
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md
@@ -0,0 +1,68 @@
+```
+/*
+ * 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.IconButton
+import androidx.wear.compose.material3.LevelIndicator
+import androidx.wear.compose.material3.StepperDefaults
+import androidx.wear.compose.material3.samples.icons.VolumeDownIcon
+import androidx.wear.compose.material3.samples.icons.VolumeUpIcon
+
+@Sampled
+@Composable
+fun LevelIndicatorSample() {
+ var value by remember { mutableFloatStateOf(0.5f) }
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ verticalArrangement = Arrangement.SpaceEvenly,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ IconButton(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ enabled = value < 1f,
+ onClick = { value += 0.1f },
+ ) {
+ VolumeUpIcon(StepperDefaults.IconSize)
+ }
+ IconButton(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ enabled = value > 0f,
+ onClick = { value -= 0.1f },
+ ) {
+ VolumeDownIcon(StepperDefaults.IconSize)
+ }
+ }
+ LevelIndicator(value = { value }, modifier = Modifier.align(Alignment.CenterStart))
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt.md b/wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt.md
new file mode 100644
index 0000000..47f8d6b
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt.md
@@ -0,0 +1,50 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.semantics
+import androidx.wear.compose.material3.LinearProgressIndicator
+import androidx.wear.compose.material3.MaterialTheme
+
+@Sampled
+@Composable
+fun LinearProgressIndicatorSample(progress: () -> Float, enabled: Boolean = true) {
+ Box(
+ modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ LinearProgressIndicator(
+ progress = progress,
+ enabled = enabled,
+ modifier =
+ Modifier.semantics(mergeDescendants = true) {
+ progressBarRangeInfo = ProgressBarRangeInfo(progress(), 0f..1f)
+ },
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md b/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md
new file mode 100644
index 0000000..8256973
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md
@@ -0,0 +1,177 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListHeaderDefaults
+import androidx.wear.compose.material3.ListSubHeader
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+
+@Sampled
+@Preview
+@Composable
+fun ListHeaderSample() {
+ val transformationSpec = rememberTransformationSpec()
+ val scrollState = rememberTransformingLazyColumnState()
+
+ AppScaffold {
+ ScreenScaffold(scrollState = scrollState) { contentPadding ->
+ TransformingLazyColumn(
+ state = scrollState,
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ contentPadding = contentPadding,
+ ) {
+ item {
+ ListHeader(
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ ListHeaderDefaults.minimumTopListContentPadding,
+ ListHeaderDefaults.minimumBottomListContentPadding,
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("Settings")
+ }
+ }
+ item {
+ ListSubHeader(
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ ListHeaderDefaults.minimumTopListContentPadding,
+ ListHeaderDefaults.minimumBottomListContentPadding,
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_connectivity),
+ contentDescription = "Connectivity",
+ )
+ },
+ label = { Text("Connectivity") },
+ )
+ }
+ item {
+ Button(
+ modifier =
+ Modifier.fillMaxWidth()
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_bluetooth),
+ contentDescription = "Bluetooth",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ ) {
+ Text("Bluetooth")
+ }
+ }
+ item {
+ Button(
+ modifier =
+ Modifier.fillMaxWidth()
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_wifi),
+ contentDescription = "Wifi",
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ )
+ },
+ ) {
+ Text("Wifi")
+ }
+ }
+ item {
+ ListSubHeader(
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ ListHeaderDefaults.minimumTopListContentPadding,
+ ListHeaderDefaults.minimumBottomListContentPadding,
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("Display")
+ }
+ }
+ item {
+ Button(
+ modifier =
+ Modifier.fillMaxWidth()
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ ) {
+ Text("Change Watchface")
+ }
+ }
+ item {
+ Button(
+ modifier =
+ Modifier.fillMaxWidth()
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ onClick = {},
+ ) {
+ Text("Brightness")
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md b/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md
new file mode 100644
index 0000000..340d53a
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md
@@ -0,0 +1,651 @@
+```
+/*
+ * Copyright 2026 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.wear.compose.material3.samples
+
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.annotation.Sampled
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollable
+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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberOverscrollEffect
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.AmbientMode
+import androidx.wear.compose.foundation.LocalAmbientModeManager
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.pager.HorizontalPager
+import androidx.wear.compose.foundation.pager.VerticalPager
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.foundation.rememberAmbientModeManager
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.HorizontalPagerScaffold
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SwitchButton
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.VerticalPagerScaffold
+import androidx.wear.compose.material3.onehandedgesture.LocalOneHandedGestureEnabled
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureDefaults
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureHorizontalPageIndicator
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGesturePageIndicatorState
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGesturePriority
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicator
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicatorState
+import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureVerticalPageIndicator
+import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture
+import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration
+import kotlinx.coroutines.launch
+
+@Sampled
+@Composable
+fun OneHandedGestureButtonSample() {
+ var label by remember { mutableStateOf("Gesturable Button") }
+ val onClick = { label = "Clicked/Gestured" }
+ val interactionSource = remember { MutableInteractionSource() }
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGestureClickIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Button(
+ onClick = onClick,
+ interactionSource = interactionSource,
+ modifier =
+ Modifier.fillMaxWidth()
+ .oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ interactionSource = interactionSource,
+ onGestureLabel = "activate the button",
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ onGesture = onClick,
+ ),
+ ) {
+ OneHandedGestureClickIndicator(gestureConfig, indicatorState) {
+ Text(label, modifier = Modifier.fillMaxWidth())
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureButtonInAmbientSample() {
+ var label by remember { mutableStateOf("Gesturable Button") }
+ val onClick = { label = "Clicked/Gestured" }
+ val interactionSource = remember { MutableInteractionSource() }
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGestureClickIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+ val activityAmbientModeManager = rememberAmbientModeManager()
+ var showGestureIndicator by remember { mutableStateOf(true) }
+
+ CompositionLocalProvider(LocalAmbientModeManager provides activityAmbientModeManager) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ val isInAmbientMode =
+ LocalAmbientModeManager.current?.currentAmbientMode is AmbientMode.Ambient
+ Button(
+ onClick = onClick,
+ interactionSource = interactionSource,
+ modifier =
+ Modifier.fillMaxWidth()
+ .oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ interactionSource = interactionSource,
+ enabledInAmbient = true,
+ onGestureLabel = "activate the button",
+ onGestureAvailable = {
+ // Bypass repeating the gesture indicator on every recomposition.
+ if (showGestureIndicator) {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ showGestureIndicator = false
+ }
+ },
+ onGesture = onClick,
+ ),
+ colors =
+ if (isInAmbientMode) ButtonDefaults.outlinedButtonColors()
+ else ButtonDefaults.buttonColors(),
+ border =
+ if (isInAmbientMode) ButtonDefaults.outlinedButtonBorder(enabled = true)
+ else null,
+ ) {
+ OneHandedGestureClickIndicator(gestureConfig, indicatorState) {
+ Text(label, modifier = Modifier.fillMaxWidth())
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureDisableButtonSample() {
+ var counter by remember { mutableIntStateOf(0) }
+ var enabled by remember { mutableStateOf(true) }
+ val interactionSource = remember { MutableInteractionSource() }
+ val coroutineScope = rememberCoroutineScope()
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ SwitchButton(checked = enabled, onCheckedChange = { enabled = it }) {
+ Text("Gestures enabled")
+ }
+ Spacer(modifier = Modifier.height(6.dp))
+ CompositionLocalProvider(LocalOneHandedGestureEnabled provides enabled) {
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGestureClickIndicatorState() }
+ Button(
+ onClick = {},
+ interactionSource = interactionSource,
+ modifier =
+ Modifier.oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ interactionSource = interactionSource,
+ onGestureLabel = "increase the counter",
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ onGesture = { counter++ },
+ ),
+ ) {
+ OneHandedGestureClickIndicator(gestureConfig, indicatorState) {
+ Text("Gestured $counter times")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureTransformingLazyColumnSample() {
+ val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+ val onClick =
+ remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } }
+ val scrollState = rememberTransformingLazyColumnState()
+ val coroutineScope = rememberCoroutineScope()
+
+ val buttonInteractionSource = remember { MutableInteractionSource() }
+ val buttonGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Clickable,
+ )
+ val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() }
+
+ val scrollGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Scrollable,
+ )
+ val scrollIndicatorState =
+ remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() }
+
+ ScreenScaffold(
+ scrollState = scrollState,
+ edgeButton = {
+ EdgeButton(
+ onClick = onClick,
+ interactionSource = buttonInteractionSource,
+ modifier =
+ if (scrollState.canScrollForward) {
+ Modifier
+ } else {
+ // Apply the one-handed gesture modifier only when the container cannot
+ // scroll further, ensuring the EdgeButton is fully visible and interactive
+ Modifier.oneHandedGesture(
+ gestureConfiguration = buttonGestureConfig,
+ interactionSource = buttonInteractionSource,
+ onGestureLabel = "close",
+ onGestureAvailable = {
+ coroutineScope.launch { buttonIndicatorState.showIndicator() }
+ },
+ onGesture = onClick,
+ )
+ } then
+ Modifier.scrollable(
+ state = scrollState,
+ orientation = Orientation.Vertical,
+ reverseDirection = true,
+ overscrollEffect = rememberOverscrollEffect(),
+ ),
+ ) {
+ OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) {
+ Text("Close")
+ }
+ }
+ },
+ scrollIndicator = {
+ OneHandedGestureScrollIndicator(
+ gestureConfiguration = scrollGestureConfig,
+ indicatorState = scrollIndicatorState,
+ scrollState = scrollState,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ )
+ },
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = scrollState,
+ contentPadding = contentPadding,
+ modifier =
+ Modifier.fillMaxSize()
+ .oneHandedGesture(
+ gestureConfiguration = scrollGestureConfig,
+ onGestureLabel = "scroll",
+ onGestureAvailable = {
+ coroutineScope.launch { scrollIndicatorState.showIndicator() }
+ },
+ onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) },
+ ),
+ ) {
+ items(10) { Text("Item $it") }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureScalingLazyColumnSample() {
+ val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+ val onClick =
+ remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } }
+ val scrollState = rememberScalingLazyListState()
+ val coroutineScope = rememberCoroutineScope()
+
+ val buttonInteractionSource = remember { MutableInteractionSource() }
+ val buttonGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Clickable,
+ )
+ val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() }
+
+ val scrollGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Scrollable,
+ )
+ val scrollIndicatorState =
+ remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() }
+
+ ScreenScaffold(
+ scrollState = scrollState,
+ edgeButton = {
+ EdgeButton(
+ onClick = onClick,
+ interactionSource = buttonInteractionSource,
+ modifier =
+ if (scrollState.canScrollForward) {
+ Modifier
+ } else {
+ // Apply the one-handed gesture modifier only when the container cannot
+ // scroll further, ensuring the EdgeButton is fully visible and interactive
+ Modifier.oneHandedGesture(
+ gestureConfiguration = buttonGestureConfig,
+ interactionSource = buttonInteractionSource,
+ onGestureLabel = "close",
+ onGestureAvailable = {
+ coroutineScope.launch { buttonIndicatorState.showIndicator() }
+ },
+ onGesture = onClick,
+ )
+ } then
+ Modifier.scrollable(
+ state = scrollState,
+ orientation = Orientation.Vertical,
+ reverseDirection = true,
+ overscrollEffect = rememberOverscrollEffect(),
+ ),
+ ) {
+ OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) {
+ Text("Close")
+ }
+ }
+ },
+ scrollIndicator = {
+ OneHandedGestureScrollIndicator(
+ gestureConfiguration = scrollGestureConfig,
+ indicatorState = scrollIndicatorState,
+ scrollState = scrollState,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ )
+ },
+ ) { contentPadding ->
+ ScalingLazyColumn(
+ state = scrollState,
+ contentPadding = contentPadding,
+ modifier =
+ Modifier.fillMaxSize()
+ .oneHandedGesture(
+ gestureConfiguration = scrollGestureConfig,
+ onGestureLabel = "scroll",
+ onGestureAvailable = {
+ coroutineScope.launch { scrollIndicatorState.showIndicator() }
+ },
+ onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) },
+ ),
+ autoCentering = null,
+ ) {
+ items(10) { Text("Item $it") }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureTransformingLazyColumnScrollToNextItemSample() {
+ val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+ val onClick =
+ remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } }
+ val scrollState = rememberTransformingLazyColumnState()
+ val coroutineScope = rememberCoroutineScope()
+
+ val buttonInteractionSource = remember { MutableInteractionSource() }
+ val buttonGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Clickable,
+ )
+ val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() }
+
+ val scrollGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Scrollable,
+ )
+ val scrollIndicatorState =
+ remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() }
+
+ ScreenScaffold(
+ scrollState = scrollState,
+ edgeButton = {
+ EdgeButton(
+ onClick = onClick,
+ interactionSource = buttonInteractionSource,
+ modifier =
+ if (scrollState.canScrollForward) {
+ Modifier
+ } else {
+ // Apply the one-handed gesture modifier only when the container cannot
+ // scroll further, ensuring the EdgeButton is fully visible and interactive
+ Modifier.oneHandedGesture(
+ gestureConfiguration = buttonGestureConfig,
+ interactionSource = buttonInteractionSource,
+ onGestureLabel = "close",
+ onGestureAvailable = {
+ coroutineScope.launch { buttonIndicatorState.showIndicator() }
+ },
+ onGesture = onClick,
+ )
+ } then
+ Modifier.scrollable(
+ state = scrollState,
+ orientation = Orientation.Vertical,
+ reverseDirection = true,
+ overscrollEffect = rememberOverscrollEffect(),
+ ),
+ ) {
+ OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) {
+ Text("Close")
+ }
+ }
+ },
+ scrollIndicator = {
+ OneHandedGestureScrollIndicator(
+ gestureConfiguration = scrollGestureConfig,
+ indicatorState = scrollIndicatorState,
+ scrollState = scrollState,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ )
+ },
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = scrollState,
+ contentPadding = contentPadding,
+ modifier =
+ Modifier.fillMaxSize()
+ .oneHandedGesture(
+ gestureConfiguration = scrollGestureConfig,
+ onGestureLabel = "scroll",
+ onGestureAvailable = {
+ coroutineScope.launch { scrollIndicatorState.showIndicator() }
+ },
+ onGesture = { OneHandedGestureDefaults.scrollDownToNextItem(scrollState) },
+ ),
+ ) {
+ items(10) { Text("Item $it") }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureScalingLazyColumnScrollToNextItemSample() {
+ val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+ val onClick =
+ remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } }
+ val scrollState = rememberScalingLazyListState()
+ val coroutineScope = rememberCoroutineScope()
+
+ val buttonInteractionSource = remember { MutableInteractionSource() }
+ val buttonGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Clickable,
+ )
+ val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() }
+
+ val scrollGestureConfig =
+ rememberOneHandedGestureConfiguration(
+ action = OneHandedGestureAction.Primary,
+ priority = OneHandedGesturePriority.Scrollable,
+ )
+ val scrollIndicatorState =
+ remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() }
+
+ ScreenScaffold(
+ scrollState = scrollState,
+ edgeButton = {
+ EdgeButton(
+ onClick = onClick,
+ interactionSource = buttonInteractionSource,
+ modifier =
+ if (scrollState.canScrollForward) {
+ Modifier
+ } else {
+ // Apply the one-handed gesture modifier only when the container cannot
+ // scroll further, ensuring the EdgeButton is fully visible and interactive
+ Modifier.oneHandedGesture(
+ gestureConfiguration = buttonGestureConfig,
+ interactionSource = buttonInteractionSource,
+ onGestureLabel = "close",
+ onGestureAvailable = {
+ coroutineScope.launch { buttonIndicatorState.showIndicator() }
+ },
+ onGesture = onClick,
+ )
+ } then
+ Modifier.scrollable(
+ state = scrollState,
+ orientation = Orientation.Vertical,
+ reverseDirection = true,
+ overscrollEffect = rememberOverscrollEffect(),
+ ),
+ ) {
+ OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) {
+ Text("Close")
+ }
+ }
+ },
+ scrollIndicator = {
+ OneHandedGestureScrollIndicator(
+ gestureConfiguration = scrollGestureConfig,
+ indicatorState = scrollIndicatorState,
+ scrollState = scrollState,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ )
+ },
+ ) { contentPadding ->
+ ScalingLazyColumn(
+ state = scrollState,
+ contentPadding = contentPadding,
+ modifier =
+ Modifier.fillMaxSize()
+ .oneHandedGesture(
+ gestureConfiguration = scrollGestureConfig,
+ onGestureLabel = "scroll",
+ onGestureAvailable = {
+ coroutineScope.launch { scrollIndicatorState.showIndicator() }
+ },
+ onGesture = { OneHandedGestureDefaults.scrollDownToNextItem(scrollState) },
+ ),
+ autoCentering = null,
+ ) {
+ items(10) { Text("Item $it") }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureHorizontalPagerSample() {
+ val pagerState = rememberPagerState(pageCount = { 10 })
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGesturePageIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ HorizontalPagerScaffold(
+ pagerState = pagerState,
+ pageIndicator = {
+ OneHandedGestureHorizontalPageIndicator(
+ gestureConfiguration = gestureConfig,
+ indicatorState = indicatorState,
+ pagerState = pagerState,
+ )
+ },
+ ) {
+ HorizontalPager(
+ state = pagerState,
+ modifier =
+ Modifier.oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ onGestureLabel = "scroll to the next page",
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ ) {
+ OneHandedGestureDefaults.scrollToNextPage(pagerState)
+ },
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ ScreenScaffold {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe left and right")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OneHandedGestureVerticalPagerSample() {
+ val pagerState = rememberPagerState(pageCount = { 10 })
+ val gestureConfig =
+ rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary)
+ val indicatorState = remember { OneHandedGesturePageIndicatorState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ VerticalPagerScaffold(
+ pagerState = pagerState,
+ pageIndicator = {
+ OneHandedGestureVerticalPageIndicator(
+ gestureConfiguration = gestureConfig,
+ indicatorState = indicatorState,
+ pagerState = pagerState,
+ )
+ },
+ ) {
+ VerticalPager(
+ state = pagerState,
+ modifier =
+ Modifier.oneHandedGesture(
+ gestureConfiguration = gestureConfig,
+ onGestureLabel = "scroll to the next page",
+ onGestureAvailable = {
+ coroutineScope.launch { indicatorState.showIndicator() }
+ },
+ ) {
+ OneHandedGestureDefaults.scrollToNextPage(pagerState)
+ },
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ ScreenScaffold {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe up and down")
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md b/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md
new file mode 100644
index 0000000..dc164fc
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md
@@ -0,0 +1,57 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.OpenOnPhoneDialog
+import androidx.wear.compose.material3.OpenOnPhoneDialogDefaults
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.openOnPhoneDialogCurvedText
+
+@Sampled
+@Composable
+fun OpenOnPhoneDialogSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Open on phone") },
+ )
+ }
+
+ val text = OpenOnPhoneDialogDefaults.text
+ val style = OpenOnPhoneDialogDefaults.curvedTextStyle
+ OpenOnPhoneDialog(
+ visible = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = { openOnPhoneDialogCurvedText(text = text, style = style) },
+ )
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md b/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md
new file mode 100644
index 0000000..6b5828e
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md
@@ -0,0 +1,110 @@
+```
+/*
+ * Copyright 2022 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.pager.HorizontalPager
+import androidx.wear.compose.foundation.pager.VerticalPager
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.HorizontalPageIndicator
+import androidx.wear.compose.material3.HorizontalPagerScaffold
+import androidx.wear.compose.material3.PagerScaffoldDefaults
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.VerticalPageIndicator
+import androidx.wear.compose.material3.VerticalPagerScaffold
+
+@Sampled
+@Composable
+fun HorizontalPageIndicatorWithPagerSample(navigateBack: () -> Unit) {
+ val pageCount = 9
+ val pagerState = rememberPagerState { pageCount }
+
+ Box {
+ HorizontalPagerScaffold(
+ pagerState = pagerState,
+ pageIndicator = { HorizontalPageIndicator(pagerState = pagerState) },
+ ) {
+ HorizontalPager(
+ state = pagerState,
+ flingBehavior =
+ PagerScaffoldDefaults.snapWithSpringFlingBehavior(state = pagerState),
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe left and right")
+ if (page == 0) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(onClick = navigateBack) { Text("Exit") }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun VerticalPageIndicatorWithPagerSample() {
+ val pageCount = 9
+ val pagerState = rememberPagerState { pageCount }
+
+ Box {
+ VerticalPagerScaffold(
+ pagerState = pagerState,
+ pageIndicator = { VerticalPageIndicator(pagerState = pagerState) },
+ ) {
+ VerticalPager(
+ state = pagerState,
+ flingBehavior =
+ PagerScaffoldDefaults.snapWithSpringFlingBehavior(state = pagerState),
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe up and down")
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md b/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md
new file mode 100644
index 0000000..1d2aced
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md
@@ -0,0 +1,204 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.pager.HorizontalPager
+import androidx.wear.compose.foundation.pager.PagerDefaults
+import androidx.wear.compose.foundation.pager.VerticalPager
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.HorizontalPagerScaffold
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.PagerScaffoldDefaults
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.VerticalPagerScaffold
+
+@Sampled
+@Composable
+fun HorizontalPagerScaffoldSample(navigateBack: () -> Unit) {
+ AppScaffold {
+ val pagerState = rememberPagerState(pageCount = { 10 })
+
+ HorizontalPagerScaffold(pagerState = pagerState) {
+ HorizontalPager(
+ state = pagerState,
+ flingBehavior =
+ PagerDefaults.snapFlingBehavior(
+ state = pagerState,
+ maxFlingPages = 1,
+ snapPositionalThreshold = PagerScaffoldDefaults.HighSnapPositionalThreshold,
+ snapAnimationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
+ ),
+ rotaryScrollableBehavior = null,
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ ScreenScaffold {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe left and right")
+ if (page == 0) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(onClick = navigateBack) { Text("Exit") }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun HorizontalPagerScaffoldWithLowSensitivitySample(navigateBack: () -> Unit) {
+ AppScaffold {
+ val pagerState = rememberPagerState(pageCount = { 3 })
+
+ HorizontalPagerScaffold(pagerState = pagerState) {
+ HorizontalPager(
+ state = pagerState,
+ flingBehavior =
+ PagerDefaults.snapFlingBehavior(
+ state = pagerState,
+ maxFlingPages = 0,
+ snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold,
+ snapAnimationSpec = PagerDefaults.SnapAnimationSpec,
+ ),
+ rotaryScrollableBehavior =
+ RotaryScrollableDefaults.snapBehavior(
+ pagerState = pagerState,
+ snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity,
+ ),
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ ScreenScaffold {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe left and right")
+ if (page == 0) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(onClick = navigateBack) { Text("Exit") }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun VerticalPagerScaffoldSample() {
+ AppScaffold {
+ val pagerState = rememberPagerState(pageCount = { 10 })
+
+ VerticalPagerScaffold(pagerState = pagerState) {
+ VerticalPager(
+ state = pagerState,
+ flingBehavior =
+ PagerDefaults.snapFlingBehavior(
+ state = pagerState,
+ maxFlingPages = 1,
+ snapPositionalThreshold = PagerScaffoldDefaults.HighSnapPositionalThreshold,
+ snapAnimationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
+ ),
+ rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(pagerState),
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ ScreenScaffold {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe up and down")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun VerticalPagerScaffoldWithLowSensitivitySample() {
+ AppScaffold {
+ val pagerState = rememberPagerState(pageCount = { 3 })
+
+ VerticalPagerScaffold(pagerState = pagerState) {
+ VerticalPager(
+ state = pagerState,
+ flingBehavior =
+ PagerDefaults.snapFlingBehavior(
+ state = pagerState,
+ maxFlingPages = 0,
+ snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold,
+ snapAnimationSpec = PagerDefaults.SnapAnimationSpec,
+ ),
+ rotaryScrollableBehavior =
+ RotaryScrollableDefaults.snapBehavior(
+ pagerState = pagerState,
+ snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity,
+ ),
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ ScreenScaffold {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(text = "Page #$page")
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Swipe up and down")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/PickerGroupSample.kt.md b/wear/wear-compose-m3/references/material3/PickerGroupSample.kt.md
new file mode 100644
index 0000000..3eceda1
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/PickerGroupSample.kt.md
@@ -0,0 +1,139 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.PickerGroup
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.rememberPickerState
+
+@Sampled
+@Composable
+fun PickerGroupSample() {
+ var selectedPickerIndex by remember { mutableIntStateOf(0) }
+ val pickerStateHour = rememberPickerState(initialNumberOfOptions = 24)
+ val pickerStateMinute = rememberPickerState(initialNumberOfOptions = 60)
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.size(30.dp))
+ val label = if (selectedPickerIndex == 0) "Hours" else "Minutes"
+ AnimatedContent(targetState = label) { targetText -> Text(text = targetText) }
+ Spacer(modifier = Modifier.size(10.dp))
+ PickerGroup(
+ selectedPickerState =
+ if (selectedPickerIndex == 0) pickerStateHour else pickerStateMinute,
+ autoCenter = false,
+ ) {
+ PickerGroupItem(
+ pickerState = pickerStateHour,
+ selected = selectedPickerIndex == 0,
+ onSelected = { selectedPickerIndex = 0 },
+ option = { optionIndex, _ -> Text(text = "%02d".format(optionIndex)) },
+ contentDescription = { "Hours" },
+ modifier = Modifier.size(80.dp, 100.dp),
+ )
+
+ PickerGroupItem(
+ pickerState = pickerStateMinute,
+ selected = selectedPickerIndex == 1,
+ onSelected = { selectedPickerIndex = 1 },
+ option = { optionIndex, _ -> Text(text = "%02d".format(optionIndex)) },
+ contentDescription = { "Minutes" },
+ modifier = Modifier.size(80.dp, 100.dp),
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun AutoCenteringPickerGroup() {
+ var selectedPickerIndex by remember { mutableIntStateOf(0) }
+ val pickerStateHour = rememberPickerState(initialNumberOfOptions = 24)
+ val pickerStateMinute = rememberPickerState(initialNumberOfOptions = 60)
+ val pickerStateSeconds = rememberPickerState(initialNumberOfOptions = 60)
+ val pickerStateMilliSeconds = rememberPickerState(initialNumberOfOptions = 1000)
+ val pickerStates = remember {
+ arrayOf(pickerStateHour, pickerStateMinute, pickerStateSeconds, pickerStateMilliSeconds)
+ }
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ val headingText = mapOf(0 to "Hours", 1 to "Minutes", 2 to "Seconds", 3 to "Milli")
+ Spacer(modifier = Modifier.size(30.dp))
+ AnimatedContent(targetState = headingText[selectedPickerIndex]!!) { targetText ->
+ Text(text = targetText)
+ }
+ Spacer(modifier = Modifier.size(10.dp))
+ PickerGroup(selectedPickerState = pickerStates[selectedPickerIndex], autoCenter = true) {
+ PickerGroupItem(
+ pickerState = pickerStateHour,
+ selected = selectedPickerIndex == 0,
+ onSelected = { selectedPickerIndex = 0 },
+ option = { optionIndex, _ -> Text(text = "%02d".format(optionIndex)) },
+ contentDescription = { "Hours" },
+ modifier = Modifier.size(80.dp, 100.dp),
+ )
+ PickerGroupItem(
+ pickerState = pickerStateMinute,
+ selected = selectedPickerIndex == 1,
+ onSelected = { selectedPickerIndex = 1 },
+ option = { optionIndex, _ -> Text(text = "%02d".format(optionIndex)) },
+ contentDescription = { "Minutes" },
+ modifier = Modifier.size(80.dp, 100.dp),
+ )
+ PickerGroupItem(
+ pickerState = pickerStateSeconds,
+ selected = selectedPickerIndex == 2,
+ onSelected = { selectedPickerIndex = 2 },
+ option = { optionIndex, _ -> Text(text = "%02d".format(optionIndex)) },
+ contentDescription = { "Seconds" },
+ modifier = Modifier.size(80.dp, 100.dp),
+ )
+ PickerGroupItem(
+ pickerState = pickerStateMilliSeconds,
+ selected = selectedPickerIndex == 3,
+ onSelected = { selectedPickerIndex = 3 },
+ option = { optionIndex, _ -> Text(text = "%03d".format(optionIndex)) },
+ contentDescription = { "Milliseconds" },
+ modifier = Modifier.size(80.dp, 100.dp),
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/PickerSample.kt.md b/wear/wear-compose-m3/references/material3/PickerSample.kt.md
new file mode 100644
index 0000000..530b02f
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/PickerSample.kt.md
@@ -0,0 +1,111 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Picker
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.rememberPickerState
+import kotlinx.coroutines.launch
+
+@Sampled
+@Composable
+fun SimplePicker() {
+ val items = listOf("One", "Two", "Three", "Four", "Five")
+ val state = rememberPickerState(items.size)
+ // We forward scroll gestures from the whole screen to the Picker which makes this sample
+ // accessible for 2-finger vertical scrolling.
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ .scrollable(
+ state = state,
+ orientation = Orientation.Vertical,
+ reverseDirection = true,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ val selectedLabel by remember {
+ derivedStateOf { "Selected: ${items[state.selectedOptionIndex]}" }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.TopCenter).padding(top = 10.dp),
+ text = selectedLabel,
+ )
+ Picker(
+ modifier = Modifier.size(100.dp, 100.dp),
+ state = state,
+ contentDescription = { "${state.selectedOptionIndex + 1}" },
+ ) {
+ Text(items[it])
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun PickerScrollToOption() {
+ val coroutineScope = rememberCoroutineScope()
+ val state = rememberPickerState(initialNumberOfOptions = 10)
+ Picker(
+ state = state,
+ verticalSpacing = 4.dp,
+ contentDescription = { "${state.selectedOptionIndex + 1}" },
+ ) {
+ Button(
+ onClick = { coroutineScope.launch { state.scrollToOption(it) } },
+ label = { Text("$it") },
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun PickerAnimateScrollToOption() {
+ val coroutineScope = rememberCoroutineScope()
+ val state = rememberPickerState(initialNumberOfOptions = 10)
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Picker(
+ state = state,
+ verticalSpacing = 4.dp,
+ contentDescription = { "${state.selectedOptionIndex + 1}" },
+ ) {
+ Button(
+ onClick = { coroutineScope.launch { state.animateScrollToOption(it) } },
+ label = { Text("$it") },
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md b/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md
new file mode 100644
index 0000000..943bca1
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md
@@ -0,0 +1,182 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.placeholder
+import androidx.wear.compose.material3.placeholderShimmer
+import androidx.wear.compose.material3.rememberPlaceholderState
+import kotlinx.coroutines.delay
+
+/**
+ * This sample applies placeholders directly over the content that is waiting to be loaded. This
+ * approach is suitable for situations where the developer has an approximate knowledge of how big
+ * the content is going to be and it doesn't have cached data that can be shown.
+ */
+@Sampled
+@Composable
+fun ButtonWithIconAndLabelAndPlaceholders() {
+ var labelText by remember { mutableStateOf("") }
+ var imageVector: ImageVector? by remember { mutableStateOf(null) }
+ val buttonPlaceholderState =
+ rememberPlaceholderState(isVisible = labelText.isEmpty() || imageVector == null)
+
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState),
+ label = {
+ Text(
+ text = labelText,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.fillMaxWidth().placeholder(buttonPlaceholderState),
+ )
+ },
+ icon = {
+ Box(
+ modifier =
+ Modifier.size(ButtonDefaults.IconSize).placeholder(buttonPlaceholderState)
+ ) {
+ if (imageVector != null) {
+ Icon(
+ imageVector = imageVector!!,
+ contentDescription = "Heart",
+ modifier =
+ Modifier.wrapContentSize(align = Alignment.Center)
+ .size(ButtonDefaults.IconSize)
+ .fillMaxSize(),
+ )
+ }
+ }
+ },
+ )
+ // Simulate content loading completing in stages
+ LaunchedEffect(Unit) {
+ delay(2000)
+ imageVector = Icons.Filled.Favorite
+ delay(1000)
+ labelText = "A label"
+ }
+}
+
+/**
+ * This sample doesn't use placeholders for the label as there is some cached data that can be shown
+ * while loading.
+ */
+@Sampled
+@Composable
+fun ButtonWithIconAndLabelCachedData() {
+ var labelText by remember { mutableStateOf("Cached Data") }
+ var imageVector: ImageVector? by remember { mutableStateOf(null) }
+ val buttonPlaceholderState =
+ rememberPlaceholderState(isVisible = labelText.isEmpty() || imageVector == null)
+
+ // Put placeholderShimmer in the container and placeholder in the elements of the content that
+ // have no cached data.
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState),
+ label = {
+ Text(
+ text = labelText,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ },
+ icon = {
+ Box(
+ modifier =
+ Modifier.size(ButtonDefaults.IconSize).placeholder(buttonPlaceholderState)
+ ) {
+ if (imageVector != null) {
+ Icon(
+ imageVector = imageVector!!,
+ contentDescription = "Heart",
+ modifier =
+ Modifier.wrapContentSize(align = Alignment.Center)
+ .size(ButtonDefaults.IconSize)
+ .fillMaxSize(),
+ )
+ }
+ }
+ },
+ )
+ // Simulate content loading completing in stages
+ LaunchedEffect(Unit) {
+ delay(2000)
+ imageVector = Icons.Filled.Favorite
+ delay(1000)
+ labelText = "A label"
+ }
+}
+
+/**
+ * This sample applies a placeholder and placeholderShimmer directly over a single composable.
+ *
+ * Note that the modifier ordering is important, the placeholderShimmer must be before the
+ * placeholder in the modifier chain - otherwise the shimmer will be drawn underneath the
+ * placeholder and will not be visible.
+ */
+@Sampled
+@Composable
+fun TextPlaceholder() {
+ var labelText by remember { mutableStateOf("") }
+ val placeholderState = rememberPlaceholderState(isVisible = labelText.isEmpty())
+
+ Text(
+ text = labelText,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ modifier =
+ Modifier.width(90.dp).placeholderShimmer(placeholderState).placeholder(placeholderState),
+ )
+
+ // Simulate content loading
+ LaunchedEffect(Unit) {
+ delay(3000)
+ labelText = "A label"
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md b/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md
new file mode 100644
index 0000000..97332f1
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md
@@ -0,0 +1,263 @@
+```
+/*
+ * Copyright 2022 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.ArcProgressIndicator
+import androidx.wear.compose.material3.ArcProgressIndicatorDefaults
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.CircularProgressIndicatorDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButton
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ProgressIndicatorDefaults
+import androidx.wear.compose.material3.SegmentedCircularProgressIndicator
+import androidx.wear.compose.material3.drawCircularProgressIndicator
+
+@Sampled
+@Composable
+fun FullScreenProgressIndicatorSample() {
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ CircularProgressIndicator(progress = { 0.25f }, startAngle = 120f, endAngle = 60f)
+ }
+}
+
+@Sampled
+@Composable
+fun MediaButtonProgressIndicatorSample() {
+ var isPlaying by remember { mutableStateOf(false) }
+ val buttonPadding = 4.dp
+ val progressStrokeWidth = 4.dp
+ val progress = 0.75f
+
+ Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
+ // The CircularProgressIndicator should be around the IconButton, with an extra gap between
+ // then of 'buttonPadding'. We multiply by 2 because the size includes progressStrokeWidth
+ // at top and bottom and the buttonPadding at top and bottom.
+ CircularProgressIndicator(
+ modifier =
+ Modifier.align(Alignment.Center)
+ .size(
+ IconButtonDefaults.DefaultButtonSize +
+ progressStrokeWidth * 2 +
+ buttonPadding * 2
+ ),
+ progress = { progress },
+ strokeWidth = progressStrokeWidth,
+ )
+
+ IconButton(
+ modifier =
+ Modifier.align(Alignment.Center)
+ .semantics {
+ // Set custom progress semantics for accessibility.
+ contentDescription =
+ String.format(
+ "Play/pause button, track progress: %.0f%%",
+ progress * 100,
+ )
+ }
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surfaceContainerLow),
+ onClick = { isPlaying = !isPlaying },
+ ) {
+ Icon(
+ imageVector = if (isPlaying) Icons.Filled.Close else Icons.Filled.PlayArrow,
+ contentDescription = null,
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun OverflowProgressIndicatorSample() {
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ CircularProgressIndicator(
+ // Overflow value of 120%
+ progress = { 1.2f },
+ allowProgressOverflow = true,
+ startAngle = 120f,
+ endAngle = 60f,
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun SmallValuesProgressIndicatorSample() {
+ Box {
+ CircularProgressIndicator(
+ // Small progress values like 2% will be rounded up to at least the stroke width.
+ progress = { 0.02f },
+ modifier =
+ Modifier.fillMaxSize().padding(CircularProgressIndicatorDefaults.FullScreenPadding),
+ startAngle = 120f,
+ endAngle = 60f,
+ strokeWidth = 10.dp,
+ colors =
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = Color.Green,
+ trackColor = Color.White,
+ ),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun CircularProgressIndicatorCustomAnimationSample() {
+ val animatedProgress = remember { Animatable(0f) }
+ val colors =
+ ProgressIndicatorDefaults.colors(indicatorColor = Color.Green, trackColor = Color.White)
+
+ LaunchedEffect(Unit) {
+ animatedProgress.animateTo(1f, tween(durationMillis = 1024, easing = LinearEasing))
+ animatedProgress.animateTo(0f, tween(durationMillis = 1024, easing = LinearEasing))
+ }
+
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ // Draw the circular progress indicator with custom animation
+ Spacer(
+ Modifier.fillMaxSize().focusable().drawBehind {
+ drawCircularProgressIndicator(
+ progress = animatedProgress.value,
+ strokeWidth = 10.dp,
+ colors = colors,
+ startAngle = 120f,
+ endAngle = 60f,
+ )
+ }
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun IndeterminateProgressIndicatorSample() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+}
+
+@Sampled
+@Composable
+fun IndeterminateProgressArcSample() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ ArcProgressIndicator(
+ modifier =
+ Modifier.align(Alignment.Center)
+ .size(ArcProgressIndicatorDefaults.recommendedIndeterminateDiameter)
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun SegmentedProgressIndicatorSample() {
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ SegmentedCircularProgressIndicator(segmentCount = 5, progress = { 0.5f })
+ }
+}
+
+@Sampled
+@Composable
+fun SegmentedProgressIndicatorBinarySample() {
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ SegmentedCircularProgressIndicator(segmentCount = 5, segmentValue = { it % 2 != 0 })
+ }
+}
+
+@Sampled
+@Composable
+fun SmallSegmentedProgressIndicatorSample() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SegmentedCircularProgressIndicator(
+ segmentCount = 6,
+ progress = { 0.75f },
+ modifier = Modifier.align(Alignment.Center).size(80.dp),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun SmallSegmentedProgressIndicatorBinarySample() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SegmentedCircularProgressIndicator(
+ segmentCount = 8,
+ segmentValue = { it % 2 != 0 },
+ modifier = Modifier.align(Alignment.Center).size(80.dp),
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md b/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md
new file mode 100644
index 0000000..3c1b941
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md
@@ -0,0 +1,109 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.RadioButton
+import androidx.wear.compose.material3.SplitRadioButton
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Preview
+@Composable
+fun RadioButtonSample() {
+ Column(modifier = Modifier.selectableGroup().fillMaxSize()) {
+ var selectedButton by remember { mutableStateOf(0) }
+ // RadioButton uses the Radio selection control by default.
+ RadioButton(
+ label = { Text("Radio button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ secondaryLabel = {
+ Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis)
+ },
+ selected = selectedButton == 0,
+ onSelect = { selectedButton = 0 },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") },
+ enabled = true,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ RadioButton(
+ label = { Text("Radio button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ secondaryLabel = {
+ Text("With secondary label", maxLines = 3, overflow = TextOverflow.Ellipsis)
+ },
+ selected = selectedButton == 1,
+ onSelect = { selectedButton = 1 },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") },
+ enabled = true,
+ )
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun SplitRadioButtonSample() {
+ Column(modifier = Modifier.selectableGroup().padding(horizontal = 10.dp)) {
+ var selectedButton by remember { mutableStateOf(0) }
+ // SplitRadioButton uses the Radio selection control by default.
+ SplitRadioButton(
+ label = { Text("First Button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ selected = selectedButton == 0,
+ onSelectionClick = { selectedButton = 0 },
+ selectionContentDescription = "First",
+ onContainerClick = {
+ /* Do something */
+ },
+ containerClickLabel = "click",
+ modifier = Modifier.fillMaxWidth(),
+ enabled = true,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ SplitRadioButton(
+ label = { Text("Second Button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ selected = selectedButton == 1,
+ onSelectionClick = { selectedButton = 1 },
+ selectionContentDescription = "Second",
+ onContainerClick = {
+ /* Do something */
+ },
+ containerClickLabel = "click",
+ modifier = Modifier.fillMaxWidth(),
+ enabled = true,
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md b/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md
new file mode 100644
index 0000000..9620d69
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md
@@ -0,0 +1,146 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.rememberOverscrollEffect
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+
+@Preview
+@Sampled
+@Composable
+fun ScaffoldSample() {
+ // Declare just one [AppScaffold] per app such as in the activity.
+ // [AppScaffold] allows static screen elements (i.e. [TimeText]) to remain visible
+ // during in-app transitions such as swipe-to-dismiss.
+ AppScaffold {
+ val transformationSpec = rememberTransformationSpec()
+
+ // Define the navigation hierarchy within the AppScaffold,
+ // such as using SwipeDismissableNavHost.
+ // For this sample, we will define a single screen inline.
+ val listState = rememberTransformingLazyColumnState()
+
+ // By default, ScreenScaffold will handle transitions showing/hiding ScrollIndicator,
+ // showing/hiding/scrolling away TimeText and optionally hosting the EdgeButton.
+ ScreenScaffold(scrollState = listState) { contentPadding ->
+ TransformingLazyColumn(
+ state = listState,
+ contentPadding = contentPadding,
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ items(10) {
+ Button(
+ onClick = {},
+ label = { Text("Item ${it + 1}") },
+ transformation = SurfaceTransformation(transformationSpec),
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .fillMaxWidth(),
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Sampled
+@Composable
+fun ScaffoldWithTLCEdgeButtonSample() {
+ // Declare just one [AppScaffold] per app such as in the activity.
+ // [AppScaffold] allows static screen elements (i.e. [TimeText]) to remain visible
+ // during in-app transitions such as swipe-to-dismiss.
+ AppScaffold(modifier = Modifier.background(Color.Black)) {
+ val transformationSpec = rememberTransformationSpec()
+
+ // Define the navigation hierarchy within the AppScaffold,
+ // such as using SwipeDismissableNavHost.
+ // For this sample, we will define a single screen inline.
+ val listState = rememberTransformingLazyColumnState()
+ // By default, ScreenScaffold will handle transitions showing/hiding ScrollIndicator,
+ // showing/hiding/scrolling away TimeText and optionally hosting the EdgeButton.
+ ScreenScaffold(
+ scrollState = listState,
+ // Define custom spacing between [EdgeButton] and [TransformingLazyColumn].
+ edgeButtonSpacing = 15.dp,
+ edgeButton = {
+ EdgeButton(
+ onClick = {},
+ modifier =
+ // In case user starts scrolling from the EdgeButton.
+ Modifier.scrollable(
+ listState,
+ orientation = Orientation.Vertical,
+ reverseDirection = true,
+ // An overscroll effect should be applied to the EdgeButton for proper
+ // scrolling behavior.
+ overscrollEffect = rememberOverscrollEffect(),
+ ),
+ ) {
+ Text("Clear All")
+ }
+ },
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = listState,
+ modifier = Modifier.fillMaxSize(),
+ // Bottom spacing is derived from [ScreenScaffold.edgeButtonSpacing].
+ contentPadding = contentPadding,
+ ) {
+ items(4) {
+ Button(
+ onClick = {},
+ label = { Text("Item ${it + 1}") },
+ transformation = SurfaceTransformation(transformationSpec),
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .fillMaxWidth(),
+ )
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md b/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md
new file mode 100644
index 0000000..7205759
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md
@@ -0,0 +1,97 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ScrollInfoProvider
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListHeaderDefaults
+import androidx.wear.compose.material3.ScreenStage
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.curvedText
+import androidx.wear.compose.material3.scrollAway
+import androidx.wear.compose.material3.timeTextSeparator
+
+@Sampled
+@Composable
+fun ScrollAwaySample() {
+ val state = rememberTransformingLazyColumnState()
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ TransformingLazyColumn(state = state, modifier = Modifier.fillMaxSize()) {
+ item {
+ ListHeader(
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ ListHeaderDefaults.minimumTopListContentPadding,
+ ListHeaderDefaults.minimumBottomListContentPadding,
+ )
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = "TLC",
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+ items(50) {
+ FilledTonalButton(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 36.dp)
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ ),
+ onClick = {},
+ label = { Text("Item ${it + 1}") },
+ )
+ }
+ }
+ TimeText(
+ // In practice, it is recommended to use the [AppScaffold] and [ScreenScaffold],
+ // so that the Material3 scroll away behavior is provided by default, rather than using
+ // [Modifier.scrollAway] directly.
+ modifier =
+ Modifier.scrollAway(
+ scrollInfoProvider = ScrollInfoProvider(state),
+ screenStage = {
+ if (state.isScrollInProgress) ScreenStage.Scrolling else ScreenStage.Idle
+ },
+ ),
+ content = { time ->
+ curvedText("List")
+ timeTextSeparator()
+ curvedText(time)
+ },
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md b/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md
new file mode 100644
index 0000000..95f8ab8
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md
@@ -0,0 +1,66 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.ScreenScaffoldDefaults
+import androidx.wear.compose.material3.ScrollIndicator
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimeText
+
+@Sampled
+@Preview
+@Composable
+fun ScrollIndicatorWithTLCSample() {
+ val scrollState = rememberTransformingLazyColumnState()
+ Box(modifier = Modifier.fillMaxSize()) {
+ TransformingLazyColumn(
+ modifier = Modifier.background(Color.Black),
+ state = scrollState,
+ contentPadding = ScreenScaffoldDefaults.contentPadding,
+ ) {
+ items(15) {
+ Button(
+ onClick = {},
+ label = { Text("Button $it") },
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .fillMaxWidth(),
+ )
+ }
+ }
+ ScrollIndicator(modifier = Modifier.align(Alignment.CenterEnd), state = scrollState)
+ TimeText()
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/SliderSample.kt.md b/wear/wear-compose-m3/references/material3/SliderSample.kt.md
new file mode 100644
index 0000000..25bb251
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/SliderSample.kt.md
@@ -0,0 +1,86 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.wear.compose.material3.Slider
+import androidx.wear.compose.material3.SliderDefaults
+
+@Sampled
+@Composable
+fun SliderSample() {
+ var value by remember { mutableStateOf(4.5f) }
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 3f..6f,
+ steps = 5,
+ segmented = false,
+ )
+}
+
+@Sampled
+@Composable
+fun ChangedSliderSample() {
+ val initialValue = 4.5f
+ var value by remember { mutableStateOf(5.0f) }
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 3f..6f,
+ steps = 5,
+ segmented = false,
+ colors =
+ if (value == initialValue) {
+ SliderDefaults.sliderColors()
+ } else {
+ SliderDefaults.variantSliderColors()
+ },
+ )
+}
+
+@Sampled
+@Composable
+fun SliderSegmentedSample() {
+ var value by remember { mutableStateOf(2f) }
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 1f..4f,
+ steps = 2,
+ segmented = true,
+ )
+}
+
+@Sampled
+@Composable
+fun SliderWithIntegerSample() {
+ var value by remember { mutableStateOf(4) }
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ valueProgression = 0..10,
+ segmented = false,
+ )
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/StepperSample.kt.md b/wear/wear-compose-m3/references/material3/StepperSample.kt.md
new file mode 100644
index 0000000..ea7c164
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/StepperSample.kt.md
@@ -0,0 +1,148 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Stepper
+import androidx.wear.compose.material3.StepperDefaults
+import androidx.wear.compose.material3.StepperLevelIndicator
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.rangeSemantics
+import androidx.wear.compose.material3.samples.icons.HeadphoneIcon
+import androidx.wear.compose.material3.samples.icons.VolumeDownIcon
+import androidx.wear.compose.material3.samples.icons.VolumeUpIcon
+
+@Sampled
+@Composable
+fun StepperSample() {
+ var value by remember { mutableFloatStateOf(2f) }
+ val valueRange = remember { 0f..4f }
+ Box(modifier = Modifier.fillMaxSize()) {
+ Stepper(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = valueRange,
+ steps = 7,
+ decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) },
+ increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) },
+ ) {
+ Text(String.format("Value: %.1f".format(value)))
+ }
+ StepperLevelIndicator(
+ value = { value },
+ valueRange = valueRange,
+ modifier = Modifier.align(Alignment.CenterStart),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun StepperWithIntegerSample() {
+ var value by remember { mutableIntStateOf(3) }
+ val valueProgression = remember { 0..10 }
+ Box(modifier = Modifier.fillMaxSize()) {
+ Stepper(
+ value = value,
+ onValueChange = { value = it },
+ valueProgression = valueProgression,
+ decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) },
+ increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) },
+ ) {
+ Text(String.format("Value: %d".format(value)))
+ }
+ StepperLevelIndicator(
+ value = { value },
+ valueProgression = valueProgression,
+ modifier = Modifier.align(Alignment.CenterStart),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun StepperWithRangeSemanticsSample() {
+ var value by remember { mutableFloatStateOf(2f) }
+ val valueRange = remember { 0f..4f }
+ val onValueChange = { i: Float -> value = i }
+ val steps = 7
+ Box(modifier = Modifier.fillMaxSize()) {
+ Stepper(
+ value = value,
+ onValueChange = onValueChange,
+ valueRange = valueRange,
+ modifier = Modifier.rangeSemantics(value, true, onValueChange, valueRange, steps),
+ steps = steps,
+ decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) },
+ increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) },
+ ) {
+ Text("Value: $value")
+ }
+ StepperLevelIndicator(
+ value = { value },
+ valueRange = valueRange,
+ modifier = Modifier.align(Alignment.CenterStart),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun StepperWithButtonSample() {
+ var value by remember { mutableFloatStateOf(2f) }
+ val valueRange = remember { 0f..4f }
+ Box(modifier = Modifier.fillMaxSize()) {
+ Stepper(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = valueRange,
+ increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) },
+ decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) },
+ steps = 7,
+ ) {
+ Text(String.format("Value: %.1f".format(value)))
+ Button(
+ onClick = {},
+ modifier = Modifier.width(150.dp),
+ label = { Text(text = "This watch", modifier = Modifier.fillMaxWidth()) },
+ secondaryLabel = { Text(text = "Headphones", modifier = Modifier.fillMaxWidth()) },
+ icon = { HeadphoneIcon(24.dp) },
+ )
+ }
+ StepperLevelIndicator(
+ value = { value },
+ valueRange = valueRange,
+ modifier = Modifier.align(Alignment.CenterStart),
+ )
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md b/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md
new file mode 100644
index 0000000..6b3b7fa
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md
@@ -0,0 +1,165 @@
+```
+/*
+ * 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TitleCard
+import androidx.wear.compose.material3.lazy.ResponsiveTransformationSpec
+import androidx.wear.compose.material3.lazy.TransformationVariableSpec
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+
+@Sampled
+@Preview
+@Composable
+fun SurfaceTransformationOnCustomComponent() {
+ @Composable
+ fun MyCardComponent(
+ title: String,
+ body: String,
+ transformation: SurfaceTransformation,
+ modifier: Modifier = Modifier,
+ ) {
+ Column(
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .paint(
+ transformation.createContainerPainter(
+ ColorPainter(color = Color.Gray),
+ shape = RoundedCornerShape(16.dp),
+ )
+ )
+ .graphicsLayer { with(transformation) { applyContainerTransformation() } }
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ Text(title)
+ Text(body)
+ }
+ }
+
+ val transformationSpec = rememberTransformationSpec()
+
+ TransformingLazyColumn {
+ items(count = 100) {
+ MyCardComponent(
+ "Message #$it",
+ "This is a body",
+ transformation = SurfaceTransformation(transformationSpec),
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ ),
+ )
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun SurfaceTransformationButtonSample() {
+ val transformationSpec =
+ rememberTransformationSpec(
+ ResponsiveTransformationSpec.smallScreen(
+ contentAlpha =
+ TransformationVariableSpec(
+ 0f,
+ transformationZoneEnterFraction = 0.4f,
+ transformationZoneExitFraction = 0.8f,
+ ),
+ containerAlpha = TransformationVariableSpec(0.3f),
+ )
+ )
+
+ TransformingLazyColumn {
+ items(count = 100) {
+ Button(
+ onClick = {},
+ transformation = SurfaceTransformation(transformationSpec),
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ ),
+ ) {
+ Text("Button #$it")
+ }
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun SurfaceTransformationCardSample() {
+ val transformationSpec = rememberTransformationSpec()
+ var expandedIndex by remember { mutableIntStateOf(-1) }
+ val state = rememberTransformingLazyColumnState()
+
+ AppScaffold {
+ ScreenScaffold(state) { contentPadding ->
+ TransformingLazyColumn(state = state, contentPadding = contentPadding) {
+ items(count = 100) {
+ TitleCard(
+ onClick = { expandedIndex = if (expandedIndex == it) -1 else it },
+ title = { Text("Card #$it") },
+ subtitle = { Text("Subtitle #$it") },
+ transformation = SurfaceTransformation(transformationSpec),
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ ),
+ ) {
+ if (it == expandedIndex) {
+ Text("Expanded content #$it")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md b/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md
new file mode 100644
index 0000000..75c908d
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md
@@ -0,0 +1,171 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.SwipeToDismissValue
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
+import androidx.wear.compose.material3.CheckboxButton
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.SwipeToDismissBox
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Composable
+fun SimpleSwipeToDismissBox(navigateBack: () -> Unit) {
+ SwipeToDismissBox(onDismissed = navigateBack) { isBackground ->
+ if (isBackground) {
+ Box(
+ modifier =
+ Modifier.fillMaxSize().background(MaterialTheme.colorScheme.secondaryContainer)
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.primary),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary)
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun StatefulSwipeToDismissBox() {
+ // State for managing a 2-level navigation hierarchy between
+ // MainScreen and ItemScreen composables.
+ // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation.
+ var showMainScreen by remember { mutableStateOf(true) }
+ val saveableStateHolder = rememberSaveableStateHolder()
+
+ // Swipe gesture dismisses ItemScreen to return to MainScreen.
+ val state = rememberSwipeToDismissBoxState()
+ LaunchedEffect(state.currentValue) {
+ if (state.currentValue == SwipeToDismissValue.Dismissed) {
+ state.snapTo(SwipeToDismissValue.Default)
+ showMainScreen = !showMainScreen
+ }
+ }
+
+ // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind
+ // the ItemScreen, otherwise there's no background to show.
+ SwipeToDismissBox(
+ state = state,
+ userSwipeEnabled = !showMainScreen,
+ backgroundKey = if (!showMainScreen) "MainKey" else "Background",
+ contentKey = if (showMainScreen) "MainKey" else "ItemKey",
+ ) { isBackground ->
+ if (isBackground || showMainScreen) {
+ // Best practice would be to use State Hoisting and leave this composable stateless.
+ // Here, we want to support MainScreen being shown from different destinations
+ // (either in the foreground or in the background during swiping) - that can be achieved
+ // using SaveableStateHolder and rememberSaveable as shown below.
+ saveableStateHolder.SaveableStateProvider(
+ key = "MainKey",
+ content = {
+ // Composable that maintains its own state
+ // and can be shown in foreground or background.
+ val checked = rememberSaveable { mutableStateOf(true) }
+ Column(
+ modifier =
+ Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
+ ) {
+ FilledTonalButton(
+ onClick = { showMainScreen = false },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text("Item details")
+ }
+ CheckboxButton(
+ label = { Text("Checkbox", maxLines = 1) },
+ checked = checked.value,
+ onCheckedChange = { checked.value = it },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ },
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.primary),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text("Show details here...", color = MaterialTheme.colorScheme.onPrimary)
+ Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary)
+ }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun EdgeSwipeForSwipeToDismiss(navigateBack: () -> Unit) {
+ val state = rememberSwipeToDismissBoxState()
+
+ // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the
+ // modifier applies exists within a SwipeToDismissBox which shares the same state.
+ SwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground ->
+ val horizontalScrollState = rememberScrollState(0)
+ if (isBackground) {
+ Box(
+ modifier =
+ Modifier.fillMaxSize().background(MaterialTheme.colorScheme.secondaryContainer)
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ modifier =
+ Modifier.align(Alignment.Center)
+ .edgeSwipeToDismiss(state)
+ .horizontalScroll(horizontalScrollState),
+ text =
+ "This text can be scrolled horizontally - to dismiss, swipe " +
+ "right from the left edge of the screen (called Edge Swiping)",
+ )
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt.md b/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt.md
new file mode 100644
index 0000000..faf8c70
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt.md
@@ -0,0 +1,364 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.items
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Card
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.RevealValue
+import androidx.wear.compose.material3.SwipeToReveal
+import androidx.wear.compose.material3.SwipeToRevealDefaults
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TitleCard
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+import androidx.wear.compose.material3.rememberRevealState
+import kotlinx.coroutines.launch
+
+@Composable
+@Sampled
+fun SwipeToRevealSample() {
+ SwipeToReveal(
+ primaryAction = {
+ PrimaryActionButton(
+ onClick = { /* This block is called when the primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ text = { Text("Delete") },
+ )
+ },
+ onSwipePrimaryAction = { /* This block is called when the full swipe gesture is performed. */
+ },
+ secondaryAction = {
+ SecondaryActionButton(
+ onClick = { /* This block is called when the secondary action is executed. */ },
+ icon = { Icon(Icons.Outlined.MoreVert, contentDescription = "Options") },
+ )
+ },
+ undoPrimaryAction = {
+ UndoActionButton(
+ onClick = { /* This block is called when the undo primary action is executed. */ },
+ text = { Text("Undo Delete") },
+ )
+ },
+ ) {
+ Button(
+ modifier =
+ Modifier.fillMaxWidth().semantics {
+ // Use custom actions to make the primary and secondary actions accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ /* Add the primary action click handler here */
+ true
+ },
+ CustomAccessibilityAction("Options") {
+ /* Add the secondary click handler here */
+ true
+ },
+ )
+ },
+ onClick = {},
+ ) {
+ Text("This Button has two actions", modifier = Modifier.fillMaxSize())
+ }
+ }
+}
+
+@Composable
+@Sampled
+fun SwipeToRevealSingleActionCardSample() {
+ SwipeToReveal(
+ primaryAction = {
+ PrimaryActionButton(
+ onClick = { /* This block is called when the primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ text = { Text("Delete") },
+ modifier = Modifier.height(SwipeToRevealDefaults.LargeActionButtonHeight),
+ )
+ },
+ onSwipePrimaryAction = { /* This block is called when the full swipe gesture is performed. */
+ },
+ undoPrimaryAction = {
+ UndoActionButton(
+ onClick = { /* This block is called when the undo primary action is executed. */ },
+ text = { Text("Undo Delete") },
+ )
+ },
+ ) {
+ Card(
+ modifier =
+ Modifier.fillMaxWidth().semantics {
+ // Use custom actions to make the primary action accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ /* Add the primary action click handler here */
+ true
+ }
+ )
+ },
+ onClick = {},
+ ) {
+ Text(
+ "This Card has one action, and the revealed button is taller",
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+@Sampled
+fun SwipeToRevealWithTransformingLazyColumnSample() {
+ val transformationSpec = rememberTransformationSpec()
+ val tlcState = rememberTransformingLazyColumnState()
+ val coroutineScope = rememberCoroutineScope()
+ val messages = remember {
+ mutableStateListOf().apply {
+ for (i in 1..100) {
+ add("Message #${i}")
+ }
+ }
+ }
+
+ TransformingLazyColumn(
+ state = tlcState,
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
+ modifier = Modifier.background(Color.Black),
+ ) {
+ items(items = messages, key = { it }) { message ->
+ val revealState = rememberRevealState()
+
+ // SwipeToReveal should be reset to covered when scrolling occurs.
+ LaunchedEffect(tlcState.isScrollInProgress) {
+ if (
+ tlcState.isScrollInProgress && revealState.currentValue != RevealValue.Covered
+ ) {
+ coroutineScope.launch {
+ revealState.animateTo(targetValue = RevealValue.Covered)
+ }
+ }
+ }
+
+ SwipeToReveal(
+ primaryAction = {
+ PrimaryActionButton(
+ onClick = { messages.remove(message) },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ text = { Text("Delete") },
+ modifier = Modifier.height(SwipeToRevealDefaults.LargeActionButtonHeight),
+ )
+ },
+ revealState = revealState,
+ onSwipePrimaryAction = { messages.remove(message) },
+ modifier =
+ Modifier.transformedHeight(this@items, transformationSpec)
+ .animateItem()
+ .graphicsLayer {
+ with(transformationSpec) {
+ applyContainerTransformation(scrollProgress)
+ }
+ // Is needed to disable clipping.
+ compositingStrategy = CompositingStrategy.ModulateAlpha
+ clip = false
+ }
+ .minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ ),
+ ) {
+ TitleCard(
+ onClick = {},
+ title = { Text(message) },
+ subtitle = { Text("Subtitle") },
+ modifier =
+ Modifier.semantics {
+ // Use custom actions to make the primary action accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ messages.remove(message)
+ true
+ }
+ )
+ },
+ ) {
+ Text("Message body which extends over multiple lines to extend the card")
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+@Sampled
+fun SwipeToRevealWithScalingLazyColumnSample() {
+ val slcState = rememberScalingLazyListState()
+ val coroutineScope = rememberCoroutineScope()
+ val messages = remember {
+ mutableStateListOf().apply {
+ for (i in 1..100) {
+ add("This Button $i has two actions")
+ }
+ }
+ }
+
+ ScalingLazyColumn(
+ state = slcState,
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
+ modifier = Modifier.background(Color.Black),
+ ) {
+ items(items = messages, key = { it }) { message ->
+ val revealState = rememberRevealState()
+
+ // SwipeToReveal should be reset to covered when scrolling occurs.
+ LaunchedEffect(slcState.isScrollInProgress) {
+ if (
+ slcState.isScrollInProgress && revealState.currentValue != RevealValue.Covered
+ ) {
+ coroutineScope.launch {
+ revealState.animateTo(targetValue = RevealValue.Covered)
+ }
+ }
+ }
+
+ SwipeToReveal(
+ revealState = revealState,
+ primaryAction = {
+ PrimaryActionButton(
+ onClick = { messages.remove(message) },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ text = { Text("Delete") },
+ )
+ },
+ onSwipePrimaryAction = { messages.remove(message) },
+ secondaryAction = {
+ SecondaryActionButton(
+ onClick = { /* This block is called when the secondary action is executed. */
+ },
+ icon = { Icon(Icons.Outlined.MoreVert, contentDescription = "Options") },
+ )
+ },
+ ) {
+ Button(
+ modifier =
+ Modifier.fillMaxWidth().semantics {
+ // Use custom actions to make the primary and secondary actions
+ // accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ messages.remove(message)
+ true
+ },
+ CustomAccessibilityAction("Options") {
+ /* Add the secondary click handler here */
+ true
+ },
+ )
+ },
+ onClick = {},
+ ) {
+ Text(message, modifier = Modifier.fillMaxSize())
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+@Sampled
+fun SwipeToRevealNoPartialRevealWithScalingLazyColumnSample() {
+ val slcState = rememberScalingLazyListState()
+ val messages = remember {
+ mutableStateListOf().apply {
+ for (i in 1..100) {
+ add("Message #${i}")
+ }
+ }
+ }
+
+ ScalingLazyColumn(
+ state = slcState,
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
+ modifier = Modifier.background(Color.Black),
+ ) {
+ items(items = messages, key = { it }) { message ->
+ SwipeToReveal(
+ hasPartiallyRevealedState = false,
+ primaryAction = {
+ PrimaryActionButton(
+ onClick = { messages.remove(message) },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ text = { Text("Delete") },
+ )
+ },
+ onSwipePrimaryAction = { messages.remove(message) },
+ ) {
+ Button(
+ modifier =
+ Modifier.fillMaxWidth().semantics {
+ // Use custom actions to make the primary action accessible
+ customActions =
+ listOf(
+ CustomAccessibilityAction("Delete") {
+ messages.remove(message)
+ true
+ }
+ )
+ },
+ onClick = {},
+ ) {
+ Text(message, modifier = Modifier.fillMaxSize())
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md b/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md
new file mode 100644
index 0000000..9e87918
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md
@@ -0,0 +1,68 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.SplitSwitchButton
+import androidx.wear.compose.material3.SwitchButton
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Preview
+@Composable
+fun SwitchButtonSample() {
+ var checked by remember { mutableStateOf(true) }
+ SwitchButton(
+ label = { Text("Switch Button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ secondaryLabel = {
+ Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis)
+ },
+ checked = checked,
+ onCheckedChange = { checked = it },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") },
+ enabled = true,
+ )
+}
+
+@Sampled
+@Preview
+@Composable
+fun SplitSwitchButtonSample() {
+ var checked by remember { mutableStateOf(true) }
+ SplitSwitchButton(
+ label = { Text("Split Switch Button", maxLines = 3, overflow = TextOverflow.Ellipsis) },
+ checked = checked,
+ onCheckedChange = { checked = it },
+ toggleContentDescription = "Split Switch Button Sample",
+ onContainerClick = {
+ /* Do something */
+ },
+ enabled = true,
+ )
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md b/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md
new file mode 100644
index 0000000..cf507f2
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md
@@ -0,0 +1,115 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TextButton
+import androidx.wear.compose.material3.TextButtonDefaults
+
+@Composable
+@Sampled
+fun TextButtonSample() {
+ TextButton(onClick = { /* Do something */ }) { Text(text = "ABC") }
+}
+
+@Composable
+@Sampled
+fun FilledTextButtonSample() {
+ TextButton(
+ onClick = { /* Do something */ },
+ colors = TextButtonDefaults.filledTextButtonColors(),
+ ) {
+ Text(text = "ABC")
+ }
+}
+
+@Composable
+@Sampled
+fun FilledVariantTextButtonSample() {
+ TextButton(
+ onClick = { /* Do something */ },
+ colors = TextButtonDefaults.filledVariantTextButtonColors(),
+ ) {
+ Text(text = "ABC")
+ }
+}
+
+@Composable
+@Sampled
+fun LargeFilledTonalTextButtonSample() {
+ TextButton(
+ onClick = { /* Do something */ },
+ colors = TextButtonDefaults.filledTonalTextButtonColors(),
+ modifier = Modifier.size(TextButtonDefaults.LargeButtonSize),
+ ) {
+ Text(text = "ABC", style = TextButtonDefaults.largeButtonTextStyle)
+ }
+}
+
+@Composable
+@Sampled
+fun FilledTonalTextButtonSample() {
+ TextButton(
+ onClick = { /* Do something */ },
+ colors = TextButtonDefaults.filledTonalTextButtonColors(),
+ ) {
+ Text(text = "ABC")
+ }
+}
+
+@Composable
+@Sampled
+fun OutlinedTextButtonSample() {
+ TextButton(
+ onClick = { /* Do something */ },
+ colors = TextButtonDefaults.outlinedTextButtonColors(),
+ border = ButtonDefaults.outlinedButtonBorder(enabled = true),
+ ) {
+ Text(text = "ABC")
+ }
+}
+
+@Sampled
+@Composable
+fun TextButtonWithOnLongClickSample(onLongClick: () -> Unit) {
+ TextButton(
+ onClick = { /* Do something for onClick*/ },
+ onLongClick = onLongClick,
+ onLongClickLabel = "Long click",
+ ) {
+ Text(text = "ABC")
+ }
+}
+
+@Composable
+@Sampled
+fun TextButtonWithCornerAnimationSample() {
+ TextButton(
+ onClick = { /* Do something */ },
+ colors = TextButtonDefaults.filledTextButtonColors(),
+ shapes = TextButtonDefaults.animatedShapes(),
+ ) {
+ Text(text = "ABC")
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md b/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md
new file mode 100644
index 0000000..11f43da
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md
@@ -0,0 +1,71 @@
+```
+/*
+ * Copyright 2023 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TextButtonDefaults
+import androidx.wear.compose.material3.TextToggleButton
+import androidx.wear.compose.material3.TextToggleButtonDefaults
+import androidx.wear.compose.material3.touchTargetAwareSize
+
+@Sampled
+@Composable
+fun TextToggleButtonSample() {
+ var checked by remember { mutableStateOf(true) }
+ TextToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = !checked },
+ shapes = TextToggleButtonDefaults.animatedShapes(),
+ ) {
+ Text(text = if (checked) "On" else "Off")
+ }
+}
+
+@Sampled
+@Composable
+fun TextToggleButtonVariantSample() {
+ var checked by remember { mutableStateOf(true) }
+ TextToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = !checked },
+ shapes = TextToggleButtonDefaults.variantAnimatedShapes(),
+ ) {
+ Text(text = if (checked) "On" else "Off")
+ }
+}
+
+@Sampled
+@Composable
+fun LargeTextToggleButtonSample() {
+ var checked by remember { mutableStateOf(true) }
+ TextToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = !checked },
+ modifier = Modifier.touchTargetAwareSize(TextButtonDefaults.LargeButtonSize),
+ ) {
+ Text(text = if (checked) "On" else "Off", style = TextToggleButtonDefaults.largeTextStyle)
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/TimePickerSample.kt.md b/wear/wear-compose-m3/references/material3/TimePickerSample.kt.md
new file mode 100644
index 0000000..e7f88f4
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/TimePickerSample.kt.md
@@ -0,0 +1,151 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimePicker
+import androidx.wear.compose.material3.TimePickerType
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+@Sampled
+@Composable
+fun TimePickerSample() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ initialTime = timePickerTime, // Initialize with last picked time on reopen
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ val formatter =
+ DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
+ .withLocale(LocalConfiguration.current.locales[0])
+
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TimePickerWithMinutesAndSecondsSample() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ timePickerType = TimePickerType.MinutesSeconds,
+ initialTime = timePickerTime, // Initialize with last picked time on reopen
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ val formatter = DateTimeFormatter.ofPattern("mm:ss")
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TimePickerWithSecondsSample() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ timePickerType = TimePickerType.HoursMinutesSeconds24H,
+ initialTime = timePickerTime, // Initialize with last picked time on reopen
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ val formatter = DateTimeFormatter.ofPattern("HH:mm:ss")
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun TimePickerWith12HourClockSample() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ timePickerType = TimePickerType.HoursMinutesAmPm12H,
+ initialTime = timePickerTime, // Initialize with last picked time on reopen
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ val formatter = DateTimeFormatter.ofPattern("hh:mm a")
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md b/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md
new file mode 100644
index 0000000..fc15a6c
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md
@@ -0,0 +1,65 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.weight
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.TimeTextDefaults
+import androidx.wear.compose.material3.curvedText
+import androidx.wear.compose.material3.timeTextCurvedText
+import androidx.wear.compose.material3.timeTextSeparator
+
+@Sampled
+@Composable
+fun TimeTextClockOnly() {
+ // TimeText displays the current time by default.
+ TimeText()
+}
+
+@Sampled
+@Composable
+fun TimeTextWithStatus() {
+ val style = TimeTextDefaults.timeTextStyle()
+ val primaryStyle = TimeTextDefaults.timeTextStyle(color = MaterialTheme.colorScheme.primary)
+ TimeText { time ->
+ timeTextCurvedText("ETA 12:48", style = primaryStyle)
+ timeTextSeparator(style)
+ timeTextCurvedText(time)
+ }
+}
+
+@Sampled
+@Composable
+fun TimeTextWithStatusEllipsized() {
+ val style = TimeTextDefaults.timeTextStyle()
+ TimeText { time ->
+ curvedText(
+ "Long status that should be ellipsized.",
+ CurvedModifier.weight(1f),
+ overflow = TextOverflow.Ellipsis,
+ )
+ timeTextSeparator(style)
+ timeTextCurvedText(time)
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md b/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md
new file mode 100644
index 0000000..84704cb
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md
@@ -0,0 +1,246 @@
+```
+/*
+ * 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy.Companion.ModulateAlpha
+import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnItemScrollProgress
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.ButtonGroup
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.ResponsiveTransformationSpec
+import androidx.wear.compose.material3.lazy.TransformationSpec
+import androidx.wear.compose.material3.lazy.TransformationVariableSpec
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+
+@Composable
+@Sampled
+@Preview
+fun CustomTransformationSpecSample() {
+ val transformationSpec = rememberTransformationSpec()
+ val morphingTransformationSpec =
+ object : TransformationSpec by transformationSpec {
+ override fun GraphicsLayerScope.applyContainerTransformation(
+ scrollProgress: TransformingLazyColumnItemScrollProgress
+ ) {
+ with(transformationSpec) { applyContainerTransformation(scrollProgress) }
+ rotationX = (scrollProgress.topOffsetFraction - 0.5f).coerceIn(0f..1f) * 270f
+ }
+ }
+
+ TransformingLazyColumn(
+ contentPadding = PaddingValues(20.dp),
+ modifier = Modifier.background(Color.Black),
+ ) {
+ items(count = 100) { index ->
+ Button(
+ onClick = {},
+ modifier =
+ Modifier.fillMaxWidth()
+ .transformedHeight(this, morphingTransformationSpec)
+ .graphicsLayer {
+ with(morphingTransformationSpec) {
+ applyContainerTransformation(scrollProgress)
+ }
+ },
+ ) {
+ Text(
+ "Item $index",
+ modifier =
+ Modifier.graphicsLayer {
+ with(morphingTransformationSpec) {
+ applyContentTransformation(scrollProgress)
+ }
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@Sampled
+@Preview
+fun CustomCompositingStrategyTransformationSpecSample() {
+ val transformationSpec = rememberTransformationSpec()
+
+ TransformingLazyColumn(
+ contentPadding = PaddingValues(20.dp),
+ modifier = Modifier.background(Color.Black),
+ ) {
+ items(count = 100) { index ->
+ Button(
+ onClick = {},
+ modifier =
+ Modifier.fillMaxWidth()
+ .transformedHeight(this, transformationSpec)
+ .graphicsLayer {
+ with(transformationSpec) {
+ applyContainerTransformation(scrollProgress)
+ }
+ // Using CompositingStrategy.ModulateAlpha can provide better
+ // performance when a container transformation involves alpha rendering,
+ // as it avoids an extra offscreen buffer.
+ //
+ // However, care must be taken with overlapping or transparent content
+ // inside the container. If the content itself uses alpha, ModulateAlpha
+ // can lead to multiple, incorrect alpha blending with the background
+ // (double-blending artifacts).
+ //
+ // To prevent this, the content's drawing layer must explicitly use
+ // CompositingStrategy.Offscreen to ensure internal elements are
+ // correctly pre-blended before the outer ModulateAlpha is applied.
+ compositingStrategy = ModulateAlpha
+ },
+ ) {
+ Text(
+ text = "Item $index",
+ modifier =
+ Modifier.graphicsLayer {
+ // Ensure content layer uses CompositingStrategy.Offscreen when
+ // container uses CompositingStrategy.ModulateAlpha.
+ // This composition is required to guarantee correct visual blending of
+ // content that contains internal alpha or complex blending.
+ // compositingStrategy is set to CompositingStrategy.Offscreen inside
+ // applyContentTransformation.
+ with(transformationSpec) { applyContentTransformation(scrollProgress) }
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@Sampled
+@Preview
+fun ResponsiveTransformationSpecButtonSample() {
+ val transformationSpec =
+ rememberTransformationSpec(
+ ResponsiveTransformationSpec.smallScreen(
+ // Makes the content disappear on the edges.
+ contentAlpha = TransformationVariableSpec(0f)
+ ),
+ ResponsiveTransformationSpec.largeScreen(
+ // Makes the content disappear on the edges, but a bit more aggressively.
+ contentAlpha =
+ TransformationVariableSpec(0f, transformationZoneEnterFraction = 0.2f)
+ ),
+ )
+ TransformingLazyColumn(
+ contentPadding = PaddingValues(20.dp),
+ modifier = Modifier.background(Color.Black),
+ ) {
+ items(count = 100) { index ->
+ Button(
+ onClick = {},
+ modifier =
+ Modifier.fillMaxWidth().transformedHeight(this@items, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("Item $index")
+ }
+ }
+ }
+}
+
+@Composable
+@Sampled
+@Preview
+fun TransformationSpecButtonRowSample() {
+ // Use the spec derived from default small and large screen specs.
+ val transformationSpec = rememberTransformationSpec()
+
+ TransformingLazyColumn(
+ contentPadding = PaddingValues(),
+ modifier = Modifier.background(Color.Black),
+ ) {
+ items(count = 100) {
+ val interactionSource1 = remember { MutableInteractionSource() }
+ val interactionSource2 = remember { MutableInteractionSource() }
+
+ ButtonGroup(
+ modifier =
+ Modifier.fillMaxWidth()
+ .graphicsLayer {
+ with(transformationSpec) {
+ applyContainerTransformation(scrollProgress)
+ }
+ }
+ .transformedHeight(this, transformationSpec)
+ .minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ ) {
+ Button(
+ onClick = {},
+ modifier = Modifier.animateWidth(interactionSource1),
+ interactionSource = interactionSource1,
+ ) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
+ Text(
+ "L",
+ modifier =
+ Modifier.graphicsLayer {
+ with(transformationSpec) {
+ applyContentTransformation(scrollProgress)
+ }
+ },
+ )
+ }
+ }
+ Button(
+ onClick = {},
+ modifier = Modifier.animateWidth(interactionSource2),
+ interactionSource = interactionSource2,
+ ) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
+ Text(
+ "R",
+ modifier =
+ Modifier.graphicsLayer {
+ with(transformationSpec) {
+ applyContentTransformation(scrollProgress)
+ }
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md b/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md
new file mode 100644
index 0000000..d9d2980
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md
@@ -0,0 +1,89 @@
+```
+/*
+ * 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.items
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TitleCard
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.transformedHeight
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnNotificationsSample() {
+ val state = rememberTransformingLazyColumnState()
+ val transformationSpec = rememberTransformationSpec()
+ data class NotificationItem(val title: String, val body: String)
+
+ val notifications =
+ listOf(
+ NotificationItem(
+ "☕ Coffee Break?",
+ "Step away from the screen and grab a pick-me-up. Step away from the screen and grab a pick-me-up.",
+ ),
+ NotificationItem("🌟 You're Awesome!", "Just a little reminder in case you forgot 😊"),
+ NotificationItem("👀 Did you know?", "Check out [app name]'s latest feature update."),
+ NotificationItem("📅 Appointment Time", "Your meeting with [name] is in 15 minutes."),
+ )
+ AppScaffold {
+ ScreenScaffold(state) { contentPadding ->
+ TransformingLazyColumn(state = state, contentPadding = contentPadding) {
+ item {
+ ListHeader(
+ transformation = SurfaceTransformation(transformationSpec),
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec).animateItem(),
+ ) {
+ Text("Notifications")
+ }
+ }
+ items(notifications) { notification ->
+ TitleCard(
+ onClick = {},
+ title = {
+ Text(
+ notification.title,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.labelLarge,
+ maxLines = 1,
+ )
+ },
+ subtitle = { Text(notification.body) },
+ transformation = SurfaceTransformation(transformationSpec),
+ modifier =
+ Modifier.transformedHeight(this, transformationSpec).animateItem(),
+ )
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md b/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md
new file mode 100644
index 0000000..379c818
--- /dev/null
+++ b/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md
@@ -0,0 +1,309 @@
+```
+/*
+ * Copyright 2024 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.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider
+import androidx.wear.compose.foundation.lazy.itemsIndexed
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.Card
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.CompactButton
+import androidx.wear.compose.material3.CompactButtonDefaults
+import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListHeaderDefaults
+import androidx.wear.compose.material3.OutlinedCard
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.SurfaceTransformation
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.lazy.firstVisibleItemLayoutItemInfo
+import androidx.wear.compose.material3.lazy.layoutItemInfoOf
+import androidx.wear.compose.material3.lazy.rememberTransformationSpec
+import androidx.wear.compose.material3.lazy.rememberTransformingLazyColumnFirstLayoutItemProvider
+import androidx.wear.compose.material3.lazy.transformedHeight
+import kotlinx.coroutines.delay
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnButtonsSample() {
+ val state = rememberTransformingLazyColumnState()
+ val transformationSpec = rememberTransformationSpec()
+ AppScaffold {
+ ScreenScaffold(state) { contentPadding ->
+ TransformingLazyColumn(state = state, contentPadding = contentPadding) {
+ items(count = 50) {
+ Button(
+ onClick = {},
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ ButtonDefaults.minimumVerticalListContentPadding
+ )
+ .fillMaxWidth()
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("Item $it")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnMinimumVerticalContentPaddingSample() {
+ val transformationSpec = rememberTransformationSpec()
+ val state = rememberTransformingLazyColumnState()
+ var elements by remember { mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) }
+ var nextElement by remember { mutableIntStateOf(10) }
+
+ fun addElementAfter(index: Int) {
+ elements =
+ elements.subList(0, index + 1) +
+ listOf(nextElement++) +
+ elements.subList(index + 1, elements.count())
+ }
+
+ fun removeElementAt(index: Int) {
+ elements = elements.subList(0, index) + elements.subList(index + 1, elements.count())
+ }
+
+ AppScaffold {
+ ScreenScaffold(
+ state,
+ edgeButton = {
+ EdgeButton(onClick = { elements = elements.shuffled() }) { Text("Shuffle") }
+ },
+ ) { contentPadding ->
+ TransformingLazyColumn(state = state, contentPadding = contentPadding) {
+ itemsIndexed(elements, key = { _, key -> key }) { index, key ->
+ if (key % 3 == 0) {
+ CompactButton(
+ onClick = { removeElementAt(index) },
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ CompactButtonDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec)
+ .animateItem(),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("CompactButton $key")
+ }
+ } else if (key % 4 == 0) {
+ ListHeader(
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ ListHeaderDefaults.minimumTopListContentPadding,
+ ListHeaderDefaults.minimumBottomListContentPadding,
+ )
+ .transformedHeight(this, transformationSpec),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("ListHeader heading")
+ }
+ } else {
+ Card(
+ onClick = {},
+ modifier =
+ Modifier.fillMaxWidth()
+ .minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec)
+ .animateItem(),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("Card $key")
+ Row {
+ Spacer(modifier = Modifier.weight(1f))
+ CompactButton(
+ onClick = { removeElementAt(index) },
+ enabled = elements.count() > 1,
+ ) {
+ Text("-")
+ }
+ CompactButton(onClick = { addElementAfter(index) }) { Text("+") }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnFirstLayoutItemProviderSample() {
+ val state = rememberTransformingLazyColumnState()
+ val transformationSpec = rememberTransformationSpec()
+
+ // This sample demonstrates how to use rememberTransformingLazyColumnFirstLayoutItemProvider
+ // to control the expansion direction of a dynamically resizing item. By using the voice input
+ // card's Bottom/End edge as the layout reference, the card predictably expands upwards.
+ // The constant key safely falls back to default layout behavior when scrolled off-screen.
+ val firstLayoutItemProvider = rememberTransformingLazyColumnFirstLayoutItemProvider {
+ state.layoutItemInfoOf(
+ itemKey = "voice_input",
+ itemEdge = TransformingLazyColumnFirstLayoutItemProvider.ItemEdge.End,
+ )
+ }
+
+ var isListening by remember { mutableStateOf(false) }
+ var lineCount by remember { mutableIntStateOf(0) }
+ // Simple loop simulating text lines arriving over time
+ LaunchedEffect(isListening) {
+ if (isListening) {
+ while (lineCount < 5) {
+ delay(300)
+ lineCount++
+ }
+ isListening = false
+ }
+ }
+
+ AppScaffold {
+ ScreenScaffold(state) { contentPadding ->
+ TransformingLazyColumn(
+ state = state,
+ contentPadding = contentPadding,
+ firstLayoutItemProvider = firstLayoutItemProvider,
+ ) {
+ items(count = 3, key = { "message_$it" }) { index ->
+ Card(
+ onClick = {},
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec)
+ .animateItem(placementSpec = null),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("Previous message $index")
+ }
+ }
+
+ item(key = "voice_input") {
+ val voiceText = (1..lineCount).joinToString("\n") { "Voice Input line $it" }
+
+ OutlinedCard(
+ onClick = {
+ if (lineCount >= 5) lineCount = 0
+ isListening = !isListening
+ },
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec)
+ .animateItem(placementSpec = null),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text(text = voiceText.ifEmpty { "Tap to speak" })
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+@Preview
+@Composable
+fun TransformingLazyColumnFirstVisibleItemLayoutItemProviderSample() {
+ val transformationSpec = rememberTransformationSpec()
+ val state = rememberTransformingLazyColumnState()
+ var elements by remember { mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) }
+ var nextElement by remember { mutableIntStateOf(10) }
+
+ fun addCardAfter(index: Int) {
+ elements =
+ elements.subList(0, index + 1) +
+ listOf(nextElement++) +
+ elements.subList(index + 1, elements.count())
+ }
+
+ fun removeCardAt(index: Int) {
+ elements = elements.subList(0, index) + elements.subList(index + 1, elements.count())
+ }
+
+ // This sample demonstrates how to use rememberTransformingLazyColumnFirstVisibleItemProvider
+ // to maintain a stable viewport top when items are added or removed dynamically.
+ // It achieves this by using the first visible item as the layout reference.
+ AppScaffold {
+ ScreenScaffold(state) { contentPadding ->
+ TransformingLazyColumn(
+ state = state,
+ contentPadding = contentPadding,
+ firstLayoutItemProvider =
+ rememberTransformingLazyColumnFirstLayoutItemProvider {
+ if (state.isScrollInProgress) null else state.firstVisibleItemLayoutItemInfo
+ },
+ ) {
+ itemsIndexed(elements, key = { _, key -> key }) { index, cardKey ->
+ Card(
+ onClick = {},
+ modifier =
+ Modifier.minimumVerticalContentPadding(
+ CardDefaults.minimumVerticalListContentPadding
+ )
+ .transformedHeight(this, transformationSpec)
+ .animateItem(),
+ transformation = SurfaceTransformation(transformationSpec),
+ ) {
+ Text("Card $cardKey")
+ Row {
+ Spacer(modifier = Modifier.weight(1f))
+ CompactButton(
+ onClick = { removeCardAt(index) },
+ enabled = elements.count() > 1,
+ ) {
+ Text("-")
+ }
+ CompactButton(onClick = { addCardAfter(index) }) { Text("+") }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md
index e5ae3e5..b26e37d 100644
--- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md
+++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md
@@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di
license: Complete terms in LICENSE.txt
metadata:
author: Google LLC
- last-updated: '2026-08-19'
+ last-updated: '2026-09-02'
keywords:
- Jetpack Compose Glimmer
- audio glasses
diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md
index 597a99b..946617c 100644
--- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md
+++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md
@@ -95,7 +95,7 @@ audio processing, or standard Bluetooth audio input.
| Recording method | Microphone access | Common use case |
|---|---|---|
-| Projected Context | Multiple microphones | Recording using a projected context lets your app access multiple microphones from the glasses and its specialized hardware features, such as: - XR-specific spatialization. - Advanced denoising. - Voice separation that distinguishes between wearer's and bystander's voices. - Maintain recording access in multidevice environments even when the glasses are not the active Bluetooth device. |
+| Projected Context | Multiple microphones | Recording using a projected context lets your app access multiple microphones from the glasses and its specialized hardware features, such as: - XR-specific spatialization. - Advanced denoising. - Voice separation that isolates the wearer's voice. - Maintain recording access in multidevice environments even when the glasses are not the active Bluetooth device. |
| Bluetooth HFP | Single microphone | Relies on the Bluetooth Hands-Free Profile (HFP) for immediate, out-of-the-box compatibility. In this mode, the glasses connect to the phone using standard Headset and Advanced Audio Distribution Profile (A2DP) [profiles](https://developer.android.com/develop/connectivity/bluetooth/profiles), functioning like a typical Bluetooth peripheral. If your app is already designed for standard Bluetooth recording, you can use this method to record audio from the glasses without integrating any XR-specific capabilities. |
### Record audio using a projected context
@@ -152,9 +152,10 @@ audioRecord.startRecording()
case.
For example, use `VOICE_COMMUNICATION` if your use case needs automatic
- noise reduction. `VOICE_RECOGNITION` is processed with acoustic echo
- cancellation (AEC). And if you need raw, unaltered audio, select
- `UNPROCESSED` or `CAMCORDER`.
+ noise reduction or to isolate the wearer's voice. `VOICE_RECOGNITION` is
+ processed with acoustic echo cancellation (AEC), which can be useful when
+ there's concurrent audio playback while recording. And if you need raw,
+ unaltered audio, select `UNPROCESSED` or `CAMCORDER`.
- To ensure compatibility with the glasses, the `audioFormat` object must
define a sample rate of 16kHz and a channel configuration of either mono or
stereo (using [`CHANNEL_IN_MONO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_mono) or [`CHANNEL_IN_STEREO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_stereo)).
@@ -327,6 +328,7 @@ private fun startCameraOnGlasses(activity: ComponentActivity) {
- A custom [`ResolutionSelector`](https://developer.android.com/reference/kotlin/androidx/camera/core/resolutionselector/ResolutionSelector) is built to precisely control the output image resolution for [`ImageCapture`](https://developer.android.com/reference/kotlin/androidx/camera/core/ImageCapture).
- Creates an `ImageCapture` use case that is configured with a custom `ResolutionSelector`.
- Binds the `ImageCapture` use case to the activity's lifecycle. This automatically manages the opening and closing of the camera based on the activity's state (for example, stopping the camera when the activity is paused).
+- **Configuring for single-stream hardware:** The glasses camera pipeline is constrained to a single active stream at a time. When `ImageCapture` is bound alone, CameraX attaches an internal repeating stream (`MeteringRepeating`) for focus and metering by default, which causes the single-stream pipeline to stall. To bind `ImageCapture` by itself without a preview stream, disable the forced repeating stream by setting [`setRepeatingStreamForced(false)`](https://developer.android.com/reference/kotlin/androidx/camera/core/CameraXConfig.Builder#setRepeatingStreamForced(kotlin.Boolean)) on [`CameraXConfig.Builder`](https://developer.android.com/reference/kotlin/androidx/camera/core/CameraXConfig.Builder) (for example, by implementing [`CameraXConfig.Provider`](https://developer.android.com/reference/kotlin/androidx/camera/core/CameraXConfig.Provider) in your `Application` class or configuring `ProcessCameraProvider` before initialization).
After the glasses' camera is set up, you can capture an image with the
CameraX's `ImageCapture` class. Refer to the CameraX's documentation to learn
diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/button-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-source.md
index b94bbbe..c1983bd 100644
--- a/xr/display-glasses-with-jetpack-compose-glimmer/references/button-source.md
+++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-source.md
@@ -106,12 +106,6 @@ public fun Button(
val iconSpacing = ButtonDefaults.iconSpacing
val minHeight = ButtonDefaults.minimumHeight(buttonSize)
- val depth =
- SurfaceDepthEffect(
- depthEffect = null,
- focusedDepthEffect = GlimmerTheme.depthEffectLevels.level1,
- )
-
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
CompositionLocalProvider(LocalTextStyle provides GlimmerTheme.typography.bodySmall) {
@@ -123,7 +117,7 @@ public fun Button(
shape = shape,
color = color,
contentColor = contentColor,
- depthEffect = depth,
+ depthEffect = null,
interactionSource = internalInteractionSource,
)
.clickable(
diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md
index 5ba0988..8c1df85 100644
--- a/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md
+++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md
@@ -45,6 +45,7 @@ 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
@@ -451,7 +452,7 @@ private fun Modifier.constrainHeightToAspectRatio(widthToHeightRatio: Float): Mo
val height =
(constraints.maxWidth / widthToHeightRatio)
- .toInt()
+ .roundToInt()
// Handle the case where the width is more than ratio times larger than available
// height
.coerceAtMost(constraints.maxHeight)
diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md
index 1b8ad16..7254725 100644
--- a/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md
+++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md
@@ -351,7 +351,6 @@ private class IconColorFilterNode(
val layer = obtainGraphicsLayer()
layer.apply { record { drawContent() } }
var cachedTintColor = Color.Unspecified
- var cachedColorFilter: ColorFilter? = null
onDrawWithContent {
val tintColor =
if (useContentColor) {
@@ -361,7 +360,7 @@ private class IconColorFilterNode(
}
if (cachedTintColor != tintColor) {
cachedTintColor = tintColor
- cachedColorFilter =
+ val cachedColorFilter =
if (tintColor.isSpecified) ColorFilter.tint(tintColor) else null
layer.colorFilter = cachedColorFilter
}
diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md
index 76c4120..f03e020 100644
--- a/xr/display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md
+++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md
@@ -76,12 +76,6 @@ public fun IconButton(
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit,
) {
- val depthEffect =
- SurfaceDepthEffect(
- depthEffect = null,
- focusedDepthEffect = GlimmerTheme.depthEffectLevels.level1,
- )
-
val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
Box(
@@ -92,7 +86,7 @@ public fun IconButton(
shape = shape,
color = color,
contentColor = contentColor,
- depthEffect = depthEffect,
+ depthEffect = null,
interactionSource = internalInteractionSource,
)
.clickable(
diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/surface-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/surface-source.md
index 56e52c6..4e2e19c 100644
--- a/xr/display-glasses-with-jetpack-compose-glimmer/references/surface-source.md
+++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/surface-source.md
@@ -71,7 +71,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
-import androidx.xr.glimmer.internal.color.withToneAndChroma
+import androidx.xr.glimmer.internal.color.withTone
import kotlin.math.ceil
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
@@ -143,7 +143,7 @@ import org.intellij.lang.annotations.Language
public fun Modifier.surface(
enabled: Boolean = true,
shape: Shape = GlimmerTheme.shapes.medium,
- color: Color = SurfaceDefaults.color(),
+ color: Color = GlimmerTheme.colors.surface,
focusedColor: Color = SurfaceDefaults.focusedColor(color),
contentColor: Color = calculateContentColor(color),
focusedContentColor: Color = calculateContentColor(focusedColor),
@@ -168,36 +168,29 @@ public fun Modifier.surface(
public object SurfaceDefaults {
/**
- * Calculates the background [Color] for a surface derived from the provided [color].
+ * Returns the background [Color] for a surface derived from the provided [baseColor].
*
- * Adjusts the provided [color] so that it is suitable for use as a surface background.
+ * Adjusts the provided [baseColor] so that it is suitable for use as a surface background.
*
- * @param color the base [Color] of the surface
+ * @param baseColor the base [Color] of the surface
* @return the surface background [Color], adjusted to improve content contrast
*/
@Composable
- public fun color(color: Color = GlimmerTheme.colors.surface): Color =
- if (color == DefaultSurfaceColor) {
- DefaultSurfaceColor
- } else {
- color.withToneAndChroma(newTone = SurfaceColorTone, newChroma = SurfaceColorChroma)
- }
+ public fun color(baseColor: Color = GlimmerTheme.colors.surface): Color =
+ baseColor.withTone(newTone = SurfaceColorTone)
/**
- * Calculates the focused background [Color] for a surface derived from the provided [color].
+ * Returns the focused background [Color] for a surface derived from the provided [baseColor].
*
- * Adjusts the provided [color] so that it is suitable for use as a focused surface background.
+ * Adjusts the provided [baseColor] so that it is suitable for use as a focused surface
+ * background.
*
- * @param color the base [Color] of the surface
+ * @param baseColor the base [Color] of the surface
* @return the focused surface background [Color], adjusted to improve content contrast
*/
@Composable
- public fun focusedColor(color: Color = GlimmerTheme.colors.surface): Color =
- if (color == DefaultSurfaceColor) {
- DefaultSurfaceColor
- } else {
- color.withToneAndChroma(newTone = FocusedColorTone, newChroma = FocusedColorChroma)
- }
+ public fun focusedColor(baseColor: Color = GlimmerTheme.colors.surface): Color =
+ baseColor.withTone(newTone = FocusedSurfaceColorTone)
}
/**
@@ -386,26 +379,11 @@ private class SurfaceNode(
private val focusedBorderColor0: Color = Color.White
- private var focusedBorderColor1: Color =
- if (focusedColor == DefaultSurfaceColor) {
- DefaultBorderFocusedColor1
- } else {
- focusedColor.withToneAndChroma(newTone = 85f)
- }
+ private var focusedBorderColor1: Color = focusedColor.withTone(newTone = 85f)
- private var focusedBorderColor2: Color =
- if (focusedColor == DefaultSurfaceColor) {
- DefaultBorderFocusedColor2
- } else {
- focusedColor.withToneAndChroma(newTone = 69f)
- }
+ private var focusedBorderColor2: Color = focusedColor.withTone(newTone = 69f)
- private var focusedBorderColor3: Color =
- if (focusedColor == DefaultSurfaceColor) {
- DefaultBorderFocusedColor3
- } else {
- focusedColor.withToneAndChroma(newTone = 77f)
- }
+ private var focusedBorderColor3: Color = focusedColor.withTone(newTone = 77f)
fun update(
enabled: Boolean,
@@ -572,20 +550,16 @@ private class SurfaceNode(
focusProgress: Float,
pressedProgress: Float,
) {
- // Focused overlay transition: #FFFFFF with 16% opacity when fully focused
- val focusOverlayColor =
- Color(red = 1f, green = 1f, blue = 1f, alpha = 0.16f * focusProgress)
val backgroundColor = lerp(color, focusedColor, focusProgress)
- val baseBackground = focusOverlayColor.compositeOver(backgroundColor)
// Pressed overlay transition: #FFFFFF with 16% opacity when pressed
val compositeBackground =
if (pressedProgress > 0f) {
val pressedOverlayColor =
PressedOverlayColor.copy(alpha = PressedOverlayAlpha * pressedProgress)
- pressedOverlayColor.compositeOver(baseBackground)
+ pressedOverlayColor.compositeOver(backgroundColor)
} else {
- baseBackground
+ backgroundColor
}
drawOutline(outline, color = compositeBackground)
@@ -729,10 +703,8 @@ private class SurfaceNode(
focusProgress: Float,
ambientProgress: Float,
) {
- val focusOverlayColor =
- Color(red = 1f, green = 1f, blue = 1f, alpha = 0.16f * focusProgress)
val backgroundColor = lerp(color, focusedColor, focusProgress)
- drawOutline(outline, color = focusOverlayColor.compositeOver(backgroundColor))
+ drawOutline(outline, backgroundColor)
borderShader =
BorderShaderHelper.configureShader(
@@ -791,15 +763,9 @@ private class SurfaceNode(
}
private fun updateFocusedBorderColors(focusedColor: Color) {
- if (focusedColor == DefaultSurfaceColor) {
- focusedBorderColor1 = DefaultBorderFocusedColor1
- focusedBorderColor2 = DefaultBorderFocusedColor2
- focusedBorderColor3 = DefaultBorderFocusedColor3
- } else {
- focusedBorderColor1 = focusedColor.withToneAndChroma(newTone = 85f)
- focusedBorderColor2 = focusedColor.withToneAndChroma(newTone = 69f)
- focusedBorderColor3 = focusedColor.withToneAndChroma(newTone = 77f)
- }
+ focusedBorderColor1 = focusedColor.withTone(newTone = 85f)
+ focusedBorderColor2 = focusedColor.withTone(newTone = 69f)
+ focusedBorderColor3 = focusedColor.withTone(newTone = 77f)
}
}
@@ -872,11 +838,8 @@ private const val UniformBorderFocusedColor1 = "uBorderFocusedColor1"
private const val UniformBorderFocusedColor2 = "uBorderFocusedColor2"
private const val UniformBorderFocusedColor3 = "uBorderFocusedColor3"
-private const val SurfaceColorTone = 33f
-private const val SurfaceColorChroma = 29f
-
-private const val FocusedColorTone = 33f
-private const val FocusedColorChroma = 29f
+private const val SurfaceColorTone = 20f
+private const val FocusedSurfaceColorTone = 34f
/** Default border width for a [surface]. */
private val DefaultSurfaceBorderWidth = 1.5.dp
@@ -898,10 +861,6 @@ private val AmbientBlurRadiusMaxEnd = 15.9.dp
private val DefaultSolidBorderIdleColor = Color(0.25f, 0.25f, 0.25f, 0.5f)
-private val DefaultBorderFocusedColor1 = Color(0.671f, 0.671f, 0.671f, 0.8f)
-private val DefaultBorderFocusedColor2 = Color(0.405f, 0.405f, 0.405f, 0.6f)
-private val DefaultBorderFocusedColor3 = Color(0.530f, 0.530f, 0.530f, 0.7f)
-
/** Enter animation for focus highlight and depth effect */
private val FocusedEnterAnimationSpec: AnimationSpec =
tween(durationMillis = 800, easing = LinearOutSlowInEasing)