From 4513ee35a08423577682d0453f7d0dd16cfd7a9e Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 19 Oct 2024 09:52:18 -0400 Subject: [PATCH 1/4] Screen formatting fixes --- .../kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt | 5 +++-- .../kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt | 5 ++++- .../com/tunjid/demo/common/ui/chatrooms/ChatRoomsScreen.kt | 1 + .../kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt | 7 +++++-- .../com/tunjid/demo/common/ui/profile/ProfileScreen.kt | 1 + .../kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt | 5 ++++- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt index b1cc018..7040366 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt @@ -62,10 +62,11 @@ fun ChatScreen( movableSharedElementScope: MovableSharedElementScope, state: State, onAction: (Action) -> Unit, + modifier: Modifier = Modifier, ) { val scrollState = rememberLazyListState() Column( - Modifier.fillMaxSize() + modifier, ) { SampleTopAppBar( title = state.room?.name ?: "", @@ -81,7 +82,7 @@ fun ChatScreen( messages = state.chats, isInPrimaryPane = state.isInPrimaryPane, navigateToProfile = onAction, - modifier = Modifier.weight(1f), + modifier = Modifier.fillMaxSize(), scrollState = scrollState, movableSharedElementScope = movableSharedElementScope, ) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt index 74ea0a2..e337e86 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt @@ -16,7 +16,9 @@ package com.tunjid.demo.common.ui.chat +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope @@ -50,7 +52,8 @@ fun chatPaneStrategy() = threePaneListDetailStrategy( ChatScreen( movableSharedElementScope = movableSharedElementScope(), state = viewModel.state.collectAsStateWithLifecycle().value, - onAction = viewModel.accept + onAction = viewModel.accept, + modifier = Modifier.fillMaxSize(), ) LaunchedEffect(paneState.pane) { viewModel.accept( diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/ChatRoomsScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/ChatRoomsScreen.kt index fc8360c..af4c127 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/ChatRoomsScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/ChatRoomsScreen.kt @@ -44,6 +44,7 @@ import kotlin.math.roundToInt fun ChatRoomsScreen( state: State, onAction: (Action) -> Unit, + modifier: Modifier = Modifier, ) { val headerState = rememberAppBarCollapsingHeaderState(200.dp) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt index bc4aa7f..3c54622 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt @@ -16,6 +16,8 @@ package com.tunjid.demo.common.ui.chatrooms +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope @@ -36,7 +38,8 @@ fun chatRoomPaneStrategy( } ChatRoomsScreen( state = viewModel.state.collectAsStateWithLifecycle().value, - onAction = viewModel.accept - ) + onAction = viewModel.accept, + modifier = Modifier.fillMaxSize(), + ) } ) \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt index 3982f7f..43bc78c 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt @@ -49,6 +49,7 @@ fun ProfileScreen( movableSharedElementScope: MovableSharedElementScope, state: State, onAction: (Action) -> Unit, + modifier: Modifier = Modifier, ) { val headerState = rememberAppBarCollapsingHeaderState(400.dp) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt index 1780f64..921b4df 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt @@ -16,6 +16,8 @@ package com.tunjid.demo.common.ui.profile +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope @@ -47,7 +49,8 @@ fun profilePaneStrategy() = threePaneListDetailStrategy( ProfileScreen( movableSharedElementScope = movableSharedElementScope(), state = viewModel.state.collectAsStateWithLifecycle().value, - onAction = viewModel.accept + onAction = viewModel.accept, + modifier = Modifier.fillMaxSize(), ) }, ) \ No newline at end of file From 0f2f6582d7614a319992560b2b17419acd1f934d Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 19 Oct 2024 19:43:16 -0400 Subject: [PATCH 2/4] Update predictive back animation --- .../kotlin/com/tunjid/demo/common/ui/DemoApp.kt | 14 ++++++++++---- .../com/tunjid/demo/common/ui/PredictiveBack.kt | 13 +++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt index 51df51e..d8d30d3 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt @@ -18,16 +18,19 @@ package com.tunjid.demo.common.ui import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable @@ -43,6 +46,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import com.tunjid.demo.common.ui.SampleAppState.Companion.rememberPanedNavHostState import com.tunjid.demo.common.ui.chat.chatPaneStrategy @@ -92,7 +96,9 @@ fun SampleApp( ) { SharedTransitionScope { sharedTransitionModifier -> val windowWidthDp = remember { mutableIntStateOf(0) } - val surfaceColor = MaterialTheme.colorScheme.surface + val surfaceColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + animateDpAsState(if (appState.predictiveBackStatus.value) 16.dp else 16.dp).value + ) val density = LocalDensity.current val movableSharedElementHostState = remember { MovableSharedElementHostState( @@ -106,14 +112,14 @@ fun SampleApp( this .paneModifierConfiguration { if (paneState.pane == ThreePane.TransientPrimary) Modifier - .background(surfaceColor) .fillMaxSize() .predictiveBackModifier( touchOffsetState = appState.backTouchOffsetState, progressState = appState.backProgressFractionState ) - else Modifier.fillMaxSize() - + .background(surfaceColor, RoundedCornerShape(16.dp)) + else Modifier + .fillMaxSize() } .threePanedNavHostConfiguration( windowWidthDpState = windowWidthDp diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt index 38edd40..af2d458 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt @@ -44,20 +44,17 @@ fun Modifier.predictiveBackModifier( minHeight = (constraints.minHeight * scale).roundToInt(), ) ) - val paneWidth = (placeable.width * scale).fastRoundToInt() - val paneHeight = (placeable.height * scale).fastRoundToInt() - - if (progress.isNaN() || paneWidth == 0 || paneHeight == 0) return@layout layout( - paneWidth, - paneHeight - ) { + if (progress.isNaN()) return@layout layout(placeable.width, placeable.height) { placeable.place(0, 0) } + val paneWidth = (placeable.width * scale).fastRoundToInt() + val paneHeight = (placeable.height * scale).fastRoundToInt() + val scaledWidth = paneWidth * scale val spaceOnEachSide = (paneWidth - scaledWidth) / 2 val margin = (BACK_PREVIEW_PADDING * progress).dp.roundToPx() - val isFromLeft = true + val isFromLeft = touchOffset.x < paneWidth / 2 val xOffset = ((spaceOnEachSide - margin) * when { isFromLeft -> 1 From f04b30e85a64ee54424ca70693543ffb34cf5a38 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 19 Oct 2024 20:17:31 -0400 Subject: [PATCH 3/4] Animate the transient primary pane --- .../commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt index d8d30d3..872cee3 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt @@ -97,7 +97,7 @@ fun SampleApp( SharedTransitionScope { sharedTransitionModifier -> val windowWidthDp = remember { mutableIntStateOf(0) } val surfaceColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - animateDpAsState(if (appState.predictiveBackStatus.value) 16.dp else 16.dp).value + animateDpAsState(if (appState.predictiveBackStatus.value) 16.dp else 0.dp).value ) val density = LocalDensity.current val movableSharedElementHostState = remember { @@ -136,12 +136,12 @@ fun SampleApp( shouldAnimatePane = { when (paneState.pane) { ThreePane.Primary, + ThreePane.TransientPrimary, ThreePane.Secondary, ThreePane.Tertiary -> true null, - ThreePane.Overlay, - ThreePane.TransientPrimary -> false + ThreePane.Overlay -> false } } ) From a28b48d14f4bbfdc235f04c5725086df6722164c Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 20 Oct 2024 12:54:04 -0400 Subject: [PATCH 4/4] Use segmented layout --- .../com/tunjid/demo/common/ui/DemoApp.kt | 70 ++++++++---- .../tunjid/demo/common/ui/SegmentedLayout.kt | 104 ++++++++++++++++++ 2 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/SegmentedLayout.kt diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt index 872cee3..cbc1676 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt @@ -18,13 +18,19 @@ package com.tunjid.demo.common.ui import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -41,8 +47,10 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset @@ -152,29 +160,53 @@ fun SampleApp( windowWidthDp.value = (it.width / density.density).roundToInt() } ) { - Row( + val order = remember { + listOf( + ThreePane.Tertiary, + ThreePane.Secondary, + ThreePane.Primary, + ) + } + val segmentedLayoutState = remember { + SegmentedLayoutState( + count = 3, + isIndexVisible = { nodeFor(order[it]) != null } + ) + } + SegmentedLayout( + state = segmentedLayoutState, modifier = Modifier .fillMaxSize() then movableSharedElementHostState.modifier then sharedTransitionModifier, - ) { - val order = remember { - listOf( - ThreePane.Tertiary, - ThreePane.Secondary, - ThreePane.Primary, - ) - } - order.forEach { pane -> - if (nodeFor(pane) == null) Spacer(Modifier) - else Box( + ) { index -> + Box( + modifier = Modifier.fillMaxSize() + ) { + val pane = order[index] + Destination(pane) + if (pane == ThreePane.Primary) Destination(ThreePane.TransientPrimary) + + val draggableState = rememberDraggableState { + segmentedLayoutState.dragBy( + index = index, + delta = with(density) { it.toDp() } + ) + } + val interactionSource = remember { MutableInteractionSource() } + val hovered by interactionSource.collectIsHoveredAsState() + val width by animateDpAsState(if (hovered) 2.dp else 2.dp) + val color by animateColorAsState(if (hovered) Color.LightGray else Color.Gray) + + Box( modifier = Modifier - .weight(1f) + .hoverable(interactionSource) + .align(Alignment.TopEnd) + .background(color) + .width(width) .fillMaxHeight() - ) { - Destination(pane) - if (pane == ThreePane.Primary) Destination(ThreePane.TransientPrimary) - } + .draggable(draggableState, Orientation.Horizontal) + ) } } } diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/SegmentedLayout.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/SegmentedLayout.kt new file mode 100644 index 0000000..008429d --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/SegmentedLayout.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.demo.common.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import androidx.compose.ui.unit.toSize + +@Stable +class SegmentedLayoutState( + val count: Int, + minWidth: Dp = 80.dp, + val isIndexVisible: (Int) -> Boolean = { true }, +) { + var minWidth by mutableStateOf(minWidth) + internal var size by mutableStateOf(DpSize.Zero) + + private val weightMap = mutableStateMapOf().apply { + (0.. put(index, 1f / count) } + } + + fun weightAt(index: Int): Float = weightMap.getValue(index) + + fun setWeightAt(index: Int, weight: Float) { + if (weight * size.width < minWidth) return + + val oldWeight = weightMap.getValue(index) + val weightDifference = oldWeight - weight + val adjustedIndex = (0.. + val searchIndex = (index + i) % count + if (searchIndex == index) return@search null + + val adjustedWidth = (weightMap.getValue(searchIndex) + weightDifference) * size.width + if (adjustedWidth < minWidth) return@search null + + searchIndex + } ?: return + + weightMap[index] = weight + weightMap[adjustedIndex] = weightMap.getValue(adjustedIndex) + weightDifference + } + + fun dragBy(index: Int, delta: Dp) { + val oldWeight = weightAt(index) + val width = oldWeight * size.width + val newWidth = width + delta + val newWeight = newWidth / size.width + setWeightAt(index = index, weight = newWeight) + } +} + +@Composable +fun SegmentedLayout( + state: SegmentedLayoutState, + modifier: Modifier = Modifier, + itemContent: @Composable (Int) -> Unit, +) { + val density = LocalDensity.current + Row( + modifier = modifier + .onSizeChanged { + state.size = with(density) { it.toSize().toDpSize() } + }, + ) { + for (index in 0..