diff --git a/CanonicalLayouts/list-detail-compose/app/build.gradle b/CanonicalLayouts/list-detail-compose/app/build.gradle index 1f3218b5e..b30b0cb05 100644 --- a/CanonicalLayouts/list-detail-compose/app/build.gradle +++ b/CanonicalLayouts/list-detail-compose/app/build.gradle @@ -20,12 +20,12 @@ plugins { android { namespace 'com.example.listdetailcompose' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.example.listdetailcompose" minSdk 21 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" @@ -62,21 +62,22 @@ android { } dependencies { - def composeBom = platform('androidx.compose:compose-bom:2024.03.00') + def composeBom = platform('androidx.compose:compose-bom:2024.09.00') implementation(composeBom) implementation "com.google.accompanist:accompanist-adaptive:0.32.0" - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation 'androidx.activity:activity-compose:1.8.2' - implementation "androidx.compose.foundation:foundation:1.6.4" - implementation "androidx.compose.ui:ui:1.6.4" + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.5' + implementation 'androidx.activity:activity-compose:1.9.2' + implementation "androidx.compose.foundation:foundation:1.7.0" + implementation "androidx.compose.ui:ui:1.7.0" implementation "androidx.compose.ui:ui-tooling-preview" - implementation "androidx.window:window:1.2.0" - implementation 'androidx.compose.material3:material3:1.3.0-alpha03' - implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-alpha09' - implementation 'androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha09' - implementation 'androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha09' - implementation "androidx.compose.material3:material3-window-size-class:1.3.0-alpha03" + implementation "androidx.window:window:1.3.0" + implementation 'androidx.compose.material3:material3:1.3.0' + implementation 'androidx.compose.material3.adaptive:adaptive:1.1.0-alpha02' + implementation 'androidx.compose.material3.adaptive:adaptive-layout:1.1.0-alpha02' + implementation 'androidx.compose.material3.adaptive:adaptive-navigation:1.1.0-alpha02' + implementation "androidx.compose.material3:material3-window-size-class:1.3.0" + implementation "androidx.compose.animation:animation:1.7.0" testImplementation 'junit:junit:4.13.2' } diff --git a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt index cc17b4eb5..3e55a7252 100644 --- a/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt +++ b/CanonicalLayouts/list-detail-compose/app/src/main/java/com/example/listdetailcompose/ui/ListDetailSample.kt @@ -14,14 +14,25 @@ * limitations under the License. */ +@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalSharedTransitionApi::class) + package com.example.listdetailcompose.ui +import android.annotation.SuppressLint import androidx.activity.compose.BackHandler +import androidx.annotation.DrawableRes +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.foundation.BorderStroke +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.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -35,9 +46,13 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +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.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.layout.PaneExpansionDragHandle +import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -45,8 +60,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowWidthSizeClass import com.example.listdetailcompose.R // Create some simple sample data @@ -56,59 +74,84 @@ private val loremIpsum = """ |Tempus quam pellentesque nec nam aliquam. Praesent semper feugiat nibh sed. Adipiscing elit duis tristique sollicitudin nibh sit. Netus et malesuada fames ac turpis egestas sed tempus urna. Quis varius quam quisque id diam vel quam. Urna duis convallis convallis tellus id interdum velit laoreet. Id eu nisl nunc mi ipsum. Fermentum dui faucibus in ornare. Nunc lobortis mattis aliquam faucibus. Vulputate mi sit amet mauris commodo quis. Porta nibh venenatis cras sed. Vitae tortor condimentum lacinia quis vel eros donec. Eu non diam phasellus vestibulum. """.trimMargin() private val sampleWords = listOf( - "Apple" to loremIpsum, - "Banana" to loremIpsum, - "Cherry" to loremIpsum, - "Date" to loremIpsum, - "Elderberry" to loremIpsum, - "Fig" to loremIpsum, - "Grape" to loremIpsum, - "Honeydew" to loremIpsum, -).map { (word, definition) -> DefinedWord(word, definition) } + "Apple" to R.drawable.ic_food, + "Banana" to R.drawable.ic_no_food, + "Cherry" to R.drawable.ic_food, + "Date" to R.drawable.ic_no_food, + "Elderberry" to R.drawable.ic_food, + "Fig" to R.drawable.ic_no_food, + "Grape" to R.drawable.ic_food, + "Honeydew" to R.drawable.ic_no_food, +).map { (word, icon) -> DefinedWord(word, icon) } private data class DefinedWord( val word: String, - val definition: String + @DrawableRes val icon: Int, + val definition: String = loremIpsum ) +@SuppressLint("UnusedContentLambdaTargetStateParameter") @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun ListDetailSample() { var selectedWordIndex: Int? by rememberSaveable { mutableStateOf(null) } val navigator = rememberListDetailPaneScaffoldNavigator() + val isListAndDetailVisible = + navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded && navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded BackHandler(enabled = navigator.canNavigateBack()) { navigator.navigateBack() } - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - listPane = { - val currentSelectedWordIndex = selectedWordIndex - val isDetailVisible = - navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded - - ListContent( - words = sampleWords.map(DefinedWord::word), - selectionState = if (isDetailVisible && currentSelectedWordIndex != null) { - SelectionVisibilityState.ShowSelection(currentSelectedWordIndex) - } else { - SelectionVisibilityState.NoSelection + SharedTransitionLayout { + AnimatedContent(targetState = isListAndDetailVisible, label = "simple sample") { + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + val currentSelectedWordIndex = selectedWordIndex + val isDetailVisible = + navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded + AnimatedPane { + ListContent( + words = sampleWords, + selectionState = if (isDetailVisible && currentSelectedWordIndex != null) { + SelectionVisibilityState.ShowSelection(currentSelectedWordIndex) + } else { + SelectionVisibilityState.NoSelection + }, + onIndexClick = { index -> + selectedWordIndex = index + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + }, + isListAndDetailVisible = isListAndDetailVisible, + isListVisible = !isDetailVisible, + animatedVisibilityScope = this@AnimatedPane, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + }, + detailPane = { + val definedWord = selectedWordIndex?.let(sampleWords::get) + val isDetailVisible = + navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded + AnimatedPane { + DetailContent( + definedWord = definedWord, + isListAndDetailVisible = isListAndDetailVisible, + isDetailVisible = isDetailVisible, + animatedVisibilityScope = this@AnimatedPane, + sharedTransitionScope = this@SharedTransitionLayout + ) + } }, - onIndexClick = { index -> - selectedWordIndex = index - navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + paneExpansionState = rememberPaneExpansionState(navigator.scaffoldValue), + paneExpansionDragHandle = { state -> + PaneExpansionDragHandle(state, Color.Red) } ) - }, - detailPane = { - val definedWord = selectedWordIndex?.let(sampleWords::get) - DetailContent( - definedWord = definedWord - ) } - ) + } } /** @@ -137,10 +180,14 @@ sealed interface SelectionVisibilityState { */ @Composable private fun ListContent( - words: List, + words: List, selectionState: SelectionVisibilityState, onIndexClick: (index: Int) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + isListAndDetailVisible: Boolean, + isListVisible: Boolean, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope ) { LazyColumn( contentPadding = PaddingValues(vertical = 16.dp), @@ -204,12 +251,33 @@ private fun ListContent( .then(interactionModifier) .fillMaxWidth() ) { - Text( - text = word, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) + Row { + val imageModifier = Modifier.padding(horizontal = 8.dp) + if (!isListAndDetailVisible && isListVisible) { + with(sharedTransitionScope) { + val state = rememberSharedContentState(key = word.word) + imageModifier.then( + Modifier.sharedElement( + state, + animatedVisibilityScope = animatedVisibilityScope + ) + ) + } + } + + Image( + painter = painterResource(id = word.icon), + contentDescription = word.word, + modifier = imageModifier + ) + Text( + text = word.word, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + } } } @@ -222,6 +290,10 @@ private fun ListContent( private fun DetailContent( definedWord: DefinedWord?, modifier: Modifier = Modifier, + isListAndDetailVisible: Boolean, + isDetailVisible: Boolean, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope ) { Column( modifier = modifier @@ -229,6 +301,28 @@ private fun DetailContent( .padding(vertical = 16.dp) ) { if (definedWord != null) { + + val imageModifier = Modifier + .padding(horizontal = 8.dp) + .then( + if (!isListAndDetailVisible && isDetailVisible) { + with(sharedTransitionScope) { + val state = rememberSharedContentState(key = definedWord.word) + Modifier.sharedElement( + state, + animatedVisibilityScope = animatedVisibilityScope + ) + } + } else { + Modifier + } + ) + + Image( + painter = painterResource(id = definedWord.icon), + contentDescription = definedWord.word, + modifier = imageModifier + ) Text( text = definedWord.word, style = MaterialTheme.typography.headlineMedium diff --git a/CanonicalLayouts/list-detail-compose/app/src/main/res/drawable/ic_food.xml b/CanonicalLayouts/list-detail-compose/app/src/main/res/drawable/ic_food.xml new file mode 100644 index 000000000..68632b468 --- /dev/null +++ b/CanonicalLayouts/list-detail-compose/app/src/main/res/drawable/ic_food.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/CanonicalLayouts/list-detail-compose/app/src/main/res/drawable/ic_no_food.xml b/CanonicalLayouts/list-detail-compose/app/src/main/res/drawable/ic_no_food.xml new file mode 100644 index 000000000..543584121 --- /dev/null +++ b/CanonicalLayouts/list-detail-compose/app/src/main/res/drawable/ic_no_food.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/CanonicalLayouts/list-detail-compose/build.gradle b/CanonicalLayouts/list-detail-compose/build.gradle index 525604a47..1cfdadc2d 100644 --- a/CanonicalLayouts/list-detail-compose/build.gradle +++ b/CanonicalLayouts/list-detail-compose/build.gradle @@ -14,7 +14,7 @@ * limitations under the License. */ plugins { - id 'com.android.application' version '8.3.2' apply false - id 'com.android.library' version '8.3.2' apply false + id 'com.android.application' version '8.6.0' apply false + id 'com.android.library' version '8.6.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.22' apply false } diff --git a/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties b/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties index 98ab018bc..f9f3c8cbd 100644 --- a/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties +++ b/CanonicalLayouts/list-detail-compose/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed May 25 14:11:15 UTC 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/CanonicalLayouts/supporting-pane-compose/app/build.gradle b/CanonicalLayouts/supporting-pane-compose/app/build.gradle index d29e14e4e..4cefe12f0 100644 --- a/CanonicalLayouts/supporting-pane-compose/app/build.gradle +++ b/CanonicalLayouts/supporting-pane-compose/app/build.gradle @@ -21,12 +21,12 @@ plugins { android { namespace 'com.example.supportingpanecompose' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.example.supportingpanecompose" minSdk 21 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" @@ -63,19 +63,19 @@ android { } dependencies { - def composeBom = platform('androidx.compose:compose-bom:2024.03.00') + def composeBom = platform('androidx.compose:compose-bom:2024.09.00') implementation(composeBom) - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation 'androidx.activity:activity-compose:1.8.2' + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.5' + implementation 'androidx.activity:activity-compose:1.9.2' implementation "androidx.compose.ui:ui" implementation "androidx.compose.ui:ui-tooling-preview" - implementation "androidx.window:window:1.2.0" - implementation 'androidx.compose.material3:material3:1.3.0-alpha03' - implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-alpha09' - implementation 'androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha09' - implementation 'androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha09' - implementation "androidx.compose.material3:material3-window-size-class:1.3.0-alpha03" + implementation "androidx.window:window:1.3.0" + implementation 'androidx.compose.material3:material3:1.3.0' + implementation 'androidx.compose.material3.adaptive:adaptive:1.1.0-alpha02' + implementation 'androidx.compose.material3.adaptive:adaptive-layout:1.1.0-alpha02' + implementation 'androidx.compose.material3.adaptive:adaptive-navigation:1.1.0-alpha02' + implementation "androidx.compose.material3:material3-window-size-class:1.3.0" testImplementation 'junit:junit:4.13.2' } \ No newline at end of file diff --git a/CanonicalLayouts/supporting-pane-compose/app/src/main/java/com/example/supportingpanecompose/ui/SupportingPaneSample.kt b/CanonicalLayouts/supporting-pane-compose/app/src/main/java/com/example/supportingpanecompose/ui/SupportingPaneSample.kt index 715685d49..18fa78ce1 100644 --- a/CanonicalLayouts/supporting-pane-compose/app/src/main/java/com/example/supportingpanecompose/ui/SupportingPaneSample.kt +++ b/CanonicalLayouts/supporting-pane-compose/app/src/main/java/com/example/supportingpanecompose/ui/SupportingPaneSample.kt @@ -29,8 +29,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.PaneExpansionDragHandle import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole +import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -39,6 +41,7 @@ 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.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.example.supportingpanecompose.R @@ -57,7 +60,7 @@ private val data = mapOf( @Composable fun SupportingPaneSample() { var selectedTopic: String by rememberSaveable { mutableStateOf(data.keys.first()) } - val navigator = rememberSupportingPaneScaffoldNavigator() + val navigator = rememberSupportingPaneScaffoldNavigator() BackHandler(enabled = navigator.canNavigateBack()) { navigator.navigateBack() @@ -128,5 +131,9 @@ fun SupportingPaneSample() { ) } } + }, + paneExpansionState = rememberPaneExpansionState(navigator.scaffoldValue), + paneExpansionDragHandle = { state -> + PaneExpansionDragHandle(state, Color.Red) }) } diff --git a/CanonicalLayouts/supporting-pane-compose/build.gradle b/CanonicalLayouts/supporting-pane-compose/build.gradle index 305709532..e18a509a7 100644 --- a/CanonicalLayouts/supporting-pane-compose/build.gradle +++ b/CanonicalLayouts/supporting-pane-compose/build.gradle @@ -14,7 +14,7 @@ * limitations under the License. */ plugins { - id 'com.android.application' version '8.3.1' apply false - id 'com.android.library' version '8.3.1' apply false + id 'com.android.application' version '8.6.0' apply false + id 'com.android.library' version '8.6.0' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false } diff --git a/CanonicalLayouts/supporting-pane-compose/gradle/wrapper/gradle-wrapper.properties b/CanonicalLayouts/supporting-pane-compose/gradle/wrapper/gradle-wrapper.properties index 98ab018bc..f9f3c8cbd 100644 --- a/CanonicalLayouts/supporting-pane-compose/gradle/wrapper/gradle-wrapper.properties +++ b/CanonicalLayouts/supporting-pane-compose/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed May 25 14:11:15 UTC 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME