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();
}
/**