Updates skills (2026-09-07 11:38) (#187)

Co-authored-by: android-devrel-github-bot <android-devrel-github-bot@users.noreply.github.com>
This commit is contained in:
android-devrel-github-bot
2026-09-07 13:56:54 +02:00
committed by GitHub
parent 725364add9
commit ac4238481a
113 changed files with 11021 additions and 378 deletions
+2
View File
@@ -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",
+10
View File
@@ -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"
+121
View File
@@ -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()
```
<br />
### 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
}
```
<br />
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
}
```
<br />
@@ -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<Integer> 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).
@@ -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);
@@ -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 `<background_information>`,
`<instruction>`, 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.
@@ -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 `<email>[content]</email>`) 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>
[email_content]
</email>
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>
[itinerary_content]
</itinerary>
@@ -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<Plant>
)
@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<T>` | For lists of supported types or nested `@Generable` classes. | `description`, `minItems`, `maxItems` |
| `List<String>` | 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 <T : Any> countTokens(request: GenerateTypedContentRequest<T>): CountTokensResponse
@@ -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.
+35 -25
View File
@@ -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.
@@ -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)
+9 -2
View File
@@ -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
@@ -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
<br />
@@ -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
@@ -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
<br />
## 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
@@ -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).
![Image showing the flow of using a digital credential](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials.png) **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:
![Image showing the flow of a normal wallet interaction](https://developer.android.com/static/identity/digital-credentials/images/normal_wallet_flowchart.svg) **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:
![Image showing the flow of a digital credential interaction](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_flowchart.svg) **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:
![Image showing the digital credentials UI in Credential Manager](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_ui.png) **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
![Image showing the UX variants of digital credentials](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_ux_variants.png) **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 <https://github.com/digitalcredentialsdev/CMWallet/actions> 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 <https://digital-credentials.dev/> 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.
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 <https://digital-credentials.dev/> 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).
+1 -1
View File
@@ -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
@@ -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" }
@@ -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" }
+1 -1
View File
@@ -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
@@ -42,8 +42,7 @@ BaseButton(
onClick = { },
style = { }
) {
BaseText("Click me")
}
BaseText("Click mnippets.kt
```
<br />
@@ -57,8 +56,7 @@ BaseButton(
onClick = { },
style = { background(Color.Blue) }
) {
BaseText("Click me")
}
BaseText("Click mnippets.kt
```
<br />
@@ -77,8 +75,7 @@ own custom components.
Row(
modifier = Modifier.styleable { }
) {
BaseText("Content")
}
BaseText("Contennippets.kt
```
<br />
@@ -93,8 +90,7 @@ Row(
background(Color.Blue)
}
) {
BaseText("Content")
}
BaseText("Contennippets.kt
```
<br />
@@ -145,8 +141,8 @@ val styleState = remember { MutableStyleState(null) }
Column(
Modifier.styleable(styleState, style)
) {
BaseText("Column content")
}
BaseText("Col)
}StylesSnippets.kt
```
<br />
@@ -172,10 +168,8 @@ Column(
}
val rowStyleState = remember { MutableStyleState(null) }
Row(
Modifier.styleable(rowStyleState, style)
) {
BaseText("Row")
}
Modifier.styleable(rowStyleState, stText("Row")
}StylesSnippets.kt
```
<br />
@@ -194,8 +188,7 @@ BaseButton(
contentPaddingStart(16.dp)
}
) {
BaseText("Button")
}
BaseText("Buttonippets.kt
```
<br />
@@ -225,8 +218,7 @@ BaseButton(
//
}
) {
BaseText("Click me!")
}
BaseText("Click menippets.kt
```
<br />
@@ -250,8 +242,7 @@ BaseButton(
},
) {
BaseText("Click me!")
}
BaseText("Click menippets.kt
```
<br />
@@ -283,8 +274,7 @@ BaseButton(
},
) {
BaseText("Click me!")
}
BaseText("Click menippets.kt
```
<br />
@@ -334,8 +324,8 @@ Column(
) {
BaseText("Children inherit", style = { width(60.dp) })
BaseText("certain properties")
BaseText("from their parents")
}
BaseText(&quents")
}StylesSnippets.kt
```
<br />
@@ -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
```
<br />
@@ -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
<br />
## 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.
<br />
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.
fundamentally different. Prefer accessing dynamic tokens inside a visual
definition rather than switching between distinct style objects.
+1 -1
View File
@@ -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
@@ -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
<br />
@@ -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
<br />
@@ -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"
+309
View File
@@ -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()
}
}
}
```
<br />
#### 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()
}
}
}
```
<br />
### 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
)
}
}
```
<br />
#### 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
)
}
}
}
```
<br />
### 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
}
}
```
<br />
#### 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)
}
```
<br />
### 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()
}
}
)
```
<br />
#### 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 */ }
)
```
<br />
## 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?
@@ -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()
}
```
<br />
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)
}
}
```
<br />
## 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()
}
}
```
<br />
Next, provide that input to your dispatcher:
```kotlin
navigationEventDispatcher.addInput(MyInput())
```
<br />
> [!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()
```
<br />
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.
@@ -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<NavigationEventInfo>(
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
}
}
```
<br />
The `addHandler` function connects the handler to the dispatcher:
```kotlin
navigationEventDispatcher.addHandler(myHandler)
```
<br />
Call `myHandler.remove()` to remove the handler from the dispatcher:
```kotlin
myHandler.remove()
```
<br />
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<out NavigationEventInfo>,
isBackEnabled: Boolean = true,
onBackCancelled: () -> Unit = {},
onBackCompleted: () -> Unit,
){
}
```
<br />
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()
}
)
}
```
<br />
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<NavigationEventInfo>(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<NavigationEventInfo>,
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
}
}
```
<br />
@@ -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.
@@ -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)
}
+1 -1
View File
@@ -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
@@ -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
<br />
@@ -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.
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.
+1 -1
View File
@@ -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
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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
@@ -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:
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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
@@ -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
<br />
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
<br />
### In the IDE
Click the gutter icon next to a `@PreviewTest` function and select **Add/Update
+1 -1
View File
@@ -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
@@ -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.
![Sample Remote](https://developer.android.com/static/training/tv/images/tv-nav-controller.png) **Figure 1.** Example of a TV remote.
![A television remote control](https://developer.android.com/static/training/tv/images/tv-nav-controller.png) **Figure 1.** Example of a TV remote.
**D-pad**
@@ -60,8 +62,7 @@ Invokes either Google Assistant or voice input.
<br />
> [!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.
![An image describing the flow of navigation when using top navigation](https://developer.android.com/static/training/tv/images/tv-nav-top.png) **Figure 2.** Flow using top navigation. ![An image describing the flow of navigation when using side navigation](https://developer.android.com/static/training/tv/images/tv-nav-left.png) **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.
![The flow of navigation when using top navigation](https://developer.android.com/static/training/tv/images/tv-nav-top.png) **Figure 2.** Flow using top navigation. ![The flow of navigation when using side navigation](https://developer.android.com/static/training/tv/images/tv-nav-left.png) **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.
![Screenshot showing a dialog asking users if they want to exit](https://developer.android.com/static/training/tv/images/tv-nav-back-button-dialog.png) **Figure 4.** Exit gating
![A dialog asking users if they want to exit](https://developer.android.com/static/training/tv/images/tv-nav-back-button-dialog.png) **Figure 4.** Exit gating
Don't.
Avoid exit gating. Let users exit out of the app without
@@ -187,7 +189,7 @@ confirmation.
<br />
![Screenshot showing navigation looping](https://developer.android.com/static/training/tv/images/tv-nav-loop.gif) **Figure 5.** Navigation loop
![Navigation looping between closing and opening the menu](https://developer.android.com/static/training/tv/images/tv-nav-loop.gif) **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:
![Screenshot showing a soft back button on the screen](https://developer.android.com/static/training/tv/images/tv-nav-no-soft-back.png) **Figure 6.** Soft back button
backward on a TV. It's not necessary to show a virtual back button on the
screen:
![A virtual back button displayed on the screen](https://developer.android.com/static/training/tv/images/tv-nav-no-soft-back.png) **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:
![Screenshot showing a soft cancel button on the screen](https://developer.android.com/static/training/tv/images/tv-nav-cancel.png) **Figure 7.** Soft cancel button.
![A cancel button displayed alongside destructive actions](https://developer.android.com/static/training/tv/images/tv-nav-cancel.png) **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.
<br />
> [!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.
![Navigation focusable example](https://developer.android.com/static/training/tv/images/tv-nav-focusable-2.png) **Figure 9.** Control focusability.
![A layout with controls placed in non-overlapping locations](https://developer.android.com/static/training/tv/images/tv-nav-focusable-2.png) **Figure 9.** Control focusability.
Do.
Place controls, like the search action shown here, in locations that don't overlap
with other clickable elements.
![Navigation focusable example](https://developer.android.com/static/training/tv/images/tv-nav-focusable-1.png) **Figure 10.** Control focusability.
![A layout with controls in hard-to-reach locations](https://developer.android.com/static/training/tv/images/tv-nav-focusable-1.png) **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.
![Navigation axes example](https://developer.android.com/static/training/tv/images/tv-nav-axes-1.png) **Figure 11.** Traversal.
![A layout using vertical categories and horizontal items](https://developer.android.com/static/training/tv/images/tv-nav-axes-1.png) **Figure 11.** Traversal.
Do.
Categories can be traversed on the vertical axis, and items within each
category can be browsed on the horizontal axis.
![Navigation axes example](https://developer.android.com/static/training/tv/images/tv-nav-axes-2.png) **Figure 12.** Traversal.
![A complex and nested layout hierarchy](https://developer.android.com/static/training/tv/images/tv-nav-axes-2.png) **Figure 12.** Traversal.
Don't.
@@ -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/).
<br />
@@ -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
+6 -6
View File
@@ -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) |
@@ -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`
@@ -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")
}
}
}
```
@@ -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))
}
}
```
@@ -388,7 +388,7 @@ fun CurvedFonts() {
)
.forEach { (name, ff) ->
basicCurvedText(
"$name",
name,
style = CurvedTextStyle(color = Color.White, fontFamily = ff),
modifier = CurvedModifier.padding(5.dp),
)
@@ -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 },
)
}
}
}
```
@@ -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,
),
)
}
}
}
}
}
}
}
}
```
@@ -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))
}
}
}
```
@@ -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))
}
}
}
```
@@ -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(),
)
}
}
}
```
@@ -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)",
)
}
}
}
}
```
@@ -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") },
)
}
}
}
}
}
}
```
@@ -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<PinnableContainer.PinnedHandle?>(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")
}
},
)
}
}
}
```
@@ -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),
)
}
}
}
```
@@ -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)
}
}
```
@@ -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") }
}
}
}
}
```
@@ -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) },
)
}
},
)
}
}
```
@@ -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")
}
}
}
}
}
```
@@ -83,7 +83,7 @@ fun NonClickableCardSample() {
@Sampled
@Composable
fun CardWithOnLongClickS>ample(onLongClickHandler: () - Unit) {
fun CardWithOnLongClickSample(onLongClickHandler: () -> Unit) {
Card(
onClick = { /* Do something */ },
onLongClick = onLongClickHandler,
@@ -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,
)
}
```
@@ -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) },
)
}
```
@@ -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,
)
}
}
}
```
@@ -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") },
)
}
}
}
```
@@ -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),
)
}
}
}
}
```
@@ -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(
@@ -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),
)
}
}
}
```
@@ -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) },
)
}
}
```
@@ -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),
)
}
}
```
@@ -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()
}
}
}
}
```
@@ -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))
}
}
```
@@ -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)
},
)
}
}
```
@@ -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")
}
}
}
}
}
}
```
@@ -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")
}
}
}
}
}
}
```
@@ -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) },
)
}
```
@@ -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")
}
}
}
}
}
}
```
@@ -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")
}
}
}
}
}
}
}
```
@@ -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),
)
}
}
}
```
@@ -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") },
)
}
}
}
```
@@ -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"
}
}
```
@@ -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),
)
}
}
```
@@ -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,
)
}
}
```
@@ -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(),
)
}
}
}
}
}
```
@@ -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)
},
)
}
}
```
@@ -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()
}
}
```
@@ -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,
)
}
```
@@ -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),
)
}
}
```
@@ -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")
}
}
}
}
}
}
}
```
@@ -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)",
)
}
}
}
}
```
@@ -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<String>().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<String>().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<String>().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())
}
}
}
}
}
```
@@ -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,
)
}
```
@@ -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")
}
}
```

Some files were not shown because too many files have changed in this diff Show More