Take RV padding and item decorations into account

RecyclerView's padding and item decorations shift the position of the
current page when idle. The padding also shrinks the page size.

Bug: 139012032
Test: ./gradlew viewpager2:cC

Change-Id: Ia03646db95bb8d392408bce4d22430981c3f9171
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
index 8185e8b..00719bf 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
@@ -39,6 +39,7 @@
 import androidx.test.espresso.action.ViewActions.actionWithAssertions
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
+import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.espresso.matcher.ViewMatchers.withText
@@ -492,7 +493,7 @@
         )
         assertThat("viewPager should be IDLE", viewPager.scrollState, equalTo(SCROLL_STATE_IDLE))
         if (value != null) {
-            onView(allOf<View>(withId(R.id.text_view), isDisplayed())).check(
+            onView(allOf<View>(withId(R.id.text_view), isCompletelyDisplayed())).check(
                 matches(withText(value))
             )
         }
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PaddingMarginDecorationTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PaddingMarginDecorationTest.kt
new file mode 100644
index 0000000..473935e
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PaddingMarginDecorationTest.kt
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.viewpager2.widget
+
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.filters.LargeTest
+import androidx.testutils.LocaleTestUtils
+import androidx.viewpager2.widget.PaddingMarginDecorationTest.Event.OnPageScrollStateChangedEvent
+import androidx.viewpager2.widget.PaddingMarginDecorationTest.Event.OnPageScrolledEvent
+import androidx.viewpager2.widget.PaddingMarginDecorationTest.Event.OnPageSelectedEvent
+import androidx.viewpager2.widget.PaddingMarginDecorationTest.TestConfig
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL
+import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING
+import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE
+import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_SETTLING
+import androidx.viewpager2.widget.swipe.ViewAdapter
+import org.hamcrest.CoreMatchers.equalTo
+import org.junit.Assert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit.SECONDS
+import kotlin.math.roundToInt
+
+@RunWith(Parameterized::class)
+@LargeTest
+class PaddingMarginDecorationTest(private val config: TestConfig) : BaseTest() {
+    data class TestConfig(
+        @ViewPager2.Orientation val orientation: Int,
+        val rtl: Boolean,
+        val vpPaddingPx: Int,
+        val rvPaddingPx: Int,
+        val itemMarginPx: Int,
+        val itemDecorationPx: Int
+    )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun spec(): List<TestConfig> = createTestSet()
+
+        // Set unequal decorations, to prevent symmetry from hiding bugs
+        // Similarly, make sure no margin is an exact multiple of another margin
+        // TODO(139452422): Set to 2/3/7/5 when PagerSnapHelper is fixed
+        const val fLeft = 2
+        const val fTop = 2
+        const val fRight = 2
+        const val fBottom = 2
+
+        fun View.applyMargin(margin: Int) {
+            val lp = layoutParams as MarginLayoutParams
+            lp.setMargins(margin * fLeft, margin * fTop, margin * fRight, margin * fBottom)
+            layoutParams = lp
+        }
+
+        fun View.applyPadding(padding: Int) {
+            setPadding(padding * fLeft, padding * fTop, padding * fRight, padding * fBottom)
+        }
+    }
+
+    private lateinit var test: Context
+    private val viewPager get() = test.viewPager
+
+    private val vpSize: Int get() {
+        return if (viewPager.isHorizontal) viewPager.width else viewPager.height
+    }
+
+    private val vpPadding: Int get() {
+        return if (viewPager.isHorizontal)
+            viewPager.paddingLeft + viewPager.paddingRight
+        else
+            viewPager.paddingTop + viewPager.paddingBottom
+    }
+
+    private val rvSize: Int get() {
+        val rv = viewPager.recyclerView
+        return if (viewPager.isHorizontal) rv.width else rv.height
+    }
+
+    private val rvMargin: Int get() {
+        return if (viewPager.isHorizontal)
+            horizontalMargin(viewPager.recyclerView.layoutParams)
+        else
+            verticalMargin(viewPager.recyclerView.layoutParams)
+    }
+
+    private val rvPadding: Int get() {
+        val rv = viewPager.recyclerView
+        return if (viewPager.isHorizontal)
+            rv.paddingLeft + rv.paddingRight
+        else
+            rv.paddingTop + rv.paddingBottom
+    }
+
+    private val itemSize: Int get() {
+        val item = viewPager.linearLayoutManager.findViewByPosition(0)!!
+        return if (viewPager.isHorizontal) item.width else item.height
+    }
+
+    private val itemMargin: Int get() {
+        val item = viewPager.linearLayoutManager.findViewByPosition(0)!!
+        return if (viewPager.isHorizontal)
+            horizontalMargin(item.layoutParams)
+        else
+            verticalMargin(item.layoutParams)
+    }
+
+    private val itemDecoration: Int get() {
+        val llm = viewPager.linearLayoutManager
+        val item = llm.findViewByPosition(0)!!
+        return if (viewPager.isHorizontal)
+            llm.getLeftDecorationWidth(item) + llm.getRightDecorationWidth(item)
+        else
+            llm.getTopDecorationHeight(item) + llm.getBottomDecorationHeight(item)
+    }
+
+    private val adapterProvider: AdapterProviderForItems get() {
+        return if (config.itemMarginPx > 0) {
+            { items -> { MarginViewAdapter(config.itemMarginPx, items) } }
+        } else {
+            { items -> { ViewAdapter(items) } }
+        }
+    }
+
+    class MarginViewAdapter(private val margin: Int, items: List<String>) : ViewAdapter(items) {
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+            return super.onCreateViewHolder(parent, viewType).apply { itemView.applyMargin(margin) }
+        }
+    }
+
+    class ItemDecorator(private val size: Int) : RecyclerView.ItemDecoration() {
+        override fun getItemOffsets(
+            outRect: Rect,
+            view: View,
+            parent: RecyclerView,
+            state: RecyclerView.State
+        ) {
+            outRect.left = size * fLeft
+            outRect.top = size * fTop
+            outRect.right = size * fRight
+            outRect.bottom = size * fBottom
+        }
+    }
+
+    override fun setUp() {
+        super.setUp()
+        if (config.rtl) {
+            localeUtil.resetLocale()
+            localeUtil.setLocale(LocaleTestUtils.RTL_LANGUAGE)
+        }
+        test = setUpTest(config.orientation)
+        test.runOnUiThreadSync {
+            viewPager.clipToPadding = false
+            viewPager.applyPadding(config.vpPaddingPx)
+            viewPager.recyclerView.clipToPadding = false
+            viewPager.recyclerView.applyPadding(config.rvPaddingPx)
+            viewPager.addItemDecoration(ItemDecorator(config.itemDecorationPx))
+        }
+    }
+
+    private fun horizontalMargin(lp: ViewGroup.LayoutParams): Int {
+        return if (lp is MarginLayoutParams) lp.leftMargin + lp.rightMargin else 0
+    }
+
+    private fun verticalMargin(lp: ViewGroup.LayoutParams): Int {
+        return if (lp is MarginLayoutParams) lp.topMargin + lp.bottomMargin else 0
+    }
+
+    @Test
+    fun test_pageSize() {
+        test.setAdapterSync(adapterProvider(stringSequence(1)))
+
+        val f = if (viewPager.isHorizontal) fLeft + fRight else fTop + fBottom
+
+        assertThat(vpPadding, equalTo(config.vpPaddingPx * f))
+        assertThat(rvPadding, equalTo(config.rvPaddingPx * f))
+        assertThat(itemMargin, equalTo(config.itemMarginPx * f))
+        assertThat(itemDecoration, equalTo(config.itemDecorationPx * f))
+
+        assertThat(viewPager.pageSize, equalTo(rvSize - rvPadding))
+        assertThat(viewPager.pageSize, equalTo(vpSize - vpPadding - rvMargin - rvPadding))
+        assertThat(viewPager.pageSize, equalTo(itemSize + itemDecoration + itemMargin))
+    }
+
+    /*
+    Sample log to guide the test
+
+    1 -> 2
+    onPageScrollStateChanged,1
+    onPageScrolled,1,0.019444,21
+    onPageScrolled,1,0.082407,88
+    onPageScrolled,1,0.173148,187
+    onPageScrollStateChanged,2
+    onPageSelected,2
+    onPageScrolled,1,0.343518,370
+    onPageScrolled,1,0.855556,924
+    onPageScrolled,1,0.984259,1063
+    onPageScrolled,2,0.000000,0
+    onPageScrollStateChanged,0
+
+    2 -> 1
+    onPageScrollStateChanged,1
+    onPageScrolled,1,0.972222,1050
+    onPageScrolled,1,0.910185,983
+    onPageScrolled,1,0.835185,902
+    onPageScrolled,1,0.764815,826
+    onPageScrollStateChanged,2
+    onPageSelected,1
+    onPageScrolled,1,0.616667,666
+    onPageScrolled,1,0.136111,147
+    onPageScrolled,1,0.015741,17
+    onPageScrolled,1,0.000000,0
+    onPageScrollStateChanged,0
+     */
+    @Test
+    fun test_swipeBetweenPages() {
+        test.setAdapterSync(adapterProvider(stringSequence(2)))
+        listOf(1, 0).forEach { targetPage ->
+            // given
+            val initialPage = viewPager.currentItem
+            assertThat(Math.abs(initialPage - targetPage), equalTo(1))
+
+            val callback = viewPager.addNewRecordingCallback()
+            val latch = viewPager.addWaitForScrolledLatch(targetPage)
+
+            // when
+            test.swipe(initialPage, targetPage)
+            latch.await(2, SECONDS)
+
+            // then
+            test.assertBasicState(targetPage)
+
+            callback.apply {
+                // verify all events
+                assertThat(draggingIx, equalTo(0))
+                assertThat(settlingIx, isBetweenInEx(firstScrolledIx + 1, lastScrolledIx))
+                assertThat(idleIx, equalTo(lastIx))
+                assertThat(pageSelectedIx(targetPage), equalTo(settlingIx + 1))
+                assertThat(scrollEventCount, equalTo(eventCount - 4))
+
+                // dive into scroll events
+                val sortOrder =
+                        if (targetPage - initialPage > 0) SortOrder.ASC
+                        else SortOrder.DESC
+                scrollEvents.assertPositionSorted(sortOrder)
+                scrollEvents.assertOffsetSorted(sortOrder)
+                scrollEvents.assertValueSanity(initialPage, targetPage, viewPager.pageSize)
+                scrollEvents.assertLastCorrect(targetPage)
+                scrollEvents.assertMaxShownPages()
+            }
+
+            viewPager.unregisterOnPageChangeCallback(callback)
+        }
+    }
+
+    /*
+    Before page 0
+    onPageScrollStateChanged,1
+    onPageScrolled,0,0.000000,0
+    onPageScrolled,0,0.000000,0
+    onPageScrolled,0,0.000000,0
+    onPageScrolled,0,0.000000,0
+    onPageScrollStateChanged,0
+
+    After page 2
+    onPageScrollStateChanged,1
+    onPageScrolled,2,0.000000,0
+    onPageScrolled,2,0.000000,0
+    onPageScrolled,2,0.000000,0
+    onPageScrollStateChanged,0
+     */
+    @Test
+    fun test_swipeBeyondEdgePages() {
+        val totalPages = 2
+        val edgePages = setOf(0, totalPages - 1)
+
+        test.setAdapterSync(adapterProvider(stringSequence(totalPages)))
+        listOf(0, 1, 1).forEach { targetPage ->
+            // given
+            val initialPage = viewPager.currentItem
+            val callback = viewPager.addNewRecordingCallback()
+            val latch = viewPager.addWaitForScrolledLatch(targetPage)
+
+            // when
+            test.swipe(initialPage, targetPage)
+            latch.await(2, SECONDS)
+
+            // then
+            test.assertBasicState(targetPage)
+
+            if (targetPage == initialPage && edgePages.contains(targetPage)) {
+                callback.apply {
+                    // verify all events
+                    assertThat("Events should start with a state change to DRAGGING",
+                        draggingIx, equalTo(0))
+                    assertThat("Last event should be a state change to IDLE",
+                        idleIx, equalTo(lastIx))
+                    assertThat("All events but the state changes to DRAGGING and IDLE" +
+                            " should be scroll events",
+                        scrollEventCount, equalTo(eventCount - 2))
+
+                    // dive into scroll events
+                    scrollEvents.forEach {
+                        assertThat("All scroll events should report page $targetPage",
+                            it.position, equalTo(targetPage))
+                        assertThat("All scroll events should report an offset of 0f",
+                            it.positionOffset, equalTo(0f))
+                        assertThat("All scroll events should report an offset of 0px",
+                            it.positionOffsetPixels, equalTo(0))
+                    }
+                }
+            }
+
+            viewPager.unregisterOnPageChangeCallback(callback)
+        }
+    }
+
+    private fun ViewPager2.addNewRecordingCallback(): RecordingCallback {
+        return RecordingCallback().also { registerOnPageChangeCallback(it) }
+    }
+
+    private sealed class Event {
+        data class OnPageScrolledEvent(
+            val position: Int,
+            val positionOffset: Float,
+            val positionOffsetPixels: Int
+        ) : Event()
+        data class OnPageSelectedEvent(val position: Int) : Event()
+        data class OnPageScrollStateChangedEvent(val state: Int) : Event()
+    }
+
+    private class RecordingCallback : ViewPager2.OnPageChangeCallback() {
+        private val events = mutableListOf<Event>()
+
+        val scrollEvents get() = events.mapNotNull { it as? OnPageScrolledEvent }
+        val eventCount get() = events.size
+        val scrollEventCount get() = scrollEvents.size
+        val lastIx get() = events.size - 1
+        val firstScrolledIx get() = events.indexOfFirst { it is OnPageScrolledEvent }
+        val lastScrolledIx get() = events.indexOfLast { it is OnPageScrolledEvent }
+        val settlingIx get() = events.indexOf(OnPageScrollStateChangedEvent(SCROLL_STATE_SETTLING))
+        val draggingIx get() = events.indexOf(OnPageScrollStateChangedEvent(SCROLL_STATE_DRAGGING))
+        val idleIx get() = events.indexOf(OnPageScrollStateChangedEvent(SCROLL_STATE_IDLE))
+        val pageSelectedIx: (page: Int) -> Int = { events.indexOf(OnPageSelectedEvent(it)) }
+
+        override fun onPageScrolled(
+            position: Int,
+            positionOffset: Float,
+            positionOffsetPixels: Int
+        ) {
+            events.add(OnPageScrolledEvent(position, positionOffset, positionOffsetPixels))
+        }
+
+        override fun onPageSelected(position: Int) {
+            events.add(OnPageSelectedEvent(position))
+        }
+
+        override fun onPageScrollStateChanged(state: Int) {
+            events.add(OnPageScrollStateChangedEvent(state))
+        }
+    }
+
+    private fun List<OnPageScrolledEvent>.assertPositionSorted(sortOrder: SortOrder) {
+        map { it.position }.assertSorted { it * sortOrder.sign }
+    }
+
+    private fun List<OnPageScrolledEvent>.assertLastCorrect(targetPage: Int) {
+        last().apply {
+            assertThat(position, equalTo(targetPage))
+            assertThat(positionOffsetPixels, equalTo(0))
+        }
+    }
+
+    private fun List<OnPageScrolledEvent>.assertValueSanity(
+        initialPage: Int,
+        otherPage: Int,
+        pageSize: Int
+    ) = forEach {
+        assertThat(it.position, isBetweenInInMinMax(initialPage, otherPage))
+        assertThat(it.positionOffset, isBetweenInEx(0f, 1f))
+        assertThat((it.positionOffset * pageSize).roundToInt(), equalTo(it.positionOffsetPixels))
+    }
+
+    private fun List<OnPageScrolledEvent>.assertOffsetSorted(sortOrder: SortOrder) {
+        map { it.position + it.positionOffset.toDouble() }.assertSorted { it * sortOrder.sign }
+    }
+
+    private fun List<OnPageScrolledEvent>.assertMaxShownPages() {
+        assertThat(map { it.position }.distinct().size, isBetweenInIn(0, 4))
+    }
+}
+
+// region Test Suite creation
+
+private fun createTestSet(): List<TestConfig> {
+    return listOf(ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL).flatMap { orientation ->
+        listOf(false, true).flatMap { rtl ->
+            listOf(
+                TestConfig(orientation, rtl, 0, 0, 0, 0),
+                TestConfig(orientation, rtl, 0, 0, 0, 10),
+                TestConfig(orientation, rtl, 0, 0, 10, 0),
+                TestConfig(orientation, rtl, 0, 10, 0, 0),
+                TestConfig(orientation, rtl, 10, 0, 0, 0),
+                TestConfig(orientation, rtl, 1, 2, 3, 4)
+            )
+        }
+    }
+}
+
+// endregion
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
index da60cc2..cc4bb92 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
@@ -40,7 +40,6 @@
 import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE
 import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_SETTLING
 import androidx.viewpager2.widget.swipe.PageSwiperManual
-import androidx.viewpager2.widget.swipe.ViewAdapter
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.CoreMatchers.not
 import org.hamcrest.Matchers.allOf
@@ -60,8 +59,7 @@
 class PageChangeCallbackTest(private val config: TestConfig) : BaseTest() {
     data class TestConfig(
         @ViewPager2.Orientation val orientation: Int,
-        val rtl: Boolean,
-        val pageMarginPx: Int
+        val rtl: Boolean
     )
 
     companion object {
@@ -78,26 +76,6 @@
         }
     }
 
-    private val adapterProvider: AdapterProviderForItems get() {
-        return if (config.pageMarginPx > 0) {
-            { items -> { MarginViewAdapter(config.pageMarginPx, items) } }
-        } else {
-            { items -> { ViewAdapter(items) } }
-        }
-    }
-
-    class MarginViewAdapter(private val margin: Int, items: List<String>) : ViewAdapter(items) {
-        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
-            val viewHolder = super.onCreateViewHolder(parent, viewType)
-            val lp = viewHolder.itemView.layoutParams as ViewGroup.MarginLayoutParams
-            // Set unequal margins, to prevent symmetry from hiding bugs
-            // Similarly, make sure no margin is an exact multiple of another margin
-            lp.setMargins(margin * 2, margin * 3, margin * 7, margin * 5)
-            viewHolder.itemView.layoutParams = lp
-            return viewHolder
-        }
-    }
-
     /*
     Sample log to guide the test
 
@@ -131,7 +109,7 @@
     @Test
     fun test_swipeBetweenPages() {
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(4)))
+            setAdapterSync(viewAdapterProvider(stringSequence(4)))
             listOf(1, 2, 3, 2, 1, 0).forEach { targetPage ->
                 // given
                 val initialPage = viewPager.currentItem
@@ -194,7 +172,7 @@
 
         setUpTest(config.orientation).apply {
 
-            setAdapterSync(adapterProvider(stringSequence(totalPages)))
+            setAdapterSync(viewAdapterProvider(stringSequence(totalPages)))
             listOf(0, 0, 1, 2, 2, 2, 1, 2, 2, 2, 1, 0, 0, 0).forEach { targetPage ->
                 // given
                 val initialPage = viewPager.currentItem
@@ -257,7 +235,7 @@
     fun test_peekOnAdjacentPage_next() {
         // given
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(3)))
+            setAdapterSync(viewAdapterProvider(stringSequence(3)))
             val callback = viewPager.addNewRecordingCallback()
             val latch = viewPager.addWaitForScrolledLatch(0)
 
@@ -316,7 +294,7 @@
     fun test_peekOnAdjacentPage_previous() {
         // given
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(3)))
+            setAdapterSync(viewAdapterProvider(stringSequence(3)))
 
             viewPager.setCurrentItemSync(2, false, 1, SECONDS)
 
@@ -393,7 +371,7 @@
     fun test_selectItemProgrammatically_smoothScroll() {
         // given
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(1000)))
+            setAdapterSync(viewAdapterProvider(stringSequence(1000)))
 
             // when
             listOf(6, 5, 6, 3, 10, 0, 0, 999, 999, 0).forEach { targetPage ->
@@ -434,7 +412,7 @@
     fun test_multiplePageChanges() {
         // given
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(10)))
+            setAdapterSync(viewAdapterProvider(stringSequence(10)))
             val targetPages = listOf(4, 9)
             val callback = viewPager.addNewRecordingCallback()
             val latch = viewPager.addWaitForScrolledLatch(targetPages.last(), true)
@@ -484,7 +462,7 @@
     fun test_noSmoothScroll_after_smoothScroll() {
         // given
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(6)))
+            setAdapterSync(viewAdapterProvider(stringSequence(6)))
             val targetPage = 4
             val marker = 1
             val callback = viewPager.addNewRecordingCallback()
@@ -602,7 +580,7 @@
         // given
         assertThat(targetPage, greaterThanOrEqualTo(4))
         setUpTest(config.orientation).apply {
-            val adapterProvider = adapterProvider(stringSequence(5))
+            val adapterProvider = viewAdapterProvider(stringSequence(5))
             setAdapterSync(adapterProvider)
             val marker = 1
             val callback = viewPager.addNewRecordingCallback()
@@ -662,7 +640,7 @@
     fun test_selectItemProgrammatically_noSmoothScroll() {
         // given
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(3)))
+            setAdapterSync(viewAdapterProvider(stringSequence(3)))
 
             // when
             listOf(2, 2, 0, 0, 1, 2, 1, 0).forEach { targetPage ->
@@ -694,7 +672,7 @@
     fun test_swipeReleaseSwipeBack() {
         // given
         val test = setUpTest(config.orientation)
-        test.setAdapterSync(adapterProvider(stringSequence(3)))
+        test.setAdapterSync(viewAdapterProvider(stringSequence(3)))
         val currentPage = test.viewPager.currentItem
         val halfPage = test.viewPager.pageSize / 2f
         val pageSwiper = PageSwiperManual(test.viewPager)
@@ -768,7 +746,7 @@
     private fun test_selectItemProgrammatically_noCallback(smoothScroll: Boolean) {
         // given
         setUpTest(config.orientation).apply {
-            setAdapterSync(adapterProvider(stringSequence(3)))
+            setAdapterSync(viewAdapterProvider(stringSequence(3)))
 
             // when
             listOf(2, 2, 0, 0, 1, 2, 1, 0).forEach { targetPage ->
@@ -923,7 +901,7 @@
     private fun test_setCurrentItem_outOfBounds(smoothScroll: Boolean) {
         val test = setUpTest(config.orientation)
         val n = 3
-        test.setAdapterSync(adapterProvider(stringSequence(n)))
+        test.setAdapterSync(viewAdapterProvider(stringSequence(n)))
         val adapterCount = test.viewPager.adapter!!.itemCount
 
         listOf(-5, -1, n, n + 1, adapterCount, adapterCount + 1).forEach { targetPage ->
@@ -1199,10 +1177,8 @@
 
 private fun createTestSet(): List<TestConfig> {
     return listOf(ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL).flatMap { orientation ->
-        listOf(true, false).flatMap { rtl ->
-            listOf(0, 10, -10).map { margin ->
-                TestConfig(orientation, rtl, margin)
-            }
+        listOf(true, false).map { rtl ->
+            TestConfig(orientation, rtl)
         }
     }
 }
diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java b/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java
index 4d8e186..f122b43 100644
--- a/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java
+++ b/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java
@@ -16,8 +16,6 @@
 
 package androidx.viewpager2.widget;
 
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-
 import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL;
 import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING;
 import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE;
@@ -27,6 +25,7 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.view.View;
+import android.view.ViewGroup.LayoutParams;
 import android.view.ViewGroup.MarginLayoutParams;
 
 import androidx.annotation.IntDef;
@@ -44,13 +43,6 @@
  * relative to the pages and exposes this position via ({@link #getRelativeScrollPosition()}.
  */
 final class ScrollEventAdapter extends RecyclerView.OnScrollListener {
-    private static final MarginLayoutParams ZERO_MARGIN_LAYOUT_PARAMS;
-
-    static {
-        ZERO_MARGIN_LAYOUT_PARAMS = new MarginLayoutParams(MATCH_PARENT, MATCH_PARENT);
-        ZERO_MARGIN_LAYOUT_PARAMS.setMargins(0, 0, 0, 0);
-    }
-
     /** @hide */
     @Retention(SOURCE)
     @IntDef({STATE_IDLE, STATE_IN_PROGRESS_MANUAL_DRAG, STATE_IN_PROGRESS_SMOOTH_SCROLL,
@@ -67,8 +59,9 @@
     private static final int NO_POSITION = -1;
 
     private OnPageChangeCallback mCallback;
-    private final @NonNull LinearLayoutManager mLayoutManager;
     private final @NonNull ViewPager2 mViewPager;
+    private final @NonNull RecyclerView mRecyclerView;
+    private final @NonNull LinearLayoutManager mLayoutManager;
 
     // state related fields
     private @AdapterState int mAdapterState;
@@ -82,8 +75,10 @@
     private boolean mFakeDragging;
 
     ScrollEventAdapter(@NonNull ViewPager2 viewPager) {
-        mLayoutManager = viewPager.mLayoutManager;
         mViewPager = viewPager;
+        mRecyclerView = mViewPager.mRecyclerView;
+        //noinspection ConstantConditions
+        mLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
         mScrollValues = new ScrollEventValues();
         resetState();
     }
@@ -239,23 +234,34 @@
             return;
         }
 
-        MarginLayoutParams margin =
-                (firstVisibleView.getLayoutParams() instanceof MarginLayoutParams)
-                        ? (MarginLayoutParams) firstVisibleView.getLayoutParams()
-                        : ZERO_MARGIN_LAYOUT_PARAMS;
+        int leftDecorations = mLayoutManager.getLeftDecorationWidth(firstVisibleView);
+        int rightDecorations = mLayoutManager.getRightDecorationWidth(firstVisibleView);
+        int topDecorations = mLayoutManager.getTopDecorationHeight(firstVisibleView);
+        int bottomDecorations = mLayoutManager.getBottomDecorationHeight(firstVisibleView);
+
+        LayoutParams params = firstVisibleView.getLayoutParams();
+        if (params instanceof MarginLayoutParams) {
+            MarginLayoutParams margin = (MarginLayoutParams) params;
+            leftDecorations += margin.leftMargin;
+            rightDecorations += margin.rightMargin;
+            topDecorations += margin.topMargin;
+            bottomDecorations += margin.bottomMargin;
+        }
+
+        int decoratedHeight = firstVisibleView.getHeight() + topDecorations + bottomDecorations;
+        int decoratedWidth = firstVisibleView.getWidth() + leftDecorations + rightDecorations;
 
         boolean isHorizontal = mLayoutManager.getOrientation() == ORIENTATION_HORIZONTAL;
         int start, sizePx;
         if (isHorizontal) {
-            sizePx = firstVisibleView.getWidth() + margin.leftMargin + margin.rightMargin;
-            if (!mViewPager.isRtl()) {
-                start = firstVisibleView.getLeft() - margin.leftMargin;
-            } else {
-                start = sizePx - firstVisibleView.getRight() - margin.rightMargin;
+            sizePx = decoratedWidth;
+            start = firstVisibleView.getLeft() - leftDecorations - mRecyclerView.getPaddingLeft();
+            if (mViewPager.isRtl()) {
+                start = -start;
             }
         } else {
-            sizePx = firstVisibleView.getHeight() + margin.topMargin + margin.bottomMargin;
-            start = firstVisibleView.getTop() - margin.topMargin;
+            sizePx = decoratedHeight;
+            start = firstVisibleView.getTop() - topDecorations - mRecyclerView.getPaddingTop();
         }
 
         values.mOffsetPx = -start;
diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
index d0aee32..3bfaf3d 100644
--- a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
+++ b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
@@ -138,10 +138,10 @@
                 }
             };
 
-    LinearLayoutManager mLayoutManager;
+    private LinearLayoutManager mLayoutManager;
     private int mPendingCurrentItem = NO_POSITION;
     private Parcelable mPendingAdapterState;
-    private RecyclerView mRecyclerView;
+    RecyclerView mRecyclerView;
     private PagerSnapHelper mPagerSnapHelper;
     ScrollEventAdapter mScrollEventAdapter;
     private CompositeOnPageChangeCallback mPageChangeEventDispatcher;
@@ -540,9 +540,10 @@
     }
 
     int getPageSize() {
+        final RecyclerView rv = mRecyclerView;
         return getOrientation() == ORIENTATION_HORIZONTAL
-                ? getWidth() - getPaddingLeft() - getPaddingRight()
-                : getHeight() - getPaddingTop() - getPaddingBottom();
+                ? rv.getWidth() - rv.getPaddingLeft() - rv.getPaddingRight()
+                : rv.getHeight() - rv.getPaddingTop() - rv.getPaddingBottom();
     }
 
     /**