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.Column
import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize 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.MaterialTheme
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi 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.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
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.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -41,9 +43,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.app.DialogFragment
import androidx.fragment.compose.AndroidFragment import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState 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.MainBottomChrome
import org.thoughtcrime.securesms.main.MainBottomChromeCallback import org.thoughtcrime.securesms.main.MainBottomChromeCallback
import org.thoughtcrime.securesms.main.MainBottomChromeState import org.thoughtcrime.securesms.main.MainBottomChromeState
import org.thoughtcrime.securesms.main.MainContentLayoutData
import org.thoughtcrime.securesms.main.MainMegaphoneState import org.thoughtcrime.securesms.main.MainMegaphoneState
import org.thoughtcrime.securesms.main.MainNavigationBar import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
@@ -231,28 +231,29 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
) )
} }
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>()
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass() val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val contentLayoutData = MainContentLayoutData.rememberContentLayoutData()
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)
}
}
}
MainContainer { 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( AppScaffold(
navigator = scaffoldNavigator, navigator = scaffoldNavigator,
bottomNavContent = { bottomNavContent = {
@@ -280,9 +281,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
Column( Column(
modifier = Modifier modifier = Modifier
.padding(start = contentLayoutData.listPaddingStart)
.fillMaxSize() .fillMaxSize()
.background(listContainerColor) .background(listContainerColor)
.clip(contentClip) .clip(contentLayoutData.shape)
) { ) {
MainToolbar( MainToolbar(
state = mainToolbarState, state = mainToolbarState,
@@ -316,13 +318,17 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
fragmentState = fragmentState, fragmentState = fragmentState,
arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." }, arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." },
modifier = Modifier modifier = Modifier
.padding(end = contentLayoutData.detailPaddingEnd)
.clip(contentLayoutData.shape)
.background(color = MaterialTheme.colorScheme.surface) .background(color = MaterialTheme.colorScheme.surface)
.fillMaxSize() .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.ExperimentalMaterial3AdaptiveApi
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.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.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator 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.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.window.core.ExperimentalWindowCoreApi import androidx.window.core.ExperimentalWindowCoreApi
import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass import androidx.window.core.layout.WindowWidthSizeClass
@@ -57,7 +62,7 @@ enum class WindowSizeClass(
) { ) {
COMPACT_PORTRAIT(Navigation.BAR), COMPACT_PORTRAIT(Navigation.BAR),
COMPACT_LANDSCAPE(Navigation.BAR), COMPACT_LANDSCAPE(Navigation.BAR),
MEDIUM_PORTRAIT(Navigation.BAR), MEDIUM_PORTRAIT(Navigation.RAIL),
MEDIUM_LANDSCAPE(Navigation.RAIL), MEDIUM_LANDSCAPE(Navigation.RAIL),
EXTENDED_PORTRAIT(Navigation.RAIL), EXTENDED_PORTRAIT(Navigation.RAIL),
EXTENDED_LANDSCAPE(Navigation.RAIL); EXTENDED_LANDSCAPE(Navigation.RAIL);
@@ -160,6 +165,7 @@ fun AppScaffold(
detailContent: @Composable () -> Unit = {}, detailContent: @Composable () -> Unit = {},
navRailContent: @Composable () -> Unit = {}, navRailContent: @Composable () -> Unit = {},
bottomNavContent: @Composable () -> Unit = {}, bottomNavContent: @Composable () -> Unit = {},
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
listContent: @Composable () -> Unit listContent: @Composable () -> Unit
) { ) {
val isForcedCompact = WindowSizeClass.checkForcedCompact() val isForcedCompact = WindowSizeClass.checkForcedCompact()
@@ -176,9 +182,10 @@ fun AppScaffold(
return return
} }
if (windowSizeClass.isMedium()) { NavigableListDetailPaneScaffold(
Row { navigator = navigator,
Box(modifier = Modifier.weight(1f)) { listPane = {
AnimatedPane {
ListAndNavigation( ListAndNavigation(
listContent = listContent, listContent = listContent,
navRailContent = navRailContent, navRailContent = navRailContent,
@@ -186,31 +193,15 @@ fun AppScaffold(
windowSizeClass = windowSizeClass windowSizeClass = windowSizeClass
) )
} }
},
Box(modifier = Modifier.weight(1f)) { detailPane = {
AnimatedPane {
detailContent() detailContent()
} }
} },
} else { paneExpansionDragHandle = paneExpansionDragHandle,
NavigableListDetailPaneScaffold( paneExpansionState = rememberPaneExpansionState()
navigator = navigator, )
listPane = {
AnimatedPane {
ListAndNavigation(
listContent = listContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass
)
}
},
detailPane = {
AnimatedPane {
detailContent()
}
}
)
}
} }
@Composable @Composable
@@ -248,6 +239,13 @@ private fun ListAndNavigation(
private fun AppScaffoldPreview() { private fun AppScaffoldPreview() {
Previews.Preview { Previews.Preview {
AppScaffold( AppScaffold(
navigator = rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
currentWindowAdaptiveInfo()
).copy(
horizontalPartitionSpacerSize = 10.dp
)
),
listContent = { listContent = {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,