mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-05 23:24:38 +00:00
ideal adapting height layout
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ actual fun LazyColumnWithScrollBar(
|
||||
horizontalAlignment: Alignment.Horizontal,
|
||||
flingBehavior: FlingBehavior,
|
||||
userScrollEnabled: Boolean,
|
||||
additionalBarHeight: State<Dp>?,
|
||||
additionalBarOffset: State<Dp>?,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ expect fun LazyColumnWithScrollBar(
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
additionalBarHeight: State<Dp>? = null,
|
||||
additionalBarOffset: State<Dp>? = null,
|
||||
content: LazyListScope.() -> Unit
|
||||
)
|
||||
|
||||
|
||||
+43
-45
@@ -9,6 +9,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -23,10 +24,10 @@ import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID
|
||||
import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout
|
||||
import chat.simplex.common.views.chatlist.NavigationBarBackground
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -79,46 +80,46 @@ fun TerminalLayout(
|
||||
composeState.value = composeState.value.copy(message = s)
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
val fontSizeSqrtMultiplier = fontSizeSqrtMultiplier
|
||||
val composeViewHeight = remember { mutableStateOf(AppBarHeight * fontSizeSqrtMultiplier) }
|
||||
TerminalLog(floating, composeViewHeight)
|
||||
NavigationBarBackground(true)
|
||||
val density = LocalDensity.current
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
.background(MaterialTheme.colors.background.copy(remember { appPrefs.barsAlpha.state }.value))
|
||||
.onSizeChanged { composeViewHeight.value = with(density) { it.height.toDp() } }
|
||||
) {
|
||||
Divider()
|
||||
Box(Modifier.padding(horizontal = 8.dp)) {
|
||||
SendMsgView(
|
||||
composeState = composeState,
|
||||
showVoiceRecordIcon = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
sendMsgEnabled = true,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
placeholder = "",
|
||||
sendMessage = { sendCommand() },
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
editPrevMessage = {},
|
||||
onMessageChange = ::onMessageChange,
|
||||
onFilesPasted = {},
|
||||
textStyle = textStyle
|
||||
)
|
||||
val composeViewHeight = remember { mutableStateOf(0.dp) }
|
||||
AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) {
|
||||
TerminalLog(floating, composeViewHeight)
|
||||
Column(
|
||||
Modifier
|
||||
.layoutId(CONSOLE_COMPOSE_LAYOUT_ID)
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
.background(MaterialTheme.colors.background.copy(remember { appPrefs.barsAlpha.state }.value))
|
||||
) {
|
||||
Divider()
|
||||
Box(Modifier.padding(horizontal = 8.dp)) {
|
||||
SendMsgView(
|
||||
composeState = composeState,
|
||||
showVoiceRecordIcon = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
sendMsgEnabled = true,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
placeholder = "",
|
||||
sendMessage = { sendCommand() },
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
editPrevMessage = {},
|
||||
onMessageChange = ::onMessageChange,
|
||||
onFilesPasted = {},
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigationBarBackground(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +134,6 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
|
||||
launch {
|
||||
snapshotFlow { listState.layoutInfo.totalItemsCount }
|
||||
.filter { autoScrollToBottom }
|
||||
.onEach { delay(100) }
|
||||
.collect {
|
||||
try {
|
||||
listState.scrollToItem(0)
|
||||
@@ -144,21 +144,19 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
|
||||
}
|
||||
launch {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.onEach { delay(100) }
|
||||
.collect {
|
||||
autoScrollToBottom = it == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyColumnWithScrollBar (
|
||||
modifier = Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)).imePadding(),
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(
|
||||
top = topPaddingToContent(),
|
||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + composeViewHeight.value
|
||||
bottom = composeViewHeight.value
|
||||
),
|
||||
state = listState,
|
||||
additionalBarHeight = composeViewHeight
|
||||
additionalBarOffset = composeViewHeight
|
||||
) {
|
||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
+25
-21
@@ -654,7 +654,6 @@ fun ChatLayout(
|
||||
val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background)
|
||||
val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
||||
val composeViewHeight = remember { mutableStateOf(0.dp) }
|
||||
val composeHeight = composeViewHeight.value
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -663,24 +662,29 @@ fun ChatLayout(
|
||||
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
|
||||
else
|
||||
Modifier)
|
||||
// don't show initial frame of chat if height of compose area is not known yet. Otherwise, the view jumps
|
||||
.graphicsLayer { alpha = if (composeHeight > 0.dp) 1f else 0f },
|
||||
) {
|
||||
val remoteHostId = remember { remoteHostId }.value
|
||||
val chatInfo = remember { chatInfo }.value
|
||||
if (chatInfo != null) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, remember { {onComposed(it)} }, developerTools, showViaProxy,
|
||||
)
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
println("LALAL height ${composeViewHeight.value} ${AppBarHeight * fontSizeSqrtMultiplier}")
|
||||
Box(Modifier.align(Alignment.BottomCenter).imePadding().navigationBarsPadding().onSizeChanged { composeViewHeight.value = with(density) { it.height.toDp() } }) {
|
||||
composeView()
|
||||
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
|
||||
if (chatInfo != null) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(Modifier
|
||||
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding()
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
composeView()
|
||||
}
|
||||
}
|
||||
NavigationBarBackground(true)
|
||||
Column {
|
||||
@@ -993,14 +997,14 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
)
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)).imePadding(),
|
||||
Modifier.align(Alignment.BottomCenter),
|
||||
state = listState,
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(
|
||||
top = topPaddingToContent(),
|
||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + composeViewHeight.value
|
||||
bottom = composeViewHeight.value
|
||||
),
|
||||
additionalBarHeight = composeViewHeight
|
||||
additionalBarOffset = composeViewHeight
|
||||
) {
|
||||
itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
|
||||
println("LALAL RELOAD ITEM $i")
|
||||
@@ -1678,7 +1682,7 @@ private fun BoxScope.BottomEndFloatingButton(
|
||||
FloatingActionButton(
|
||||
onClick = onClickCounter,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).imePadding().navigationBarsPadding().size(48.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Text(
|
||||
@@ -1692,7 +1696,7 @@ private fun BoxScope.BottomEndFloatingButton(
|
||||
FloatingActionButton(
|
||||
onClick = onClickArrowDown,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).imePadding().navigationBarsPadding().size(48.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Icon(
|
||||
|
||||
+44
@@ -10,6 +10,7 @@ import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -320,6 +321,8 @@ fun CIMarkdownText(
|
||||
|
||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble"
|
||||
const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose"
|
||||
const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose"
|
||||
/**
|
||||
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
|
||||
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
|
||||
@@ -398,6 +401,47 @@ fun DependentLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect).
|
||||
// So, steps are:
|
||||
// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0)
|
||||
// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height
|
||||
// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size
|
||||
// - every added/removed line in compose view goes through the same process.
|
||||
@Composable
|
||||
fun AdaptingBottomPaddingLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
mainLayoutId: String,
|
||||
expectedHeight: MutableState<Dp>,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() }
|
||||
Layout(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
) { measureable, constraints ->
|
||||
require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" }
|
||||
val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints)
|
||||
val placeables: List<Placeable> = measureable.map {
|
||||
if (it.layoutId == mainLayoutId)
|
||||
mainPlaceable
|
||||
else
|
||||
it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) }
|
||||
expectedHeight.value = mainPlaceable.measuredHeight.toDp()
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
var y = 0
|
||||
placeables.forEach {
|
||||
if (it !== mainPlaceable) {
|
||||
it.place(0, y)
|
||||
y += it.measuredHeight
|
||||
} else {
|
||||
it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight)
|
||||
y += it.measuredHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
class EditedProvider: PreviewParameterProvider<Boolean> {
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ actual fun LazyColumnWithScrollBar(
|
||||
horizontalAlignment: Alignment.Horizontal,
|
||||
flingBehavior: FlingBehavior,
|
||||
userScrollEnabled: Boolean,
|
||||
additionalBarHeight: State<Dp>?,
|
||||
additionalBarOffset: State<Dp>?,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -87,7 +87,7 @@ actual fun LazyColumnWithScrollBar(
|
||||
}
|
||||
Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) {
|
||||
LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarHeight)
|
||||
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user