diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt index 9f05642..cae07b2 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt @@ -28,8 +28,9 @@ import com.tunjid.treenav.compose.utilities.DefaultBoundsTransform * Creates movable shared elements that may be shared amongst different [PaneScope] * instances. */ +@OptIn(ExperimentalSharedTransitionApi::class) @Stable -interface MovableSharedElementScope { +interface MovableSharedElementScope : SharedTransitionScope { /** * Creates a movable shared element that accepts a single argument [T] and a [Modifier]. @@ -184,11 +185,12 @@ class MovableSharedElementHostState( * Other implementations of [MovableSharedElementScope] may delegate to this for their own * movable shared element implementations. */ +@OptIn(ExperimentalSharedTransitionApi::class) @Stable class PanedMovableSharedElementScope( paneScope: PaneScope, private val movableSharedElementHostState: MovableSharedElementHostState, -) : MovableSharedElementScope { +) : MovableSharedElementScope, SharedTransitionScope by movableSharedElementHostState { var paneScope by mutableStateOf(paneScope) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt index a43c83a..8a7505a 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt @@ -2,6 +2,7 @@ package com.tunjid.treenav.compose.threepane.configurations import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope.OverlayClip import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize import androidx.compose.foundation.layout.Box @@ -69,11 +70,12 @@ fun PaneScope.movableSharedElementS return this } +@OptIn(ExperimentalSharedTransitionApi::class) @Stable private class ThreePaneMovableSharedElementScope( private val hostState: MovableSharedElementHostState, private val delegate: PanedMovableSharedElementScope, -) : MovableSharedElementScope, +) : MovableSharedElementScope, SharedTransitionScope by delegate, PaneScope by delegate.paneScope { @OptIn(ExperimentalSharedTransitionApi::class) override fun movableSharedElementOf( diff --git a/libraryVersion.properties b/libraryVersion.properties index aa48e0f..a9cb5e2 100644 --- a/libraryVersion.properties +++ b/libraryVersion.properties @@ -14,6 +14,6 @@ # limitations under the License. # groupId=com.tunjid.treenav -treenav_version=0.0.10 -strings_version=0.0.10 -compose_version=0.0.10 \ No newline at end of file +treenav_version=0.0.11 +strings_version=0.0.11 +compose_version=0.0.11 \ No newline at end of file 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 e3481c0..32dfd90 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 @@ -176,7 +176,7 @@ fun Message( }, ) { movableSharedElementScope.updatedMovableSharedElementOf( - key = item.sender.name, + key = "$roomName-${item.sender.name}", state = ProfilePhotoArgs( profileName = item.sender.name, contentScale = ContentScale.Crop, diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatViewModel.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatViewModel.kt index 3223a55..50b5522 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatViewModel.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatViewModel.kt @@ -42,8 +42,8 @@ import kotlinx.coroutines.flow.flatMapLatest class ChatViewModel( coroutineScope: LifecycleCoroutineScope, - chatsRepository: ChatsRepository, - profileRepository: ProfileRepository, + chatsRepository: ChatsRepository = ChatsRepository, + profileRepository: ProfileRepository = ProfileRepository, navigationRepository: NavigationRepository = NavigationRepository, chat: SampleDestination.Chat, ) : ViewModel() { 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 e337e86..b8fa6dd 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 @@ -44,8 +44,6 @@ fun chatPaneStrategy() = threePaneListDetailStrategy( val viewModel = viewModel { ChatViewModel( coroutineScope = scope, - chatsRepository = ChatsRepository, - profileRepository = ProfileRepository, chat = destination, ) } 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 861b78a..88b504e 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 @@ -16,32 +16,48 @@ package com.tunjid.demo.common.ui.chatrooms +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset 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.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.tunjid.composables.collapsingheader.CollapsingHeaderLayout import com.tunjid.composables.collapsingheader.CollapsingHeaderState +import com.tunjid.demo.common.ui.ProfilePhoto +import com.tunjid.demo.common.ui.ProfilePhotoArgs import com.tunjid.demo.common.ui.SampleTopAppBar import com.tunjid.demo.common.ui.data.ChatRoom +import com.tunjid.demo.common.ui.data.Message import com.tunjid.demo.common.ui.rememberAppBarCollapsingHeaderState +import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf import kotlin.math.roundToInt @Composable fun ChatRoomsScreen( + movableSharedElementScope: MovableSharedElementScope, state: State, onAction: (Action) -> Unit, modifier: Modifier = Modifier, @@ -55,7 +71,11 @@ fun ChatRoomsScreen( Header(headerState) }, body = { - ChatRooms(state, onAction) + ChatRooms( + movableSharedElementScope = movableSharedElementScope, + state = state, + onAction = onAction, + ) } ) } @@ -82,6 +102,7 @@ private fun Header(headerState: CollapsingHeaderState) { @Composable private fun ChatRooms( + movableSharedElementScope: MovableSharedElementScope, state: State, onAction: (Action) -> Unit ) { @@ -93,7 +114,12 @@ private fun ChatRooms( key = ChatRoom::name, itemContent = { room -> ChatRoomListItem( + movableSharedElementScope = movableSharedElementScope, roomName = room.name, + participants = room.messages + .map(Message::sender) + .distinct() + .take(3), onRoomClicked = { onAction(Action.Navigation.ToRoom(roomName = it)) } @@ -105,7 +131,9 @@ private fun ChatRooms( @Composable fun ChatRoomListItem( + movableSharedElementScope: MovableSharedElementScope, roomName: String, + participants: List, modifier: Modifier = Modifier, onRoomClicked: (String) -> Unit, ) { @@ -118,12 +146,63 @@ fun ChatRoomListItem( Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .height(68.dp) + .padding( + horizontal = 8.dp, + ), verticalAlignment = Alignment.CenterVertically ) { + ChatRoomParticipants( + movableSharedElementScope = movableSharedElementScope, + participants = participants, + roomName = roomName, + ) Text( text = roomName, - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class, ExperimentalSharedTransitionApi::class) +@Composable +fun ChatRoomParticipants( + movableSharedElementScope: MovableSharedElementScope, + participants: List, + roomName: String, +) = with(movableSharedElementScope) { + FlowRow( + modifier = Modifier + .width(64.dp) + .rotate( + if (participants.size == 2) 45f + else 0f + ), + horizontalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Center, + ) { + participants.forEach { profileName -> + updatedMovableSharedElementOf( + key = "$roomName-${profileName}", + state = ProfilePhotoArgs( + profileName = profileName, + contentScale = ContentScale.Crop, + contentDescription = null, + cornerRadius = 20.dp, + ), + modifier = Modifier + .padding(horizontal = 2.dp) + .size(28.dp) + .clip(RoundedCornerShape(28.dp)) + .rotate( + if (participants.size == 2) -45f + else 0f + ), + sharedElement = { args: ProfilePhotoArgs, innerModifier: Modifier -> + ProfilePhoto(args, innerModifier) + } ) } } 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 6780e1d..30340a2 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 @@ -24,6 +24,7 @@ import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel import com.tunjid.demo.common.ui.data.ChatsRepository import com.tunjid.demo.common.ui.data.SampleDestination +import com.tunjid.treenav.compose.threepane.configurations.movableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneListDetailStrategy fun chatRoomPaneStrategy( @@ -37,6 +38,7 @@ fun chatRoomPaneStrategy( ) } ChatRoomsScreen( + movableSharedElementScope = movableSharedElementScope(), state = viewModel.state.collectAsStateWithLifecycle().value, onAction = viewModel.accept, modifier = Modifier.fillMaxSize(), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt index fed9a3f..e220aaf 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt @@ -34,6 +34,7 @@ fun mePaneStrategy( ProfileViewModel( coroutineScope = scope, profileName = null, + roomName = null, ) } ProfileScreen( 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 f911855..e5a946f 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 @@ -115,7 +115,7 @@ private fun ProfilePhoto( val profileName = state.profileName ?: state.profile?.name if (profileName != null) { movableSharedElementScope.updatedMovableSharedElementOf( - key = profileName, + key = "${state.roomName}-$profileName", state = ProfilePhotoArgs( profileName = profileName, contentScale = ContentScale.Crop, diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileViewModel.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileViewModel.kt index 2a6788a..2fdd5bc 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileViewModel.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileViewModel.kt @@ -37,10 +37,12 @@ class ProfileViewModel( profileRepository: ProfileRepository = ProfileRepository, navigationRepository: NavigationRepository = NavigationRepository, profileName: String?, + roomName: String?, ) : ViewModel() { private val mutator = coroutineScope.actionStateFlowMutator( initialState = State( - profileName = profileName + roomName = roomName, + profileName = profileName, ), inputs = listOf( profileRepository.profileMutations(profileName) @@ -70,6 +72,7 @@ private fun ProfileRepository.profileMutations( .mapToMutation { copy(profile = it) } data class State( + val roomName: String? = null, val profileName: String? = null, val profile: Profile? = null ) 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 921b4df..26658a3 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 @@ -44,6 +44,7 @@ fun profilePaneStrategy() = threePaneListDetailStrategy( ProfileViewModel( coroutineScope = scope, profileName = destination.profileName, + roomName = destination.roomName, ) } ProfileScreen(