Fix lazy item animator target calculation when moving away

This change ensures items that are moving away towards the
end bound use the end of the last visible item as the
starting point of their target position calculation during
the lookahead pass.

The previous impl did not account of the size of the last
visible item. As a result, the animation may animate towards
the wrong direction when the last visible item is much larger
than the disappearing item.

Test: new test added
Change-Id: Iab83183122fd9185c630859d72df5b452c30ece6
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index 34cd31b..f266963 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -80,6 +80,7 @@
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.findRootCoordinates
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
@@ -110,11 +111,10 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
-import androidx.test.espresso.action.ViewActions.swipeLeft
-import androidx.test.espresso.action.ViewActions.swipeUp
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.collect.Range
@@ -2023,6 +2023,79 @@
     }
 
     @Test
+    fun testLookaheadItemPlacementAnimatorTarget() {
+        var mutableSize by mutableStateOf(80)
+        var lastItemOffset by mutableStateOf(Offset.Zero)
+        val initialSize = IntSize(200, 200)
+        val largerCrossAxisSize = if (vertical) IntSize(300, 200) else IntSize(200, 300)
+        var containerSize by mutableStateOf(initialSize)
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadScope {
+                    LazyColumnOrRow(
+                        modifier =
+                            Modifier.requiredSize(containerSize.width.dp, containerSize.height.dp),
+                        beyondBoundsItemCount = 1
+                    ) {
+                        item { // item 0
+                            Box(Modifier.requiredSize(40.dp))
+                        }
+                        item { // item 1. Will change size from 80.dp to 160.dp
+                            Box(Modifier.requiredSize(mutableSize.dp))
+                        }
+                        item { // item 2
+                            Box(Modifier.requiredSize(40.dp))
+                        }
+                        item { // item 3
+                            Box(
+                                Modifier.animateItem(
+                                        fadeInSpec = null,
+                                        fadeOutSpec = null,
+                                        placementSpec = tween(160, easing = LinearEasing)
+                                    )
+                                    .onGloballyPositioned { lastItemOffset = it.positionInRoot() }
+                                    .requiredSize(80.dp)
+                            )
+                        }
+                        item { // item 4
+                            Box(Modifier.requiredSize(1.dp))
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.mainClock.autoAdvance = false
+
+        containerSize = largerCrossAxisSize
+        mutableSize = 160
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+
+        containerSize = initialSize
+        rule.waitForIdle()
+
+        // Expect last item to move from 160 to 240 within 10 frames
+        while (lastItemOffset.mainAxisPosition == 160) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        repeat(9) {
+            val expected = (it + 1) * (240 - 160) / 10 + 160
+            if (expected <= 200) { // within the viewport
+                assertEquals((it + 1) * 8 + 160, lastItemOffset.mainAxisPosition)
+            } else {
+                // Once the item moves out of the viewport, we don't enforce the exact offset
+                assertTrue(lastItemOffset.mainAxisPosition >= 200)
+            }
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @Test
     fun testLookaheadPositionWithTwoInBoundTwoOutBound() {
         testLookaheadPositionWithPlacementAnimator(
             initialList = listOf(0, 1, 2, 3, 4, 5),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
index 482ae1f..5893f00 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
@@ -315,10 +315,14 @@
                 val itemInfo = keyToItemInfoMap[item.key]!!
                 val accumulatedOffset = accumulatedOffsetPerLane.updateAndReturnOffsetFor(item)
                 val mainAxisOffset =
-                    if (isLookingAhead) positionedItems.last().mainAxisOffset
-                    else {
-                        itemInfo.layoutMaxOffset - item.mainAxisSizeWithSpacings
-                    } + accumulatedOffset
+                    if (isLookingAhead) {
+                        // Position the moving away items starting from the end of the last
+                        // visible item.
+                        val lastVisibleItem = positionedItems.last()
+                        lastVisibleItem.mainAxisOffset + lastVisibleItem.mainAxisSizeWithSpacings
+                    } else {
+                        itemInfo.layoutMaxOffset
+                    } - item.mainAxisSizeWithSpacings + accumulatedOffset
 
                 item.position(
                     mainAxisOffset = mainAxisOffset,