Add support for resizing pane on main activity.

This commit is contained in:
Alex Hart
2025-08-22 09:00:29 -03:00
committed by Michelle Tang
parent 47fb0deca4
commit 114524adc6
3 changed files with 140 additions and 28 deletions

View File

@@ -25,6 +25,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
@@ -36,10 +37,8 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -145,8 +144,10 @@ import org.thoughtcrime.securesms.util.SplashScreenUtil
import org.thoughtcrime.securesms.util.TopToastPopup
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
import org.thoughtcrime.securesms.window.AppPaneDragHandle
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback {
@@ -318,9 +319,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
MainContainer {
val wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
val paneExpansionState = rememberPaneExpansionState()
val mutableInteractionSource = remember { MutableInteractionSource() }
AppScaffold(
navigator = wrappedNavigator,
paneExpansionState = paneExpansionState,
bottomNavContent = {
if (isNavigationVisible) {
Column(
@@ -379,6 +383,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.ARCHIVE -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
@@ -387,6 +392,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.CALLS -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
@@ -395,6 +401,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.STORIES -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
@@ -448,7 +455,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
},
paneExpansionDragHandle = if (contentLayoutData.hasDragHandle()) {
{ }
{
AppPaneDragHandle(
paneExpansionState = paneExpansionState,
mutableInteractionSource = mutableInteractionSource
)
}
} else null
)
}
@@ -488,14 +500,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
contentLayoutData: MainContentLayoutData,
maxWidth: Dp
): ThreePaneScaffoldNavigator<Any> {
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
)
val scaffoldNavigator = rememberAppScaffoldNavigator(
isSplitPane = windowSizeClass.isSplitPane(),
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
)
val coroutine = rememberCoroutineScope()
@@ -506,6 +514,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
is MainNavigationDetailLocation.Conversation -> {
startActivity(detailLocation.intent)
}
MainNavigationDetailLocation.Empty -> Unit
}
}
@@ -524,7 +533,9 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
val modifier = if (windowSizeClass.isSplitPane()) {
Modifier.systemBarsPadding().displayCutoutPadding()
Modifier
.systemBarsPadding()
.displayCutoutPadding()
} else {
Modifier
}

View File

@@ -42,7 +42,7 @@ data class MainContentLayoutData(
*/
@Composable
fun hasDragHandle(): Boolean {
return WindowSizeClass.rememberWindowSizeClass().isExtended()
return !WindowSizeClass.rememberWindowSizeClass().isCompact()
}
/**

View File

@@ -8,11 +8,15 @@ package org.thoughtcrime.securesms.window
import android.content.res.Configuration
import android.content.res.Resources
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
@@ -20,6 +24,7 @@ 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.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
@@ -28,11 +33,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layout
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.compose.ui.unit.dp
import androidx.window.core.ExperimentalWindowCoreApi
import androidx.window.core.layout.WindowHeightSizeClass
@@ -44,6 +52,7 @@ import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationState
import org.thoughtcrime.securesms.util.RemoteConfig
import java.lang.Integer.max
enum class Navigation {
RAIL,
@@ -169,6 +178,7 @@ enum class WindowSizeClass(
MEDIUM_LANDSCAPE
}
}
WindowWidthSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
else -> error("Unsupported.")
}
@@ -191,6 +201,7 @@ fun AppScaffold(
detailContent: @Composable () -> Unit = {},
navRailContent: @Composable () -> Unit = {},
bottomNavContent: @Composable () -> Unit = {},
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(),
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
listContent: @Composable () -> Unit
) {
@@ -208,25 +219,68 @@ fun AppScaffold(
return
}
val minPaneWidth = 300.dp
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
AnimatedPane {
ListAndNavigation(
listContent = listContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass
)
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
)
}
}
) {
ListAndNavigation(
listContent = listContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
windowSizeClass = windowSizeClass
)
}
}
},
detailPane = {
AnimatedPane {
detailContent()
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 = constraints.maxWidth -
max(constraints.maxWidth, placeable.width),
y = 0
)
}
}
) {
detailContent()
}
}
},
paneExpansionDragHandle = paneExpansionDragHandle,
paneExpansionState = rememberPaneExpansionState()
paneExpansionState = paneExpansionState
)
}
@@ -271,12 +325,10 @@ private fun AppScaffoldPreview() {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
AppScaffold(
navigator = rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
horizontalPartitionSpacerSize = 10.dp
)
navigator = rememberAppScaffoldNavigator(
isSplitPane = windowSizeClass.navigation != Navigation.BAR,
defaultPanePreferredWidth = 416.dp,
horizontalPartitionSpacerSize = 16.dp
),
listContent = {
Box(
@@ -316,7 +368,56 @@ private fun AppScaffoldPreview() {
state = MainNavigationState(),
onDestinationSelected = {}
)
},
paneExpansionState = rememberPaneExpansionState(),
paneExpansionDragHandle = {
AppPaneDragHandle(
paneExpansionState = it,
mutableInteractionSource = remember { MutableInteractionSource() }
)
}
)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldScope.AppPaneDragHandle(
paneExpansionState: PaneExpansionState,
mutableInteractionSource: MutableInteractionSource
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.paneExpansionDraggable(
state = paneExpansionState,
minTouchTargetSize = LocalMinimumInteractiveComponentSize.current,
interactionSource = mutableInteractionSource,
semanticsProperties = paneExpansionState.defaultDragHandleSemantics()
)
) {
Box(
modifier = Modifier
.size(4.dp, 48.dp)
.background(color = Color(0xFF605F5D), RoundedCornerShape(percent = 50))
)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun rememberAppScaffoldNavigator(
isSplitPane: Boolean,
horizontalPartitionSpacerSize: Dp,
defaultPanePreferredWidth: Dp
): ThreePaneScaffoldNavigator<Any> {
return rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
maxHorizontalPartitions = if (isSplitPane) 2 else 1,
horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
defaultPanePreferredWidth = defaultPanePreferredWidth
)
)
}