1414 * limitations under the License.
1515 */
1616
17+ @file:OptIn(ExperimentalSharedTransitionApi ::class , ExperimentalSharedTransitionApi ::class )
18+
1719package com.example.listdetailcompose.ui
1820
21+ import android.annotation.SuppressLint
1922import androidx.activity.compose.BackHandler
23+ import androidx.annotation.DrawableRes
24+ import androidx.compose.animation.AnimatedContent
25+ import androidx.compose.animation.AnimatedVisibilityScope
26+ import androidx.compose.animation.ExperimentalSharedTransitionApi
27+ import androidx.compose.animation.SharedTransitionLayout
28+ import androidx.compose.animation.SharedTransitionScope
2029import androidx.compose.foundation.BorderStroke
30+ import androidx.compose.foundation.Image
2131import androidx.compose.foundation.clickable
2232import androidx.compose.foundation.layout.Arrangement
2333import androidx.compose.foundation.layout.Column
2434import androidx.compose.foundation.layout.PaddingValues
35+ import androidx.compose.foundation.layout.Row
2536import androidx.compose.foundation.layout.fillMaxWidth
2637import androidx.compose.foundation.layout.padding
2738import androidx.compose.foundation.lazy.LazyColumn
@@ -35,18 +46,25 @@ import androidx.compose.material3.CardDefaults
3546import androidx.compose.material3.MaterialTheme
3647import androidx.compose.material3.Text
3748import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
49+ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
50+ import androidx.compose.material3.adaptive.layout.AnimatedPane
3851import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
3952import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
4053import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
54+ import androidx.compose.material3.adaptive.layout.PaneExpansionDragHandle
55+ import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
4156import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
4257import androidx.compose.runtime.Composable
4358import androidx.compose.runtime.getValue
4459import androidx.compose.runtime.mutableStateOf
4560import androidx.compose.runtime.saveable.rememberSaveable
4661import androidx.compose.runtime.setValue
4762import androidx.compose.ui.Modifier
63+ import androidx.compose.ui.graphics.Color
64+ import androidx.compose.ui.res.painterResource
4865import androidx.compose.ui.res.stringResource
4966import androidx.compose.ui.unit.dp
67+ import androidx.window.core.layout.WindowWidthSizeClass
5068import com.example.listdetailcompose.R
5169
5270// Create some simple sample data
@@ -56,59 +74,84 @@ private val loremIpsum = """
5674 |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.
5775 """ .trimMargin()
5876private val sampleWords = listOf (
59- " Apple" to loremIpsum ,
60- " Banana" to loremIpsum ,
61- " Cherry" to loremIpsum ,
62- " Date" to loremIpsum ,
63- " Elderberry" to loremIpsum ,
64- " Fig" to loremIpsum ,
65- " Grape" to loremIpsum ,
66- " Honeydew" to loremIpsum ,
67- ).map { (word, definition ) -> DefinedWord (word, definition ) }
77+ " Apple" to R .drawable.ic_food ,
78+ " Banana" to R .drawable.ic_no_food ,
79+ " Cherry" to R .drawable.ic_food ,
80+ " Date" to R .drawable.ic_no_food ,
81+ " Elderberry" to R .drawable.ic_food ,
82+ " Fig" to R .drawable.ic_no_food ,
83+ " Grape" to R .drawable.ic_food ,
84+ " Honeydew" to R .drawable.ic_no_food ,
85+ ).map { (word, icon ) -> DefinedWord (word, icon ) }
6886
6987private data class DefinedWord (
7088 val word : String ,
71- val definition : String
89+ @DrawableRes val icon : Int ,
90+ val definition : String = loremIpsum
7291)
7392
93+ @SuppressLint(" UnusedContentLambdaTargetStateParameter" )
7494@OptIn(ExperimentalMaterial3AdaptiveApi ::class )
7595@Composable
7696fun ListDetailSample () {
7797 var selectedWordIndex: Int? by rememberSaveable { mutableStateOf(null ) }
7898 val navigator = rememberListDetailPaneScaffoldNavigator<Nothing >()
99+ val isListAndDetailVisible =
100+ navigator.scaffoldValue[ListDetailPaneScaffoldRole .Detail ] == PaneAdaptedValue .Expanded && navigator.scaffoldValue[ListDetailPaneScaffoldRole .List ] == PaneAdaptedValue .Expanded
79101
80102 BackHandler (enabled = navigator.canNavigateBack()) {
81103 navigator.navigateBack()
82104 }
83105
84- ListDetailPaneScaffold (
85- directive = navigator.scaffoldDirective,
86- value = navigator.scaffoldValue,
87- listPane = {
88- val currentSelectedWordIndex = selectedWordIndex
89- val isDetailVisible =
90- navigator.scaffoldValue[ListDetailPaneScaffoldRole .Detail ] == PaneAdaptedValue .Expanded
91-
92- ListContent (
93- words = sampleWords.map(DefinedWord ::word),
94- selectionState = if (isDetailVisible && currentSelectedWordIndex != null ) {
95- SelectionVisibilityState .ShowSelection (currentSelectedWordIndex)
96- } else {
97- SelectionVisibilityState .NoSelection
106+ SharedTransitionLayout {
107+ AnimatedContent (targetState = isListAndDetailVisible, label = " simple sample" ) {
108+ ListDetailPaneScaffold (
109+ directive = navigator.scaffoldDirective,
110+ value = navigator.scaffoldValue,
111+ listPane = {
112+ val currentSelectedWordIndex = selectedWordIndex
113+ val isDetailVisible =
114+ navigator.scaffoldValue[ListDetailPaneScaffoldRole .Detail ] == PaneAdaptedValue .Expanded
115+ AnimatedPane {
116+ ListContent (
117+ words = sampleWords,
118+ selectionState = if (isDetailVisible && currentSelectedWordIndex != null ) {
119+ SelectionVisibilityState .ShowSelection (currentSelectedWordIndex)
120+ } else {
121+ SelectionVisibilityState .NoSelection
122+ },
123+ onIndexClick = { index ->
124+ selectedWordIndex = index
125+ navigator.navigateTo(ListDetailPaneScaffoldRole .Detail )
126+ },
127+ isListAndDetailVisible = isListAndDetailVisible,
128+ isListVisible = ! isDetailVisible,
129+ animatedVisibilityScope = this @AnimatedPane,
130+ sharedTransitionScope = this @SharedTransitionLayout
131+ )
132+ }
133+ },
134+ detailPane = {
135+ val definedWord = selectedWordIndex?.let (sampleWords::get)
136+ val isDetailVisible =
137+ navigator.scaffoldValue[ListDetailPaneScaffoldRole .Detail ] == PaneAdaptedValue .Expanded
138+ AnimatedPane {
139+ DetailContent (
140+ definedWord = definedWord,
141+ isListAndDetailVisible = isListAndDetailVisible,
142+ isDetailVisible = isDetailVisible,
143+ animatedVisibilityScope = this @AnimatedPane,
144+ sharedTransitionScope = this @SharedTransitionLayout
145+ )
146+ }
98147 },
99- onIndexClick = { index ->
100- selectedWordIndex = index
101- navigator.navigateTo( ListDetailPaneScaffoldRole . Detail )
148+ paneExpansionState = rememberPaneExpansionState(navigator.scaffoldValue),
149+ paneExpansionDragHandle = { state ->
150+ PaneExpansionDragHandle (state, Color . Red )
102151 }
103152 )
104- },
105- detailPane = {
106- val definedWord = selectedWordIndex?.let (sampleWords::get)
107- DetailContent (
108- definedWord = definedWord
109- )
110153 }
111- )
154+ }
112155}
113156
114157/* *
@@ -137,10 +180,14 @@ sealed interface SelectionVisibilityState {
137180 */
138181@Composable
139182private fun ListContent (
140- words : List <String >,
183+ words : List <DefinedWord >,
141184 selectionState : SelectionVisibilityState ,
142185 onIndexClick : (index: Int ) -> Unit ,
143- modifier : Modifier = Modifier
186+ modifier : Modifier = Modifier ,
187+ isListAndDetailVisible : Boolean ,
188+ isListVisible : Boolean ,
189+ sharedTransitionScope : SharedTransitionScope ,
190+ animatedVisibilityScope : AnimatedVisibilityScope
144191) {
145192 LazyColumn (
146193 contentPadding = PaddingValues (vertical = 16 .dp),
@@ -204,12 +251,33 @@ private fun ListContent(
204251 .then(interactionModifier)
205252 .fillMaxWidth()
206253 ) {
207- Text (
208- text = word,
209- modifier = Modifier
210- .fillMaxWidth()
211- .padding(8 .dp)
212- )
254+ Row {
255+ val imageModifier = Modifier .padding(horizontal = 8 .dp)
256+ if (! isListAndDetailVisible && isListVisible) {
257+ with (sharedTransitionScope) {
258+ val state = rememberSharedContentState(key = word.word)
259+ imageModifier.then(
260+ Modifier .sharedElement(
261+ state,
262+ animatedVisibilityScope = animatedVisibilityScope
263+ )
264+ )
265+ }
266+ }
267+
268+ Image (
269+ painter = painterResource(id = word.icon),
270+ contentDescription = word.word,
271+ modifier = imageModifier
272+ )
273+ Text (
274+ text = word.word,
275+ modifier = Modifier
276+ .fillMaxWidth()
277+ .padding(8 .dp)
278+ )
279+ }
280+
213281 }
214282 }
215283 }
@@ -222,13 +290,39 @@ private fun ListContent(
222290private fun DetailContent (
223291 definedWord : DefinedWord ? ,
224292 modifier : Modifier = Modifier ,
293+ isListAndDetailVisible : Boolean ,
294+ isDetailVisible : Boolean ,
295+ sharedTransitionScope : SharedTransitionScope ,
296+ animatedVisibilityScope : AnimatedVisibilityScope
225297) {
226298 Column (
227299 modifier = modifier
228300 .verticalScroll(rememberScrollState())
229301 .padding(vertical = 16 .dp)
230302 ) {
231303 if (definedWord != null ) {
304+
305+ val imageModifier = Modifier
306+ .padding(horizontal = 8 .dp)
307+ .then(
308+ if (! isListAndDetailVisible && isDetailVisible) {
309+ with (sharedTransitionScope) {
310+ val state = rememberSharedContentState(key = definedWord.word)
311+ Modifier .sharedElement(
312+ state,
313+ animatedVisibilityScope = animatedVisibilityScope
314+ )
315+ }
316+ } else {
317+ Modifier
318+ }
319+ )
320+
321+ Image (
322+ painter = painterResource(id = definedWord.icon),
323+ contentDescription = definedWord.word,
324+ modifier = imageModifier
325+ )
232326 Text (
233327 text = definedWord.word,
234328 style = MaterialTheme .typography.headlineMedium
0 commit comments