ideal adapting height layout

This commit is contained in:
Avently
2024-10-22 13:31:58 +07:00
parent d527ba3771
commit 4b985907a2
6 changed files with 116 additions and 70 deletions
@@ -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()
@@ -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
)
@@ -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
@@ -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(
@@ -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> {
@@ -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)
}
}