import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap
import kotlin.math.abs
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LazyImeColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
includeNavigationBarsHeight: Boolean = false,
content: LazyListScope.() -> Unit
) {
val density = LocalDensity.current
val navigationBarHeight = WindowInsets.navigationBars.getBottom(density)
val imeTargetOffset = WindowInsets.imeAnimationTarget.getBottom(density)
var imeHeight by remember { mutableIntStateOf(0) }
var lastScrollPosition by remember { mutableStateOf(Pair(0, 0)) }
var maxLastItemOffset by remember { mutableIntStateOf(0) }
LazyColumn(
modifier = modifier,
state = state,
contentPadding = contentPadding,
flingBehavior = flingBehavior,
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
reverseLayout = reverseLayout,
userScrollEnabled = userScrollEnabled,
content = content
)
LaunchedEffect(state.canScrollForward) {
if (!state.canScrollForward)
maxLastItemOffset = maxOf(maxLastItemOffset, abs(state.firstVisibleItemScrollOffset))
}
LaunchedEffect(imeTargetOffset) {
if (state.layoutInfo.totalItemsCount > 0) {
when (imeTargetOffset) {
0 -> {
if (state.canScrollForward) {
// I don't know how to deal with the situation that is very close to the bottom of the LazyColumn, but not more than one IME height from the bottom of the LazyColumn
// Because in this case, if you scroll an IME height, it will deviate from the original position.
state.animateScrollBy(
value = -imeHeight.toFloat() + when (includeNavigationBarsHeight) {
true -> navigationBarHeight.toFloat()
else -> 0f
},
animationSpec = tween(250)
)
}
}
else -> {
imeHeight = imeTargetOffset
lastScrollPosition = Pair(state.firstVisibleItemIndex, state.firstVisibleItemScrollOffset)
state.animateScrollBy(
value = imeTargetOffset.toFloat() - when (includeNavigationBarsHeight) {
true -> navigationBarHeight.toFloat()
else -> 0f
},
animationSpec = tween(300)
)
}
}
}
}
}
Last active
June 19, 2024 16:20
-
-
Save whitescent/72f9b403540b1daaeb0752b4795abb3b to your computer and use it in GitHub Desktop.
LazyColumn IME animation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment