diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 0d08e261..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - -version: 2 -updates: - - package-ecosystem: "github-actions" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "weekly" diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml index 79c68e23..c0662449 100644 --- a/.github/workflows/apply_spotless.yml +++ b/.github/workflows/apply_spotless.yml @@ -30,12 +30,12 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: token: ${{ secrets.PAT || github.token }} - name: set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: '17' @@ -44,6 +44,6 @@ jobs: run: ./gradlew :compose:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - name: Auto-commit if spotlessApply has changes - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: Apply Spotless diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51f9b6cb..c5b1819d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,11 +29,11 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: token: ${{ secrets.PAT || github.token }} - name: set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: '17' diff --git a/.github/workflows/sync_main_latest.yml b/.github/workflows/sync_main_latest.yml index 38c56bbb..a196b5da 100644 --- a/.github/workflows/sync_main_latest.yml +++ b/.github/workflows/sync_main_latest.yml @@ -1,6 +1,5 @@ name: Sync main and latest on: - workflow_dispatch: push: branches: - main @@ -11,17 +10,14 @@ jobs: name: Syncing branches steps: - name: Checkout - uses: actions/checkout@v4 - - - name: Set git config user - run: git config user.email "compose-devrel-github-bot@google.com" && git config user.name "compose-devrel-github-bot" + uses: actions/checkout@v3 - name: Merge main into latest - run: git fetch && git switch latest && git merge -s ours origin/main --allow-unrelated-histories + run: git checkout latest && git merge main - name: Create pull request id: cpr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v4 with: token: ${{ secrets.PAT }} commit-message: 🤖 Sync main to latest @@ -31,5 +27,5 @@ jobs: branch: bot-sync-main delete-branch: true title: '🤖 Sync main to latest' - body: 'Update `latest` with `main`' + body: Updated dependencies reviewers: ${{ github.actor }} diff --git a/.github/workflows/update_deps.yml b/.github/workflows/update_deps.yml index ffe44b0e..9f92a69b 100644 --- a/.github/workflows/update_deps.yml +++ b/.github/workflows/update_deps.yml @@ -7,9 +7,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v3 with: java-version: 17 distribution: 'zulu' @@ -19,7 +19,7 @@ jobs: run: ./gradlew versionCatalogUpdate - name: Create pull request id: cpr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v4 with: token: ${{ secrets.PAT }} commit-message: 🤖 Update Dependencies diff --git a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt index cd3540ab..3f7be75e 100644 --- a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt +++ b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt @@ -26,7 +26,7 @@ import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -43,7 +43,7 @@ class MainActivity : ComponentActivity() { @Composable private fun Content() { - var counter by remember { mutableIntStateOf(0) } + var counter by remember { mutableStateOf(0) } Column( Modifier .fillMaxSize() diff --git a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt index 6638a7fa..fd44e823 100644 --- a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt +++ b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt @@ -16,140 +16,100 @@ package com.example.android.compose.recomposehighlighter +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.lerp -import androidx.compose.ui.node.DrawModifierNode -import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.node.invalidateDraw -import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.dp -import java.util.Objects import kotlin.math.min -import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.launch /** * A [Modifier] that draws a border around elements that are recomposing. The border increases in * size and interpolates from red to green as more recompositions occur before a timeout. */ @Stable -fun Modifier.recomposeHighlighter(): Modifier = this.then(RecomposeHighlighterElement()) - -private class RecomposeHighlighterElement : ModifierNodeElement() { - - override fun InspectorInfo.inspectableProperties() { - debugInspectorInfo { name = "recomposeHighlighter" } - } - - override fun create(): RecomposeHighlighterModifier = RecomposeHighlighterModifier() - - override fun update(node: RecomposeHighlighterModifier) { - node.incrementCompositions() - } - - // It's never equal, so that every recomposition triggers the update function. - override fun equals(other: Any?): Boolean = false - - override fun hashCode(): Int = Objects.hash(this) -} - -private class RecomposeHighlighterModifier : Modifier.Node(), DrawModifierNode { - - private var timerJob: Job? = null - - /** - * The total number of compositions that have occurred. - */ - private var totalCompositions: Long = 0 - set(value) { - if (field == value) return - restartTimer() - field = value - invalidateDraw() - } - - fun incrementCompositions() { - totalCompositions++ - } - - override fun onAttach() { - super.onAttach() - restartTimer() - } - - override val shouldAutoInvalidate: Boolean = false - - override fun onDetach() { - timerJob?.cancel() - } - - /** - * Start the timeout, and reset everytime there's a recomposition. - */ - private fun restartTimer() { - if (!isAttached) return - - timerJob?.cancel() - timerJob = coroutineScope.launch { +fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier) + +// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations +// Modifier.composed will still remember unique data per call site. +private val recomposeModifier = + Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) { + // The total number of compositions that have occurred. We're not using a State<> here be + // able to read/write the value without invalidating (which would cause infinite + // recomposition). + val totalCompositions = remember { arrayOf(0L) } + totalCompositions[0]++ + + // The value of totalCompositions at the last timeout. + val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) } + + // Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions + // as the key is really just to cause the timer to restart every composition). + LaunchedEffect(totalCompositions[0]) { delay(3000) - totalCompositions = 0 - invalidateDraw() + totalCompositionsAtLastTimeout.value = totalCompositions[0] } - } - override fun ContentDrawScope.draw() { - // Draw actual content. - drawContent() + Modifier.drawWithCache { + onDrawWithContent { + // Draw actual content. + drawContent() - // Below is to draw the highlight, if necessary. A lot of the logic is copied from Modifier.border + // Below is to draw the highlight, if necessary. A lot of the logic is copied from + // Modifier.border + val numCompositionsSinceTimeout = + totalCompositions[0] - totalCompositionsAtLastTimeout.value - val hasValidBorderParams = size.minDimension > 0f - if (!hasValidBorderParams || totalCompositions <= 0) { - return - } - - val (color, strokeWidthPx) = - when (totalCompositions) { - // We need at least one composition to draw, so draw the smallest border - // color in blue. - 1L -> Color.Blue to 1f - // 2 compositions is _probably_ okay. - 2L -> Color.Green to 2.dp.toPx() - // 3 or more compositions before timeout may indicate an issue. lerp the - // color from yellow to red, and continually increase the border size. - else -> { - lerp( - Color.Yellow.copy(alpha = 0.8f), - Color.Red.copy(alpha = 0.5f), - min(1f, (totalCompositions - 1).toFloat() / 100f), - ) to totalCompositions.toInt().dp.toPx() + val hasValidBorderParams = size.minDimension > 0f + if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) { + return@onDrawWithContent } - } - - val halfStroke = strokeWidthPx / 2 - val topLeft = Offset(halfStroke, halfStroke) - val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx) - val fillArea = (strokeWidthPx * 2) > size.minDimension - val rectTopLeft = if (fillArea) Offset.Zero else topLeft - val size = if (fillArea) size else borderSize - val style = if (fillArea) Fill else Stroke(strokeWidthPx) - - drawRect( - brush = SolidColor(color), - topLeft = rectTopLeft, - size = size, - style = style, - ) + val (color, strokeWidthPx) = + when (numCompositionsSinceTimeout) { + // We need at least one composition to draw, so draw the smallest border + // color in blue. + 1L -> Color.Blue to 1f + // 2 compositions is _probably_ okay. + 2L -> Color.Green to 2.dp.toPx() + // 3 or more compositions before timeout may indicate an issue. lerp the + // color from yellow to red, and continually increase the border size. + else -> { + lerp( + Color.Yellow.copy(alpha = 0.8f), + Color.Red.copy(alpha = 0.5f), + min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f) + ) to numCompositionsSinceTimeout.toInt().dp.toPx() + } + } + + val halfStroke = strokeWidthPx / 2 + val topLeft = Offset(halfStroke, halfStroke) + val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx) + + val fillArea = (strokeWidthPx * 2) > size.minDimension + val rectTopLeft = if (fillArea) Offset.Zero else topLeft + val size = if (fillArea) size else borderSize + val style = if (fillArea) Fill else Stroke(strokeWidthPx) + + drawRect( + brush = SolidColor(color), + topLeft = rectTopLeft, + size = size, + style = style + ) + } + } } -} diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index acfa8ddc..5a2c59cd 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { implementation(composeBom) androidTestImplementation(composeBom) + implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.util) implementation(libs.androidx.compose.ui.graphics) @@ -93,8 +94,7 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3.adaptive) - implementation(libs.androidx.compose.material3.adaptive.layout) - implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation.suite) implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.runtime) diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/deviceconfigurationoverride/DeviceConfigurationOverrideSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/deviceconfigurationoverride/DeviceConfigurationOverrideSnippets.kt deleted file mode 100644 index 36db665a..00000000 --- a/compose/snippets/src/androidTest/java/com/example/compose/snippets/deviceconfigurationoverride/DeviceConfigurationOverrideSnippets.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.deviceconfigurationoverride - -import androidx.compose.material3.Text -import androidx.compose.ui.test.DeviceConfigurationOverride -import androidx.compose.ui.test.FontScale -import androidx.compose.ui.test.FontWeightAdjustment -import androidx.compose.ui.test.ForcedSize -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.then -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import com.example.compose.snippets.interop.MyScreen -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test - -class DeviceConfigurationOverrideSnippetsTest { - @get:Rule - val composeTestRule = createComposeRule() - - @Ignore("Snippet test") - @Test - fun forcedSize() { - // [START android_compose_deviceconfigurationoverride_forcedsize] - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp)) - ) { - MyScreen() // will be rendered in the space for 1280dp by 800dp without clipping - } - } - // [END android_compose_deviceconfigurationoverride_forcedsize] - } - - @Ignore("Snippet test") - @Test - fun then() { - // [START android_compose_deviceconfigurationoverride_then] - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.FontScale(1.5f) then - DeviceConfigurationOverride.FontWeightAdjustment(200) - ) { - Text(text = "text with increased scale and weight") - } - } - // [END android_compose_deviceconfigurationoverride_then] - } -} diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml index c9b15e6c..81d85e38 100644 --- a/compose/snippets/src/main/AndroidManifest.xml +++ b/compose/snippets/src/main/AndroidManifest.xml @@ -33,6 +33,7 @@ android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" + android:enableOnBackInvokedCallback="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets"> 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 31037647..412a1281 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,6 +28,8 @@ 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.CustomPredictiveBackHandle +import com.example.compose.snippets.animations.sharedelement.PlaceholderSizeAnimated_Demo import com.example.compose.snippets.components.AppBarExamples import com.example.compose.snippets.components.ButtonExamples import com.example.compose.snippets.components.ChipExamples @@ -39,7 +41,7 @@ import com.example.compose.snippets.components.ScaffoldExample import com.example.compose.snippets.components.SliderExamples import com.example.compose.snippets.components.SwitchExamples 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 @@ -70,13 +72,15 @@ 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() + Destination.CustomPredictiveBackExample -> CustomPredictiveBackHandle() } } } 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 154ee4b3..850f5381 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 @@ -16,7 +16,6 @@ package com.example.compose.snippets.adaptivelayouts -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -28,11 +27,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Card import androidx.compose.material3.ListItem import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.AnimatedPane import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.material3.adaptive.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -49,11 +48,7 @@ import androidx.compose.ui.unit.sp @Composable fun SampleListDetailPaneScaffoldParts() { // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part02] - val navigator = rememberListDetailPaneScaffoldNavigator() - - BackHandler(navigator.canNavigateBack()) { - navigator.navigateBack() - } + val navigator = rememberListDetailPaneScaffoldNavigator() // [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part02] // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part01] @@ -64,8 +59,7 @@ fun SampleListDetailPaneScaffoldParts() { // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part03] ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, + scaffoldState = navigator.scaffoldState, // [START_EXCLUDE] listPane = {}, detailPane = {}, @@ -75,8 +69,7 @@ fun SampleListDetailPaneScaffoldParts() { // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part04] ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, + scaffoldState = navigator.scaffoldState, listPane = { AnimatedPane(Modifier) { MyList( @@ -97,8 +90,7 @@ fun SampleListDetailPaneScaffoldParts() { // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part05] ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, + scaffoldState = navigator.scaffoldState, listPane = // [START_EXCLUDE] {}, @@ -125,15 +117,10 @@ fun SampleListDetailPaneScaffoldFull() { } // Create the ListDetailPaneScaffoldState - val navigator = rememberListDetailPaneScaffoldNavigator() - - BackHandler(navigator.canNavigateBack()) { - navigator.navigateBack() - } + val navigator = rememberListDetailPaneScaffoldNavigator() ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, + scaffoldState = navigator.scaffoldState, listPane = { AnimatedPane(Modifier) { MyList( 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..62537f74 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt @@ -0,0 +1,335 @@ +@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.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.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.ExperimentalLayoutApi +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.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +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.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.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), +) + +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) + } +} + +private fun animationSpec() = tween(durationMillis = 500) +private val boundsTransition = BoundsTransform { _, _ -> animationSpec() } +private val shapeForSharedElement = RoundedCornerShape(16.dp) + +@OptIn(ExperimentalSharedTransitionApi::class) +@Preview +@Composable +private fun AnimatedVisibilitySharedElementFullExample() { + 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()) { + LazyVerticalGrid( + modifier = Modifier + .fillMaxSize() + .background(Color.LightGray.copy(alpha = 0.5f)) + .blurLayer(graphicsLayer, animateBlurRadius.value) + .padding(16.dp), + columns = GridCells.Adaptive(150.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(listSnacks) { index, snack -> + SnackItem( + snack = snack, + onClick = { + selectedSnack = snack + }, + visible = selectedSnack != snack + ) + } + } + + SnackEditDetails( + snack = selectedSnack, + onConfirmClick = { + selectedSnack = null + } + ) + } +} + +@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 + ) + } + } +} + +@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalLayoutApi::class) +@Preview +@Composable +private fun AnimatedVisibilitySharedElementShortenedExample() { + // [START android_compose_shared_elements_animated_visibility] + var selectedSnack by remember { mutableStateOf(null) } + + SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { + LazyVerticalGrid( + // [START_EXCLUDE] + modifier = Modifier + .fillMaxSize() + .background(Color.LightGray.copy(alpha = 0.5f)) + .padding(16.dp), + columns = GridCells.Adaptive(150.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + // [END_EXCLUDE] + ) { + itemsIndexed(listSnacks) { index, snack -> + AnimatedVisibility( + visible = snack != selectedSnack, + // [START_EXCLUDE] + enter = fadeIn(animationSpec = animationSpec()) + scaleIn( + animationSpec() + ), + exit = fadeOut(animationSpec = animationSpec()) + scaleOut( + animationSpec() + ) + // [END_EXCLUDE] + ) { + Box( + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"), + // Using the scope provided by AnimatedVisibility + 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 = { + selectedSnack = snack + } + ) + } + } + } + } + + 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(animationSpec()) togetherWith fadeOut(animationSpec()) + }, + 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, + boundsTransform = boundsTransition, + clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) + ) + .background(Color.White, shapeForSharedElement) + .clip(shapeForSharedElement) + ) { + + SnackContents( + snack = targetSnack, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = targetSnack.name), + animatedVisibilityScope = this@AnimatedContent, + boundsTransform = boundsTransition, + ), + 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(1f), + 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..37bb56a0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/BasicSharedElementSnippets.kt @@ -0,0 +1,501 @@ +/* + * 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 SharedElementScope 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] +} \ No newline at end of file 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..cb7db448 --- /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..4fa935c0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt @@ -0,0 +1,299 @@ +/* + * 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.activity.compose.BackHandler +import androidx.activity.compose.PredictiveBackHandler +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.SeekableTransitionState +import androidx.compose.animation.core.rememberTransition +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.height +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.Slider +import androidx.compose.material3.SliderState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.glance.currentState +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 +import com.example.compose.snippets.lists.Book +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +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( + this@SharedTransitionLayout, + this@composable, + onItemClick = { index -> + navController.navigate("details/$index") + } + ) + } + composable( + "details/{item}", + arguments = listOf(navArgument("item") { type = NavType.IntType }) + ) { backStackEntry -> + val id = backStackEntry.arguments?.getInt("item") + val snack = listSnacks[id!!] + DetailsScreen( + id, + snack, + this@SharedTransitionLayout, + this@composable, + onBackPressed = { + navController.navigate("home") + } + ) + } + } + } +} + +@Composable +private fun DetailsScreen( + id: Int, + snack: Snack, + sharedTransitionScope: SharedTransitionScope, + animatedContentScope: AnimatedContentScope, + onBackPressed: () -> Unit +) { + with(sharedTransitionScope) { + Column( + Modifier + .fillMaxSize() + .clickable { + onBackPressed() + } + ) { + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier.Companion + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "image-$id"), + animatedVisibilityScope = animatedContentScope + ) + .aspectRatio(1f) + .fillMaxWidth() + ) + Text( + snack.name, fontSize = 18.sp, + modifier = + Modifier.Companion + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "text-$id"), + animatedVisibilityScope = animatedContentScope + ) + .fillMaxWidth() + ) + } + } +} + +@Composable +private fun HomeScreen( + sharedTransitionScope: SharedTransitionScope, + animatedContentScope: AnimatedContentScope, + onItemClick: (Int) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(listSnacks) { index, item -> + Row( + Modifier.clickable { + onItemClick(index) + } + ) { + Spacer(modifier = Modifier.width(8.dp)) + with(sharedTransitionScope) { + Image( + painterResource(id = item.image), + contentDescription = item.description, + contentScale = ContentScale.Crop, + modifier = Modifier.Companion + .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] + + +private sealed class Screen { + data object Home : Screen() + data class Details(val id: Int) : Screen() +} + +@Preview +@Composable +fun CustomPredictiveBackHandle() { + // [START android_compose_shared_element_custom_seeking] + val seekableTransitionState = remember { + SeekableTransitionState(Screen.Home) + } + val transition = rememberTransition(transitionState = seekableTransitionState) + + PredictiveBackHandler(seekableTransitionState.currentState is Screen.Details) { progress -> + try { + progress.collect { backEvent -> + // code for progress + seekableTransitionState.seekTo(backEvent.progress, targetState = Screen.Home) + } + // code for completion + seekableTransitionState.animateTo(seekableTransitionState.targetState) + } catch (e: CancellationException) { + // code for cancellation + seekableTransitionState.animateTo(seekableTransitionState.currentState) + } + } + val coroutineScope = rememberCoroutineScope() + var lastNavigatedIndex by remember { + mutableIntStateOf(0) + } + Column { + Slider(modifier = Modifier.height(48.dp), + value = seekableTransitionState.fraction, + onValueChange = { + coroutineScope.launch { + if (seekableTransitionState.currentState is Screen.Details){ + seekableTransitionState.seekTo(it, Screen.Home) + } else { + // seek to the previously navigated index + seekableTransitionState.seekTo(it, Screen.Details(lastNavigatedIndex)) + } + }}) + SharedTransitionLayout(modifier = Modifier.weight(1f)) { + transition.AnimatedContent { targetState -> + when (targetState) { + Screen.Home -> { + HomeScreen( + this@SharedTransitionLayout, + this@AnimatedContent, + onItemClick = { + coroutineScope.launch { + lastNavigatedIndex = it + seekableTransitionState.animateTo(Screen.Details(it)) + } + } + ) + } + + is Screen.Details -> { + val snack = listSnacks[targetState.id] + DetailsScreen( + targetState.id, + snack, + this@SharedTransitionLayout, + this@AnimatedContent, + onBackPressed = { + coroutineScope.launch { + seekableTransitionState.animateTo(Screen.Home) + } + } + ) + } + } + } + } + } + + // [END android_compose_shared_element_custom_seeking] +} \ No newline at end of file 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..540af22e 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") package com.example.compose.snippets.designsystems 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 2566a514..4e1af17d 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/ShapesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt index f254da28..de0a5ee9 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt @@ -44,14 +44,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.asComposePath import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.graphicsLayer @@ -68,11 +66,9 @@ import androidx.graphics.shapes.Cubic import androidx.graphics.shapes.Morph import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.star -import androidx.graphics.shapes.toPath import com.example.compose.snippets.R import kotlin.math.PI import kotlin.math.cos -import kotlin.math.max import kotlin.math.sin @Preview @@ -88,7 +84,8 @@ fun BasicShapeCanvas() { centerX = size.width / 2, centerY = size.height / 2 ) - val roundedPolygonPath = roundedPolygon.toPath().asComposePath() + val roundedPolygonPath = roundedPolygon.cubics + .toPath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Blue) } @@ -115,7 +112,8 @@ private fun RoundedShapeExample() { smoothing = 1f ) ) - val roundedPolygonPath = roundedPolygon.toPath().asComposePath() + val roundedPolygonPath = roundedPolygon.cubics + .toPath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Black) } @@ -142,7 +140,8 @@ private fun RoundedShapeSmoothnessExample() { smoothing = 0.1f ) ) - val roundedPolygonPath = roundedPolygon.toPath().asComposePath() + val roundedPolygonPath = roundedPolygon.cubics + .toPath() onDrawBehind { drawPath(roundedPolygonPath, color = Color.Black) } @@ -179,7 +178,7 @@ private fun MorphExample() { val morph = Morph(start = triangle, end = square) val morphPath = morph - .toPath(progress = 0.5f).asComposePath() + .toComposePath(progress = 0.5f) onDrawBehind { drawPath(morphPath, color = Color.Black) @@ -226,8 +225,7 @@ private fun MorphExampleAnimation() { val morph = Morph(start = triangle, end = square) val morphPath = morph - .toPath(progress = morphProgress.value) - .asComposePath() + .toComposePath(progress = morphProgress.value) onDrawBehind { drawPath(morphPath, color = Color.Black) @@ -238,7 +236,6 @@ private fun MorphExampleAnimation() { // [END android_compose_graphics_polygon_morph_animation] } -// [START android_compose_morph_to_path] /** * Transforms the morph at a given progress into a [Path]. * It can optionally be scaled, using the origin (0,0) as pivot point. @@ -260,11 +257,10 @@ fun Morph.toComposePath(progress: Float, scale: Float = 1f, path: Path = Path()) path.close() return path } -// [END android_compose_morph_to_path] + /** * Function used to create a Path from a list of Cubics. */ -// [START android_compose_list_cubics_to_path] fun List.toPath(path: Path = Path(), scale: Float = 1f): Path { path.rewind() firstOrNull()?.let { first -> @@ -280,7 +276,6 @@ fun List.toPath(path: Path = Path(), scale: Float = 1f): Path { path.close() return path } -// [END android_compose_list_cubics_to_path] // [START android_compose_morph_clip_shape] class MorphPolygonShape( @@ -299,7 +294,7 @@ class MorphPolygonShape( matrix.scale(size.width / 2f, size.height / 2f) matrix.translate(1f, 1f) - val path = morph.toPath(progress = percentage).asComposePath() + val path = morph.toComposePath(progress = percentage) path.transform(matrix) return Outline.Generic(path) } @@ -350,26 +345,22 @@ private fun MorphOnClick() { } // [START android_compose_shapes_polygon_compose_shape] -fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) } class RoundedPolygonShape( - private val polygon: RoundedPolygon, - private var matrix: Matrix = Matrix() + private val polygon: RoundedPolygon ) : Shape { - private var path = Path() + private val matrix = Matrix() override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { - path.rewind() - path = polygon.toPath().asComposePath() - matrix.reset() - val bounds = polygon.getBounds() - val maxDimension = max(bounds.width, bounds.height) - matrix.scale(size.width / maxDimension, size.height / maxDimension) - matrix.translate(-bounds.left, -bounds.top) - + val path = polygon.cubics.toPath() + // below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f + // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. + matrix.scale(size.width / 2f, size.height / 2f) + matrix.translate(1f, 1f) path.transform(matrix) + return Outline.Generic(path) } } @@ -458,7 +449,7 @@ class CustomRotatingMorphShape( matrix.translate(1f, 1f) matrix.rotateZ(rotation) - val path = morph.toPath(progress = percentage).asComposePath() + val path = morph.toComposePath(progress = percentage) path.transform(matrix) return Outline.Generic(path) @@ -572,7 +563,8 @@ private fun CartesianPoints() { Box( modifier = Modifier .drawWithCache { - val roundedPolygonPath = polygon.toPath().asComposePath() + val roundedPolygonPath = polygon.cubics + .toPath() onDrawBehind { scale(size.width * 0.5f, size.width * 0.5f) { translate(size.width * 0.5f, size.height * 0.5f) { 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..b3bb8692 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.material.Divider 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,12 @@ 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) + Divider(modifier = Modifier.align(Alignment.BottomCenter)) } } 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..203f1283 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.foundation.verticalScroll import androidx.compose.material.Chip import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Text +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,10 +277,10 @@ private fun FlowItems() { @OptIn(ExperimentalMaterialApi::class) @Composable -fun ChipItem(text: String) { +fun ChipItem(text: String, onClick: () -> Unit = {}) { Chip( modifier = Modifier.padding(end = 4.dp), - onClick = {}, + onClick = onClick, leadingIcon = {}, border = BorderStroke(1.dp, Color(0xFF3B3A3C)) ) { @@ -426,9 +441,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/PagerSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt index f65c96e6..e2bf4c43 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 @@ -426,7 +426,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..a4a405b3 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 @@ -20,6 +20,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 @@ -434,7 +436,7 @@ private fun LazyItemAnimations() { // [START android_compose_layouts_lazy_column_item_animation] LazyColumn { items(books, key = { it.id }) { - Row(Modifier.animateItemPlacement()) { + Row(Modifier.animateItem()) { // ... } } @@ -452,8 +454,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) ) ) { // ... 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 60c07901..89934f81 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,8 @@ 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"), + CustomPredictiveBackExample("predictiveBackCustom", "Custom Predictive back") } // Enum class for compose components navigation screen. diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt index 953c5723..8830493f 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Android Open Source Project + * 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. @@ -17,330 +17,80 @@ package com.example.compose.snippets.pictureinpicture import android.app.PictureInPictureParams -import android.app.RemoteAction -import android.content.BroadcastReceiver import android.content.Context import android.content.ContextWrapper -import android.content.Intent -import android.content.IntentFilter import android.os.Build import android.util.Log -import android.util.Rational import androidx.activity.ComponentActivity -import androidx.annotation.RequiresApi -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect 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.toAndroidRectF -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalContext -import androidx.core.app.PictureInPictureModeChangedInfo -import androidx.core.content.ContextCompat -import androidx.core.graphics.toRect -import androidx.core.util.Consumer -import androidx.media3.common.Player -import androidx.media3.exoplayer.ExoPlayer -var shouldEnterPipMode by mutableStateOf(false) private const val PIP_TAG = "PiP info" - -// [START android_compose_pip_broadcast_receiver_constants] -// Constant for broadcast receiver -const val ACTION_BROADCAST_CONTROL = "broadcast_control" - -// Intent extras for broadcast controls from Picture-in-Picture mode. -const val EXTRA_CONTROL_TYPE = "control_type" -const val EXTRA_CONTROL_PLAY = 1 -const val EXTRA_CONTROL_PAUSE = 2 -// [END android_compose_pip_broadcast_receiver_constants] - -@Composable -fun PiPBuilderSetAutoEnterEnabled( - modifier: Modifier = Modifier -) { - val context = LocalContext.current - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // [START android_compose_pip_builder_auto_enter] - val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> - val builder = PictureInPictureParams.Builder() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setAutoEnterEnabled(true) - } - context.findActivity().setPictureInPictureParams(builder.build()) - } - VideoPlayer(pipModifier) - // [END android_compose_pip_builder_auto_enter] - } else { - Log.i(PIP_TAG, "API does not support PiP") - } -} - -// [START android_compose_pip_find_activity] -internal fun Context.findActivity(): ComponentActivity { - var context = this - while (context is ContextWrapper) { - if (context is ComponentActivity) return context - context = context.baseContext - } - throw IllegalStateException("Picture in picture should be called in the context of an Activity") -} -// [END android_compose_pip_find_activity] - -@Composable -fun EnterPiPThroughButton() { - // [START android_compose_pip_button_click] - val context = LocalContext.current - Button(onClick = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.findActivity().enterPictureInPictureMode( - PictureInPictureParams.Builder().build() - ) - } else { - Log.i(PIP_TAG, "API does not support PiP") - } - }) { - Text(text = "Enter PiP mode!") - } - // [END android_compose_pip_button_click] -} - -// [START android_compose_pip_is_in_pip_mode] @Composable -fun rememberIsInPipMode(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val activity = LocalContext.current.findActivity() - var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) } - DisposableEffect(activity) { - val observer = Consumer { info -> - pipMode = info.isInPictureInPictureMode +fun PipListenerPreAPI12(shouldEnterPipMode: Boolean) { + // [START android_compose_pip_pre12_listener] + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + Build.VERSION.SDK_INT < Build.VERSION_CODES.S + ) { + val context = LocalContext.current + DisposableEffect(context) { + val onUserLeaveBehavior: () -> Unit = { + context.findActivity() + .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } - activity.addOnPictureInPictureModeChangedListener( - observer - ) - onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } - } - return pipMode - } else { - return false - } -} -// [END android_compose_pip_is_in_pip_mode] - -@Composable -fun VideoPlayer() { -} -@Composable -fun VideoPlayer(modifier: Modifier) { -} - -@Composable -fun ToggleUIBasedOnPiP( - modifier: Modifier = Modifier, -) { - // [START android_compose_pip_ui_toggle] - val inPipMode = rememberIsInPipMode() - - Column(modifier = modifier) { - // This text will only show up when the app is not in PiP mode - if (!inPipMode) { - Text( - text = "Picture in Picture", + context.findActivity().addOnUserLeaveHintListener( + onUserLeaveBehavior ) - } - VideoPlayer() - } - // [END android_compose_pip_ui_toggle] -} - -fun initializePlayer(context: Context) { - val player = ExoPlayer.Builder(context.applicationContext) - .build().apply {} - - // [START android_compose_pip_toggle_pip_on_if_video_is_playing] - player.addListener(object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - shouldEnterPipMode = isPlaying - } - }) - // [END android_compose_pip_toggle_pip_on_if_video_is_playing] -} - -// [START android_compose_pip_release_player] -fun releasePlayer() { - shouldEnterPipMode = false -} -// [END android_compose_pip_release_player] - -@Composable -fun PiPBuilderSetAutoEnterEnabledUsingState( - shouldEnterPipMode: Boolean, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // [START android_compose_pip_post_12_should_enter_pip] - val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> - val builder = PictureInPictureParams.Builder() - - // Add autoEnterEnabled for versions S and up - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setAutoEnterEnabled(shouldEnterPipMode) + onDispose { + context.findActivity().removeOnUserLeaveHintListener( + onUserLeaveBehavior + ) } - context.findActivity().setPictureInPictureParams(builder.build()) } - - VideoPlayer(pipModifier) - // [END android_compose_pip_post_12_should_enter_pip] } else { Log.i(PIP_TAG, "API does not support PiP") } + // [END android_compose_pip_pre12_listener] } @Composable -fun PiPBuilderSetSourceRect( - shouldEnterPipMode: Boolean, - modifier: Modifier = Modifier, -) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // [START android_compose_pip_set_source_rect] +fun EnterPiPPre12(shouldEnterPipMode: Boolean) { + // [START android_compose_pip_pre12_should_enter_pip] + val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + Build.VERSION.SDK_INT < Build.VERSION_CODES.S + ) { val context = LocalContext.current - - val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> - val builder = PictureInPictureParams.Builder() - if (shouldEnterPipMode) { - val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() - builder.setSourceRectHint(sourceRect) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setAutoEnterEnabled(shouldEnterPipMode) + DisposableEffect(context) { + val onUserLeaveBehavior: () -> Unit = { + if (currentShouldEnterPipMode) { + context.findActivity() + .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) + } } - context.findActivity().setPictureInPictureParams(builder.build()) - } - - VideoPlayer(pipModifier) - // [END android_compose_pip_set_source_rect] - } else { - Log.i(PIP_TAG, "API does not support PiP") - } -} - -@Composable -fun PiPBuilderSetAspectRatio( - shouldEnterPipMode: Boolean, - modifier: Modifier = Modifier, -) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // [START android_compose_pip_set_aspect_ratio] - val context = LocalContext.current - - val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> - val builder = PictureInPictureParams.Builder() - - if (shouldEnterPipMode) { - val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() - builder.setSourceRectHint(sourceRect) - builder.setAspectRatio( - Rational(sourceRect.width(), sourceRect.height()) + context.findActivity().addOnUserLeaveHintListener( + onUserLeaveBehavior + ) + onDispose { + context.findActivity().removeOnUserLeaveHintListener( + onUserLeaveBehavior ) } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setAutoEnterEnabled(shouldEnterPipMode) - } - context.findActivity().setPictureInPictureParams(builder.build()) } - - VideoPlayer(pipModifier) - // [END android_compose_pip_set_aspect_ratio] } else { Log.i(PIP_TAG, "API does not support PiP") } + // [END android_compose_pip_pre12_should_enter_pip] } -// [START android_compose_pip_broadcast_receiver] -@RequiresApi(Build.VERSION_CODES.O) -@Composable -fun PlayerBroadcastReceiver(player: Player?) { - val isInPipMode = rememberIsInPipMode() - if (!isInPipMode || player == null) { - // Broadcast receiver is only used if app is in PiP mode and player is non null - return - } - val context = LocalContext.current - - DisposableEffect(player) { - val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) { - return - } - - when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) { - EXTRA_CONTROL_PAUSE -> player.pause() - EXTRA_CONTROL_PLAY -> player.play() - } - } - } - ContextCompat.registerReceiver( - context, - broadcastReceiver, - IntentFilter(ACTION_BROADCAST_CONTROL), - ContextCompat.RECEIVER_NOT_EXPORTED - ) - onDispose { - context.unregisterReceiver(broadcastReceiver) - } - } -} -// [END android_compose_pip_broadcast_receiver] - -@RequiresApi(Build.VERSION_CODES.O) -fun listOfRemoteActions(): List { - return listOf() -} - -@Composable -fun PiPBuilderAddRemoteActions( - shouldEnterPipMode: Boolean, - modifier: Modifier = Modifier, -) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // [START android_compose_pip_add_remote_actions] - val context = LocalContext.current - - val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> - val builder = PictureInPictureParams.Builder() - builder.setActions( - listOfRemoteActions() - ) - - if (shouldEnterPipMode) { - val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() - builder.setSourceRectHint(sourceRect) - builder.setAspectRatio( - Rational(sourceRect.width(), sourceRect.height()) - ) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setAutoEnterEnabled(shouldEnterPipMode) - } - context.findActivity().setPictureInPictureParams(builder.build()) - } - VideoPlayer(modifier = pipModifier) - // [END android_compose_pip_add_remote_actions] - } else { - Log.i(PIP_TAG, "API does not support PiP") +internal fun Context.findActivity(): ComponentActivity { + var context = this + while (context is ContextWrapper) { + if (context is ComponentActivity) return context + context = context.baseContext } + throw IllegalStateException("Picture in picture should be called in the context of an Activity") } 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..283e8f7f 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 @@ -21,8 +21,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.material.Typography import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -118,16 +118,16 @@ fun DownloadableFontsText() { private object TextDownloadableFontsSnippet4 { // [START android_compose_text_typography_definition] val MyTypography = Typography( - labelMedium = TextStyle( + body1 = TextStyle( fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp/*...*/ ), - labelLarge = TextStyle( + body2 = TextStyle( fontFamily = fontFamily, fontWeight = FontWeight.Bold, letterSpacing = 2.sp, /*...*/ ), - displayMedium = TextStyle( + h4 = 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..bfb8b837 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 @@ -19,16 +19,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 -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.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.ClickableText @@ -36,7 +27,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextField @@ -46,7 +36,6 @@ import androidx.compose.runtime.mutableStateOf 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush @@ -66,13 +55,10 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.style.Hyphens -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.withStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp @@ -591,188 +577,7 @@ private object TextEffectiveStateManagement2 { // [END android_compose_text_state_management] } -@Composable -private fun TextSample(samples: MapUnit>) { - MaterialTheme { - Box( - Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxWidth() - ) { - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.padding(10.dp) - ) { - samples.forEach { (title, content) -> - Row(Modifier.fillMaxWidth()) { - content() - Text( - text = title, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterVertically) - ) - } - } - } - } - } -} - -private const val SAMPLE_LONG_TEXT = - "Jetpack Compose is Android’s modern toolkit for building native UI. " + - "It simplifies and accelerates UI development on Android bringing your apps " + - "to life with less code, powerful tools, and intuitive Kotlin APIs. " + - "It makes building Android UI faster and easier." -@Composable -@Preview -fun LineBreakSample() { - // [START android_compose_text_line_break] - TextSample( - samples = mapOf( - "Simple" to { - Text( - text = SAMPLE_LONG_TEXT, - modifier = Modifier - .width(130.dp) - .border(BorderStroke(1.dp, Color.Gray)), - fontSize = 14.sp, - style = TextStyle.Default.copy( - lineBreak = LineBreak.Simple - ) - ) - }, - "Paragraph" to { - Text( - text = SAMPLE_LONG_TEXT, - modifier = Modifier - .width(130.dp) - .border(BorderStroke(1.dp, Color.Gray)), - fontSize = 14.sp, - style = TextStyle.Default.copy( - lineBreak = LineBreak.Paragraph - ) - ) - } - ) - ) - // [END android_compose_text_line_break] -} - -@Preview -@Composable -fun SmallScreenTextSnippet() { - // [START android_compose_text_paragraph] - TextSample( - samples = mapOf( - "Balanced" to { - val smallScreenAdaptedParagraph = - LineBreak.Paragraph.copy(strategy = LineBreak.Strategy.Balanced) - Text( - text = SAMPLE_LONG_TEXT, - modifier = Modifier - .width(200.dp) - .border(BorderStroke(1.dp, Color.Gray)), - fontSize = 14.sp, - style = TextStyle.Default.copy( - lineBreak = smallScreenAdaptedParagraph - ) - ) - }, - "Default" to { - Text( - text = SAMPLE_LONG_TEXT, - modifier = Modifier - .width(200.dp) - .border(BorderStroke(1.dp, Color.Gray)), - fontSize = 14.sp, - style = TextStyle.Default - ) - } - ) - ) - // [END android_compose_text_paragraph] -} - -private object CJKTextSnippet { - @Composable - fun CJKSample() { - // [START android_compose_text_cjk] - val customTitleLineBreak = LineBreak( - strategy = LineBreak.Strategy.HighQuality, - strictness = LineBreak.Strictness.Strict, - wordBreak = LineBreak.WordBreak.Phrase - ) - Text( - text = "あなたに寄り添う最先端のテクノロジー。", - modifier = Modifier.width(250.dp), - fontSize = 14.sp, - style = TextStyle.Default.copy( - lineBreak = customTitleLineBreak - ) - ) - // [END android_compose_text_cjk] - } -} - -@Preview -@Composable -fun HyphenateTextSnippet() { - // [START android_compose_text_hyphen] - TextSample( - samples = mapOf( - "Hyphens - None" to { - Text( - text = SAMPLE_LONG_TEXT, - modifier = Modifier - .width(130.dp) - .border(BorderStroke(1.dp, Color.Gray)), - fontSize = 14.sp, - style = TextStyle.Default.copy( - lineBreak = LineBreak.Paragraph, - hyphens = Hyphens.None - ) - ) - }, - "Hyphens - Auto" to { - Text( - text = SAMPLE_LONG_TEXT, - modifier = Modifier - .width(130.dp) - .border(BorderStroke(1.dp, Color.Gray)), - fontSize = 14.sp, - style = TextStyle.Default.copy( - lineBreak = LineBreak.Paragraph, - hyphens = Hyphens.Auto - ) - ) - } - ) - ) - // [END android_compose_text_hyphen] -} - -@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. - Column(Modifier.width(400.dp)) { - Text( - "Learn about why it's great to use Jetpack Compose", - modifier = Modifier.basicMarquee(), - fontSize = 50.sp - ) - } -} -// [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..2391d48b 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,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION_ERROR") + package com.example.compose.snippets.touchinput.focus import androidx.compose.foundation.ExperimentalFoundationApi 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 173db1d6..be0e9735 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] accompanist = "0.32.0" -androidGradlePlugin = "8.2.2" +androidGradlePlugin = "8.4.0" androidx-activity-compose = "1.9.0-alpha03" androidx-appcompat = "1.6.1" -androidx-compose-bom = "2024.04.00" +androidx-compose-bom = "2024.05.00" androidx-compose-ui-test = "1.7.0-alpha03" androidx-constraintlayout = "2.1.4" androidx-constraintlayout-compose = "1.0.1" @@ -23,6 +23,7 @@ coil = "2.5.0" # @keep compileSdk = "34" compose-compiler = "1.5.4" +compose-latest = "1.7.0-beta01" coroutines = "1.7.3" google-maps = "18.2.0" gradle-versions = "0.51.0" @@ -33,8 +34,8 @@ kotlin = "1.9.20" ksp = "1.8.0-1.0.9" maps-compose = "4.3.2" material = "1.11.0" -material3-adaptive = "1.0.0-alpha08" -material3-adaptive-navigation-suite = "1.0.0-alpha05" +material3-adaptive = "1.0.0-alpha05" +material3-adaptive-navigation-suite = "1.0.0-alpha02" media3 = "1.2.1" # @keep minSdk = "21" @@ -51,25 +52,24 @@ 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" } -androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" } +androidx-compose-material3-adaptive = { module = "androidx.compose.material3:material3-adaptive", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" } androidx-compose-materialWindow = { module = "androidx.compose.material3:material3-window-size-class" } 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-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } +androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.0-beta01" 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" } @@ -84,14 +84,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" } @@ -109,6 +106,8 @@ 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" } +androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e093..b81fa3bb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ +#Tue May 14 19:06:12 BST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME