Rewrite bottom navigation in compose.

This commit is contained in:
Alex Hart
2025-03-24 14:34:27 -03:00
committed by Cody Henthorne
parent a9ed6b6154
commit 32b710a3ca
10 changed files with 669 additions and 690 deletions

View File

@@ -0,0 +1,382 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.main
import androidx.annotation.RawRes
import androidx.annotation.StringRes
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
import com.airbnb.lottie.compose.rememberLottieDynamicProperty
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
private val LOTTIE_SIZE = 28.dp
enum class MainNavigationDestination(
@StringRes val label: Int,
@RawRes val icon: Int,
@StringRes val contentDescription: Int = label
) {
CHATS(
label = R.string.ConversationListTabs__chats,
icon = R.raw.chats_28
),
CALLS(
label = R.string.ConversationListTabs__calls,
icon = R.raw.calls_28
),
STORIES(
label = R.string.ConversationListTabs__stories,
icon = R.raw.stories_28
)
}
data class MainNavigationState(
val chatsCount: Int = 0,
val callsCount: Int = 0,
val storiesCount: Int = 0,
val storyFailure: Boolean = false,
val selectedDestination: MainNavigationDestination = MainNavigationDestination.CHATS,
val compact: Boolean = false
)
/**
* Chats list bottom navigation bar.
*/
@Composable
fun MainNavigationBar(
state: MainNavigationState,
onDestinationSelected: (MainNavigationDestination) -> Unit
) {
NavigationBar(
containerColor = SignalTheme.colors.colorSurface2,
contentColor = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.height(if (state.compact) 48.dp else 80.dp)
) {
MainNavigationDestination.entries.forEach { destination ->
val badgeCount = when (destination) {
MainNavigationDestination.CHATS -> state.chatsCount
MainNavigationDestination.CALLS -> state.callsCount
MainNavigationDestination.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)
)
}
}
}
/**
* Draws badge over navigation bar item. We do this since they're required to be inside a row,
* and things get really funky or clip weird if we try to use a normal composable.
*/
@Composable
private fun Modifier.drawNavigationBarBadge(count: Int, compact: Boolean): Modifier {
return if (count <= 0) {
this
} else {
val formatted = formatCount(count)
val textMeasurer = rememberTextMeasurer()
val color = colorResource(R.color.ConversationListTabs__unread)
val textStyle = MaterialTheme.typography.labelMedium
val textLayoutResult = remember(formatted) {
textMeasurer.measure(formatted, textStyle)
}
var size by remember { mutableStateOf(IntSize.Zero) }
val padding = with(LocalDensity.current) {
4.dp.toPx()
}
val xOffsetExtra = with(LocalDensity.current) {
4.dp.toPx()
}
val yOffset = with(LocalDensity.current) {
if (compact) 6.dp.toPx() else 10.dp.toPx()
}
this
.onSizeChanged {
size = it
}
.drawWithContent {
drawContent()
val xOffset = size.width.toFloat() / 2f + xOffsetExtra
if (size != IntSize.Zero) {
drawRoundRect(
color = color,
topLeft = Offset(xOffset, yOffset),
size = Size(textLayoutResult.size.width.toFloat() + padding * 2, textLayoutResult.size.height.toFloat()),
cornerRadius = CornerRadius(20f, 20f)
)
drawText(
textLayoutResult = textLayoutResult,
color = Color.White,
topLeft = Offset(xOffset + padding, yOffset)
)
}
}
}
}
/**
* Navigation Rail for medium and large form factor devices.
*/
@Composable
fun MainNavigationRail(
state: MainNavigationState,
onDestinationSelected: (MainNavigationDestination) -> Unit
) {
NavigationRail(
containerColor = SignalTheme.colors.colorSurface1,
header = {
FilledTonalIconButton(
onClick = { },
shape = RoundedCornerShape(18.dp),
modifier = Modifier
.padding(top = 56.dp, bottom = 16.dp)
.size(56.dp),
enabled = true,
colors = IconButtonDefaults.filledTonalIconButtonColors()
.copy(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onBackground
)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_edit_24),
contentDescription = null
)
}
FilledTonalIconButton(
onClick = { },
shape = RoundedCornerShape(18.dp),
modifier = Modifier
.padding(bottom = 80.dp)
.size(56.dp),
enabled = true,
colors = IconButtonDefaults.filledTonalIconButtonColors()
.copy(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_camera_24),
contentDescription = null
)
}
}
) {
MainNavigationDestination.entries.forEachIndexed { idx, destination ->
val selected = state.selectedDestination == destination
Box {
NavigationRailItem(
modifier = Modifier.padding(bottom = if (MainNavigationDestination.entries.lastIndex == idx) 0.dp else 16.dp),
icon = {
NavigationDestinationIcon(
destination = destination,
selected = selected
)
},
label = {
NavigationDestinationLabel(destination)
},
selected = selected,
onClick = {
onDestinationSelected(destination)
}
)
NavigationRailCountIndicator(
state = state,
destination = destination
)
}
}
}
}
@Composable
private fun BoxScope.NavigationRailCountIndicator(
state: MainNavigationState,
destination: MainNavigationDestination
) {
val count = remember(state, destination) {
when (destination) {
MainNavigationDestination.CHATS -> state.chatsCount
MainNavigationDestination.CALLS -> state.callsCount
MainNavigationDestination.STORIES -> state.storiesCount
}
}
if (count > 0) {
Box(
modifier = Modifier
.padding(start = 42.dp)
.height(16.dp)
.defaultMinSize(minWidth = 16.dp)
.background(color = colorResource(R.color.ConversationListTabs__unread), shape = RoundedCornerShape(percent = 50))
.align(Alignment.TopStart)
) {
Text(
text = formatCount(count),
style = MaterialTheme.typography.labelMedium,
color = Color.White,
modifier = Modifier
.align(Alignment.Center)
.padding(horizontal = 4.dp)
)
}
}
}
@Composable
private fun NavigationDestinationIcon(
destination: MainNavigationDestination,
selected: Boolean
) {
val dynamicProperties = rememberLottieDynamicProperties(
rememberLottieDynamicProperty(
property = LottieProperty.COLOR_FILTER,
value = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
MaterialTheme.colorScheme.onSurface.hashCode(),
BlendModeCompat.SRC_ATOP
),
keyPath = arrayOf("**")
)
)
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(destination.icon))
val progress by animateFloatAsState(if (selected) 1f else 0f)
LottieAnimation(
composition = composition,
progress = { progress },
dynamicProperties = dynamicProperties,
modifier = Modifier.size(LOTTIE_SIZE)
)
}
@Composable
private fun NavigationDestinationLabel(destination: MainNavigationDestination) {
Text(stringResource(destination.label))
}
@Composable
private fun formatCount(count: Int): String {
if (count > 99) {
return stringResource(R.string.ConversationListTabs__99p)
}
return count.toString()
}
@SignalPreview
@Composable
private fun MainNavigationRailPreview() {
Previews.Preview {
var selected by remember { mutableStateOf(MainNavigationDestination.CHATS) }
MainNavigationRail(
state = MainNavigationState(
chatsCount = 500,
callsCount = 10,
storiesCount = 5,
selectedDestination = selected
),
onDestinationSelected = { selected = it }
)
}
}
@SignalPreview
@Composable
private fun MainNavigationBarPreview() {
Previews.Preview {
var selected by remember { mutableStateOf(MainNavigationDestination.CHATS) }
MainNavigationBar(
state = MainNavigationState(
chatsCount = 500,
callsCount = 10,
storiesCount = 5,
selectedDestination = selected,
compact = false
),
onDestinationSelected = { selected = it }
)
}
}

View File

@@ -1,247 +1,57 @@
package org.thoughtcrime.securesms.stories.tabs
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.view.animation.PathInterpolatorCompat
import androidx.fragment.app.Fragment
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rxjava3.subscribeAsState
import androidx.fragment.app.viewModels
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.dp
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.ConversationListTabsBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.main.MainNavigationState
/**
* Displays the "Chats" and "Stories" tab to a user.
*/
class ConversationListTabsFragment : Fragment(R.layout.conversation_list_tabs) {
class ConversationListTabsFragment : ComposeFragment() {
private val viewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
private val disposables: LifecycleDisposable = LifecycleDisposable()
private val binding by ViewBinderDelegate(ConversationListTabsBinding::bind)
private var shouldBeImmediate = true
private var pillAnimator: Animator? = null
private val largeConstraintSet: ConstraintSet = ConstraintSet()
private val smallConstraintSet: ConstraintSet = ConstraintSet()
@Composable
override fun FragmentContent() {
val state by viewModel.state.subscribeAsState(ConversationListTabsState())
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
disposables.bindTo(viewLifecycleOwner)
val iconTint = ContextCompat.getColor(requireContext(), R.color.signal_colorOnSecondaryContainer)
largeConstraintSet.clone(binding.root)
smallConstraintSet.clone(requireContext(), R.layout.conversation_list_tabs_small)
binding.chatsTabIcon.addValueCallback(
KeyPath("**"),
LottieProperty.COLOR
) { iconTint }
binding.callsTabIcon.addValueCallback(
KeyPath("**"),
LottieProperty.COLOR
) { iconTint }
binding.storiesTabIcon.addValueCallback(
KeyPath("**"),
LottieProperty.COLOR
) { iconTint }
view.findViewById<View>(R.id.chats_tab_touch_point).setOnClickListener {
viewModel.onChatsSelected()
val navState = remember(state) {
MainNavigationState(
chatsCount = state.unreadMessagesCount.toInt(),
callsCount = state.unreadCallsCount.toInt(),
storiesCount = state.unreadStoriesCount.toInt(),
storyFailure = state.hasFailedStory,
selectedDestination = when (state.tab) {
ConversationListTab.CHATS -> MainNavigationDestination.CHATS
ConversationListTab.CALLS -> MainNavigationDestination.CALLS
ConversationListTab.STORIES -> MainNavigationDestination.STORIES
},
compact = state.compact
)
}
view.findViewById<View>(R.id.calls_tab_touch_point).setOnClickListener {
viewModel.onCallsSelected()
}
view.findViewById<View>(R.id.stories_tab_touch_point).setOnClickListener {
viewModel.onStoriesSelected()
}
updateTabsVisibility()
disposables += viewModel.state.subscribeBy {
update(it, shouldBeImmediate)
shouldBeImmediate = false
if (state.visibilityState.isVisible()) {
MainNavigationBar(
state = navState,
onDestinationSelected = {
when (it) {
MainNavigationDestination.CHATS -> viewModel.onChatsSelected()
MainNavigationDestination.CALLS -> viewModel.onCallsSelected()
MainNavigationDestination.STORIES -> viewModel.onStoriesSelected()
}
}
)
}
}
override fun onResume() {
super.onResume()
updateTabsVisibility()
}
private fun updateTabsVisibility() {
if (SignalStore.settings.useCompactNavigationBar) {
smallConstraintSet.applyTo(binding.root)
binding.root.minHeight = 48.dp
} else {
largeConstraintSet.applyTo(binding.root)
binding.root.minHeight = 80.dp
}
listOf(
binding.callsPill,
binding.callsTabIcon,
binding.callsTabContainer,
binding.callsTabLabel,
binding.callsUnreadIndicator,
binding.callsTabTouchPoint
).forEach {
it.visible = true
}
listOf(
binding.storiesPill,
binding.storiesTabIcon,
binding.storiesTabContainer,
binding.storiesTabLabel,
binding.storiesUnreadIndicator,
binding.storiesTabTouchPoint
).forEach {
it.visible = Stories.isFeatureEnabled()
}
if (SignalStore.settings.useCompactNavigationBar) {
listOf(
binding.callsTabLabel,
binding.chatsTabLabel,
binding.storiesTabLabel
).forEach {
it.visible = false
}
}
update(viewModel.stateSnapshot, true)
}
private fun update(state: ConversationListTabsState, immediate: Boolean) {
binding.chatsTabIcon.isSelected = state.tab == ConversationListTab.CHATS
binding.chatsPill.isSelected = state.tab == ConversationListTab.CHATS
if (Stories.isFeatureEnabled()) {
binding.storiesTabIcon.isSelected = state.tab == ConversationListTab.STORIES
binding.storiesPill.isSelected = state.tab == ConversationListTab.STORIES
}
binding.callsTabIcon.isSelected = state.tab == ConversationListTab.CALLS
binding.callsPill.isSelected = state.tab == ConversationListTab.CALLS
val hasStateChange = state.tab != state.prevTab
if (immediate) {
binding.chatsTabIcon.pauseAnimation()
binding.chatsTabIcon.progress = if (state.tab == ConversationListTab.CHATS) 1f else 0f
if (Stories.isFeatureEnabled()) {
binding.storiesTabIcon.pauseAnimation()
binding.storiesTabIcon.progress = if (state.tab == ConversationListTab.STORIES) 1f else 0f
}
binding.callsTabIcon.pauseAnimation()
binding.callsTabIcon.progress = if (state.tab == ConversationListTab.CALLS) 1f else 0f
runPillAnimation(
0,
listOfNotNull(
binding.chatsPill,
binding.callsPill,
binding.storiesPill.takeIf { Stories.isFeatureEnabled() }
)
)
} else if (hasStateChange) {
runLottieAnimations(
listOfNotNull(
binding.chatsTabIcon,
binding.callsTabIcon,
binding.storiesTabIcon.takeIf { Stories.isFeatureEnabled() }
)
)
runPillAnimation(
150,
listOfNotNull(
binding.chatsPill,
binding.callsPill,
binding.storiesPill.takeIf { Stories.isFeatureEnabled() }
)
)
}
binding.chatsUnreadIndicator.visible = state.unreadMessagesCount > 0
binding.chatsUnreadIndicator.text = formatCount(state.unreadMessagesCount)
if (Stories.isFeatureEnabled()) {
binding.storiesUnreadIndicator.visible = state.unreadStoriesCount > 0 || state.hasFailedStory
binding.storiesUnreadIndicator.text = if (state.hasFailedStory) "!" else formatCount(state.unreadStoriesCount)
}
binding.callsUnreadIndicator.visible = state.unreadCallsCount > 0
binding.callsUnreadIndicator.text = formatCount(state.unreadCallsCount)
requireView().visible = state.visibilityState.isVisible()
}
private fun runLottieAnimations(toAnimate: List<LottieAnimationView>) {
toAnimate.forEach {
if (it.isSelected) {
it.resumeAnimation()
} else {
if (it.isAnimating) {
it.pauseAnimation()
}
it.progress = 0f
}
}
}
private fun runPillAnimation(duration: Long, toAnimate: List<ImageView>) {
val (selected, unselected) = toAnimate.partition { it.isSelected }
pillAnimator?.cancel()
pillAnimator = AnimatorSet().apply {
this.duration = duration
interpolator = PathInterpolatorCompat.create(0.17f, 0.17f, 0f, 1f)
playTogether(
selected.map { view ->
view.visibility = View.VISIBLE
ValueAnimator.ofInt(view.paddingLeft, 0).apply {
addUpdateListener {
view.setPadding(it.animatedValue as Int, 0, it.animatedValue as Int, 0)
}
}
}
)
start()
}
unselected.forEach {
val smallPad = DimensionUnit.DP.toPixels(16f).toInt()
it.setPadding(smallPad, 0, smallPad, 0)
it.visibility = View.INVISIBLE
}
}
private fun formatCount(count: Long): String {
if (count > 99L) {
return getString(R.string.ConversationListTabs__99p)
}
return count.toString()
viewModel.refreshNavigationBarState()
}
}

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.stories.tabs
import org.thoughtcrime.securesms.keyvalue.SignalStore
data class ConversationListTabsState(
val tab: ConversationListTab = ConversationListTab.CHATS,
val prevTab: ConversationListTab = if (tab == ConversationListTab.CHATS) ConversationListTab.STORIES else ConversationListTab.CHATS,
@@ -7,7 +9,8 @@ data class ConversationListTabsState(
val unreadCallsCount: Long = 0L,
val unreadStoriesCount: Long = 0L,
val hasFailedStory: Boolean = false,
val visibilityState: VisibilityState = VisibilityState()
val visibilityState: VisibilityState = VisibilityState(),
val compact: Boolean = SignalStore.settings.useCompactNavigationBar
) {
data class VisibilityState(
val isSearchOpen: Boolean = false,

View File

@@ -11,6 +11,7 @@ import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.rx.RxStore
@@ -48,6 +49,10 @@ class ConversationListTabsViewModel(startingTab: ConversationListTab, repository
}
}
fun refreshNavigationBarState() {
store.update { it.copy(compact = SignalStore.settings.useCompactNavigationBar) }
}
override fun onCleared() {
disposables.clear()
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.window
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationState
enum class Navigation {
RAIL,
BAR
}
/**
* Describes the size of screen we are displaying, and what components should be displayed.
*
* https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes
*/
enum class WindowSizeClass(
val navigation: Navigation
) {
COMPACT_PORTRAIT(Navigation.BAR),
COMPACT_LANDSCAPE(Navigation.BAR),
MEDIUM_PORTRAIT(Navigation.BAR),
MEDIUM_LANDSCAPE(Navigation.RAIL),
EXTENDED_PORTRAIT(Navigation.RAIL),
EXTENDED_LANDSCAPE(Navigation.RAIL);
companion object {
@Composable
fun rememberWindowSizeClass(): WindowSizeClass {
val wsc = currentWindowAdaptiveInfo().windowSizeClass
val orientation = LocalConfiguration.current.orientation
return remember(orientation, wsc) {
when (orientation) {
Configuration.ORIENTATION_PORTRAIT, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_SQUARE -> {
when (wsc.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> COMPACT_PORTRAIT
WindowWidthSizeClass.MEDIUM -> MEDIUM_PORTRAIT
WindowWidthSizeClass.EXPANDED -> EXTENDED_PORTRAIT
else -> error("Unsupported.")
}
}
Configuration.ORIENTATION_LANDSCAPE -> {
when (wsc.windowHeightSizeClass) {
WindowHeightSizeClass.COMPACT -> COMPACT_LANDSCAPE
WindowHeightSizeClass.MEDIUM -> MEDIUM_LANDSCAPE
WindowHeightSizeClass.EXPANDED -> EXTENDED_LANDSCAPE
else -> error("Unsupported.")
}
}
else -> error("Unexpected orientation: $orientation")
}
}
}
}
}
/**
* Composable who's precise layout will depend on the window size class of the device it is being utilized on.
* This is built to be generic so that we can use it throughout the application to support different device classes.
*/
@Composable
fun AppScaffold(
listContent: @Composable () -> Unit,
detailContent: @Composable () -> Unit,
navRailContent: @Composable () -> Unit,
bottomNavContent: @Composable () -> Unit
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
Row {
if (windowSizeClass.navigation == Navigation.RAIL) {
navRailContent()
}
BoxWithConstraints(
modifier = Modifier.weight(1f)
) {
val listWidth = when (windowSizeClass) {
WindowSizeClass.COMPACT_PORTRAIT -> maxWidth
WindowSizeClass.COMPACT_LANDSCAPE -> maxWidth
WindowSizeClass.MEDIUM_PORTRAIT -> maxWidth * 0.5f
WindowSizeClass.MEDIUM_LANDSCAPE -> 360.dp
WindowSizeClass.EXTENDED_PORTRAIT -> 360.dp
WindowSizeClass.EXTENDED_LANDSCAPE -> 360.dp
}
val detailWidth = maxWidth - listWidth
Row {
Column(
modifier = Modifier.width(listWidth)
) {
Box(modifier = Modifier.weight(1f)) {
listContent()
}
if (windowSizeClass.navigation == Navigation.BAR) {
bottomNavContent()
}
}
if (detailWidth > 0.dp) {
// TODO -- slider to divide sizing?
Box(modifier = Modifier.width(detailWidth)) {
detailContent()
}
}
}
}
}
}
@Preview(device = "spec:width=360dp,height=640dp,orientation=portrait")
@Preview(device = "spec:width=640dp,height=360dp,orientation=landscape")
@Preview(device = "spec:width=600dp,height=1024dp,orientation=portrait")
@Preview(device = "spec:width=1024dp,height=600dp,orientation=landscape")
@Preview(device = "spec:width=840dp,height=1280dp,orientation=portrait")
@Preview(device = "spec:width=1280dp,height=840dp,orientation=landscape")
@Composable
private fun AppScaffoldPreview() {
Previews.Preview {
AppScaffold(
listContent = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(color = Color.Red)
) {
Text(
text = "ListContent",
textAlign = TextAlign.Center
)
}
},
detailContent = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(color = Color.Blue)
) {
Text(
text = "DetailContent",
textAlign = TextAlign.Center
)
}
},
navRailContent = {
MainNavigationRail(
state = MainNavigationState(),
onDestinationSelected = {}
)
},
bottomNavContent = {
MainNavigationBar(
state = MainNavigationState(),
onDestinationSelected = {}
)
}
)
}
}

View File

@@ -1,230 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/signal_colorSurface2"
android:minHeight="80dp">
<View
android:id="@+id/chats_tab_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/calls_tab_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/calls_tab_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/stories_tab_container"
app:layout_constraintStart_toEndOf="@id/chats_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/stories_tab_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/calls_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/chats_tab_touch_point"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/chats_tab_container"
app:layout_constraintStart_toStartOf="@id/chats_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/calls_tab_touch_point"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/calls_tab_container"
app:layout_constraintStart_toStartOf="@id/calls_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/stories_tab_touch_point"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/stories_tab_container"
app:layout_constraintStart_toStartOf="@id/stories_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/chats_pill"
android:layout_width="64dp"
android:layout_height="32dp"
android:importantForAccessibility="no"
android:src="@drawable/conversation_tab_icon_background"
app:layout_constraintBottom_toBottomOf="@id/chats_tab_icon"
app:layout_constraintEnd_toEndOf="@id/chats_tab_icon"
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
app:layout_constraintTop_toTopOf="@id/chats_tab_icon" />
<ImageView
android:id="@+id/calls_pill"
android:layout_width="64dp"
android:layout_height="32dp"
android:importantForAccessibility="no"
android:src="@drawable/conversation_tab_icon_background"
app:layout_constraintBottom_toBottomOf="@id/calls_tab_icon"
app:layout_constraintEnd_toEndOf="@id/calls_tab_icon"
app:layout_constraintStart_toStartOf="@id/calls_tab_icon"
app:layout_constraintTop_toTopOf="@id/calls_tab_icon" />
<ImageView
android:id="@+id/stories_pill"
android:layout_width="64dp"
android:layout_height="32dp"
android:importantForAccessibility="no"
android:paddingHorizontal="16dp"
android:src="@drawable/conversation_tab_icon_background"
app:layout_constraintBottom_toBottomOf="@id/stories_tab_icon"
app:layout_constraintEnd_toEndOf="@id/stories_tab_icon"
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
app:layout_constraintTop_toTopOf="@id/stories_tab_icon" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/chats_tab_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/chats_tab_label"
app:layout_constraintEnd_toEndOf="@id/chats_tab_touch_point"
app:layout_constraintStart_toStartOf="@id/chats_tab_touch_point"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:lottie_rawRes="@raw/chats_28" />
<TextView
android:id="@+id/chats_tab_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ConversationListTabs__chats"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_colorOnBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/chats_tab_icon"
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
app:layout_constraintTop_toBottomOf="@id/chats_tab_icon" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/calls_tab_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/calls_tab_label"
app:layout_constraintEnd_toEndOf="@id/calls_tab_touch_point"
app:layout_constraintStart_toStartOf="@id/calls_tab_touch_point"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:lottie_rawRes="@raw/calls_28" />
<TextView
android:id="@+id/calls_tab_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ConversationListTabs__calls"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_colorOnBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/calls_tab_icon"
app:layout_constraintStart_toStartOf="@id/calls_tab_icon"
app:layout_constraintTop_toBottomOf="@id/calls_tab_icon" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/stories_tab_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/stories_tab_label"
app:layout_constraintEnd_toEndOf="@id/stories_tab_touch_point"
app:layout_constraintStart_toStartOf="@id/stories_tab_touch_point"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:lottie_rawRes="@raw/stories_28" />
<TextView
android:id="@+id/stories_tab_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ConversationListTabs__stories"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_colorOnBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/stories_tab_icon"
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
app:layout_constraintTop_toBottomOf="@id/stories_tab_icon" />
<TextView
android:id="@+id/chats_unread_indicator"
android:layout_width="wrap_content"
android:layout_height="16sp"
android:layout_marginStart="18dp"
android:layout_marginBottom="14dp"
android:background="@drawable/tab_unread_circle"
android:gravity="center_horizontal"
android:minWidth="16sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/white"
android:textSize="11sp"
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
app:layout_constraintBottom_toBottomOf="@id/chats_tab_icon"
tools:text="3" />
<TextView
android:id="@+id/calls_unread_indicator"
android:layout_width="wrap_content"
android:layout_height="16sp"
android:layout_marginStart="18dp"
android:layout_marginBottom="14dp"
android:background="@drawable/tab_unread_circle"
android:gravity="center_horizontal"
android:minWidth="16sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/white"
android:textSize="11sp"
app:layout_constraintStart_toStartOf="@id/calls_tab_icon"
app:layout_constraintBottom_toBottomOf="@+id/calls_tab_icon"
tools:text="3" />
<TextView
android:id="@+id/stories_unread_indicator"
android:layout_width="wrap_content"
android:layout_height="16sp"
android:layout_marginStart="18dp"
android:layout_marginBottom="14dp"
android:background="@drawable/tab_unread_circle"
android:gravity="center_horizontal"
android:minWidth="16sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/white"
android:textSize="11sp"
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
app:layout_constraintBottom_toBottomOf="@+id/stories_tab_icon"
tools:text="99+" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,232 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/signal_colorSurface2"
android:minHeight="48dp">
<View
android:id="@+id/chats_tab_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/calls_tab_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/calls_tab_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/stories_tab_container"
app:layout_constraintStart_toEndOf="@id/chats_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/stories_tab_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/calls_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/chats_tab_touch_point"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/chats_tab_container"
app:layout_constraintStart_toStartOf="@id/chats_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/calls_tab_touch_point"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/calls_tab_container"
app:layout_constraintStart_toStartOf="@id/calls_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/stories_tab_touch_point"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/stories_tab_container"
app:layout_constraintStart_toStartOf="@id/stories_tab_container"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/chats_pill"
android:layout_width="64dp"
android:layout_height="30dp"
android:importantForAccessibility="no"
android:src="@drawable/conversation_tab_icon_background"
app:layout_constraintBottom_toBottomOf="@id/chats_tab_icon"
app:layout_constraintEnd_toEndOf="@id/chats_tab_icon"
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
app:layout_constraintTop_toTopOf="@id/chats_tab_icon" />
<ImageView
android:id="@+id/calls_pill"
android:layout_width="64dp"
android:layout_height="30dp"
android:importantForAccessibility="no"
android:src="@drawable/conversation_tab_icon_background"
app:layout_constraintBottom_toBottomOf="@id/calls_tab_icon"
app:layout_constraintEnd_toEndOf="@id/calls_tab_icon"
app:layout_constraintStart_toStartOf="@id/calls_tab_icon"
app:layout_constraintTop_toTopOf="@id/calls_tab_icon" />
<ImageView
android:id="@+id/stories_pill"
android:layout_width="64dp"
android:layout_height="30dp"
android:importantForAccessibility="no"
android:src="@drawable/conversation_tab_icon_background"
app:layout_constraintBottom_toBottomOf="@id/stories_tab_icon"
app:layout_constraintEnd_toEndOf="@id/stories_tab_icon"
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
app:layout_constraintTop_toTopOf="@id/stories_tab_icon" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/chats_tab_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/chats_tab_label"
app:layout_constraintEnd_toEndOf="@id/chats_tab_touch_point"
app:layout_constraintStart_toStartOf="@id/chats_tab_touch_point"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:lottie_rawRes="@raw/chats_28" />
<TextView
android:id="@+id/chats_tab_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ConversationListTabs__chats"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_colorOnBackground"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/chats_tab_icon"
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
app:layout_constraintTop_toBottomOf="@id/chats_tab_icon" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/calls_tab_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/calls_tab_label"
app:layout_constraintEnd_toEndOf="@id/calls_tab_touch_point"
app:layout_constraintStart_toStartOf="@id/calls_tab_touch_point"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:lottie_rawRes="@raw/calls_28" />
<TextView
android:id="@+id/calls_tab_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ConversationListTabs__calls"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_colorOnBackground"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/calls_tab_icon"
app:layout_constraintStart_toStartOf="@id/calls_tab_icon"
app:layout_constraintTop_toBottomOf="@id/calls_tab_icon" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/stories_tab_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/stories_tab_label"
app:layout_constraintEnd_toEndOf="@id/stories_tab_touch_point"
app:layout_constraintStart_toStartOf="@id/stories_tab_touch_point"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:lottie_rawRes="@raw/stories_28" />
<TextView
android:id="@+id/stories_tab_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ConversationListTabs__stories"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_colorOnBackground"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/stories_tab_icon"
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
app:layout_constraintTop_toBottomOf="@id/stories_tab_icon" />
<TextView
android:id="@+id/chats_unread_indicator"
android:layout_width="wrap_content"
android:layout_height="16sp"
android:layout_marginStart="18dp"
android:layout_marginBottom="14dp"
android:background="@drawable/tab_unread_circle"
android:gravity="center_horizontal"
android:minWidth="16sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/white"
android:textSize="11sp"
app:layout_constraintStart_toStartOf="@id/chats_tab_icon"
app:layout_constraintBottom_toBottomOf="@id/chats_tab_icon"
tools:text="3" />
<TextView
android:id="@+id/calls_unread_indicator"
android:layout_width="wrap_content"
android:layout_height="16sp"
android:layout_marginStart="18dp"
android:layout_marginBottom="14dp"
android:background="@drawable/tab_unread_circle"
android:gravity="center_horizontal"
android:minWidth="16sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/white"
android:textSize="11sp"
app:layout_constraintStart_toStartOf="@id/calls_tab_icon"
app:layout_constraintBottom_toBottomOf="@id/calls_tab_icon"
tools:text="3" />
<TextView
android:id="@+id/stories_unread_indicator"
android:layout_width="wrap_content"
android:layout_height="16sp"
android:layout_marginStart="18dp"
android:layout_marginBottom="14dp"
android:background="@drawable/tab_unread_circle"
android:gravity="center_horizontal"
android:minWidth="16sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/white"
android:textSize="11sp"
app:layout_constraintStart_toStartOf="@id/stories_tab_icon"
app:layout_constraintBottom_toBottomOf="@id/stories_tab_icon"
tools:text="99+" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -24,6 +24,7 @@ dependencies {
}
api(libs.androidx.compose.material3)
api(libs.androidx.compose.material3.adaptive)
api(libs.androidx.compose.ui.tooling.preview)
debugApi(libs.androidx.compose.ui.tooling.core)
}

View File

@@ -41,6 +41,7 @@ androidx-benchmark-gradle-plugin = "androidx.benchmark:benchmark-gradle-plugin:1
# Compose
androidx-compose-bom = "androidx.compose:compose-bom:2024.12.01"
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive"}
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-ui-tooling-core = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }

View File

@@ -698,6 +698,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="89a673451d542de4819c0da6c0c680f5cc15d93e234eb67a3feed3a1a774d348" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material3.adaptive" name="adaptive" version="1.0.0">
<artifact name="adaptive-1.0.0.module">
<sha256 value="f7c0e6c85e7499bd4dc16050783b68acf5c946d127687fa61e0c490885b09142" origin="Generated by Gradle"/>
</artifact>
<artifact name="adaptive-metadata-1.0.0.jar">
<sha256 value="4d0d435b144606c9431d844993ae8e7fdeaa3b49b917382fa07add3e7e29aebb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material3.adaptive" name="adaptive-android" version="1.0.0">
<artifact name="adaptive-android-1.0.0.module">
<sha256 value="f2a0e2be8c962b7e8944627fa075a09a46fca7e7a0c367643c4889be6435cf0a" origin="Generated by Gradle"/>
</artifact>
<artifact name="adaptive-release.aar">
<sha256 value="d1a8c84e6947220f3d1843a725460d7d16a8c05ddba6168193455771a9eb96d0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material3.adaptive" name="adaptive-desktop" version="1.0.0">
<artifact name="adaptive-desktop-1.0.0.jar">
<sha256 value="1857cd739d45cb4ae5a9d79653285cf125068db5dfcbd7f2bf6963f208a8769b" origin="Generated by Gradle"/>
</artifact>
<artifact name="adaptive-desktop-1.0.0.module">
<sha256 value="f9cffc9236455e3cb5986297c1781883d2c354a19b123b77ac2f452903e4f308" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
@@ -2741,6 +2765,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8bdd866119755df849bb7710161437e836888c3aadb18b884f49b67b254434bd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.window" name="window-core" version="1.3.0">
<artifact name="window-core-1.3.0.module">
<sha256 value="7410a607fc8cc30af9b8fb0ae0783063d24c0e5575bbc105b938ebd26fbb387f" origin="Generated by Gradle"/>
</artifact>
<artifact name="window-core-metadata-1.3.0.jar">
<sha256 value="3cc622558f9fe0dce8af12224f3ec921df332f67a416714bb66aea7faa6b21dc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.window" name="window-core-android" version="1.3.0">
<artifact name="window-core-android-1.3.0.module">
<sha256 value="479f25789d4ed1ab41e29af4dd31c52b6c684c1f04aec06981e64a3c39543cf6" origin="Generated by Gradle"/>
</artifact>
<artifact name="window-core-release.aar">
<sha256 value="7e0a44287cc647b2c8580d4ed72a9a511adddd9dc8387fd202127a9ea6862bf7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.window" name="window-core-jvm" version="1.3.0">
<artifact name="window-core-jvm-1.3.0.jar">
<sha256 value="12faee8e75da9e25ad2afdf0e733f348530ec38f7bb6edcf3fbb5c31257a83f6" origin="Generated by Gradle"/>
</artifact>
<artifact name="window-core-jvm-1.3.0.module">
<sha256 value="81403484e926aac83e86d39641d1082756c052319d750badc53d3dda5ea55ec6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.window" name="window-java" version="1.3.0">
<artifact name="window-java-1.3.0.aar">
<sha256 value="d41b0e3e17efe85b426069a19dab0777ac24222edebced1d6747b2ff2e123c61" origin="Generated by Gradle"/>