|
|
@ -17,47 +17,53 @@
|
|
|
|
package com.google.samples.apps.nowinandroid.ui.interests2pane
|
|
|
|
package com.google.samples.apps.nowinandroid.ui.interests2pane
|
|
|
|
|
|
|
|
|
|
|
|
import androidx.activity.compose.BackHandler
|
|
|
|
import androidx.activity.compose.BackHandler
|
|
|
|
import androidx.annotation.Keep
|
|
|
|
import androidx.compose.animation.AnimatedContent
|
|
|
|
|
|
|
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
|
|
|
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
|
|
|
|
|
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
|
|
|
|
|
|
|
import androidx.compose.material3.VerticalDragHandle
|
|
|
|
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
|
|
|
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
|
|
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
|
|
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
|
|
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
|
|
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
|
|
|
import androidx.compose.material3.adaptive.layout.AnimatedPane
|
|
|
|
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.ListDetailPaneScaffoldRole
|
|
|
|
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
|
|
|
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
|
|
|
|
|
|
|
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
|
|
|
|
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
|
|
|
|
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
|
|
|
|
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
|
|
|
|
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
|
|
|
|
|
|
|
|
import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics
|
|
|
|
|
|
|
|
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
|
|
|
|
|
|
|
|
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
|
|
|
|
|
|
|
|
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
|
|
|
|
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
|
|
|
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
|
|
|
|
|
|
|
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldPredictiveBackHandler
|
|
|
|
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
|
|
|
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
import androidx.compose.runtime.key
|
|
|
|
|
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
|
import androidx.compose.runtime.remember
|
|
|
|
import androidx.compose.runtime.remember
|
|
|
|
import androidx.compose.runtime.saveable.Saver
|
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
|
|
|
|
|
|
import androidx.compose.runtime.setValue
|
|
|
|
import androidx.compose.runtime.setValue
|
|
|
|
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
|
|
|
|
import androidx.compose.ui.draw.clipToBounds
|
|
|
|
|
|
|
|
import androidx.compose.ui.layout.layout
|
|
|
|
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
import androidx.hilt.navigation.compose.hiltViewModel
|
|
|
|
import androidx.hilt.navigation.compose.hiltViewModel
|
|
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
|
|
import androidx.navigation.NavGraphBuilder
|
|
|
|
import androidx.navigation.NavGraphBuilder
|
|
|
|
import androidx.navigation.compose.NavHost
|
|
|
|
|
|
|
|
import androidx.navigation.compose.composable
|
|
|
|
import androidx.navigation.compose.composable
|
|
|
|
import androidx.navigation.compose.rememberNavController
|
|
|
|
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder
|
|
|
|
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen
|
|
|
|
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.TopicViewModel
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
|
|
|
|
|
|
|
|
import kotlinx.serialization.Serializable
|
|
|
|
import kotlinx.serialization.Serializable
|
|
|
|
import java.util.UUID
|
|
|
|
import kotlin.math.max
|
|
|
|
|
|
|
|
|
|
|
|
@Serializable internal object TopicPlaceholderRoute
|
|
|
|
@Serializable internal object TopicPlaceholderRoute
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Remove @Keep when https://issuetracker.google.com/353898971 is fixed
|
|
|
|
|
|
|
|
@Keep
|
|
|
|
|
|
|
|
@Serializable internal object DetailPaneNavHostRoute
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fun NavGraphBuilder.interestsListDetailScreen() {
|
|
|
|
fun NavGraphBuilder.interestsListDetailScreen() {
|
|
|
|
composable<InterestsRoute> {
|
|
|
|
composable<InterestsRoute> {
|
|
|
|
InterestsListDetailScreen()
|
|
|
|
InterestsListDetailScreen()
|
|
|
@ -93,69 +99,138 @@ internal fun InterestsListDetailScreen(
|
|
|
|
},
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
BackHandler(listDetailNavigator.canNavigateBack()) {
|
|
|
|
val coroutineScope = rememberCoroutineScope()
|
|
|
|
listDetailNavigator.navigateBack()
|
|
|
|
|
|
|
|
|
|
|
|
val paneExpansionState = rememberPaneExpansionState(
|
|
|
|
|
|
|
|
anchors = listOf(
|
|
|
|
|
|
|
|
PaneExpansionAnchor.Proportion(0f),
|
|
|
|
|
|
|
|
PaneExpansionAnchor.Proportion(0.5f),
|
|
|
|
|
|
|
|
PaneExpansionAnchor.Proportion(1f),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ThreePaneScaffoldPredictiveBackHandler(
|
|
|
|
|
|
|
|
listDetailNavigator,
|
|
|
|
|
|
|
|
BackNavigationBehavior.PopUntilScaffoldValueChange,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
BackHandler(
|
|
|
|
|
|
|
|
paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(0f) &&
|
|
|
|
|
|
|
|
listDetailNavigator.isListPaneVisible() &&
|
|
|
|
|
|
|
|
listDetailNavigator.isDetailPaneVisible(),
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
coroutineScope.launch {
|
|
|
|
|
|
|
|
paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(1f))
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var nestedNavHostStartRoute by remember {
|
|
|
|
var topicRoute by remember {
|
|
|
|
val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute
|
|
|
|
val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute
|
|
|
|
mutableStateOf(route)
|
|
|
|
mutableStateOf(route)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var nestedNavKey by rememberSaveable(
|
|
|
|
|
|
|
|
stateSaver = Saver({ it.toString() }, UUID::fromString),
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
mutableStateOf(UUID.randomUUID())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
val nestedNavController = key(nestedNavKey) {
|
|
|
|
|
|
|
|
rememberNavController()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fun onTopicClickShowDetailPane(topicId: String) {
|
|
|
|
fun onTopicClickShowDetailPane(topicId: String) {
|
|
|
|
onTopicClick(topicId)
|
|
|
|
onTopicClick(topicId)
|
|
|
|
if (listDetailNavigator.isDetailPaneVisible()) {
|
|
|
|
topicRoute = TopicRoute(id = topicId)
|
|
|
|
// If the detail pane was visible, then use the nestedNavController navigate call
|
|
|
|
coroutineScope.launch {
|
|
|
|
// directly
|
|
|
|
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
|
|
|
|
nestedNavController.navigateToTopic(topicId) {
|
|
|
|
}
|
|
|
|
popUpTo<DetailPaneNavHostRoute>()
|
|
|
|
if (paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(1f)) {
|
|
|
|
|
|
|
|
coroutineScope.launch {
|
|
|
|
|
|
|
|
paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(0f))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// Otherwise, recreate the NavHost entirely, and start at the new destination
|
|
|
|
|
|
|
|
nestedNavHostStartRoute = TopicRoute(id = topicId)
|
|
|
|
|
|
|
|
nestedNavKey = UUID.randomUUID()
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ListDetailPaneScaffold(
|
|
|
|
val mutableInteractionSource = remember { MutableInteractionSource() }
|
|
|
|
value = listDetailNavigator.scaffoldValue,
|
|
|
|
val minPaneWidth = 300.dp
|
|
|
|
directive = listDetailNavigator.scaffoldDirective,
|
|
|
|
|
|
|
|
|
|
|
|
NavigableListDetailPaneScaffold(
|
|
|
|
|
|
|
|
navigator = listDetailNavigator,
|
|
|
|
listPane = {
|
|
|
|
listPane = {
|
|
|
|
AnimatedPane {
|
|
|
|
AnimatedPane {
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
|
|
modifier = Modifier.clipToBounds()
|
|
|
|
|
|
|
|
.layout { measurable, constraints ->
|
|
|
|
|
|
|
|
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
|
|
|
|
|
|
|
|
val placeable = measurable.measure(
|
|
|
|
|
|
|
|
constraints.copy(
|
|
|
|
|
|
|
|
minWidth = minPaneWidth.roundToPx(),
|
|
|
|
|
|
|
|
maxWidth = width,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
layout(constraints.maxWidth, placeable.height) {
|
|
|
|
|
|
|
|
placeable.placeRelative(
|
|
|
|
|
|
|
|
x = 0,
|
|
|
|
|
|
|
|
y = 0,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
) {
|
|
|
|
InterestsRoute(
|
|
|
|
InterestsRoute(
|
|
|
|
onTopicClick = ::onTopicClickShowDetailPane,
|
|
|
|
onTopicClick = ::onTopicClickShowDetailPane,
|
|
|
|
highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
|
|
|
|
shouldHighlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
detailPane = {
|
|
|
|
detailPane = {
|
|
|
|
AnimatedPane {
|
|
|
|
AnimatedPane {
|
|
|
|
key(nestedNavKey) {
|
|
|
|
Box(
|
|
|
|
NavHost(
|
|
|
|
modifier = Modifier.clipToBounds()
|
|
|
|
navController = nestedNavController,
|
|
|
|
.layout { measurable, constraints ->
|
|
|
|
startDestination = nestedNavHostStartRoute,
|
|
|
|
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
|
|
|
|
route = DetailPaneNavHostRoute::class,
|
|
|
|
val placeable = measurable.measure(
|
|
|
|
|
|
|
|
constraints.copy(
|
|
|
|
|
|
|
|
minWidth = minPaneWidth.roundToPx(),
|
|
|
|
|
|
|
|
maxWidth = width,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
layout(constraints.maxWidth, placeable.height) {
|
|
|
|
|
|
|
|
placeable.placeRelative(
|
|
|
|
|
|
|
|
x = constraints.maxWidth -
|
|
|
|
|
|
|
|
max(constraints.maxWidth, placeable.width),
|
|
|
|
|
|
|
|
y = 0,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
topicScreen(
|
|
|
|
AnimatedContent(topicRoute) { route ->
|
|
|
|
|
|
|
|
when (route) {
|
|
|
|
|
|
|
|
is TopicRoute -> {
|
|
|
|
|
|
|
|
TopicScreen(
|
|
|
|
showBackButton = !listDetailNavigator.isListPaneVisible(),
|
|
|
|
showBackButton = !listDetailNavigator.isListPaneVisible(),
|
|
|
|
onBackClick = listDetailNavigator::navigateBack,
|
|
|
|
onBackClick = {
|
|
|
|
|
|
|
|
coroutineScope.launch {
|
|
|
|
|
|
|
|
listDetailNavigator.navigateBack()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
onTopicClick = ::onTopicClickShowDetailPane,
|
|
|
|
onTopicClick = ::onTopicClickShowDetailPane,
|
|
|
|
|
|
|
|
viewModel = hiltViewModel<TopicViewModel, TopicViewModel.Factory>(
|
|
|
|
|
|
|
|
key = route.id,
|
|
|
|
|
|
|
|
) { factory ->
|
|
|
|
|
|
|
|
factory.create(route.id)
|
|
|
|
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
composable<TopicPlaceholderRoute> {
|
|
|
|
}
|
|
|
|
|
|
|
|
is TopicPlaceholderRoute -> {
|
|
|
|
TopicDetailPlaceholder()
|
|
|
|
TopicDetailPlaceholder()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
paneExpansionState = paneExpansionState,
|
|
|
|
|
|
|
|
paneExpansionDragHandle = {
|
|
|
|
|
|
|
|
VerticalDragHandle(
|
|
|
|
|
|
|
|
modifier = Modifier.paneExpansionDraggable(
|
|
|
|
|
|
|
|
state = paneExpansionState,
|
|
|
|
|
|
|
|
minTouchTargetSize = LocalMinimumInteractiveComponentSize.current,
|
|
|
|
|
|
|
|
interactionSource = mutableInteractionSource,
|
|
|
|
|
|
|
|
semanticsProperties = paneExpansionState.defaultDragHandleSemantics(),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
interactionSource = mutableInteractionSource,
|
|
|
|
|
|
|
|
)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|