mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Dynamic split pane support via internal setting.
This commit is contained in:
committed by
Cody Henthorne
parent
2cfe321274
commit
893725e304
@@ -29,11 +29,12 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
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.layout.calculatePaneScaffoldDirective
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -93,6 +94,7 @@ import org.thoughtcrime.securesms.main.MainToolbar
|
||||
import org.thoughtcrime.securesms.main.MainToolbarCallback
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat
|
||||
import org.thoughtcrime.securesms.main.SnackbarState
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
@@ -236,9 +238,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
MainContainer {
|
||||
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
currentWindowAdaptiveInfo()
|
||||
).copy(
|
||||
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
|
||||
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
|
||||
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
)
|
||||
@@ -258,10 +261,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
navigator = scaffoldNavigator,
|
||||
bottomNavContent = {
|
||||
if (isNavigationVisible) {
|
||||
MainNavigationBar(
|
||||
state = mainNavigationState,
|
||||
onDestinationSelected = mainNavigationCallback
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(contentLayoutData.navigationBarShape)
|
||||
.background(color = SignalTheme.colors.colorSurface2)
|
||||
) {
|
||||
MainNavigationBar(
|
||||
state = mainNavigationState,
|
||||
onDestinationSelected = mainNavigationCallback
|
||||
)
|
||||
|
||||
if (!windowSizeClass.isSplitPane()) {
|
||||
NavigationBarSpacerCompat()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
navRailContent = {
|
||||
@@ -360,11 +373,6 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
@Composable
|
||||
private fun MainContainer(content: @Composable BoxWithConstraintsScope.() -> Unit) {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
val modifier = if (windowSizeClass.isLandscape()) {
|
||||
Modifier.displayCutoutPadding()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(this)) {
|
||||
val backgroundColor = if (windowSizeClass.isCompact()) {
|
||||
@@ -373,6 +381,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
SignalTheme.colors.colorSurface1
|
||||
}
|
||||
|
||||
val modifier = if (windowSizeClass.isSplitPane()) {
|
||||
Modifier.systemBarsPadding().displayCutoutPadding()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.background(color = backgroundColor)
|
||||
|
||||
@@ -174,6 +174,16 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
isEnabled = state.largeScreenUi,
|
||||
title = DSLSettingsText.from("Force split pane UI on landscape phones."),
|
||||
summary = DSLSettingsText.from("This setting requires split pane UI to be enabled."),
|
||||
isChecked = state.forceSplitPaneOnCompactLandscape,
|
||||
onClick = {
|
||||
viewModel.setForceSplitPaneOnCompactLandscape(!state.forceSplitPaneOnCompactLandscape)
|
||||
}
|
||||
)
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from("Playgrounds"))
|
||||
|
||||
clickPref(
|
||||
|
||||
@@ -26,5 +26,6 @@ data class InternalSettingsState(
|
||||
val hasPendingOneTimeDonation: Boolean,
|
||||
val hevcEncoding: Boolean,
|
||||
val newCallingUi: Boolean,
|
||||
val largeScreenUi: Boolean
|
||||
val largeScreenUi: Boolean,
|
||||
val forceSplitPaneOnCompactLandscape: Boolean
|
||||
)
|
||||
|
||||
@@ -167,7 +167,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
hasPendingOneTimeDonation = SignalStore.inAppPayments.getPendingOneTimeDonation() != null,
|
||||
hevcEncoding = SignalStore.internal.hevcEncoding,
|
||||
newCallingUi = SignalStore.internal.newCallingUi,
|
||||
largeScreenUi = SignalStore.internal.largeScreenUi
|
||||
largeScreenUi = SignalStore.internal.largeScreenUi,
|
||||
forceSplitPaneOnCompactLandscape = SignalStore.internal.forceSplitPaneOnCompactLandscape
|
||||
)
|
||||
|
||||
fun onClearOnboardingState() {
|
||||
@@ -188,6 +189,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setForceSplitPaneOnCompactLandscape(forceSplitPaneOnCompactLandscape: Boolean) {
|
||||
SignalStore.internal.forceSplitPaneOnCompactLandscape = forceSplitPaneOnCompactLandscape
|
||||
refresh()
|
||||
}
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
|
||||
|
||||
@@ -590,7 +590,7 @@ class ConversationFragment :
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.toolbar.isBackInvokedCallbackEnabled = false
|
||||
|
||||
binding.root.setUseWindowTypes(resources.getWindowSizeClass().isCompact())
|
||||
binding.root.setUseWindowTypes(!resources.getWindowSizeClass().isSplitPane())
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
@@ -1370,9 +1370,7 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
private fun presentNavigationIconForNormal() {
|
||||
val windowSizeClass = resources.getWindowSizeClass()
|
||||
|
||||
if (windowSizeClass.isCompact()) {
|
||||
if (!resources.getWindowSizeClass().isSplitPane()) {
|
||||
binding.toolbar.setNavigationIcon(R.drawable.ic_arrow_left_24)
|
||||
binding.toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
|
||||
@@ -29,6 +29,7 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
|
||||
const val ENCODE_HEVC: String = "internal.hevc_encoding"
|
||||
const val NEW_CALL_UI: String = "internal.new.call.ui"
|
||||
const val LARGE_SCREEN_UI: String = "internal.large.screen.ui"
|
||||
const val FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE: String = "internal.force.split.pane.on.compact.landscape.ui"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() = Unit
|
||||
@@ -40,6 +41,11 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
|
||||
*/
|
||||
var largeScreenUi by booleanValue(LARGE_SCREEN_UI, false).defaultForExternalUsers()
|
||||
|
||||
/**
|
||||
* Force split-pane mode on compact landscape
|
||||
*/
|
||||
var forceSplitPaneOnCompactLandscape by booleanValue(FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE, false).defaultForExternalUsers()
|
||||
|
||||
/**
|
||||
* Members will not be added directly to a GV2 even if they could be.
|
||||
*/
|
||||
|
||||
@@ -15,10 +15,14 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
|
||||
private val MEDIUM_CONTENT_CORNERS = 18.dp
|
||||
private val EXTENDED_CONTENT_CORNERS = 14.dp
|
||||
|
||||
/**
|
||||
* 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 shape The clipping shape of each of the list and detail fragments
|
||||
* @param navigationBarShape The clipping shape applied to the navigation bar, if present.
|
||||
* @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
|
||||
@@ -26,6 +30,7 @@ import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
@Immutable
|
||||
data class MainContentLayoutData(
|
||||
val shape: Shape,
|
||||
val navigationBarShape: Shape,
|
||||
val partitionWidth: Dp,
|
||||
val listPaddingStart: Dp,
|
||||
val detailPaddingEnd: Dp
|
||||
@@ -49,9 +54,9 @@ data class MainContentLayoutData(
|
||||
|
||||
return remember(maxWidth, windowSizeClass) {
|
||||
when {
|
||||
windowSizeClass.isCompact() -> maxWidth
|
||||
windowSizeClass.isMedium() -> (maxWidth - extraPadding) / 2f
|
||||
else -> 416.dp
|
||||
!windowSizeClass.isSplitPane() -> maxWidth
|
||||
windowSizeClass.isExtended() -> 416.dp
|
||||
else -> (maxWidth - extraPadding) / 2f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,24 +72,29 @@ data class MainContentLayoutData(
|
||||
return remember(windowSizeClass) {
|
||||
MainContentLayoutData(
|
||||
shape = when {
|
||||
windowSizeClass.isCompact() -> RectangleShape
|
||||
windowSizeClass.isMedium() -> RoundedCornerShape(18.dp)
|
||||
else -> RoundedCornerShape(14.dp)
|
||||
!windowSizeClass.isSplitPane() -> RectangleShape
|
||||
windowSizeClass.isExtended() -> RoundedCornerShape(EXTENDED_CONTENT_CORNERS)
|
||||
else -> RoundedCornerShape(MEDIUM_CONTENT_CORNERS)
|
||||
},
|
||||
navigationBarShape = when {
|
||||
!windowSizeClass.isSplitPane() -> RectangleShape
|
||||
windowSizeClass.isExtended() -> RoundedCornerShape(0.dp, 0.dp, EXTENDED_CONTENT_CORNERS, EXTENDED_CONTENT_CORNERS)
|
||||
else -> RoundedCornerShape(0.dp, 0.dp, MEDIUM_CONTENT_CORNERS, MEDIUM_CONTENT_CORNERS)
|
||||
},
|
||||
partitionWidth = when {
|
||||
windowSizeClass.isCompact() -> 0.dp
|
||||
windowSizeClass.isMedium() -> 13.dp
|
||||
else -> 16.dp
|
||||
!windowSizeClass.isSplitPane() -> 0.dp
|
||||
windowSizeClass.isExtended() -> 16.dp
|
||||
else -> 13.dp
|
||||
},
|
||||
listPaddingStart = when {
|
||||
windowSizeClass.isCompact() -> 0.dp
|
||||
windowSizeClass.isMedium() -> 12.dp
|
||||
else -> 16.dp
|
||||
!windowSizeClass.isSplitPane() -> 0.dp
|
||||
windowSizeClass.isExtended() -> 16.dp
|
||||
else -> 12.dp
|
||||
},
|
||||
detailPaddingEnd = when {
|
||||
windowSizeClass.isCompact() -> 0.dp
|
||||
windowSizeClass.isMedium() -> 12.dp
|
||||
else -> 24.dp
|
||||
!windowSizeClass.isSplitPane() -> 0.dp
|
||||
windowSizeClass.isExtended() -> 24.dp
|
||||
else -> 12.dp
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -97,50 +96,46 @@ fun MainNavigationBar(
|
||||
state: MainNavigationState,
|
||||
onDestinationSelected: (MainNavigationListLocation) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.background(color = SignalTheme.colors.colorSurface2)) {
|
||||
NavigationBar(
|
||||
containerColor = SignalTheme.colors.colorSurface2,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.height(if (state.compact) 48.dp else 80.dp),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) {
|
||||
val entries = remember(state.isStoriesFeatureEnabled) {
|
||||
if (state.isStoriesFeatureEnabled) {
|
||||
MainNavigationListLocation.entries
|
||||
} else {
|
||||
MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES }
|
||||
}
|
||||
}
|
||||
|
||||
entries.forEach { destination ->
|
||||
|
||||
val badgeCount = when (destination) {
|
||||
MainNavigationListLocation.CHATS -> state.chatsCount
|
||||
MainNavigationListLocation.CALLS -> state.callsCount
|
||||
MainNavigationListLocation.STORIES -> state.storiesCount
|
||||
}
|
||||
|
||||
val selected = state.selectedDestination == destination
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
icon = {
|
||||
NavigationDestinationIcon(
|
||||
destination = destination,
|
||||
selected = selected
|
||||
)
|
||||
},
|
||||
label = if (state.compact) null else {
|
||||
{ NavigationDestinationLabel(destination) }
|
||||
},
|
||||
onClick = {
|
||||
onDestinationSelected(destination)
|
||||
},
|
||||
modifier = Modifier.drawNavigationBarBadge(count = badgeCount, compact = state.compact)
|
||||
)
|
||||
NavigationBar(
|
||||
containerColor = SignalTheme.colors.colorSurface2,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.height(if (state.compact) 48.dp else 80.dp),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) {
|
||||
val entries = remember(state.isStoriesFeatureEnabled) {
|
||||
if (state.isStoriesFeatureEnabled) {
|
||||
MainNavigationListLocation.entries
|
||||
} else {
|
||||
MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES }
|
||||
}
|
||||
}
|
||||
|
||||
NavigationBarSpacerCompat()
|
||||
entries.forEach { destination ->
|
||||
|
||||
val badgeCount = when (destination) {
|
||||
MainNavigationListLocation.CHATS -> state.chatsCount
|
||||
MainNavigationListLocation.CALLS -> state.callsCount
|
||||
MainNavigationListLocation.STORIES -> state.storiesCount
|
||||
}
|
||||
|
||||
val selected = state.selectedDestination == destination
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
icon = {
|
||||
NavigationDestinationIcon(
|
||||
destination = destination,
|
||||
selected = selected
|
||||
)
|
||||
},
|
||||
label = if (state.compact) null else {
|
||||
{ NavigationDestinationLabel(destination) }
|
||||
},
|
||||
onClick = {
|
||||
onDestinationSelected(destination)
|
||||
},
|
||||
modifier = Modifier.drawNavigationBarBadge(count = badgeCount, compact = state.compact)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.background
|
||||
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.material3.Text
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
@@ -18,7 +19,7 @@ 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.calculatePaneScaffoldDirective
|
||||
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
|
||||
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
@@ -83,6 +84,14 @@ enum class WindowSizeClass(
|
||||
|
||||
fun isLandscape(): Boolean = this == COMPACT_LANDSCAPE || this == MEDIUM_LANDSCAPE || this == EXTENDED_LANDSCAPE
|
||||
|
||||
fun isSplitPane(): Boolean {
|
||||
return if (SignalStore.internal.largeScreenUi && SignalStore.internal.forceSplitPaneOnCompactLandscape) {
|
||||
this != COMPACT_PORTRAIT
|
||||
} else {
|
||||
this.navigation != Navigation.BAR
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@OptIn(ExperimentalWindowCoreApi::class)
|
||||
fun Resources.getWindowSizeClass(): WindowSizeClass {
|
||||
@@ -221,7 +230,11 @@ private fun ListAndNavigation(
|
||||
bottomNavContent: @Composable () -> Unit,
|
||||
windowSizeClass: WindowSizeClass
|
||||
) {
|
||||
Row {
|
||||
Row(
|
||||
modifier = if (windowSizeClass.isLandscape()) {
|
||||
Modifier.displayCutoutPadding()
|
||||
} else Modifier
|
||||
) {
|
||||
if (windowSizeClass.navigation == Navigation.RAIL) {
|
||||
navRailContent()
|
||||
}
|
||||
@@ -250,7 +263,7 @@ private fun AppScaffoldPreview() {
|
||||
Previews.Preview {
|
||||
AppScaffold(
|
||||
navigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
currentWindowAdaptiveInfo()
|
||||
).copy(
|
||||
horizontalPartitionSpacerSize = 10.dp
|
||||
|
||||
Reference in New Issue
Block a user