Add MainContentLayoutData object and proper scaffolding directive.

This commit is contained in:
Alex Hart
2025-04-17 10:22:02 -03:00
committed by Cody Henthorne
parent 49853b2cca
commit c3d61bece1
3 changed files with 150 additions and 53 deletions

View File

@@ -28,10 +28,12 @@ import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -41,9 +43,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import androidx.fragment.app.DialogFragment
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState
@@ -83,6 +82,7 @@ import org.thoughtcrime.securesms.main.MainActivityListHostFragment
import org.thoughtcrime.securesms.main.MainBottomChrome
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
import org.thoughtcrime.securesms.main.MainBottomChromeState
import org.thoughtcrime.securesms.main.MainContentLayoutData
import org.thoughtcrime.securesms.main.MainMegaphoneState
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
@@ -231,28 +231,29 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
)
}
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>()
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val contentClip: Shape = remember(windowSizeClass) {
if (windowSizeClass.isExtended()) {
RoundedCornerShape(18.dp)
} else {
RectangleShape
}
}
LaunchedEffect(detailLocation) {
if (detailLocation is MainNavigationDetailLocation.Conversation) {
if (SignalStore.internal.largeScreenUi) {
scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation)
} else {
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
}
}
}
val contentLayoutData = MainContentLayoutData.rememberContentLayoutData()
MainContainer {
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
currentWindowAdaptiveInfo()
).copy(
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
)
)
LaunchedEffect(detailLocation) {
if (detailLocation is MainNavigationDetailLocation.Conversation) {
if (SignalStore.internal.largeScreenUi) {
scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation)
} else {
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
}
}
}
AppScaffold(
navigator = scaffoldNavigator,
bottomNavContent = {
@@ -280,9 +281,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
Column(
modifier = Modifier
.padding(start = contentLayoutData.listPaddingStart)
.fillMaxSize()
.background(listContainerColor)
.clip(contentClip)
.clip(contentLayoutData.shape)
) {
MainToolbar(
state = mainToolbarState,
@@ -316,13 +318,17 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
fragmentState = fragmentState,
arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." },
modifier = Modifier
.padding(end = contentLayoutData.detailPaddingEnd)
.clip(contentLayoutData.shape)
.background(color = MaterialTheme.colorScheme.surface)
.fillMaxSize()
.clip(contentClip)
)
}
}
}
},
paneExpansionDragHandle = if (contentLayoutData.hasDragHandle()) {
{ }
} else null
)
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.main
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.thoughtcrime.securesms.window.WindowSizeClass
/**
* Describes metrics for the content layout (list and detail) of the main screen.
*
* @param shape The shape of each of the list and detail fragments
* @param partitionWidth The width of the divider between list and detail
* @param listPaddingStart The padding between the list pane and the navigation rail
* @param detailPaddingEnd The padding at the end of the detail pane
*/
@Immutable
data class MainContentLayoutData(
val shape: Shape,
val partitionWidth: Dp,
val listPaddingStart: Dp,
val detailPaddingEnd: Dp
) {
private val extraPadding: Dp = partitionWidth + listPaddingStart + detailPaddingEnd
/**
* Whether or not the WindowSizeClass supports drag handles.
*/
@Composable
fun hasDragHandle(): Boolean {
return WindowSizeClass.rememberWindowSizeClass().isExtended()
}
/**
* Calculates the default preferred width
*/
@Composable
fun rememberDefaultPanePreferredWidth(maxWidth: Dp): Dp {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
return remember(maxWidth, windowSizeClass) {
when {
windowSizeClass.isCompact() -> maxWidth
windowSizeClass.isMedium() -> (maxWidth - extraPadding) / 2f
else -> 416.dp
}
}
}
companion object {
/**
* Uses the WindowSizeClass to build out a MainContentLayoutData.
*/
@Composable
fun rememberContentLayoutData(): MainContentLayoutData {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
return remember(windowSizeClass) {
MainContentLayoutData(
shape = when {
windowSizeClass.isCompact() -> RectangleShape
windowSizeClass.isMedium() -> RoundedCornerShape(18.dp)
else -> RoundedCornerShape(14.dp)
},
partitionWidth = when {
windowSizeClass.isCompact() -> 0.dp
windowSizeClass.isMedium() -> 13.dp
else -> 16.dp
},
listPaddingStart = when {
windowSizeClass.isCompact() -> 0.dp
windowSizeClass.isMedium() -> 12.dp
else -> 16.dp
},
detailPaddingEnd = when {
windowSizeClass.isCompact() -> 0.dp
windowSizeClass.isMedium() -> 12.dp
else -> 24.dp
}
)
}
}
}
}

View File

@@ -16,6 +16,10 @@ 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.PaneExpansionState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
@@ -28,6 +32,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.window.core.ExperimentalWindowCoreApi
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
@@ -57,7 +62,7 @@ enum class WindowSizeClass(
) {
COMPACT_PORTRAIT(Navigation.BAR),
COMPACT_LANDSCAPE(Navigation.BAR),
MEDIUM_PORTRAIT(Navigation.BAR),
MEDIUM_PORTRAIT(Navigation.RAIL),
MEDIUM_LANDSCAPE(Navigation.RAIL),
EXTENDED_PORTRAIT(Navigation.RAIL),
EXTENDED_LANDSCAPE(Navigation.RAIL);
@@ -160,6 +165,7 @@ fun AppScaffold(
detailContent: @Composable () -> Unit = {},
navRailContent: @Composable () -> Unit = {},
bottomNavContent: @Composable () -> Unit = {},
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
listContent: @Composable () -> Unit
) {
val isForcedCompact = WindowSizeClass.checkForcedCompact()
@@ -176,9 +182,10 @@ fun AppScaffold(
return
}
if (windowSizeClass.isMedium()) {
Row {
Box(modifier = Modifier.weight(1f)) {
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
AnimatedPane {
ListAndNavigation(
listContent = listContent,
navRailContent = navRailContent,
@@ -186,31 +193,15 @@ fun AppScaffold(
windowSizeClass = windowSizeClass
)
}
Box(modifier = Modifier.weight(1f)) {
},
detailPane = {
AnimatedPane {
detailContent()
}
}
} else {
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
AnimatedPane {
ListAndNavigation(
listContent = listContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass
)
}
},
detailPane = {
AnimatedPane {
detailContent()
}
}
)
}
},
paneExpansionDragHandle = paneExpansionDragHandle,
paneExpansionState = rememberPaneExpansionState()
)
}
@Composable
@@ -248,6 +239,13 @@ private fun ListAndNavigation(
private fun AppScaffoldPreview() {
Previews.Preview {
AppScaffold(
navigator = rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
currentWindowAdaptiveInfo()
).copy(
horizontalPartitionSpacerSize = 10.dp
)
),
listContent = {
Box(
contentAlignment = Alignment.Center,