android, desktop: scrolling moves title to app bar (#4703)

* android, desktop: scrolling moves title to app bar

* one place should be without padding

* scroll related changes for both platforms

* adapt code to universal ColumnWithScrollBar

* show in center

* small adjustments

* new chat sheet fix

* divider + mix background color for desktop

* coerce

* different transition

* desktop title starts from left

* host starts from left too

* different coefficient

* settings title
This commit is contained in:
Stanislav Dmitrenko
2024-08-19 18:43:54 +00:00
committed by GitHub
parent 75a468434c
commit 885aa9cfa5
28 changed files with 583 additions and 418 deletions
@@ -77,6 +77,9 @@ actual fun windowOrientation(): WindowOrientation =
@Composable
actual fun windowWidth(): Dp = simplexWindowState.windowState.size.width
@Composable
actual fun windowHeight(): Dp = simplexWindowState.windowState.size.height
actual fun desktopExpandWindowToWidth(width: Dp) {
if (simplexWindowState.windowState.size.width >= width) return
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width)
@@ -13,16 +13,18 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.helpers.detectCursorMove
import chat.simplex.common.views.helpers.mixWith
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter
import kotlin.math.absoluteValue
@Composable
actual fun LazyColumnWithScrollBar(
modifier: Modifier,
state: LazyListState,
state: LazyListState?,
contentPadding: PaddingValues,
reverseLayout: Boolean,
verticalArrangement: Arrangement.Vertical,
@@ -30,44 +32,6 @@ actual fun LazyColumnWithScrollBar(
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
content: LazyListScope.() -> Unit
) {
if (appPlatform.isAndroid) {
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
} else {
val scope = rememberCoroutineScope()
val scrollBarAlpha = remember { Animatable(0f) }
val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) }
val scrollModifier = remember {
Modifier
.pointerInput(Unit) {
detectCursorMove {
scope.launch {
scrollBarAlpha.animateTo(1f)
}
scrollJob.value.cancel()
scrollJob.value = scope.launch {
delay(1000L)
scrollBarAlpha.animateTo(0f)
}
}
}
}
Box {
LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout)
}
}
}
}
@Composable
actual fun ColumnWithScrollBar(
modifier: Modifier,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
state: ScrollState,
content: @Composable ColumnScope.() -> Unit
) {
val scope = rememberCoroutineScope()
val scrollBarAlpha = remember { Animatable(0f) }
@@ -87,20 +51,95 @@ actual fun ColumnWithScrollBar(
}
}
}
Column(modifier.verticalScroll(state).then(scrollModifier), verticalArrangement, horizontalAlignment, content)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false)
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState()
val connection = LocalAppBarHandler.current?.connection
// When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state
// (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row)
val scrollBarDraggingState = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) {
connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) {
LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState)
}
}
}
@Composable
fun DesktopScrollBar(adapter: androidx.compose.foundation.v2.ScrollbarAdapter, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean) {
actual fun ColumnWithScrollBar(
modifier: Modifier,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
state: ScrollState?,
maxIntrinsicSize: Boolean,
content: @Composable() (ColumnScope.() -> Unit)
) {
val scope = rememberCoroutineScope()
val scrollBarAlpha = remember { Animatable(0f) }
val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) }
val scrollModifier = remember {
Modifier
.pointerInput(Unit) {
detectCursorMove {
scope.launch {
scrollBarAlpha.animateTo(1f)
}
scrollJob.value.cancel()
scrollJob.value = scope.launch {
delay(1000L)
scrollBarAlpha.animateTo(0f)
}
}
}
}
val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState()
val connection = LocalAppBarHandler.current?.connection
// When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state
// (exact scroll position is available but in Int, not Float)
val scrollBarDraggingState = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
snapshotFlow { state.value }
.collect { scrollPosition ->
val offset = connection?.appBarOffset
if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) {
connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) {
Column(
if (maxIntrinsicSize) {
modifier.verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier)
} else {
modifier.verticalScroll(state).then(scrollModifier)
},
verticalArrangement, horizontalAlignment, content)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState)
}
}
}
@Composable
fun DesktopScrollBar(adapter: androidx.compose.foundation.v2.ScrollbarAdapter, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean, updateDraggingState: MutableState<Boolean> = remember { mutableStateOf(false) }) {
val scope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }
val isHovered by interactionSource.collectIsHoveredAsState()
val isDragged by interactionSource.collectIsDraggedAsState()
LaunchedEffect(isHovered, isDragged) {
scrollJob.value.cancel()
updateDraggingState.value = isDragged
if (isHovered || isDragged) {
scrollBarAlpha.animateTo(1f)
} else {