Skip to content

Instantly share code, notes, and snippets.

@whitescent
Last active June 19, 2024 16:20
Show Gist options
  • Save whitescent/72f9b403540b1daaeb0752b4795abb3b to your computer and use it in GitHub Desktop.
Save whitescent/72f9b403540b1daaeb0752b4795abb3b to your computer and use it in GitHub Desktop.
LazyColumn IME animation
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)
          )
        }
      }
    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment