diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt index c79aae13..5555c720 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.sample.android.bluetoothle.java.MainActivity class MainActivity : AppCompatActivity() { diff --git a/build.gradle.kts b/build.gradle.kts index 547023ae..22d9028a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,8 @@ plugins { alias(libs.plugins.kapt) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.kotlin.serialization) apply false } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") diff --git a/compose/recomposehighlighter/build.gradle.kts b/compose/recomposehighlighter/build.gradle.kts index 1bbdf6ea..fe8b79af 100644 --- a/compose/recomposehighlighter/build.gradle.kts +++ b/compose/recomposehighlighter/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) } android { compileSdk = libs.versions.compileSdk.get().toInt() @@ -40,9 +41,6 @@ android { viewBinding = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } } dependencies { val composeBom = platform(libs.androidx.compose.bom) diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index 3930f561..5d2b661f 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -20,6 +20,8 @@ plugins { alias(libs.plugins.kapt) alias(libs.plugins.hilt) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) } android { @@ -62,10 +64,6 @@ android { viewBinding = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } - packaging.resources { // Multiple dependency bring these files in. Exclude them to enable // our test APK to build (has no effect on our AARs) @@ -138,6 +136,7 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.recyclerview) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt index aaed4916..3979a110 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt @@ -28,9 +28,11 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.example.compose.snippets.animations.AnimationExamplesScreen +import com.example.compose.snippets.animations.sharedelement.PlaceholderSizeAnimated_Demo import com.example.compose.snippets.components.AppBarExamples import com.example.compose.snippets.components.BadgeExamples import com.example.compose.snippets.components.ButtonExamples +import com.example.compose.snippets.components.CardExamples import com.example.compose.snippets.components.CheckboxExamples import com.example.compose.snippets.components.ChipExamples import com.example.compose.snippets.components.ComponentsScreen @@ -45,14 +47,13 @@ import com.example.compose.snippets.components.SliderExamples import com.example.compose.snippets.components.SwitchExamples import com.example.compose.snippets.components.TimePickerExamples import com.example.compose.snippets.graphics.ApplyPolygonAsClipImage -import com.example.compose.snippets.graphics.BitmapFromComposableSnippet +import com.example.compose.snippets.graphics.BitmapFromComposableFullSnippet import com.example.compose.snippets.graphics.BrushExamplesScreen import com.example.compose.snippets.images.ImageExamplesScreen import com.example.compose.snippets.landing.LandingScreen import com.example.compose.snippets.navigation.Destination import com.example.compose.snippets.navigation.TopComponentsDestination import com.example.compose.snippets.ui.theme.SnippetsTheme -import com.example.topcomponents.CardExamples class SnippetsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -76,13 +77,14 @@ class SnippetsActivity : ComponentActivity() { Destination.BrushExamples -> BrushExamplesScreen() Destination.ImageExamples -> ImageExamplesScreen() Destination.AnimationQuickGuideExamples -> AnimationExamplesScreen() - Destination.ScreenshotExample -> BitmapFromComposableSnippet() + Destination.ScreenshotExample -> BitmapFromComposableFullSnippet() Destination.ComponentsExamples -> ComponentsScreen { navController.navigate( it.route ) } Destination.ShapesExamples -> ApplyPolygonAsClipImage() + Destination.SharedElementExamples -> PlaceholderSizeAnimated_Demo() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleListDetailPaneScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleListDetailPaneScaffold.kt index 68c14dfd..b79f7ee7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleListDetailPaneScaffold.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleListDetailPaneScaffold.kt @@ -35,8 +35,6 @@ import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt index f5c461f6..76006e68 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class) - package com.example.compose.snippets.adaptivelayouts import androidx.annotation.StringRes @@ -29,7 +27,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationQuickGuide.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationQuickGuide.kt index 297f579a..7c649357 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationQuickGuide.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationQuickGuide.kt @@ -87,7 +87,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind @@ -720,7 +719,6 @@ private fun LoadingScreen() { } } -@OptIn(ExperimentalComposeUiApi::class) @Preview @Composable fun AnimationLayout() { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index 80c53540..9db1748d 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -23,7 +23,6 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.EnterExitState -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColor import androidx.compose.animation.animateContentSize @@ -50,6 +49,7 @@ import androidx.compose.animation.core.createChildTransition import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.repeatable import androidx.compose.animation.core.snap import androidx.compose.animation.core.spring @@ -65,7 +65,7 @@ import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.animation.shrinkVertically import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.with +import androidx.compose.animation.togetherWith import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -80,18 +80,19 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.Button +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -155,7 +156,12 @@ private fun AnimatedVisibilityWithEnterAndExit() { ), exit = slideOutVertically() + shrinkVertically() + fadeOut() ) { - Text("Hello", Modifier.fillMaxWidth().height(200.dp)) + Text( + "Hello", + Modifier + .fillMaxWidth() + .height(200.dp) + ) } // [END android_compose_animations_animated_visibility_enter_exit] } @@ -190,7 +196,6 @@ private fun AnimatedVisibilityMutable() { // [END android_compose_animations_animated_visibility_mutable] } -@OptIn(ExperimentalAnimationApi::class) @Composable @Preview private fun AnimatedVisibilityAnimateEnterExitChildren() { @@ -203,7 +208,11 @@ private fun AnimatedVisibilityAnimateEnterExitChildren() { exit = fadeOut() ) { // Fade in/out the background and the foreground. - Box(Modifier.fillMaxSize().background(Color.DarkGray)) { + Box( + Modifier + .fillMaxSize() + .background(Color.DarkGray) + ) { Box( Modifier .align(Alignment.Center) @@ -222,7 +231,6 @@ private fun AnimatedVisibilityAnimateEnterExitChildren() { // [END android_compose_animations_animated_visibility_animate_enter_exit_children] } -@OptIn(ExperimentalAnimationApi::class) @Preview @Composable private fun AnimatedVisibilityTransition() { @@ -239,7 +247,11 @@ private fun AnimatedVisibilityTransition() { val background by transition.animateColor(label = "color") { state -> if (state == EnterExitState.Visible) Color.Blue else Color.Gray } - Box(modifier = Modifier.size(128.dp).background(background)) + Box( + modifier = Modifier + .size(128.dp) + .background(background) + ) } // [END android_compose_animations_animated_visibility_transition] } @@ -250,26 +262,29 @@ private fun AnimateAsStateSimple() { // [START android_compose_animations_animate_as_state] var enabled by remember { mutableStateOf(true) } - val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f) + val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") Box( - Modifier.fillMaxSize() + Modifier + .fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) ) // [END android_compose_animations_animate_as_state] } -@OptIn(ExperimentalAnimationApi::class) @Preview @Composable private fun AnimatedContentSimple() { // [START android_compose_animations_animated_content_simple] Row { - var count by remember { mutableStateOf(0) } + var count by remember { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text("Add") } - AnimatedContent(targetState = count) { targetCount -> + AnimatedContent( + targetState = count, + label = "animated content" + ) { targetCount -> // Make sure to use `targetCount`, not `count`. Text(text = "Count: $targetCount") } @@ -277,7 +292,6 @@ private fun AnimatedContentSimple() { // [END android_compose_animations_animated_content_simple] } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun AnimatedContentTransitionSpec(count: Int) { // [START android_compose_animations_animated_content_transition_spec] @@ -288,25 +302,25 @@ private fun AnimatedContentTransitionSpec(count: Int) { if (targetState > initialState) { // If the target number is larger, it slides up and fades in // while the initial (smaller) number slides up and fades out. - slideInVertically { height -> height } + fadeIn() with + slideInVertically { height -> height } + fadeIn() togetherWith slideOutVertically { height -> -height } + fadeOut() } else { // If the target number is smaller, it slides down and fades in // while the initial number slides down and fades out. - slideInVertically { height -> -height } + fadeIn() with + slideInVertically { height -> -height } + fadeIn() togetherWith slideOutVertically { height -> height } + fadeOut() }.using( // Disable clipping since the faded slide-in/out should // be displayed out of bounds. SizeTransform(clip = false) ) - } + }, label = "animated content" ) { targetCount -> Text(text = "$targetCount") } // [END android_compose_animations_animated_content_transition_spec] } -@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class) + @Composable private fun AnimatedContentSizeTransform() { // [START android_compose_animations_animated_content_size_transform] @@ -318,7 +332,7 @@ private fun AnimatedContentSizeTransform() { AnimatedContent( targetState = expanded, transitionSpec = { - fadeIn(animationSpec = tween(150, 150)) with + fadeIn(animationSpec = tween(150, 150)) togetherWith fadeOut(animationSpec = tween(150)) using SizeTransform { initialSize, targetSize -> if (targetState) { @@ -335,7 +349,7 @@ private fun AnimatedContentSizeTransform() { } } } - } + }, label = "size transform" ) { targetExpanded -> if (targetExpanded) { Expanded() @@ -352,7 +366,9 @@ private fun AnimateContentSizeSimple() { // [START android_compose_animations_animated_content_size_modifier_simple] var message by remember { mutableStateOf("Hello") } Box( - modifier = Modifier.background(Color.Blue).animateContentSize() + modifier = Modifier + .background(Color.Blue) + .animateContentSize() ) { Text(text = message) } // [END android_compose_animations_animated_content_size_modifier_simple] } @@ -361,7 +377,7 @@ private fun AnimateContentSizeSimple() { private fun CrossfadeSimple() { // [START android_compose_animations_crossfade_simple] var currentPage by remember { mutableStateOf("A") } - Crossfade(targetState = currentPage) { screen -> + Crossfade(targetState = currentPage, label = "cross fade") { screen -> when (screen) { "A" -> Text("Page A") "B" -> Text("Page B") @@ -412,6 +428,7 @@ private object UpdateTransitionEnumState { when { BoxState.Expanded isTransitioningTo BoxState.Collapsed -> spring(stiffness = 50f) + else -> tween(durationMillis = 500) } @@ -431,7 +448,7 @@ private object UpdateTransitionEnumState { // Start in collapsed state and immediately animate to expanded var currentState = remember { MutableTransitionState(BoxState.Collapsed) } currentState.targetState = BoxState.Expanded - val transition = updateTransition(currentState, label = "box state") + val transition = rememberTransition(currentState, label = "box state") // …… // [END android_compose_animations_transitions_state] } @@ -479,7 +496,6 @@ private object UpdateTransitionCreateChildTransition { // [END android_compose_animations_transitions_dialer_example] } -@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class) @Composable private fun UpdateTransitionAnimatedVisibility() { // [START android_compose_animations_transitions_animated_visibility] @@ -496,9 +512,13 @@ private fun UpdateTransitionAnimatedVisibility() { onClick = { selected = !selected }, shape = RoundedCornerShape(8.dp), border = BorderStroke(2.dp, borderColor), - elevation = elevation + shadowElevation = elevation ) { - Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { Text(text = "Hello, world!") // AnimatedVisibility as a part of the transition. transition.AnimatedVisibility( @@ -569,17 +589,22 @@ private object UpdateTransitionEncapsulating { @Composable private fun RememberInfiniteTransitionSimple() { // [START android_compose_animations_infinite_transition_simple] - val infiniteTransition = rememberInfiniteTransition() + val infiniteTransition = rememberInfiniteTransition(label = "infinite") val color by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse - ) + ), + label = "color" ) - Box(Modifier.fillMaxSize().background(color)) + Box( + Modifier + .fillMaxSize() + .background(color) + ) // [END android_compose_animations_infinite_transition_simple] } @@ -591,9 +616,14 @@ private fun AnimatableSimple(ok: Boolean) { LaunchedEffect(ok) { color.animateTo(if (ok) Color.Green else Color.Red) } - Box(Modifier.fillMaxSize().background(color.value)) + Box( + Modifier + .fillMaxSize() + .background(color.value) + ) // [END android_compose_animations_animatable_simple] } + @Composable private fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean) { // [START android_compose_animations_target_based_animation_simple] @@ -605,7 +635,7 @@ private fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean) { targetValue = 1000f ) } - var playTime by remember { mutableStateOf(0L) } + var playTime by remember { mutableLongStateOf(0L) } LaunchedEffect(anim) { val startTime = withFrameNanos { it } @@ -624,7 +654,8 @@ private fun AnimationSpecTween(enabled: Boolean) { val alpha: Float by animateFloatAsState( targetValue = if (enabled) 1f else 0.5f, // Configure the animation duration and easing. - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + label = "alpha" ) // [END android_compose_animations_spec_tween] } @@ -637,7 +668,8 @@ private fun AnimationSpecSpring() { animationSpec = spring( dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessMedium - ) + ), + label = "spring spec" ) // [END android_compose_animations_spec_spring] } @@ -651,7 +683,8 @@ private fun AnimationSpecTween() { durationMillis = 300, delayMillis = 50, easing = LinearOutSlowInEasing - ) + ), + label = "tween delay" ) // [END android_compose_animations_spec_tween_delay] } @@ -663,11 +696,12 @@ private fun AnimationSpecKeyframe() { targetValue = 1f, animationSpec = keyframes { durationMillis = 375 - 0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms - 0.2f at 15 with FastOutLinearInEasing // for 15-75 ms + 0.0f at 0 using LinearOutSlowInEasing // for 0-15 ms + 0.2f at 15 using FastOutLinearInEasing // for 15-75 ms 0.4f at 75 // ms 0.4f at 225 // ms - } + }, + label = "keyframe" ) // [END android_compose_animations_spec_keyframe] } @@ -681,7 +715,8 @@ private fun AnimationSpecRepeatable() { iterations = 3, animation = tween(durationMillis = 300), repeatMode = RepeatMode.Reverse - ) + ), + label = "repeatable spec" ) // [END android_compose_animations_spec_repeatable] } @@ -694,7 +729,8 @@ private fun AnimationSpecInfiniteRepeatable() { animationSpec = infiniteRepeatable( animation = tween(durationMillis = 300), repeatMode = RepeatMode.Reverse - ) + ), + label = "infinite repeatable" ) // [END android_compose_animations_spec_infinite_repeatable] } @@ -704,7 +740,8 @@ private fun AnimationSpecSnap() { // [START android_compose_animations_spec_snap] val value by animateFloatAsState( targetValue = 1f, - animationSpec = snap(delayMillis = 50) + animationSpec = snap(delayMillis = 50), + label = "snap spec" ) // [END android_compose_animations_spec_snap] } @@ -720,7 +757,8 @@ private object Easing { animationSpec = tween( durationMillis = 300, easing = CustomEasing - ) + ), + label = "custom easing" ) // …… } @@ -750,7 +788,8 @@ private object AnimationVectorCustomType { convertFromVector = { vector: AnimationVector2D -> MySize(vector.v1.dp, vector.v2.dp) } - ) + ), + label = "size" ) } // [END android_compose_animations_vector_convertor_custom_type] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt new file mode 100644 index 00000000..92bee909 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt @@ -0,0 +1,168 @@ +/* + * 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 + * + * https://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(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +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.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlurEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +private fun animationSpec() = tween(durationMillis = 500) +private val boundsTransition = BoundsTransform { _, _ -> animationSpec() } +private val shapeForSharedElement = RoundedCornerShape(16.dp) + +@OptIn(ExperimentalSharedTransitionApi::class) +@Preview +@Composable +private fun AnimatedVisibilitySharedElementBlurLayer() { + var selectedSnack by remember { mutableStateOf(null) } + val graphicsLayer = rememberGraphicsLayer() + val animateBlurRadius = animateFloatAsState( + targetValue = if (selectedSnack != null) 20f else 0f, + label = "blur radius", + animationSpec = animationSpec() + ) + + SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(Color.LightGray.copy(alpha = 0.5f)) + .blurLayer(graphicsLayer, animateBlurRadius.value) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed(listSnacks, key = { index, snack -> snack.name }) { index, snack -> + SnackItem( + snack = snack, + onClick = { + selectedSnack = snack + }, + visible = selectedSnack != snack, + modifier = Modifier.animateItem( + placementSpec = animationSpec(), + fadeOutSpec = animationSpec(), + fadeInSpec = animationSpec() + ) + ) + } + } + + SnackEditDetails( + snack = selectedSnack, + onConfirmClick = { + selectedSnack = null + } + ) + } +} + +fun Modifier.blurLayer(layer: GraphicsLayer, radius: Float): Modifier { + return if (radius == 0f) this else this.drawWithContent { + layer.apply { + record { + this@drawWithContent.drawContent() + } + // will apply a blur on API 31+ + this.renderEffect = BlurEffect(radius, radius, TileMode.Decal) + } + drawLayer(layer) + } +} +@Composable +fun SharedTransitionScope.SnackItem( + snack: Snack, + visible: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = fadeIn(animationSpec = animationSpec()) + scaleIn( + animationSpec() + ), + exit = fadeOut(animationSpec = animationSpec()) + scaleOut( + animationSpec() + ) + ) { + Box( + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"), + animatedVisibilityScope = this, + boundsTransform = boundsTransition, + clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) + ) + .background(Color.White, shapeForSharedElement) + .clip(shapeForSharedElement) + ) { + SnackContents( + snack = snack, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = snack.name), + animatedVisibilityScope = this@AnimatedVisibility, + boundsTransform = boundsTransition, + ), + onClick = onClick + ) + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt new file mode 100644 index 00000000..955dbe46 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt @@ -0,0 +1,232 @@ +/* + * 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 + * + * https://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(ExperimentalSharedTransitionApi::class, ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +private val shapeForSharedElement = RoundedCornerShape(16.dp) + +@OptIn(ExperimentalSharedTransitionApi::class) +@Preview +@Composable +private fun AnimatedVisibilitySharedElementShortenedExample() { + // [START android_compose_shared_elements_animated_visibility] + var selectedSnack by remember { mutableStateOf(null) } + + SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { + LazyColumn( + // [START_EXCLUDE] + modifier = Modifier + .fillMaxSize() + .background(Color.LightGray.copy(alpha = 0.5f)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + // [END_EXCLUDE] + ) { + items(listSnacks) { snack -> + AnimatedVisibility( + visible = snack != selectedSnack, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + modifier = Modifier.animateItem() + ) { + Box( + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"), + // Using the scope provided by AnimatedVisibility + animatedVisibilityScope = this, + clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) + ) + .background(Color.White, shapeForSharedElement) + .clip(shapeForSharedElement) + ) { + SnackContents( + snack = snack, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = snack.name), + animatedVisibilityScope = this@AnimatedVisibility + ), + onClick = { + selectedSnack = snack + } + ) + } + } + } + } + // Contains matching AnimatedContent with sharedBounds modifiers. + SnackEditDetails( + snack = selectedSnack, + onConfirmClick = { + selectedSnack = null + } + ) + } + // [END android_compose_shared_elements_animated_visibility] +} + +@Composable +fun SharedTransitionScope.SnackEditDetails( + snack: Snack?, + modifier: Modifier = Modifier, + onConfirmClick: () -> Unit +) { + AnimatedContent( + modifier = modifier, + targetState = snack, + transitionSpec = { + fadeIn() togetherWith fadeOut() + }, + label = "SnackEditDetails" + ) { targetSnack -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (targetSnack != null) { + Box( + modifier = Modifier + .fillMaxSize() + .clickable { + onConfirmClick() + } + .background(Color.Black.copy(alpha = 0.5f)) + ) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${targetSnack.name}-bounds"), + animatedVisibilityScope = this@AnimatedContent, + clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) + ) + .background(Color.White, shapeForSharedElement) + .clip(shapeForSharedElement) + ) { + + SnackContents( + snack = targetSnack, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = targetSnack.name), + animatedVisibilityScope = this@AnimatedContent, + ), + onClick = { + onConfirmClick() + } + ) + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, end = 8.dp), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = { onConfirmClick() }) { + Text(text = "Save changes") + } + } + } + } + } + } +} + +@Composable +fun SnackContents( + snack: Snack, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Column( + modifier = modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + onClick() + } + ) { + Image( + painter = painterResource(id = snack.image), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(20f / 9f), + contentScale = ContentScale.Crop, + contentDescription = null + ) + Text( + text = snack.name, + modifier = Modifier + .wrapContentWidth() + .padding(8.dp), + style = MaterialTheme.typography.titleSmall + ) + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/BasicSharedElementSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/BasicSharedElementSnippets.kt new file mode 100644 index 00000000..c246a809 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/BasicSharedElementSnippets.kt @@ -0,0 +1,499 @@ +/* + * 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 + * + * https://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(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +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.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.LavenderLight +import com.example.compose.snippets.ui.theme.RoseLight + +private class SharedElementBasicUsage2 { + @Preview + @Composable + private fun SharedElementApp() { + // [START android_compose_animations_shared_element_step1] + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } + // [END android_compose_animations_shared_element_step1] + } + + @Composable + private fun MainContent( + onShowDetails: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Row( + // [START_EXCLUDE] + modifier = Modifier + .padding(8.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable { + onShowDetails() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text("Cupcake", fontSize = 21.sp) + // [END_EXCLUDE] + } + } + + @Composable + private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Column( + // [START_EXCLUDE] + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable { + onBack() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text("Cupcake", fontSize = 28.sp) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + // [END_EXCLUDE] + } + } +} + +private class SharedElementBasicUsage3 { + + @Preview + @Composable + private fun SharedElementApp() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } + } + + // [START android_compose_animations_shared_element_step2] + @Composable + private fun MainContent( + onShowDetails: () -> Unit, + modifier: Modifier = Modifier, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Row( + // [START_EXCLUDE] + modifier = Modifier + .padding(8.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable { + onShowDetails() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + with(sharedTransitionScope) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text( + "Cupcake", fontSize = 21.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + // [END_EXCLUDE] + } + } + } + + @Composable + private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Column( + // [START_EXCLUDE] + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable { + onBack() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + with(sharedTransitionScope) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text( + "Cupcake", fontSize = 28.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + // [END_EXCLUDE] + } + } + } + // [END android_compose_animations_shared_element_step2] +} + +@Preview +@Composable +private fun SharedElement_ManualVisibleControl() { + // [START android_compose_shared_element_manual_control] + var selectFirst by remember { mutableStateOf(true) } + val key = remember { Any() } + SharedTransitionLayout( + Modifier + .fillMaxSize() + .padding(10.dp) + .clickable { + selectFirst = !selectFirst + } + ) { + Box( + Modifier + .sharedElementWithCallerManagedVisibility( + rememberSharedContentState(key = key), + !selectFirst + ) + .background(Color.Red) + .size(100.dp) + ) { + Text(if (!selectFirst) "false" else "true", color = Color.White) + } + Box( + Modifier + .offset(180.dp, 180.dp) + .sharedElementWithCallerManagedVisibility( + rememberSharedContentState( + key = key, + ), + selectFirst + ) + .alpha(0.5f) + .background(Color.Blue) + .size(180.dp) + ) { + Text(if (selectFirst) "false" else "true", color = Color.White) + } + } + // [END android_compose_shared_element_manual_control] +} + +@Preview +@Composable +private fun UnmatchedBoundsExample() { + // [START android_compose_animation_shared_element_bounds_unmatched] + var selectFirst by remember { mutableStateOf(true) } + val key = remember { Any() } + SharedTransitionLayout( + Modifier + .fillMaxSize() + .padding(10.dp) + .clickable { + selectFirst = !selectFirst + } + ) { + AnimatedContent(targetState = selectFirst, label = "AnimatedContent") { targetState -> + if (targetState) { + Box( + Modifier + .padding(12.dp) + .sharedBounds( + rememberSharedContentState(key = key), + animatedVisibilityScope = this@AnimatedContent + ) + .border(2.dp, Color.Red) + ) { + Text( + "Hello", + fontSize = 20.sp + ) + } + } else { + Box( + Modifier + .offset(180.dp, 180.dp) + .sharedBounds( + rememberSharedContentState( + key = key, + ), + animatedVisibilityScope = this@AnimatedContent + ) + .border(2.dp, Color.Red) + // This padding is placed after sharedBounds, but it doesn't match the + // other shared elements modifier order, resulting in visual jumps + .padding(12.dp) + + ) { + Text( + "Hello", + fontSize = 36.sp + ) + } + } + } + } + // [END android_compose_animation_shared_element_bounds_unmatched] +} + +private object UniqueKeySnippet { + // [START android_compose_shared_elements_unique_key] + data class SnackSharedElementKey( + val snackId: Long, + val origin: String, + val type: SnackSharedElementType + ) + + enum class SnackSharedElementType { + Bounds, + Image, + Title, + Tagline, + Background + } + + @Composable + fun SharedElementUniqueKey() { + // [START_EXCLUDE] + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [END_EXCLUDE] + Box( + modifier = Modifier + .sharedElement( + rememberSharedContentState( + key = SnackSharedElementKey( + snackId = 1, + origin = "latest", + type = SnackSharedElementType.Image + ) + ), + animatedVisibilityScope = this@AnimatedVisibility + ) + ) + // [START_EXCLUDE] + } + } + // [END_EXCLUDE] + } + // [END android_compose_shared_elements_unique_key] +} + +// [START android_compose_shared_element_scope] +val LocalNavAnimatedVisibilityScope = compositionLocalOf { null } +val LocalSharedTransitionScope = compositionLocalOf { null } + +@Composable +private fun SharedElementScope_CompositionLocal() { + // An example of how to use composition locals to pass around the shared transition scope, far down your UI tree. + // [START_EXCLUDE] + val state = remember { + mutableStateOf(false) + } + // [END_EXCLUDE] + SharedTransitionLayout { + CompositionLocalProvider( + LocalSharedTransitionScope provides this + ) { + // This could also be your top-level NavHost as this provides an AnimatedContentScope + AnimatedContent(state, label = "Top level AnimatedContent") { targetState -> + CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this) { + // Now we can access the scopes in any nested composables as follows: + val sharedTransitionScope = LocalSharedTransitionScope.current + ?: throw IllegalStateException("No SharedElementScope found") + val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current + ?: throw IllegalStateException("No AnimatedVisibility found") + } + // [START_EXCLUDE] + if (targetState.value) { + // do something + } + // [END_EXCLUDE] + } + } + } +} +// [END android_compose_shared_element_scope] + +private object SharedElementScope_Extensions { + // [START android_compose_shared_element_parameters] + @Composable + fun MainContent( + animatedVisibilityScope: AnimatedVisibilityScope, + sharedTransitionScope: SharedTransitionScope + ) { + } + + @Composable + fun Details( + animatedVisibilityScope: AnimatedVisibilityScope, + sharedTransitionScope: SharedTransitionScope + ) { + } + // [END android_compose_shared_element_parameters] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt new file mode 100644 index 00000000..3e60c10e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -0,0 +1,630 @@ +/* + * 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 + * + * https://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(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.ArcMode +import androidx.compose.animation.core.ExperimentalAnimationSpecApi +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +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.requiredHeightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Create +import androidx.compose.material.icons.outlined.Favorite +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.draw.clip +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.LavenderLight +import com.example.compose.snippets.ui.theme.RoseLight + +@Preview +@Composable +fun SharedElementApp_BoundsTransformExample() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun MainContent( + onShowDetails: () -> Unit, + modifier: Modifier = Modifier, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Box(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn( + tween( + boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + exit = fadeOut( + tween( + boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + boundsTransform = boundsTransform + ) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + onShowDetails() + } + .padding(8.dp) + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform + ) + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + val textBoundsTransform = BoundsTransform { initialBounds, targetBounds -> + keyframes { + durationMillis = boundsAnimationDurationMillis + initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing + targetBounds at boundsAnimationDurationMillis + } + } + Text( + "Cupcake", fontSize = 21.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = textBoundsTransform + ) + ) + } + } + } +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn( + tween( + durationMillis = boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + exit = fadeOut( + tween( + durationMillis = boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + boundsTransform = boundsTransform + ) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + onBack() + } + .padding(8.dp) + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform + ) + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START android_compose_shared_element_text_bounds_transform] + val textBoundsTransform = BoundsTransform { initialBounds, targetBounds -> + keyframes { + durationMillis = boundsAnimationDurationMillis + initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing + targetBounds at boundsAnimationDurationMillis + } + } + Text( + "Cupcake", fontSize = 28.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = textBoundsTransform + ) + ) + // [END android_compose_shared_element_text_bounds_transform] + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus", + modifier = Modifier.skipToLookaheadSize() + ) + } + } + } +} + +private val boundsTransform = BoundsTransform { _: Rect, _: Rect -> + tween(durationMillis = boundsAnimationDurationMillis, easing = FastOutSlowInEasing) +} +private const val boundsAnimationDurationMillis = 500 + +@Preview +@Composable +private fun SharedElement_Clipping() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + Row( + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = this@AnimatedContent + ) + .background(Color.Green.copy(alpha = 0.5f)) + .padding(8.dp) + .clickable { + showDetails = true + } + ) { + // [START android_compose_animations_shared_element_clipping] + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(100.dp) + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = this@AnimatedContent + ) + .clip(RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + // [END android_compose_animations_shared_element_clipping] + Text( + "Lorem ipsum dolor sit amet.", fontSize = 21.sp, + modifier = Modifier.sharedElement( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = this@AnimatedContent, + + ) + ) + } + } else { + Column( + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = this@AnimatedContent + ) + .background(Color.Green.copy(alpha = 0.7f)) + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .clickable { + showDetails = false + } + + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(200.dp) + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = this@AnimatedContent + ) + .clip(RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + Text( + "Lorem ipsum dolor sit amet.", fontSize = 21.sp, + modifier = Modifier.sharedElement( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = this@AnimatedContent + ) + ) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + } + } + } + } +} + +@Composable +private fun JetsnackBottomBar(modifier: Modifier) { +} + +@Composable +private fun EnterExitJetsnack() { + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [START android_compose_shared_element_enter_exit] + JetsnackBottomBar( + modifier = Modifier + .renderInSharedTransitionScopeOverlay( + zIndexInOverlay = 1f, + ) + .animateEnterExit( + enter = fadeIn() + slideInVertically { + it + }, + exit = fadeOut() + slideOutVertically { + it + } + ) + ) + // [END android_compose_shared_element_enter_exit] + } + } +} + +@Preview +@Composable +private fun SharedElement_SkipLookaheadSize() { + // Nested shared bounds sample. + val selectionColor = Color(0xff3367ba) + var expanded by remember { mutableStateOf(true) } + SharedTransitionLayout( + Modifier + .fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + expanded = !expanded + } + .background(Color(0x88000000)) + ) { + AnimatedVisibility( + visible = expanded, + enter = EnterTransition.None, + exit = ExitTransition.None + ) { + Box(modifier = Modifier.fillMaxSize()) { + Surface( + Modifier + .align(Alignment.BottomCenter) + .padding(20.dp) + .sharedBounds( + rememberSharedContentState(key = "container"), + this@AnimatedVisibility + ) + .requiredHeightIn(max = 60.dp), + shape = RoundedCornerShape(50), + ) { + Row( + Modifier + .padding(10.dp) + // By using Modifier.skipToLookaheadSize(), we are telling the layout + // system to layout the children of this node as if the animations had + // all finished. This avoid re-laying out the Row with animated width, + // which is _sometimes_ desirable. Try removing this modifier and + // observe the effect. + .skipToLookaheadSize() + ) { + Icon( + Icons.Outlined.Share, + contentDescription = "Share", + modifier = Modifier.padding( + top = 10.dp, + bottom = 10.dp, + start = 10.dp, + end = 20.dp + ) + ) + Icon( + Icons.Outlined.Favorite, + contentDescription = "Favorite", + modifier = Modifier.padding( + top = 10.dp, + bottom = 10.dp, + start = 10.dp, + end = 20.dp + ) + ) + Icon( + Icons.Outlined.Create, + contentDescription = "Create", + tint = Color.White, + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "icon_background"), + this@AnimatedVisibility + ) + .background(selectionColor, RoundedCornerShape(50)) + .padding( + top = 10.dp, + bottom = 10.dp, + start = 20.dp, + end = 20.dp + ) + .sharedElement( + rememberSharedContentState(key = "icon"), + this@AnimatedVisibility + ) + ) + } + } + } + } + AnimatedVisibility( + visible = !expanded, + enter = EnterTransition.None, + exit = ExitTransition.None + ) { + Box(modifier = Modifier.fillMaxSize()) { + Surface( + Modifier + .align(Alignment.BottomEnd) + .padding(30.dp) + .sharedBounds( + rememberSharedContentState(key = "container"), + this@AnimatedVisibility, + enter = EnterTransition.None, + ) + .sharedBounds( + rememberSharedContentState(key = "icon_background"), + this@AnimatedVisibility, + enter = EnterTransition.None, + exit = ExitTransition.None + ), + shape = RoundedCornerShape(30.dp), + color = selectionColor + ) { + Icon( + Icons.Outlined.Create, + contentDescription = "Create", + tint = Color.White, + modifier = Modifier + .padding(30.dp) + .size(40.dp) + .sharedElement( + rememberSharedContentState(key = "icon"), + this@AnimatedVisibility + ) + ) + } + } + } + } +} + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +@Preview +@Composable +fun PlaceholderSizeAnimated_Demo() { + // This demo shows how other items in a layout can respond to shared elements changing in size. + // [START android_compose_shared_element_placeholder_size] + SharedTransitionLayout { + + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = "home" + ) { + composable("home", enterTransition = { fadeIn() }, exitTransition = { fadeOut() }) { + Column(modifier = Modifier.fillMaxSize()) { + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + (listSnacks).forEachIndexed { index, snack -> + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(8.dp) + .sharedBounds( + rememberSharedContentState(key = "image-${snack.name}"), + animatedVisibilityScope = this@composable, + placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize + ) + .clickable { + navController.navigate("details/$index") + } + .height(180.dp) + .clip(RoundedCornerShape(8.dp)) + .aspectRatio(9f / 16f) + + ) + } + } + Text("Nearby snacks") + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + (listSnacks).forEach { snack -> + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .height(200.dp) + .aspectRatio(16f / 9f) + .padding(8.dp) + ) + } + } + } + } + composable( + "details/{id}", + arguments = listOf(navArgument("id") { type = NavType.IntType }), + enterTransition = { fadeIn() }, exitTransition = { fadeOut() } + ) { backStackEntry -> + val id = backStackEntry.arguments?.getInt("id") + val snack = listSnacks[id!!] + Column( + Modifier + .fillMaxSize() + .clickable { + navController.navigateUp() + } + ) { + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "image-${snack.name}"), + animatedVisibilityScope = this@composable, + placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize + ) + .clip(RoundedCornerShape(8.dp)) + .fillMaxWidth() + .aspectRatio(9f / 16f) + ) + } + } + } + } +// [END android_compose_shared_element_placeholder_size] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedBoundsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedBoundsSnippets.kt new file mode 100644 index 00000000..3878fa6a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedBoundsSnippets.kt @@ -0,0 +1,200 @@ +/* + * 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 + * + * https://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(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.LavenderLight +import com.example.compose.snippets.ui.theme.RoseLight + +@Preview +@Composable +fun SharedBoundsDemo() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } +} + +// [START android_compose_animations_shared_element_shared_bounds] +@Composable +private fun MainContent( + onShowDetails: () -> Unit, + modifier: Modifier = Modifier, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Row( + modifier = Modifier + .padding(8.dp) + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() + ) + // [START_EXCLUDE] + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable { + onShowDetails() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + // [START_EXCLUDE] + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + Text( + "Cupcake", fontSize = 21.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + // [END_EXCLUDE] + } + } +} + +@Composable +private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Column( + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() + ) + // [START_EXCLUDE] + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable { + onBack() + } + .padding(8.dp) + // [END_EXCLUDE] + + ) { + // [START_EXCLUDE] + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + Text( + "Cupcake", fontSize = 28.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + // [END_EXCLUDE] + } + } +} +// [END android_compose_animations_shared_element_shared_bounds] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementCommonUseCaseSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementCommonUseCaseSnippets.kt new file mode 100644 index 00000000..944122d1 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementCommonUseCaseSnippets.kt @@ -0,0 +1,101 @@ +/* + * 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 + * + * https://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(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest + +@Preview +@Composable +private fun SharedAsyncImage() { + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [START android_compose_shared_element_async_image_tip] + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("your-image-url") + .crossfade(true) + .placeholderMemoryCacheKey("image-key") // same key as shared element key + .memoryCacheKey("image-key") // same key as shared element key + .build(), + placeholder = null, + contentDescription = null, + modifier = Modifier + .size(120.dp) + .sharedBounds( + rememberSharedContentState( + key = "image-key" + ), + animatedVisibilityScope = this + ) + ) + // [END android_compose_shared_element_async_image_tip] + } + } +} + +@Composable +fun debugPlaceholder(@DrawableRes debugPreview: Int) = + if (LocalInspectionMode.current) { + painterResource(id = debugPreview) + } else { + null + } + +@Preview +@Composable +private fun SharedElementTypicalUseText() { + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [START android_compose_shared_element_text_tip] + Text( + text = "This is an example of how to share text", + modifier = Modifier + .wrapContentWidth() + .sharedBounds( + rememberSharedContentState( + key = "shared Text" + ), + animatedVisibilityScope = this, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() + ) + ) + // [END android_compose_shared_element_text_tip] + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt new file mode 100644 index 00000000..da1d178f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt @@ -0,0 +1,195 @@ +/* + * 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 + * + * https://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(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.example.compose.snippets.R + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +// [START android_compose_shared_element_predictive_back] +@Preview +@Composable +fun SharedElement_PredictiveBack() { + SharedTransitionLayout { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = "home" + ) { + composable("home") { + HomeScreen( + navController, + this@SharedTransitionLayout, + this@composable + ) + } + composable( + "details/{item}", + arguments = listOf(navArgument("item") { type = NavType.IntType }) + ) { backStackEntry -> + val id = backStackEntry.arguments?.getInt("item") + val snack = listSnacks[id!!] + DetailsScreen( + navController, + id, + snack, + this@SharedTransitionLayout, + this@composable + ) + } + } + } +} + +@Composable +private fun DetailsScreen( + navController: NavHostController, + id: Int, + snack: Snack, + sharedTransitionScope: SharedTransitionScope, + animatedContentScope: AnimatedContentScope +) { + with(sharedTransitionScope) { + Column( + Modifier + .fillMaxSize() + .clickable { + navController.navigate("home") + } + ) { + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "image-$id"), + animatedVisibilityScope = animatedContentScope + ) + .aspectRatio(1f) + .fillMaxWidth() + ) + Text( + snack.name, fontSize = 18.sp, + modifier = + Modifier + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "text-$id"), + animatedVisibilityScope = animatedContentScope + ) + .fillMaxWidth() + ) + } + } +} + +@Composable +private fun HomeScreen( + navController: NavHostController, + sharedTransitionScope: SharedTransitionScope, + animatedContentScope: AnimatedContentScope +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(listSnacks) { index, item -> + Row( + Modifier.clickable { + navController.navigate("details/$index") + } + ) { + Spacer(modifier = Modifier.width(8.dp)) + with(sharedTransitionScope) { + Image( + painterResource(id = item.image), + contentDescription = item.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "image-$index"), + animatedVisibilityScope = animatedContentScope + ) + .size(100.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + item.name, fontSize = 18.sp, + modifier = Modifier + .align(Alignment.CenterVertically) + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "text-$index"), + animatedVisibilityScope = animatedContentScope, + ) + ) + } + } + } + } +} + +data class Snack( + val name: String, + val description: String, + @DrawableRes val image: Int +) +// [END android_compose_shared_element_predictive_back] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureSnippets.kt index b22421ff..3f9f41c3 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureSnippets.kt @@ -18,13 +18,14 @@ package com.example.compose.snippets.architecture import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue @@ -70,7 +71,7 @@ private object ArchitectureSnippets2 { private object ArchitectureSnippets3 { val localizedString = "" - + @OptIn(ExperimentalMaterial3Api::class) // [START android_compose_architecture_architecture3] @Composable fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) { @@ -87,7 +88,7 @@ private object ArchitectureSnippets3 { navigationIcon = { IconButton(onClick = onBackPressed) { Icon( - Icons.Filled.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, contentDescription = localizedString ) } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Card.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Card.kt index 1259685e..1b755819 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/Card.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Card.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.topcomponents +package com.example.compose.snippets.components import android.util.Log import androidx.compose.foundation.BorderStroke @@ -27,7 +27,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text @@ -55,7 +54,6 @@ fun CardExamples() { } } -@OptIn(ExperimentalMaterial3Api::class) // [START android_compose_components_customcard] @Composable fun CustomCardExample(event: () -> Unit) { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Chip.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Chip.kt index 0737ddc8..36533db7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/Chip.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Chip.kt @@ -29,7 +29,6 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -86,7 +85,6 @@ fun AssistChipExample() { } // [END android_compose_components_assistchip] -@OptIn(ExperimentalMaterial3Api::class) @Preview // [START android_compose_components_filterchip] @Composable @@ -116,7 +114,6 @@ fun FilterChipExample() { // You could set this up similarly to the filter chip above and have it toggleable, but this is // an example of a chip that can dismiss with a click. -@OptIn(ExperimentalMaterial3Api::class) // [START android_compose_components_inputchip] @Composable fun InputChipExample( diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Dialog.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Dialog.kt index b2ab9b72..beb0356b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/Dialog.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Dialog.kt @@ -32,7 +32,6 @@ import androidx.compose.material.icons.filled.Info import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -220,7 +219,6 @@ fun DialogWithImage( // [END android_compose_components_dialogwithimage] // [START android_compose_components_alertdialog] -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AlertDialogExample( onDismissRequest: () -> Unit, diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/AppCompatThemeSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/AppCompatThemeSnippets.kt deleted file mode 100644 index 56cf8e37..00000000 --- a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/AppCompatThemeSnippets.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 - * - * https://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 com.example.compose.snippets.designsystems - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -// [START android_compose_designsystems_interop_appcompattheme] -import com.google.accompanist.themeadapter.appcompat.AppCompatTheme - -class AppCompatThemeExample : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - // Colors and typography have been read from the - // View-based theme used in this Activity - // Shapes are the default for M2 as this didn't exist in M1 - AppCompatTheme { - // Your app-level composable here - } - } - } -} -// [END android_compose_designsystems_interop_appcompattheme] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/CustomDesignSystem.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/CustomDesignSystem.kt index fd864ac3..8b42ab1b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/CustomDesignSystem.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/CustomDesignSystem.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.unit.sp private object CustomDesignSystemExtend { // [START android_compose_designsystems_custom_extend] + // Use with MaterialTheme.colorScheme.snackbarAction val ColorScheme.snackbarAction: Color @Composable @@ -326,6 +327,7 @@ object FullyCustomDesignSystem { .compositeOver(CustomTheme.colors.component), disabledContentColor = CustomTheme.colors.content .copy(alpha = 0.38f) + ), shape = ButtonShape, elevation = ButtonDefaults.elevatedButtonElevation( diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt index f70cd415..e838a1b7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:Suppress("unused") +@file:Suppress("unused", "DEPRECATION_ERROR", "UsingMaterialAndMaterial3Libraries") package com.example.compose.snippets.designsystems @@ -38,7 +38,7 @@ import androidx.compose.material.Typography import androidx.compose.material.contentColorFor import androidx.compose.material.darkColors import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.lightColors import androidx.compose.material.primarySurface import androidx.compose.material.ripple.LocalRippleTheme @@ -158,7 +158,7 @@ fun ColorUsage() { CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) { Icon( // [START_EXCLUDE] - Icons.Filled.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null // [END_EXCLUDE] ) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material3Snippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material3Snippets.kt index 18f1ceb9..5d5d72d3 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material3Snippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material3Snippets.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -53,7 +52,6 @@ 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.graphics.Color import androidx.compose.ui.graphics.RectangleShape @@ -72,7 +70,6 @@ import com.example.compose.snippets.ui.theme.Typography private object Material3Snippets { // [START android_compose_material3_experimental_annotation] // import androidx.compose.material3.ExperimentalMaterial3Api - @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppComposable() { // M3 composables diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/MdcThemeSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/MdcThemeSnippets.kt deleted file mode 100644 index 514b9af0..00000000 --- a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/MdcThemeSnippets.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 - * - * https://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 com.example.compose.snippets.designsystems - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -// [START android_compose_designsystems_interop_mdctheme] -import com.google.accompanist.themeadapter.material.MdcTheme - -class MdcThemeExample : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // Use MdcTheme instead of M2 MaterialTheme - // Colors, typography, and shapes have been read from the - // View-based theme used in this Activity - setContent { - MdcTheme { - // Your app-level composable here - } - } - } -} -// [END android_compose_designsystems_interop_mdctheme] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt index aaea6a62..e1300358 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt @@ -149,17 +149,14 @@ private object CreateUI { provideContent { // create your AppWidget here - GlanceTheme { - MyContent() - } + MyContent() } } @Composable private fun MyContent() { Column( - modifier = GlanceModifier.fillMaxSize() - .background(GlanceTheme.colors.background), + modifier = GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Top, horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt index 900f38da..bc16c96a 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.content.Intent.createChooser import android.graphics.Bitmap -import android.graphics.Picture import android.media.MediaScannerConnection import android.net.Uri import android.os.Build @@ -29,7 +28,9 @@ import android.os.Environment import android.provider.MediaStore import androidx.compose.foundation.Image 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.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -44,18 +45,17 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue 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.drawWithCache +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.draw -import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -87,14 +87,44 @@ import kotlinx.coroutines.suspendCancellableCoroutine * limitations under the License. */ +@Preview +@Composable +private fun CreateBitmapFromGraphicsLayer() { + // [START android_compose_graphics_layer_bitmap_basics] + val coroutineScope = rememberCoroutineScope() + val graphicsLayer = rememberGraphicsLayer() + Box( + modifier = Modifier + .drawWithContent { + // call record to capture the content in the graphics layer + graphicsLayer.record { + // draw the contents of the composable into the graphics layer + this@drawWithContent.drawContent() + } + // draw the graphics layer on the visible canvas + drawLayer(graphicsLayer) + } + .clickable { + coroutineScope.launch { + val bitmap = graphicsLayer.toImageBitmap() + // do something with the newly acquired bitmap + } + } + .background(Color.White) + ) { + Text("Hello Android", fontSize = 26.sp) + } + // [END android_compose_graphics_layer_bitmap_basics] +} + @OptIn(ExperimentalPermissionsApi::class) @Preview @Composable -fun BitmapFromComposableSnippet() { +fun BitmapFromComposableFullSnippet() { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val picture = remember { Picture() } + val graphicsLayer = rememberGraphicsLayer() val writeStorageAccessState = rememberMultiplePermissionsState( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -104,14 +134,15 @@ fun BitmapFromComposableSnippet() { listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) } ) + // This logic should live in your ViewModel - trigger a side effect to invoke URI sharing. // checks permissions granted, and then saves the bitmap from a Picture that is already capturing content // and shares it with the default share sheet. fun shareBitmapFromComposable() { if (writeStorageAccessState.allPermissionsGranted) { coroutineScope.launch { - val bitmap = createBitmapFromPicture(picture) - val uri = bitmap.saveToDisk(context) + val bitmap = graphicsLayer.toImageBitmap() + val uri = bitmap.asAndroidBitmap().saveToDisk(context) shareBitmap(context, uri) } } else if (writeStorageAccessState.shouldShowRationale) { @@ -140,39 +171,22 @@ fun BitmapFromComposableSnippet() { } } ) { padding -> - // [START android_compose_draw_into_bitmap] Column( modifier = Modifier .padding(padding) .fillMaxSize() .drawWithCache { - // Example that shows how to redirect rendering to an Android Picture and then - // draw the picture into the original destination - val width = this.size.width.toInt() - val height = this.size.height.toInt() - onDrawWithContent { - val pictureCanvas = - androidx.compose.ui.graphics.Canvas( - picture.beginRecording( - width, - height - ) - ) - // requires at least 1.6.0-alpha01+ - draw(this, this.layoutDirection, pictureCanvas, this.size) { + graphicsLayer.record { this@onDrawWithContent.drawContent() } - picture.endRecording() - - drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) } + drawLayer(graphicsLayer) } } ) { ScreenContentToCapture() } - // [END android_compose_draw_into_bitmap] } } @@ -207,25 +221,6 @@ private fun ScreenContentToCapture() { } } -private fun createBitmapFromPicture(picture: Picture): Bitmap { - // [START android_compose_draw_into_bitmap_convert_picture] - val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - Bitmap.createBitmap(picture) - } else { - val bitmap = Bitmap.createBitmap( - picture.width, - picture.height, - Bitmap.Config.ARGB_8888 - ) - val canvas = android.graphics.Canvas(bitmap) - canvas.drawColor(android.graphics.Color.WHITE) - canvas.drawPicture(picture) - bitmap - } - // [END android_compose_draw_into_bitmap_convert_picture] - return bitmap -} - private suspend fun Bitmap.saveToDisk(context: Context): Uri { val file = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/BrushExampleSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/BrushExampleSnippets.kt index 0aaf13b9..23a6a58e 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/BrushExampleSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/BrushExampleSnippets.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.graphics.drawscope.inset import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.imageResource -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -244,7 +243,6 @@ fun GraphicsBrushSizeRecreationExample() { // [END android_compose_graphics_brush_recreation] } -@OptIn(ExperimentalTextApi::class) @Preview @Composable fun GraphicsImageBrush() { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/CanvasSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/CanvasSnippets.kt index 74e1a4d3..4b53db6e 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/CanvasSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/CanvasSnippets.kt @@ -16,7 +16,6 @@ package com.example.compose.snippets.graphics -import android.graphics.Canvas import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.OvalShape import androidx.compose.animation.core.Animatable @@ -48,7 +47,6 @@ import androidx.compose.ui.graphics.drawscope.withTransform import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.res.imageResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer @@ -59,7 +57,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.toSize import com.example.compose.snippets.R -import java.util.Collections.rotate /* * Copyright 2022 The Android Open Source Project @@ -192,7 +189,6 @@ fun CanvasMultipleTransformations() { // [END android_compose_graphics_canvas_multiple_transforms] } -@OptIn(ExperimentalTextApi::class) @Preview @Composable fun CanvasDrawText() { @@ -238,7 +234,6 @@ fun CanvasDrawPath() { // [END android_compose_graphics_canvas_draw_path] } -@OptIn(ExperimentalTextApi::class) @Preview @Composable fun CanvasMeasureText() { @@ -268,7 +263,6 @@ fun CanvasMeasureText() { // [END android_compose_graphics_canvas_draw_text_measure] } -@OptIn(ExperimentalTextApi::class) @Preview @Composable fun CanvasMeasureTextOverflow() { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeWithOtherLibraries.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeWithOtherLibraries.kt index 3fc79a66..fe532068 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeWithOtherLibraries.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeWithOtherLibraries.kt @@ -26,8 +26,8 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt index 6b146981..59ac0129 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt @@ -36,10 +36,10 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Image -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer @@ -64,10 +64,12 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable class RVActivity : ComponentActivity() { private lateinit var composeView: ComposeView @@ -104,13 +106,13 @@ class RVActivity : ComponentActivity() { } @Composable - fun commonUseCase2(data: List) { + fun CommonUseCase2(data: List) { // [START android_compose_interop_migration_common_scenarios_recyclerview_common_use_case_2] LazyColumn(Modifier.fillMaxSize()) { itemsIndexed(data) { index, d -> ListItem(d) if (index != data.size - 1) { - Divider() + HorizontalDivider() } } } @@ -141,6 +143,13 @@ class SampleActivity : ComponentActivity() { } // [END android_compose_interop_migration_common_scenarios_navigation_step_2] +// [START android_compose_interop_migration_common_scenarios_navigation_step_2_1] +@Serializable data object First +@Serializable data class Second(val id: String) +@Serializable data object Third + +// [END android_compose_interop_migration_common_scenarios_navigation_step_2_1] + private object MigrationCommonScenariosNavigationStep3 { // [START android_compose_interop_migration_common_scenarios_navigation_step_3] @Composable @@ -164,7 +173,7 @@ private object MigrationCommonScenariosNavigationStep4 { fun SampleNavHost( navController: NavHostController ) { - NavHost(navController = navController, startDestination = "first") { + NavHost(navController = navController, startDestination = First) { // ... } } @@ -191,11 +200,11 @@ class FirstFragment : Fragment() { fun SampleNavHost( navController: NavHostController ) { - NavHost(navController = navController, startDestination = "first") { - composable("first") { + NavHost(navController = navController, startDestination = First) { + composable { FirstScreen(/* ... */) // EXTRACT TO HERE } - composable("second") { + composable { SecondScreen(/* ... */) } // ... @@ -220,20 +229,22 @@ private object MigrationCommonScenariosNavigationStep7 { fun SampleNavHost( navController: NavHostController ) { - NavHost(navController = navController, startDestination = "first") { - composable("first") { + NavHost(navController = navController, startDestination = First) { + composable { FirstScreen( onButtonClick = { // findNavController().navigate(firstScreenToSecondScreenAction) - navController.navigate("second_screen_route") + navController.navigate(Second(id = "ABC")) } ) } - composable("second") { + composable { backStackEntry -> + val secondRoute = backStackEntry.toRoute() SecondScreen( + id = secondRoute.id, onIconClick = { // findNavController().navigate(secondScreenToThirdScreenAction) - navController.navigate("third_screen_route") + navController.navigate(Third) } ) } @@ -260,7 +271,6 @@ class CoordinatorLayoutActivity : ComponentActivity() { // [END android_compose_interop_migration_common_scenarios_coordinatorlayout_step2] } - @OptIn(ExperimentalFoundationApi::class) private fun step3() { // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_step3] composeView.setContent { @@ -315,7 +325,7 @@ class CoordinatorLayoutActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable - private fun commonUseCaseToolbars() { + private fun CommonUseCaseToolbars() { // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_toolbars] // 1. Create the TopAppBarScrollBehavior val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() @@ -344,13 +354,13 @@ class CoordinatorLayoutActivity : ComponentActivity() { } @Composable - private fun commonUseCaseDrawers() { + private fun CommonUseCaseDrawers() { // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_drawers] ModalNavigationDrawer( drawerContent = { ModalDrawerSheet { Text("Drawer title", modifier = Modifier.padding(16.dp)) - Divider() + HorizontalDivider() NavigationDrawerItem( label = { Text(text = "Drawer Item") }, selected = false, @@ -371,7 +381,7 @@ class CoordinatorLayoutActivity : ComponentActivity() { } @Composable - private fun commonUseCaseSnackbars() { + private fun CommonUseCaseSnackbars() { // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_snackbars] val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } @@ -410,6 +420,7 @@ fun SampleApp() { @Composable fun SecondScreen( + id: String = "", onIconClick: () -> Unit = {}, ) { } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt index 9edd96be..12c5618b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt @@ -34,8 +34,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -43,7 +43,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester.Companion.createRefs @@ -220,7 +219,6 @@ fun destructuring() { // [END android_compose_kotlin_destructuring] } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun DestructuringCompose() { // [START android_compose_kotlin_destructuring_compose] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt index 6b7c1a87..79083bd3 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt @@ -16,14 +16,17 @@ package com.example.compose.snippets.landing +import androidx.compose.foundation.clickable 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -65,12 +68,11 @@ fun LandingScreen( fun NavigationItems(navigate: (Destination) -> Unit) { LazyColumn( modifier = Modifier - .padding(16.dp) .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - items(Destination.values().toList()) { destination -> + items(Destination.entries) { destination -> NavigationItem(destination) { navigate( destination @@ -82,9 +84,14 @@ fun NavigationItems(navigate: (Destination) -> Unit) { @Composable fun NavigationItem(destination: Destination, onClick: () -> Unit) { - Button( - onClick = { onClick() } + Box( + modifier = Modifier + .heightIn(min = 48.dp) + .clickable { + onClick() + } ) { Text(destination.title) + HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt index c66253cd..7980eae0 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt @@ -21,7 +21,6 @@ package com.example.compose.snippets.layouts import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -48,7 +47,6 @@ import androidx.window.core.layout.WindowSizeClass * limitations under the License. */ // [START android_compose_adaptive_layouts_basic] -@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun MyApp( windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AlignmentLinesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AlignmentLinesSnippets.kt index e312de9b..8946384b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AlignmentLinesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AlignmentLinesSnippets.kt @@ -35,8 +35,8 @@ package com.example.compose.snippets.layouts import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CommonLayoutExamples.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CommonLayoutExamples.kt index fd3b257b..98f5b03a 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CommonLayoutExamples.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CommonLayoutExamples.kt @@ -19,7 +19,6 @@ package com.example.compose.snippets.layouts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -53,7 +52,6 @@ import kotlin.random.Random * See the License for the specific language governing permissions and * limitations under the License. */ -@OptIn(ExperimentalLayoutApi::class) @Composable @Preview fun Layout_Graph_Vertical() { @@ -80,7 +78,6 @@ fun Layout_Graph_Vertical() { // [END android_compose_layout_vertical_graph] } -@OptIn(ExperimentalLayoutApi::class) @Composable @Preview fun Layout_Graph_Horizontal() { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintLayoutSnippets.kt index fe2106a9..d9763de1 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintLayoutSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintLayoutSnippets.kt @@ -19,8 +19,8 @@ package com.example.compose.snippets.layouts import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CustomLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CustomLayoutSnippets.kt index 72c6cb0a..a92c0c2d 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CustomLayoutSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CustomLayoutSnippets.kt @@ -34,7 +34,7 @@ package com.example.compose.snippets.layouts * limitations under the License. */ import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.AlignmentLine diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt index 941ae99d..164dd3ea 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt @@ -21,25 +21,40 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ContextualFlowRow +import androidx.compose.foundation.layout.ContextualFlowRowOverflow +import androidx.compose.foundation.layout.ContextualFlowRowOverflowScope import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowColumn import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer +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.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Chip +import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Text +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Text 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.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.example.compose.snippets.util.MaterialColors @Preview @@ -262,15 +277,17 @@ private fun FlowItems() { @OptIn(ExperimentalMaterialApi::class) @Composable -fun ChipItem(text: String) { - Chip( +fun ChipItem(text: String, onClick: () -> Unit = {}) { + FilterChip( modifier = Modifier.padding(end = 4.dp), - onClick = {}, + onClick = onClick, leadingIcon = {}, - border = BorderStroke(1.dp, Color(0xFF3B3A3C)) - ) { - Text(text) - } + border = BorderStroke(1.dp, Color(0xFF3B3A3C)), + label = { + Text(text) + }, + selected = false + ) } @Composable @@ -426,9 +443,115 @@ fun FlowLayout_FractionalSizing() { ) { val itemModifier = Modifier .clip(RoundedCornerShape(8.dp)) - Box(modifier = itemModifier.height(200.dp).width(60.dp).background(Color.Red)) - Box(modifier = itemModifier.height(200.dp).fillMaxWidth(0.7f).background(Color.Blue)) - Box(modifier = itemModifier.height(200.dp).weight(1f).background(Color.Magenta)) + Box( + modifier = itemModifier + .height(200.dp) + .width(60.dp) + .background(Color.Red) + ) + Box( + modifier = itemModifier + .height(200.dp) + .fillMaxWidth(0.7f) + .background(Color.Blue) + ) + Box( + modifier = itemModifier + .height(200.dp) + .weight(1f) + .background(Color.Magenta) + ) } // [END android_compose_flow_layout_fractional_sizing] } + +@OptIn(ExperimentalLayoutApi::class) +@Preview +@Composable +fun ContextualFlowLayoutExample() { + // [START android_compose_layouts_contextual_flow] + val totalCount = 40 + var maxLines by remember { + mutableStateOf(2) + } + + val moreOrCollapseIndicator = @Composable { scope: ContextualFlowRowOverflowScope -> + val remainingItems = totalCount - scope.shownItemCount + ChipItem(if (remainingItems == 0) "Less" else "+$remainingItems", onClick = { + if (remainingItems == 0) { + maxLines = 2 + } else { + maxLines += 5 + } + }) + } + ContextualFlowRow( + modifier = Modifier + .safeDrawingPadding() + .fillMaxWidth(1f) + .padding(16.dp) + .wrapContentHeight(align = Alignment.Top) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + maxLines = maxLines, + overflow = ContextualFlowRowOverflow.expandOrCollapseIndicator( + minRowsToShowCollapse = 4, + expandIndicator = moreOrCollapseIndicator, + collapseIndicator = moreOrCollapseIndicator + ), + itemCount = totalCount + ) { index -> + ChipItem("Item $index") + } + // [END android_compose_layouts_contextual_flow] +} + +@OptIn(ExperimentalLayoutApi::class) +@Preview +@Composable +fun FillMaxColumnWidth() { + // [START android_compose_flow_layouts_fill_max_column_width] + FlowColumn( + Modifier + .padding(20.dp) + .fillMaxHeight() + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + maxItemsInEachColumn = 5, + ) { + repeat(listDesserts.size) { + Box( + Modifier + .fillMaxColumnWidth() + .border(1.dp, Color.DarkGray, RoundedCornerShape(8.dp)) + .padding(8.dp) + ) { + + Text( + text = listDesserts[it], + fontSize = 18.sp, + modifier = Modifier.padding(3.dp) + ) + } + } + } + // [END android_compose_flow_layouts_fill_max_column_width] +} +private val listDesserts = listOf( + "Apple", + "Banana", + "Cupcake", + "Donut", + "Eclair", + "Froyo", + "Gingerbread", + "Honeycomb", + "Ice Cream Sandwich", + "Jellybean", + "KitKat", + "Lollipop", + "Marshmallow", + "Nougat", +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt index 2bbf276b..4599fd39 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt @@ -25,7 +25,6 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -115,7 +114,6 @@ fun ConsumedFromSiblingsSnippet() { @Composable fun ConsumedFromPaddingSnippet() { // [START android_compose_insets_consumed_from_padding] - @OptIn(ExperimentalLayoutApi::class) Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) { // content Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) @@ -123,7 +121,6 @@ fun ConsumedFromPaddingSnippet() { // [END android_compose_insets_consumed_from_padding] } -@OptIn(ExperimentalLayoutApi::class) @Preview @Composable fun M3SupportScaffoldSnippet() { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt index 012f09e5..9bc39b29 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt @@ -39,10 +39,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material.Divider -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -71,7 +71,7 @@ private object IntrinsicsSnippet1 { .wrapContentWidth(Alignment.Start), text = text1 ) - Divider( + HorizontalDivider( color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp) ) @@ -111,7 +111,7 @@ private object IntrinsicsSnippet2 { .wrapContentWidth(Alignment.Start), text = text1 ) - Divider( + HorizontalDivider( color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp) ) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt index 9c7f9d4d..b027c6ea 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt @@ -28,12 +28,12 @@ import androidx.compose.material.icons.filled.Image import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet @@ -291,7 +291,7 @@ fun DrawerDemo() { drawerContent = { ModalDrawerSheet { Text("Drawer title", modifier = Modifier.padding(16.dp)) - Divider() + HorizontalDivider() NavigationDrawerItem( label = { Text(text = "Drawer Item") }, selected = false, diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt index f65c96e6..ec8e84cb 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt @@ -14,15 +14,11 @@ * limitations under the License. */ -@file:OptIn( - ExperimentalFoundationApi::class -) @file:Suppress("unused") package com.example.compose.snippets.layouts import android.util.Log -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -123,7 +119,6 @@ fun VerticalPagerSample() { // [END android_compose_layouts_pager_vertical_basic] } -@OptIn(ExperimentalFoundationApi::class) @Preview @Composable fun PagerScrollToItem() { @@ -214,7 +209,6 @@ fun PageChangesSample() { // [END android_compose_layouts_pager_notify_page_changes] } -@OptIn(ExperimentalFoundationApi::class) @Composable @Preview fun PagerWithTabsExample() { @@ -358,7 +352,6 @@ fun PagerWithTabs() { } // [END android_compose_layouts_pager_with_tabs] } -@OptIn(ExperimentalFoundationApi::class) @Preview @Composable fun PagerIndicator() { @@ -410,7 +403,6 @@ private val threePagesPerViewport = object : PageSize { } // [END android_compose_pager_custom_page_size] -@OptIn(ExperimentalFoundationApi::class) @Preview @Composable private fun CustomSnapDistance() { @@ -426,7 +418,7 @@ private fun CustomSnapDistance() { HorizontalPager( state = pagerState, pageSize = PageSize.Fixed(200.dp), - beyondBoundsPageCount = 10, + beyondViewportPageCount = 10, flingBehavior = fling ) { PagerSampleItem(page = it) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/lists/LazyListSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/lists/LazyListSnippets.kt index e1107eba..c8864f12 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/lists/LazyListSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/lists/LazyListSnippets.kt @@ -19,7 +19,8 @@ package com.example.compose.snippets.lists import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.horizontalScroll @@ -263,7 +264,6 @@ private object ListsSnippetsReactingScrollPosition1 { private object ListsSnippetsReactingScrollPosition2 { // [START android_compose_layouts_lazy_column_scroll_to_top] - @OptIn(ExperimentalAnimationApi::class) @Composable fun MessageList(messages: List) { Box { @@ -425,7 +425,6 @@ private fun LazyColumnRememberSaveable() { // [END android_compose_layouts_lazy_column_any_key_saveable] } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun LazyItemAnimations() { val books = remember { @@ -433,8 +432,9 @@ private fun LazyItemAnimations() { } // [START android_compose_layouts_lazy_column_item_animation] LazyColumn { + // It is important to provide a key to each item to ensure animateItem() works as expected. items(books, key = { it.id }) { - Row(Modifier.animateItemPlacement()) { + Row(Modifier.animateItem()) { // ... } } @@ -442,7 +442,6 @@ private fun LazyItemAnimations() { // [END android_compose_layouts_lazy_column_item_animation] } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun LazyItemAnimationWithSpec() { val books = remember { @@ -452,8 +451,10 @@ private fun LazyItemAnimationWithSpec() { LazyColumn { items(books, key = { it.id }) { Row( - Modifier.animateItemPlacement( - tween(durationMillis = 250) + Modifier.animateItem( + fadeInSpec = tween(durationMillis = 250), + fadeOutSpec = tween(durationMillis = 100), + placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) ) ) { // ... @@ -630,7 +631,6 @@ private fun ContentTypeExample() { } @Preview -@OptIn(ExperimentalFoundationApi::class) @Composable fun LazyStaggeredGridSnippet() { // [START android_compose_layouts_lazy_staggered_grid_adaptive] @@ -653,7 +653,6 @@ fun LazyStaggeredGridSnippet() { // [END android_compose_layouts_lazy_staggered_grid_adaptive] } @Preview -@OptIn(ExperimentalFoundationApi::class) @Composable fun LazyStaggeredGridSnippetFixed() { // [START android_compose_layouts_lazy_staggered_grid_fixed] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/mentalmodel/ThinkingInComposeSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/mentalmodel/ThinkingInComposeSnippets.kt index 3cc21a96..eb4e4dac 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/mentalmodel/ThinkingInComposeSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/mentalmodel/ThinkingInComposeSnippets.kt @@ -24,11 +24,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Button -import androidx.compose.material.Checkbox -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -137,7 +137,7 @@ fun NamePicker( Column { // this will recompose when [header] changes, but not when [names] changes Text(header, style = MaterialTheme.typography.bodyLarge) - Divider() + HorizontalDivider() // LazyColumn is the Compose version of a RecyclerView. // The lambda passed to items() is similar to a RecyclerView.ViewHolder. diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt index 9007ba4d..9e711050 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt @@ -23,6 +23,7 @@ enum class Destination(val route: String, val title: String) { ComponentsExamples("topComponents", "Top Compose Components"), ScreenshotExample("screenshotExample", "Screenshot Examples"), ShapesExamples("shapesExamples", "Shapes Examples"), + SharedElementExamples("sharedElement", "Shared elements") } // Enum class for compose components navigation screen. diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt index b6b225a0..0a2f34a5 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt @@ -23,18 +23,17 @@ import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.layout.padding -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource @@ -47,7 +46,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import com.example.compose.snippets.R -@OptIn(ExperimentalComposeUiApi::class) @Composable fun Strings() { // [START android_compose_resources_strings] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt index c68791a0..976d7ac7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt @@ -43,10 +43,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import com.example.compose.snippets.interop.FirebaseAnalytics import com.example.compose.snippets.interop.User import com.example.compose.snippets.kotlin.Message diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/state/SavingUIStateSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/state/SavingUIStateSnippets.kt index cfd35591..cc35c128 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/state/SavingUIStateSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/state/SavingUIStateSnippets.kt @@ -20,8 +20,8 @@ package com.example.compose.snippets.state import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Text -import androidx.compose.material.TextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/state/StateOverviewSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateOverviewSnippets.kt index 503732ad..5ac1e71b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/state/StateOverviewSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateOverviewSnippets.kt @@ -16,6 +16,8 @@ @file:Suppress("unused") +package com.example.compose.snippets.state + import android.content.res.Resources import android.graphics.BitmapShader import android.graphics.Shader @@ -244,7 +246,7 @@ private fun RememberKeysSnippet3() { } /** - * Add fake Parcelize and Parcelable to avoid adding dependency on + * Add fake com.example.compose.snippets.state.Parcelize and com.example.compose.snippets.state.Parcelable to avoid adding dependency on * kotlin-parcelize just for snippets */ private annotation class Parcelize diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiSnippets.kt index eb0495ca..7a32ed36 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiSnippets.kt @@ -24,7 +24,7 @@ import androidx.activity.ComponentActivity import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatTextView import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.text.EmojiSupportMatch diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextDownloadableFontsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextDownloadableFontsSnippets.kt index 95b69e9c..4eaffb64 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextDownloadableFontsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextDownloadableFontsSnippets.kt @@ -20,8 +20,8 @@ package com.example.compose.snippets.text import android.util.Log import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -118,16 +118,16 @@ fun DownloadableFontsText() { private object TextDownloadableFontsSnippet4 { // [START android_compose_text_typography_definition] val MyTypography = Typography( - labelMedium = TextStyle( + bodyMedium = TextStyle( fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp/*...*/ ), - labelLarge = TextStyle( + bodyLarge = TextStyle( fontFamily = fontFamily, fontWeight = FontWeight.Bold, letterSpacing = 2.sp, /*...*/ ), - displayMedium = TextStyle( + headlineMedium = TextStyle( fontFamily = fontFamily, fontWeight = FontWeight.SemiBold/*...*/ ), /*...*/ diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt index dd5cf5a6..57e521cf 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt @@ -18,9 +18,7 @@ package com.example.compose.snippets.text -import android.util.Log import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border @@ -31,7 +29,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer @@ -53,11 +50,13 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color.Companion.Cyan import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.Font @@ -71,6 +70,7 @@ import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -417,54 +417,6 @@ private object TextPartiallySelectableSnippet { // [END android_compose_text_partial_selection] } -private object TextClickableSnippet { - // [START android_compose_text_clickable] - @Composable - fun SimpleClickableText() { - ClickableText(text = AnnotatedString("Click Me"), onClick = { offset -> - Log.d("ClickableText", "$offset -th character is clicked.") - }) - } - // [END android_compose_text_clickable] -} - -private object TextClickableAnnotatedSnippet { - // [START android_compose_text_clickable_annotated] - @Composable - fun AnnotatedClickableText() { - val annotatedText = buildAnnotatedString { - append("Click ") - - // We attach this *URL* annotation to the following content - // until `pop()` is called - pushStringAnnotation( - tag = "URL", annotation = "https://developer.android.com" - ) - withStyle( - style = SpanStyle( - color = Color.Blue, fontWeight = FontWeight.Bold - ) - ) { - append("here") - } - - pop() - } - - ClickableText(text = annotatedText, onClick = { offset -> - // We check if there is an *URL* annotation attached to the text - // at the clicked position - annotatedText.getStringAnnotations( - tag = "URL", start = offset, end = offset - ).firstOrNull()?.let { annotation -> - // If yes, we log its value - Log.d("Clicked URL", annotation.item) - } - }) - } - // [END android_compose_text_clickable_annotated] -} - private object TextTextFieldSnippet { // [START android_compose_text_textfield_filled] @Composable @@ -591,6 +543,50 @@ private object TextEffectiveStateManagement2 { // [END android_compose_text_state_management] } +// [START android_compose_text_link_1] +@Composable +fun AnnotatedStringWithLinkSample() { + // Display a link in the text + Text( + buildAnnotatedString { + append("Build better apps faster with ") + withLink( + LinkAnnotation.Url( + "https://developer.android.com/jetpack/compose", + TextLinkStyles(style = SpanStyle(color = Color.Blue)) + ) + ) { + append("Jetpack Compose") + } + } + ) +} +// [END android_compose_text_link_1] + +// [START android_compose_text_link_2] +@Composable +fun AnnotatedStringWithListenerSample() { + // Display a link in the text and log metrics whenever user clicks on it. In that case we handle + // the link using openUri method of the LocalUriHandler + val uriHandler = LocalUriHandler.current + Text( + buildAnnotatedString { + append("Build better apps faster with ") + val link = + LinkAnnotation.Url( + "https://developer.android.com/jetpack/compose", + TextLinkStyles(SpanStyle(color = Color.Blue)) + ) { + val url = (it as LinkAnnotation.Url).url + // log some metrics + uriHandler.openUri(url) + } + withLink(link) { append("Jetpack Compose") } + } + ) +} +// [END android_compose_text_link_2] + @Composable private fun TextSample(samples: MapUnit>) { MaterialTheme { @@ -756,7 +752,6 @@ fun HyphenateTextSnippet() { @Preview(showBackground = true) // [START android_compose_text_marquee] -@OptIn(ExperimentalFoundationApi::class) @Composable fun BasicMarqueeSample() { // Marquee only animates when the content doesn't fit in the max width. @@ -770,9 +765,7 @@ fun BasicMarqueeSample() { } // [END android_compose_text_marquee] -// Using null just sets the font family to default, which is easier than supplying -// the actual font file in the snippets repo. This fixes a build warning. -private val firaSansFamily = null +private val firaSansFamily = FontFamily() val LightBlue = Color(0xFF0066FF) val Purple = Color(0xFF800080) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt new file mode 100644 index 00000000..14b012da --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt @@ -0,0 +1,643 @@ +/* + * 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 + * + * https://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 com.example.compose.snippets.touchinput + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +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.ShoppingCart +import androidx.compose.material.ripple +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonElevation +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Brush.Companion.linearGradient +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.sign +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +private fun InteractionsSnippet1() { + // [START android_compose_interactions_interaction_state] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button( + onClick = { /* do something */ }, + interactionSource = interactionSource + ) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_interaction_state] +} + +// [START android_compose_interactions_interaction_source_input] +fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { + // [START_EXCLUDE] + return this + // [END_EXCLUDE] +} +// [END android_compose_interactions_interaction_source_input] + +// [START android_compose_interactions_mutable_interaction_source_input] +fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { + // [START_EXCLUDE] + return this + // [END_EXCLUDE] +} +// [END android_compose_interactions_mutable_interaction_source_input] + +// [START android_compose_interactions_high_level_component] +@Composable +fun Button( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + + // exposes MutableInteractionSource as a parameter + interactionSource: MutableInteractionSource? = null, + + elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = null, + colors: ButtonColors = ButtonDefaults.buttonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + content: @Composable RowScope.() -> Unit +) { /* content() */ } +// [END android_compose_interactions_high_level_component] + +@Composable +fun HoverExample() { + // [START android_compose_interactions_hoverable] + // This InteractionSource will emit hover interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_hoverable] +} + +@Composable +fun FocusableExample() { + // [START android_compose_interactions_focusable] + // This InteractionSource will emit hover and focus interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource) + .focusable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_focusable] +} + +@Composable +fun ClickableExample() { + // [START android_compose_interactions_clickable] + // This InteractionSource will emit hover, focus, and press interactions + val interactionSource = remember { MutableInteractionSource() } + Box( + Modifier + .size(100.dp) + .clickable( + onClick = {}, + interactionSource = interactionSource, + + // Also show a ripple effect + indication = ripple() + ), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_clickable] +} + +@Composable +private fun InteractionsSnippet2() { + // [START android_compose_interactions_flow_apis] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + } + } + } + // [END android_compose_interactions_flow_apis] +} + +@Composable +private fun InteractionsSnippet3() { + // [START android_compose_interactions_add_remove] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is PressInteraction.Release -> { + interactions.remove(interaction.press) + } + is PressInteraction.Cancel -> { + interactions.remove(interaction.press) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + is DragInteraction.Stop -> { + interactions.remove(interaction.start) + } + is DragInteraction.Cancel -> { + interactions.remove(interaction.start) + } + } + } + } + // [END android_compose_interactions_add_remove] + + // [START android_compose_interactions_is_pressed_or_dragged] + val isPressedOrDragged = interactions.isNotEmpty() + // [END android_compose_interactions_is_pressed_or_dragged] + + // [START android_compose_interactions_last] + val lastInteraction = when (interactions.lastOrNull()) { + is DragInteraction.Start -> "Dragged" + is PressInteraction.Press -> "Pressed" + else -> "No state" + } + // [END android_compose_interactions_last] +} + +@Composable +private fun InteractionsSnippet4() { + // [START android_compose_interactions_batched] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button(onClick = { /* do something */ }, interactionSource = interactionSource) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_batched] +} + +// [START android_compose_interactions_press_icon_button] +@Composable +fun PressIconButton( + onClick: () -> Unit, + icon: @Composable () -> Unit, + text: @Composable () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource? = null +) { + val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false + + Button( + onClick = onClick, + modifier = modifier, + interactionSource = interactionSource + ) { + AnimatedVisibility(visible = isPressed) { + if (isPressed) { + Row { + icon() + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + } + } + } + text() + } +} +// [END android_compose_interactions_press_icon_button] + +@Composable +fun PressIconButtonUsage() { +// [START android_compose_interactions_press_icon_button_usage] + PressIconButton( + onClick = {}, + icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, + text = { Text("Add to cart") } + ) +// [END android_compose_interactions_press_icon_button_usage] +} + +@Composable +fun InteractionsSnippet5() { +// [START android_compose_interactions_indication] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") + + Button( + modifier = Modifier.scale(scale), + onClick = { }, + interactionSource = interactionSource + ) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } +// [END android_compose_interactions_indication] +} + +// [START android_compose_interactions_scale_node] +private class ScaleNode(private val interactionSource: InteractionSource) : + Modifier.Node(), DrawModifierNode { + + var currentPressPosition: Offset = Offset.Zero + val animatedScalePercent = Animatable(1f) + + private suspend fun animateToPressed(pressPosition: Offset) { + currentPressPosition = pressPosition + animatedScalePercent.animateTo(0.9f, spring()) + } + + private suspend fun animateToResting() { + animatedScalePercent.animateTo(1f, spring()) + } + + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collectLatest { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } + + override fun ContentDrawScope.draw() { + scale( + scale = animatedScalePercent.value, + pivot = currentPressPosition + ) { + this@draw.drawContent() + } + } +} +// [END android_compose_interactions_scale_node] + +// [START android_compose_interactions_scale_node_factory] +object ScaleIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleNode(interactionSource) + } + + override fun equals(other: Any?): Boolean = other === ScaleIndication + override fun hashCode() = 100 +} +// [END android_compose_interactions_scale_node_factory] + +@Composable +fun InteractionSnippets6() { +// [START android_compose_interactions_button_indication] + Box( + modifier = Modifier + .size(100.dp) + .clickable( + onClick = {}, + indication = ScaleIndication, + interactionSource = null + ) + .background(Color.Blue), + contentAlignment = Alignment.Center + ) { + Text("Hello!", color = Color.White) + } +// [END android_compose_interactions_button_indication] +} + +// [START android_compose_interactions_scale_button] +@Composable +fun ScaleButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + shape: Shape = CircleShape, + content: @Composable RowScope.() -> Unit +) { + Row( + modifier = modifier + .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) + .clickable( + enabled = enabled, + indication = ScaleIndication, + interactionSource = interactionSource, + onClick = onClick + ) + .border(width = 2.dp, color = Color.Blue, shape = shape) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content + ) +} +// [END android_compose_interactions_scale_button] + +@Composable +fun ScaleButtonExample() { +// [START android_compose_interactions_scale_button_example] + ScaleButton(onClick = {}) { + Icon(Icons.Filled.ShoppingCart, "") + Spacer(Modifier.padding(10.dp)) + Text(text = "Add to cart!") + } +// [END android_compose_interactions_scale_button_example] +} + +// [START android_compose_interactions_neon_node] +private class NeonNode( + private val shape: Shape, + private val borderWidth: Dp, + private val interactionSource: InteractionSource +) : Modifier.Node(), DrawModifierNode { + var currentPressPosition: Offset = Offset.Zero + val animatedProgress = Animatable(0f) + val animatedPressAlpha = Animatable(1f) + + var pressedAnimation: Job? = null + var restingAnimation: Job? = null + + private suspend fun animateToPressed(pressPosition: Offset) { + // Finish any existing animations, in case of a new press while we are still showing + // an animation for a previous one + restingAnimation?.cancel() + pressedAnimation?.cancel() + pressedAnimation = coroutineScope.launch { + currentPressPosition = pressPosition + animatedPressAlpha.snapTo(1f) + animatedProgress.snapTo(0f) + animatedProgress.animateTo(1f, tween(450)) + } + } + + private fun animateToResting() { + restingAnimation = coroutineScope.launch { + // Wait for the existing press animation to finish if it is still ongoing + pressedAnimation?.join() + animatedPressAlpha.animateTo(0f, tween(250)) + animatedProgress.snapTo(0f) + } + } + + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } + + override fun ContentDrawScope.draw() { + val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( + currentPressPosition, size + ) + val brush = animateBrush( + startPosition = startPosition, + endPosition = endPosition, + progress = animatedProgress.value + ) + val alpha = animatedPressAlpha.value + + drawContent() + + val outline = shape.createOutline(size, layoutDirection, this) + // Draw overlay on top of content + drawOutline( + outline = outline, + brush = brush, + alpha = alpha * 0.1f + ) + // Draw border on top of overlay + drawOutline( + outline = outline, + brush = brush, + alpha = alpha, + style = Stroke(width = borderWidth.toPx()) + ) + } + + /** + * Calculates a gradient start / end where start is the point on the bounding rectangle of + * size [size] that intercepts with the line drawn from the center to [pressPosition], + * and end is the intercept on the opposite end of that line. + */ + private fun calculateGradientStartAndEndFromPressPosition( + pressPosition: Offset, + size: Size + ): Pair { + // Convert to offset from the center + val offset = pressPosition - size.center + // y = mx + c, c is 0, so just test for x and y to see where the intercept is + val gradient = offset.y / offset.x + // We are starting from the center, so halve the width and height - convert the sign + // to match the offset + val width = (size.width / 2f) * sign(offset.x) + val height = (size.height / 2f) * sign(offset.y) + val x = height / gradient + val y = gradient * width + + // Figure out which intercept lies within bounds + val intercept = if (abs(y) <= abs(height)) { + Offset(width, y) + } else { + Offset(x, height) + } + + // Convert back to offsets from 0,0 + val start = intercept + size.center + val end = Offset(size.width - start.x, size.height - start.y) + return start to end + } + + private fun animateBrush( + startPosition: Offset, + endPosition: Offset, + progress: Float + ): Brush { + if (progress == 0f) return TransparentBrush + + // This is *expensive* - we are doing a lot of allocations on each animation frame. To + // recreate a similar effect in a performant way, it would be better to create one large + // gradient and translate it on each frame, instead of creating a whole new gradient + // and shader. The current approach will be janky! + val colorStops = buildList { + when { + progress < 1 / 6f -> { + val adjustedProgress = progress * 6f + add(0f to Blue) + add(adjustedProgress to Color.Transparent) + } + progress < 2 / 6f -> { + val adjustedProgress = (progress - 1 / 6f) * 6f + add(0f to Purple) + add(adjustedProgress * MaxBlueStop to Blue) + add(adjustedProgress to Blue) + add(1f to Color.Transparent) + } + progress < 3 / 6f -> { + val adjustedProgress = (progress - 2 / 6f) * 6f + add(0f to Pink) + add(adjustedProgress * MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 4 / 6f -> { + val adjustedProgress = (progress - 3 / 6f) * 6f + add(0f to Orange) + add(adjustedProgress * MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 5 / 6f -> { + val adjustedProgress = (progress - 4 / 6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + else -> { + val adjustedProgress = (progress - 5 / 6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxYellowStop to Yellow) + add(MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + } + } + + return linearGradient( + colorStops = colorStops.toTypedArray(), + start = startPosition, + end = endPosition + ) + } + + companion object { + val TransparentBrush = SolidColor(Color.Transparent) + val Blue = Color(0xFF30C0D8) + val Purple = Color(0xFF7848A8) + val Pink = Color(0xFFF03078) + val Orange = Color(0xFFF07800) + val Yellow = Color(0xFFF0D800) + const val MaxYellowStop = 0.16f + const val MaxOrangeStop = 0.33f + const val MaxPinkStop = 0.5f + const val MaxPurpleStop = 0.67f + const val MaxBlueStop = 0.83f + } +} +// [END android_compose_interactions_neon_node] + +// [START android_compose_interactions_neon_indication] +data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { + + override fun create(interactionSource: InteractionSource): DelegatableNode { + return NeonNode( + shape, + // Double the border size for a stronger press effect + borderWidth * 2, + interactionSource + ) + } +} +// [END android_compose_interactions_neon_indication] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt index 4a5c7c4c..82b5b2d1 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt @@ -14,9 +14,10 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION_ERROR") + package com.example.compose.snippets.touchinput.focus -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.background @@ -111,7 +112,6 @@ private fun BasicSample2() { } } -@OptIn(ExperimentalComposeUiApi::class) @Preview @Composable fun OverrideDefaultOrder() { @@ -175,7 +175,6 @@ fun OverrideDefaultOrder() { // [END android_compose_touchinput_focus_override_use] } -@OptIn(ExperimentalComposeUiApi::class) @Preview @Composable fun OverrideTwoDimensionalOrder() { @@ -195,7 +194,6 @@ fun OverrideTwoDimensionalOrder() { // [END android_compose_touchinput_focus_override_2d] } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun FocusGroup() { @@ -317,7 +315,6 @@ private fun Capture() { // [END android_compose_touchinput_focus_capture] } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun ModifierOrder() { @@ -401,7 +398,6 @@ private fun RedirectFocus() { // [END android_compose_touchinput_focus_redirect] } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun FocusAdvancing() { // [START android_compose_touchinput_focus_advancing] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt index dcddc073..d5f7fffe 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt @@ -56,7 +56,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush @@ -208,7 +207,6 @@ private fun AutomaticNestedScroll() { private object NestedScrollInterop { // [START android_compose_touchinput_gestures_nested_scroll_interop_activity] open class MainActivity : ComponentActivity() { - @OptIn(ExperimentalComposeUiApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/TapAndPress.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/TapAndPress.kt index fbfa95cd..dbe22282 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/TapAndPress.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/TapAndPress.kt @@ -60,7 +60,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset @@ -291,7 +290,6 @@ private fun FullScreenImage( } // [START android_compose_touchinput_pointerinput_scrim] -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun Scrim(onClose: () -> Unit, modifier: Modifier = Modifier) { val strClose = stringResource(R.string.close) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt index 285dfef6..07248a6f 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt @@ -14,21 +14,30 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION_ERROR") + package com.example.compose.snippets.touchinput.userinteractions import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.spring import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.IndicationNodeFactory import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalRippleConfiguration +import androidx.compose.material.LocalUseFallbackRippleImplementation +import androidx.compose.material.RippleConfiguration +import androidx.compose.material.ripple import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleTheme import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -38,8 +47,11 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode import com.example.compose.snippets.architecture.Button import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch // [START android_compose_userinteractions_scale_indication] // [START android_compose_userinteractions_scale_indication_object] @@ -105,6 +117,21 @@ private fun RememberRippleExample() { // [END android_compose_userinteractions_material_remember_ripple] } +// [START android_compose_userinteractions_material_ripple] +@Composable +private fun RippleExample() { + Box( + Modifier.clickable( + onClick = {}, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple() + ) + ) { + // ... + } +} +// [END android_compose_userinteractions_material_ripple] + // [START android_compose_userinteractions_disabled_ripple_theme] private object DisabledRippleTheme : RippleTheme { @@ -154,3 +181,114 @@ private fun MyComposable2() { } // [END_EXCLUDE] // [END android_compose_userinteractions_disabled_ripple_theme_color_alpha] + +// Snippets for new ripple API + +// [START android_compose_userinteractions_scale_indication_node_factory] +object ScaleIndicationNodeFactory : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleIndicationNode(interactionSource) + } + + override fun hashCode(): Int = -1 + + override fun equals(other: Any?) = other === this +} +// [END android_compose_userinteractions_scale_indication_node_factory] + +// [START android_compose_userinteractions_scale_indication_node] +private class ScaleIndicationNode( + private val interactionSource: InteractionSource +) : Modifier.Node(), DrawModifierNode { + var currentPressPosition: Offset = Offset.Zero + val animatedScalePercent = Animatable(1f) + + private suspend fun animateToPressed(pressPosition: Offset) { + currentPressPosition = pressPosition + animatedScalePercent.animateTo(0.9f, spring()) + } + + private suspend fun animateToResting() { + animatedScalePercent.animateTo(1f, spring()) + } + + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collectLatest { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } + + override fun ContentDrawScope.draw() { + scale( + scale = animatedScalePercent.value, + pivot = currentPressPosition + ) { + this@draw.drawContent() + } + } +} +// [END android_compose_userinteractions_scale_indication_node] + +@Composable +fun App() { +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun LocalUseFallbackRippleImplementationExample() { +// [START android_compose_userinteractions_localusefallbackrippleimplementation] + CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { + MaterialTheme { + App() + } + } +// [END android_compose_userinteractions_localusefallbackrippleimplementation] +} + +// [START android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun MyAppTheme(content: @Composable () -> Unit) { + CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { + MaterialTheme(content = content) + } +} +// [END android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun MyComposableDisabledRippleConfig() { + // [START android_compose_userinteractions_disabled_ripple_configuration] + CompositionLocalProvider(LocalRippleConfiguration provides null) { + Button { + // ... + } + } + // [END android_compose_userinteractions_disabled_ripple_configuration] +} + +// [START android_compose_userinteractions_my_ripple_configuration] +@OptIn(ExperimentalMaterialApi::class) +private val MyRippleConfiguration = + RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha) + +// [START_EXCLUDE] +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun MyComposableMyRippleConfig() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_my_ripple_configuration] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt index 21909a8c..6c6006ea 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt @@ -25,3 +25,9 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) + +val LavenderDark = Color(0xff23009e) +val LavenderLight = Color(0xFFDDBEFC) + +val RoseDark = Color(0xffaf0060) +val RoseLight = Color(0xFFFFAFC9) diff --git a/compose/snippets/src/main/res/drawable/cupcake.jpg b/compose/snippets/src/main/res/drawable/cupcake.jpg new file mode 100644 index 00000000..0696e8a6 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/cupcake.jpg differ diff --git a/compose/snippets/src/main/res/drawable/donut.jpeg b/compose/snippets/src/main/res/drawable/donut.jpeg new file mode 100644 index 00000000..57d0199f Binary files /dev/null and b/compose/snippets/src/main/res/drawable/donut.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/eclair.jpeg b/compose/snippets/src/main/res/drawable/eclair.jpeg new file mode 100644 index 00000000..6ec767a0 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/eclair.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/froyo.jpeg b/compose/snippets/src/main/res/drawable/froyo.jpeg new file mode 100644 index 00000000..f3126d50 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/froyo.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/gingerbread.jpg b/compose/snippets/src/main/res/drawable/gingerbread.jpg new file mode 100644 index 00000000..8345d47e Binary files /dev/null and b/compose/snippets/src/main/res/drawable/gingerbread.jpg differ diff --git a/compose/snippets/src/main/res/drawable/honeycomb.jpg b/compose/snippets/src/main/res/drawable/honeycomb.jpg new file mode 100644 index 00000000..94c892b5 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/honeycomb.jpg differ diff --git a/gradle.properties b/gradle.properties index 6e7fda85..2dedea84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,20 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #Mon May 22 14:59:56 BST 2023 -android.enableJetifier=false +org.gradle.jvmargs=-Xmx2048m + +# Turn on parallel compilation, caching and on-demand configuration +org.gradle.configureondemand=true +org.gradle.caching=true +org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -org.gradle.unsafe.configuration-cache=true + +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +# Enable R8 full mode. +android.enableR8.fullMode=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cccf9af4..8649031d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,44 +1,45 @@ [versions] -accompanist = "0.32.0" -androidGradlePlugin = "8.2.2" -androidx-activity-compose = "1.9.0-alpha03" -androidx-appcompat = "1.6.1" -androidx-compose-bom = "2024.04.01" +accompanist = "0.34.0" +androidGradlePlugin = "8.4.2" +androidx-activity-compose = "1.9.2" +androidx-appcompat = "1.7.0" +androidx-compose-bom = "2024.09.00" androidx-compose-ui-test = "1.7.0-alpha08" androidx-constraintlayout = "2.1.4" androidx-constraintlayout-compose = "1.0.1" androidx-coordinator-layout = "1.2.0" -androidx-corektx = "1.9.0" -androidx-emoji2-views = "1.4.0" -androidx-fragment-ktx = "1.6.2" -androidx-glance-appwidget = "1.0.0" -androidx-lifecycle-compose = "2.8.0-rc01" -androidx-lifecycle-runtime-compose = "2.8.0-rc01" -androidx-navigation = "2.7.7" -androidx-paging = "3.2.1" -androidx-test = "1.5.0" +androidx-corektx = "1.13.1" +androidx-emoji2-views = "1.5.0" +androidx-fragment-ktx = "1.8.3" +androidx-glance-appwidget = "1.1.0" +androidx-lifecycle-compose = "2.8.5" +androidx-lifecycle-runtime-compose = "2.8.5" +androidx-navigation = "2.8.0" +androidx-paging = "3.3.2" +androidx-test = "1.6.1" androidx-test-espresso = "3.5.1" -androidx-window = "1.3.0-beta02" +androidx-window = "1.3.0" androidxHiltNavigationCompose = "1.2.0" -coil = "2.5.0" +coil = "2.6.0" # @keep compileSdk = "34" -compose-compiler = "1.5.4" coreSplashscreen = "1.0.1" +compose-latest = "1.7.0" coroutines = "1.7.3" google-maps = "18.2.0" gradle-versions = "0.51.0" hilt = "2.50" horologist = "0.5.24" junit = "4.13.2" -# @pin Update in conjuction with Compose Compiler -kotlin = "1.9.20" +kotlin = "2.0.0" +kotlinxSerializationJson = "1.6.3" + ksp = "1.8.0-1.0.9" maps-compose = "4.3.2" material = "1.4.0-beta01" -material3-adaptive = "1.0.0-alpha12" -material3-adaptive-navigation-suite = "1.0.0-alpha07" -media3 = "1.2.1" +material3-adaptive = "1.0.0" +material3-adaptive-navigation-suite = "1.3.0" +media3 = "1.3.1" # @keep minSdk = "21" playServicesWearable = "18.2.0" @@ -48,7 +49,7 @@ targetSdk = "34" version-catalog-update = "0.8.3" wearComposeFoundation = "1.4.0" wearComposeMaterial = "1.4.0" -composeUiTooling = "1.3.1" +composeUiTooling = "1.4.0" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } @@ -58,13 +59,13 @@ accompanist-theme-adapter-material = { module = "com.google.accompanist:accompan accompanist-theme-adapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "accompanist" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } -androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" } +androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" , version.ref = "compose-latest" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } -androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } -androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } -androidx-compose-material = { module = "androidx.compose.material:material" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-latest" } +androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "compose-latest" } +androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } -androidx-compose-material-ripple = { module = "androidx.compose.material:material-ripple" } +androidx-compose-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "compose-latest" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" } @@ -72,10 +73,11 @@ androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.ma androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } -androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-latest" } androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } +androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.0-rc01" androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } -androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "androidx-compose-ui-test" } +androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test" } androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } @@ -91,14 +93,11 @@ androidx-emoji2-views = { module = "androidx.emoji2:emoji2-views", version.ref = androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment-ktx" } androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "androidx-glance-appwidget" } androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "androidx-glance-appwidget" } -androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.0-alpha05" androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } -androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } -androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } @@ -121,6 +120,9 @@ junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } compose-ui-tooling = { group = "androidx.wear.compose", name = "compose-ui-tooling", version.ref = "composeUiTooling" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } @@ -128,9 +130,11 @@ androidx-material-icons-core = { module = "androidx.compose.material:material-ic [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce..b81fa3bb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,3 +1,4 @@ +#Tue May 14 19:06:12 BST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt index 9f8c6c75..2f3228cc 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt @@ -14,26 +14,30 @@ * limitations under the License. */ -package com.example.android.coroutines.testing.repo +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.example.android.coroutines.testing -import com.example.android.coroutines.testing.MainDispatcherRule import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.runner.RunWith // Helper function to let code below compile -private fun Repository(): Repository = Repository(Dispatchers.IO) +private fun ExampleRepository(): Repository = Repository(Dispatchers.IO) + -private // [START coroutine_test_repo_with_rule_blank] -class Repository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } +class ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { - private val repository = Repository(/* What TestDispatcher? */) + private val repository = ExampleRepository(/* What TestDispatcher? */) @get:Rule val mainDispatcherRule = MainDispatcherRule() @@ -48,13 +52,14 @@ class RepositoryTestWithRule { } // [END coroutine_test_repo_with_rule_blank] +@RunWith(Enclosed::class) class DispatchersOutsideTests { // [START coroutine_test_repo_with_rule] class RepositoryTestWithRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() - private val repository = Repository(mainDispatcherRule.testDispatcher) + private val repository = ExampleRepository(mainDispatcherRule.testDispatcher) @Test fun someRepositoryTest() = runTest { // Takes scheduler from Main @@ -70,7 +75,7 @@ class DispatchersOutsideTests { class RepositoryTest { // Creates the single test scheduler private val testDispatcher = UnconfinedTestDispatcher() - private val repository = Repository(testDispatcher) + private val repository = ExampleRepository(testDispatcher) @Test fun someRepositoryTest() = runTest(testDispatcher.scheduler) { @@ -84,3 +89,5 @@ class DispatchersOutsideTests { } // [END coroutine_test_repo_without_rule] } + + diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt index 882488f8..54a5a9bd 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt index 9620ccbe..e2c625e4 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt @@ -5,10 +5,9 @@ package com.example.android.coroutines.testing import com.example.android.coroutines.testing.scope.FakeUserRepository import com.example.android.coroutines.testing.scope.UserState import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Test // [START android_coroutine_test_user_state_test] diff --git a/shared/src/main/res/layout/activity_main.xml b/shared/src/main/res/layout/activity_main.xml deleted file mode 100644 index 0a8e972d..00000000 --- a/shared/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index b8ca7edd..9725e539 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) } android { @@ -39,9 +40,7 @@ android { buildFeatures { compose = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}"