Add fake dragging to ViewPager2

Adds fake dragging functionality to ViewPager2, similar to the fake
dragging functionality in ViewPager. Tests can be found in FakeDragTest
and a sample in FakeDragActivity in the testapp.

Bug: 122655918
Test: ./gradlew viewpager2:connectedCheck

Change-Id: I0363b825b23e9e2de2aad9b2d338289c51e5de9d
diff --git a/viewpager2/api/1.0.0-alpha03.txt b/viewpager2/api/1.0.0-alpha03.txt
index 5607352..4b1a6aa 100644
--- a/viewpager2/api/1.0.0-alpha03.txt
+++ b/viewpager2/api/1.0.0-alpha03.txt
@@ -32,9 +32,14 @@
     ctor public ViewPager2(android.content.Context, android.util.AttributeSet?);
     ctor public ViewPager2(android.content.Context, android.util.AttributeSet?, int);
     ctor @RequiresApi(21) public ViewPager2(android.content.Context, android.util.AttributeSet?, int, int);
+    method public boolean beginFakeDrag();
+    method public boolean endFakeDrag();
+    method public boolean fakeDragBy(float);
     method public androidx.recyclerview.widget.RecyclerView.Adapter? getAdapter();
     method public int getCurrentItem();
     method @androidx.viewpager2.widget.ViewPager2.Orientation public int getOrientation();
+    method @androidx.viewpager2.widget.ViewPager2.ScrollState public int getScrollState();
+    method public boolean isFakeDragging();
     method public boolean isUserInputEnabled();
     method public void registerOnPageChangeCallback(androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback);
     method public void setAdapter(androidx.recyclerview.widget.RecyclerView.Adapter?);
diff --git a/viewpager2/api/current.txt b/viewpager2/api/current.txt
index 5607352..4b1a6aa 100644
--- a/viewpager2/api/current.txt
+++ b/viewpager2/api/current.txt
@@ -32,9 +32,14 @@
     ctor public ViewPager2(android.content.Context, android.util.AttributeSet?);
     ctor public ViewPager2(android.content.Context, android.util.AttributeSet?, int);
     ctor @RequiresApi(21) public ViewPager2(android.content.Context, android.util.AttributeSet?, int, int);
+    method public boolean beginFakeDrag();
+    method public boolean endFakeDrag();
+    method public boolean fakeDragBy(float);
     method public androidx.recyclerview.widget.RecyclerView.Adapter? getAdapter();
     method public int getCurrentItem();
     method @androidx.viewpager2.widget.ViewPager2.Orientation public int getOrientation();
+    method @androidx.viewpager2.widget.ViewPager2.ScrollState public int getScrollState();
+    method public boolean isFakeDragging();
     method public boolean isUserInputEnabled();
     method public void registerOnPageChangeCallback(androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback);
     method public void setAdapter(androidx.recyclerview.widget.RecyclerView.Adapter?);
diff --git a/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/BaseTest.kt b/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/BaseTest.kt
index c6f68d6..ab76cb1 100644
--- a/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/BaseTest.kt
+++ b/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/BaseTest.kt
@@ -19,17 +19,24 @@
 import android.view.View
 import androidx.annotation.LayoutRes
 import androidx.fragment.app.FragmentActivity
+import androidx.test.espresso.Espresso.onData
 import androidx.test.espresso.Espresso.onIdle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.espresso.matcher.ViewMatchers.withText
 import androidx.test.rule.ActivityTestRule
 import androidx.viewpager2.widget.ViewPager2
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL
 import com.example.androidx.viewpager2.test.ViewPagerIdleWatcher
 import com.example.androidx.viewpager2.test.onCurrentPage
 import com.example.androidx.viewpager2.test.onViewPager
 import com.example.androidx.viewpager2.test.swipeNext
 import com.example.androidx.viewpager2.test.swipePrevious
+import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.Matcher
 import org.junit.After
 import org.junit.Before
@@ -65,6 +72,17 @@
         idleWatcher.unregister()
     }
 
+    fun selectOrientation(@ViewPager2.Orientation orientation: Int) {
+        onView(withId(R.id.orientation_spinner)).perform(click())
+        onData(equalTo(
+            when (orientation) {
+                ORIENTATION_HORIZONTAL -> "horizontal"
+                ORIENTATION_VERTICAL -> "vertical"
+                else -> throw IllegalArgumentException("Orientation $orientation doesn't exist")
+            }
+        )).perform(click())
+    }
+
     fun swipeToNextPage() {
         onViewPager().perform(swipeNext())
         idleWatcher.waitForIdle()
diff --git a/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/FakeDragTest.kt b/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/FakeDragTest.kt
new file mode 100644
index 0000000..825f04c
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/FakeDragTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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 com.example.androidx.viewpager2
+
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import androidx.test.espresso.Espresso.onIdle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.action.ViewActions.swipeDown
+import androidx.test.espresso.action.ViewActions.swipeLeft
+import androidx.test.espresso.action.ViewActions.swipeRight
+import androidx.test.espresso.action.ViewActions.swipeUp
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.viewpager2.widget.ViewPager2
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class FakeDragTest(private val config: TestConfig) :
+    BaseTest<FakeDragActivity>(FakeDragActivity::class.java) {
+    data class TestConfig(
+        @ViewPager2.Orientation val orientation: Int
+    )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun spec(): List<TestConfig> {
+            return listOf(ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL).map { orientation ->
+                TestConfig(orientation)
+            }
+        }
+    }
+
+    private val twoOfSpadesPage = "2\n♣"
+    private val threeOfSpadesPage = "3\n♣"
+
+    override val layoutId get() = R.id.viewPager
+
+    private val phoneOrientation
+        get() = getInstrumentation().targetContext.resources.configuration.orientation
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+        selectOrientation(config.orientation)
+    }
+
+    @Test
+    fun testFakeDragging() {
+        // test if ViewPager2 goes to the next page when fake dragging
+        fakeDragForward()
+        verifyCurrentPage(threeOfSpadesPage)
+
+        // test if ViewPager2 goes back to the first page when fake dragging the other way
+        fakeDragBackward()
+        verifyCurrentPage(twoOfSpadesPage)
+    }
+
+    private fun fakeDragForward() {
+        onTouchpad().perform(swipeNext())
+        idleWatcher.waitForIdle()
+        onIdle()
+    }
+
+    private fun fakeDragBackward() {
+        onTouchpad().perform(swipePrevious())
+        idleWatcher.waitForIdle()
+        onIdle()
+    }
+
+    private fun onTouchpad(): ViewInteraction {
+        return onView(withId(R.id.touchpad))
+    }
+
+    private fun swipeNext(): ViewAction {
+        return when (phoneOrientation) {
+            ORIENTATION_LANDSCAPE -> swipeDown()
+            ORIENTATION_PORTRAIT -> swipeLeft()
+            else -> throw RuntimeException("Orientation should be landscape or portrait")
+        }
+    }
+
+    private fun swipePrevious(): ViewAction {
+        return when (phoneOrientation) {
+            ORIENTATION_LANDSCAPE -> swipeUp()
+            ORIENTATION_PORTRAIT -> swipeRight()
+            else -> throw RuntimeException("Orientation should be landscape or portrait")
+        }
+    }
+}
diff --git a/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/ViewPagerBaseTest.kt b/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/ViewPagerBaseTest.kt
index 38cf9f2..3fdb56f 100644
--- a/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/ViewPagerBaseTest.kt
+++ b/viewpager2/integration-tests/testapp/src/androidTest/java/com/example/androidx/viewpager2/ViewPagerBaseTest.kt
@@ -16,7 +16,6 @@
 
 package com.example.androidx.viewpager2
 
-import androidx.test.espresso.Espresso.onData
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.action.ViewActions.click
 import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
@@ -26,7 +25,6 @@
 import androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL
 import com.example.androidx.viewpager2.test.AnimationVerifier
 import org.hamcrest.CoreMatchers.allOf
-import org.hamcrest.CoreMatchers.equalTo
 import org.junit.Before
 import org.junit.Test
 import org.junit.runners.Parameterized
@@ -73,7 +71,7 @@
     @Before
     override fun setUp() {
         super.setUp()
-        selectOrientation()
+        selectOrientation(config.orientation)
         if (config.animateRotate) check(R.id.rotate_checkbox)
         if (config.animateTranslate) check(R.id.translate_checkbox)
         if (config.animateScale) check(R.id.scale_checkbox)
@@ -96,17 +94,6 @@
         verifyCurrentPage(twoOfSpades)
     }
 
-    private fun selectOrientation() {
-        onView(withId(R.id.orientation_spinner)).perform(click())
-        onData(equalTo(
-            when (config.orientation) {
-                ORIENTATION_HORIZONTAL -> "horizontal"
-                ORIENTATION_VERTICAL -> "vertical"
-                else -> "unknown"
-            }
-        )).perform(click())
-    }
-
     private fun check(id: Int) {
         onView(allOf(withId(id), isNotChecked())).perform(click())
     }
diff --git a/viewpager2/integration-tests/testapp/src/main/AndroidManifest.xml b/viewpager2/integration-tests/testapp/src/main/AndroidManifest.xml
index c7c6848..a6fea0b 100644
--- a/viewpager2/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/viewpager2/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -59,6 +59,13 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".FakeDragActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.SAMPLE_CODE"/>
+            </intent-filter>
+        </activity>
+
         <activity android:name=".BrowseActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BaseCardActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BaseCardActivity.kt
index f90425b..291cdda3f 100644
--- a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BaseCardActivity.kt
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BaseCardActivity.kt
@@ -17,8 +17,6 @@
 package com.example.androidx.viewpager2
 
 import android.os.Bundle
-import android.view.View
-import android.widget.AdapterView
 import android.widget.ArrayAdapter
 import android.widget.Button
 import android.widget.CheckBox
@@ -36,20 +34,17 @@
  */
 abstract class BaseCardActivity : FragmentActivity() {
 
-    lateinit var viewPager: ViewPager2
+    protected lateinit var viewPager: ViewPager2
     private lateinit var cardSelector: Spinner
     private lateinit var smoothScrollCheckBox: CheckBox
     private lateinit var rotateCheckBox: CheckBox
     private lateinit var translateCheckBox: CheckBox
     private lateinit var scaleCheckBox: CheckBox
     private lateinit var gotoPage: Button
-    private lateinit var orientationSelector: Spinner
-    private lateinit var disableUserInputCheckBox: CheckBox
-    private var orientation: Int = ORIENTATION_HORIZONTAL
 
-    private val translateX get() = orientation == ORIENTATION_VERTICAL &&
+    private val translateX get() = viewPager.orientation == ORIENTATION_VERTICAL &&
             translateCheckBox.isChecked
-    private val translateY get() = orientation == ORIENTATION_HORIZONTAL &&
+    private val translateY get() = viewPager.orientation == ORIENTATION_HORIZONTAL &&
             translateCheckBox.isChecked
 
     protected open val layoutId: Int = R.layout.activity_no_tablayout
@@ -76,8 +71,6 @@
         setContentView(layoutId)
 
         viewPager = findViewById(R.id.view_pager)
-        orientationSelector = findViewById(R.id.orientation_spinner)
-        disableUserInputCheckBox = findViewById(R.id.disable_user_input_checkbox)
         cardSelector = findViewById(R.id.card_spinner)
         smoothScrollCheckBox = findViewById(R.id.smooth_scroll_checkbox)
         rotateCheckBox = findViewById(R.id.rotate_checkbox)
@@ -85,32 +78,12 @@
         scaleCheckBox = findViewById(R.id.scale_checkbox)
         gotoPage = findViewById(R.id.jump_button)
 
-        disableUserInputCheckBox.setOnCheckedChangeListener { _, isDisabled ->
-            viewPager.isUserInputEnabled = !isDisabled
-        }
-
-        orientationSelector.adapter = createOrientationAdapter()
+        UserInputController(viewPager, findViewById(R.id.disable_user_input_checkbox)).setup()
+        OrientationController(viewPager, findViewById(R.id.orientation_spinner)).setup()
         cardSelector.adapter = createCardAdapter()
 
         viewPager.setPageTransformer(mAnimator)
 
-        orientationSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
-            override fun onItemSelected(
-                parent: AdapterView<*>,
-                view: View?,
-                position: Int,
-                id: Long
-            ) {
-                when (parent.selectedItem.toString()) {
-                    HORIZONTAL -> orientation = ORIENTATION_HORIZONTAL
-                    VERTICAL -> orientation = ORIENTATION_VERTICAL
-                }
-                viewPager.orientation = orientation
-            }
-
-            override fun onNothingSelected(adapterView: AdapterView<*>) {}
-        }
-
         gotoPage.setOnClickListener {
             val card = cardSelector.selectedItemPosition
             val smoothScroll = smoothScrollCheckBox.isChecked
@@ -123,17 +96,4 @@
         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
         return adapter
     }
-
-    private fun createOrientationAdapter(): SpinnerAdapter {
-        val adapter = ArrayAdapter(this,
-                android.R.layout.simple_spinner_item, arrayOf(HORIZONTAL, VERTICAL))
-        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
-        return adapter
-    }
-
-    companion object {
-        val cards = Card.DECK
-        private const val HORIZONTAL = "horizontal"
-        private const val VERTICAL = "vertical"
-    }
 }
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BrowseActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BrowseActivity.kt
index ca544d3..b98a912 100644
--- a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BrowseActivity.kt
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/BrowseActivity.kt
@@ -49,6 +49,8 @@
                 "intent" to activityToIntent(MutableCollectionFragmentActivity::class.java.name)))
         myData.add(mapOf("title" to "ViewPager2 with a TabLayout (Views)",
                 "intent" to activityToIntent(CardViewTabLayoutActivity::class.java.name)))
+        myData.add(mapOf("title" to "ViewPager2 with Fake Dragging",
+                "intent" to activityToIntent(FakeDragActivity::class.java.name)))
 
         return myData
     }
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.kt
index 4049be5..794104d 100644
--- a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.kt
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.kt
@@ -40,11 +40,11 @@
 
         viewPager.adapter = object : FragmentStateAdapter(supportFragmentManager) {
             override fun getItem(position: Int): Fragment {
-                return CardFragment.create(cards[position])
+                return CardFragment.create(Card.DECK[position])
             }
 
             override fun getItemCount(): Int {
-                return cards.size
+                return Card.DECK.size
             }
         }
     }
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewActivity.kt
index 71fc7d3..2508f92 100644
--- a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewActivity.kt
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewActivity.kt
@@ -17,12 +17,8 @@
 package com.example.androidx.viewpager2
 
 import android.os.Bundle
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
 import androidx.viewpager2.widget.ViewPager2
-
-import com.example.androidx.viewpager2.cards.Card
-import com.example.androidx.viewpager2.cards.CardView
+import com.example.androidx.viewpager2.cards.CardViewAdapter
 
 /**
  * Shows how to use [ViewPager2.setAdapter] with Views.
@@ -30,34 +26,8 @@
  * @see CardFragmentActivity for an example of using {@link ViewPager2} with Fragments.
  */
 open class CardViewActivity : BaseCardActivity() {
-
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-
-        viewPager.adapter = object : RecyclerView.Adapter<CardViewHolder>() {
-            override fun onCreateViewHolder(
-                parent: ViewGroup,
-                viewType: Int
-            ): CardViewHolder {
-                return CardViewHolder(CardView(layoutInflater, parent))
-            }
-
-            override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
-                holder.bind(cards[position])
-            }
-
-            override fun getItemCount(): Int {
-                return cards.size
-            }
-        }
-    }
-
-    class CardViewHolder internal constructor(
-        private val cardView: CardView
-    ) : RecyclerView.ViewHolder(cardView.view) {
-
-        internal fun bind(card: Card) {
-            cardView.bind(card)
-        }
+        viewPager.adapter = CardViewAdapter()
     }
 }
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewTabLayoutActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewTabLayoutActivity.kt
index a34b936..75d3a82 100644
--- a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewTabLayoutActivity.kt
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/CardViewTabLayoutActivity.kt
@@ -17,6 +17,7 @@
 package com.example.androidx.viewpager2
 
 import android.os.Bundle
+import com.example.androidx.viewpager2.cards.Card
 import com.google.android.material.tabs.TabLayout
 
 class CardViewTabLayoutActivity : CardViewActivity() {
@@ -30,7 +31,7 @@
 
         tabLayout = findViewById(R.id.tabs)
         TabLayoutMediator(tabLayout, viewPager) { tab, position ->
-            tab.text = cards[position].toString()
+            tab.text = Card.DECK[position].toString()
         }.attach()
     }
 }
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/FakeDragActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/FakeDragActivity.kt
new file mode 100644
index 0000000..a1b27b5
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/FakeDragActivity.kt
@@ -0,0 +1,73 @@
+/*
+ * 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 com.example.androidx.viewpager2
+
+import android.content.res.Configuration
+import android.os.Bundle
+import android.view.MotionEvent
+import android.view.View
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager2.widget.ViewPager2
+import com.example.androidx.viewpager2.cards.CardViewAdapter
+
+class FakeDragActivity : FragmentActivity() {
+
+    private lateinit var viewPager: ViewPager2
+    private var landscape = false
+    private var lastValue: Float = 0f
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_fakedrag)
+        landscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+
+        viewPager = findViewById(R.id.viewPager)
+        viewPager.adapter = CardViewAdapter()
+        viewPager.isUserInputEnabled = false
+        UserInputController(viewPager, findViewById(R.id.disable_user_input_checkbox)).setup()
+        OrientationController(viewPager, findViewById(R.id.orientation_spinner)).setup()
+
+        findViewById<View>(R.id.touchpad).setOnTouchListener { _, event ->
+            handleOnTouchEvent(event)
+        }
+    }
+
+    private fun getValue(event: MotionEvent): Float {
+        return if (landscape) event.y else event.x
+    }
+
+    private fun handleOnTouchEvent(event: MotionEvent): Boolean {
+        when (event.action) {
+            MotionEvent.ACTION_DOWN -> {
+                lastValue = getValue(event)
+                viewPager.beginFakeDrag()
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                val value = getValue(event)
+                val delta = value - lastValue
+                viewPager.fakeDragBy(delta)
+                lastValue = value
+            }
+
+            MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
+                viewPager.endFakeDrag()
+            }
+        }
+        return true
+    }
+}
\ No newline at end of file
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/OrientationController.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/OrientationController.kt
new file mode 100644
index 0000000..4f7e295
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/OrientationController.kt
@@ -0,0 +1,76 @@
+/*
+ * 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 com.example.androidx.viewpager2
+
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import androidx.viewpager2.widget.ViewPager2
+
+/**
+ * It configures a spinner to show orientations and sets the orientation of a ViewPager2 when an orientation is selected
+ */
+class OrientationController(private val viewPager: ViewPager2, private val spinner: Spinner) {
+    fun setup() {
+        val orientation = viewPager.orientation
+        val adapter = ArrayAdapter(spinner.context, android.R.layout.simple_spinner_item,
+            arrayOf(HORIZONTAL, VERTICAL))
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+        spinner.adapter = adapter
+
+        val initialPosition = adapter.getPosition(orientationToString(orientation))
+        if (initialPosition >= 0) {
+            spinner.setSelection(initialPosition)
+        }
+
+        spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+            override fun onItemSelected(
+                parent: AdapterView<*>,
+                view: View?,
+                position: Int,
+                id: Long
+            ) {
+                viewPager.orientation = stringToOrientation(parent.selectedItem.toString())
+            }
+
+            override fun onNothingSelected(adapterView: AdapterView<*>) {}
+        }
+    }
+
+    private fun orientationToString(@ViewPager2.Orientation orientation: Int): String {
+        return when (orientation) {
+            ViewPager2.ORIENTATION_HORIZONTAL -> HORIZONTAL
+            ViewPager2.ORIENTATION_VERTICAL -> VERTICAL
+            else -> throw IllegalArgumentException("Orientation $orientation doesn't exist")
+        }
+    }
+
+    @ViewPager2.Orientation
+    internal fun stringToOrientation(string: String): Int {
+        return when (string) {
+            HORIZONTAL -> ViewPager2.ORIENTATION_HORIZONTAL
+            VERTICAL -> ViewPager2.ORIENTATION_VERTICAL
+            else -> throw IllegalArgumentException("Orientation $string doesn't exist")
+        }
+    }
+
+    companion object {
+        private const val HORIZONTAL = "horizontal"
+        private const val VERTICAL = "vertical"
+    }
+}
\ No newline at end of file
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/UserInputController.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/UserInputController.kt
new file mode 100644
index 0000000..c4c1e26
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/UserInputController.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 com.example.androidx.viewpager2
+
+import android.widget.CheckBox
+import androidx.viewpager2.widget.ViewPager2
+
+class UserInputController(private val viewPager: ViewPager2, private val disableBox: CheckBox) {
+    fun setup() {
+        disableBox.isChecked = !viewPager.isUserInputEnabled
+        disableBox.setOnCheckedChangeListener { _, isDisabled ->
+            viewPager.isUserInputEnabled = !isDisabled
+        }
+    }
+}
\ No newline at end of file
diff --git a/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/cards/CardViewAdapter.kt b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/cards/CardViewAdapter.kt
new file mode 100644
index 0000000..90739f0
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/java/com/example/androidx/viewpager2/cards/CardViewAdapter.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 com.example.androidx.viewpager2.cards
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+
+class CardViewAdapter : RecyclerView.Adapter<CardViewHolder>() {
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
+        return CardViewHolder(CardView(LayoutInflater.from(parent.context), parent))
+    }
+
+    override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
+        holder.bind(Card.DECK[position])
+    }
+
+    override fun getItemCount(): Int {
+        return Card.DECK.size
+    }
+}
+
+class CardViewHolder internal constructor(private val cardView: CardView) :
+    RecyclerView.ViewHolder(cardView.view) {
+    internal fun bind(card: Card) {
+        cardView.bind(card)
+    }
+}
diff --git a/viewpager2/integration-tests/testapp/src/main/res/layout-land/activity_fakedrag.xml b/viewpager2/integration-tests/testapp/src/main/res/layout-land/activity_fakedrag.xml
new file mode 100644
index 0000000..47b1d78
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/res/layout-land/activity_fakedrag.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    tools:background="#FFFFFF">
+
+    <include layout="@layout/controls_fakedrag" />
+
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/viewPager"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/viewpager2/integration-tests/testapp/src/main/res/layout-land/controls_fakedrag.xml b/viewpager2/integration-tests/testapp/src/main/res/layout-land/controls_fakedrag.xml
new file mode 100644
index 0000000..0934069
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/res/layout-land/controls_fakedrag.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_margin="16dp"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/label_orientation"
+            android:textAppearance="@android:style/TextAppearance.Medium" />
+
+        <Spinner
+            android:id="@+id/orientation_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <CheckBox
+            android:id="@+id/disable_user_input_checkbox"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/disable_user_input"
+            android:textAppearance="@android:style/TextAppearance.Medium" />
+
+        <View
+            android:id="@+id/touchpad"
+            android:layout_width="64dp"
+            android:layout_height="0dp"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="16dp"
+            android:layout_weight="1"
+            android:background="#EBEBEB" />
+
+    </LinearLayout>
+
+    <View
+        android:layout_width="1dp"
+        android:layout_height="match_parent"
+        android:background="#000000" />
+
+</merge>
diff --git a/viewpager2/integration-tests/testapp/src/main/res/layout/activity_fakedrag.xml b/viewpager2/integration-tests/testapp/src/main/res/layout/activity_fakedrag.xml
new file mode 100644
index 0000000..6f943ee
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/res/layout/activity_fakedrag.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:background="#FFFFFF">
+
+    <include layout="@layout/controls_fakedrag" />
+
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/viewPager"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/viewpager2/integration-tests/testapp/src/main/res/layout/controls_fakedrag.xml b/viewpager2/integration-tests/testapp/src/main/res/layout/controls_fakedrag.xml
new file mode 100644
index 0000000..78db760
--- /dev/null
+++ b/viewpager2/integration-tests/testapp/src/main/res/layout/controls_fakedrag.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="16dp"
+        android:layout_marginRight="16dp"
+        android:layout_marginTop="16dp"
+        android:gravity="center_vertical|start"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/label_orientation"
+            android:textAppearance="@android:style/TextAppearance.Medium" />
+
+        <Spinner
+            android:id="@+id/orientation_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <CheckBox
+        android:id="@+id/disable_user_input_checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginLeft="16dp"
+        android:text="@string/disable_user_input"
+        android:textAppearance="@android:style/TextAppearance.Medium" />
+
+    <View
+        android:id="@+id/touchpad"
+        android:layout_width="match_parent"
+        android:layout_height="64dp"
+        android:layout_margin="16dp"
+        android:background="#EBEBEB" />
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="#000000" />
+
+</merge>
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
index 192bc62..c70085e 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
@@ -45,10 +45,13 @@
 import androidx.viewpager2.adapter.FragmentStateAdapter
 import androidx.viewpager2.test.R
 import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
+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.FragmentAdapter
 import androidx.viewpager2.widget.swipe.PageSwiper
 import androidx.viewpager2.widget.swipe.PageSwiperEspresso
+import androidx.viewpager2.widget.swipe.PageSwiperFakeDrag
 import androidx.viewpager2.widget.swipe.PageSwiperManual
 import androidx.viewpager2.widget.swipe.SelfChecking
 import androidx.viewpager2.widget.swipe.TestActivity
@@ -145,7 +148,8 @@
 
         enum class SwipeMethod {
             ESPRESSO,
-            MANUAL
+            MANUAL,
+            FAKE_DRAG
         }
 
         fun swipe(currentPageIx: Int, nextPageIx: Int, method: SwipeMethod = SwipeMethod.ESPRESSO) {
@@ -192,11 +196,9 @@
 
         private fun swiper(method: SwipeMethod = SwipeMethod.ESPRESSO): PageSwiper {
             return when (method) {
-                SwipeMethod.ESPRESSO -> PageSwiperEspresso(
-                    viewPager.orientation,
-                    isRtl
-                )
+                SwipeMethod.ESPRESSO -> PageSwiperEspresso(viewPager.orientation, isRtl)
                 SwipeMethod.MANUAL -> PageSwiperManual(viewPager, isRtl)
+                SwipeMethod.FAKE_DRAG -> PageSwiperFakeDrag(viewPager)
             }
         }
 
@@ -281,11 +283,15 @@
     }
 
     fun ViewPager2.addWaitForIdleLatch(): CountDownLatch {
+        return addWaitForStateLatch(SCROLL_STATE_IDLE)
+    }
+
+    fun ViewPager2.addWaitForStateLatch(targetState: Int): CountDownLatch {
         val latch = CountDownLatch(1)
 
         registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
             override fun onPageScrollStateChanged(state: Int) {
-                if (state == SCROLL_STATE_IDLE) {
+                if (state == targetState) {
                     latch.countDown()
                     post { unregisterOnPageChangeCallback(this) }
                 }
@@ -333,14 +339,16 @@
     /**
      * Checks:
      * 1. Expected page is the current ViewPager2 page
-     * 2. Expected text is displayed
-     * 3. Internal activity state is valid (as per activity self-test)
+     * 2. Expected state is SCROLL_STATE_IDLE
+     * 3. Expected text is displayed
+     * 4. Internal activity state is valid (as per activity self-test)
      */
     fun Context.assertBasicState(pageIx: Int, value: String = pageIx.toString()) {
         assertThat<Int>(
             "viewPager.getCurrentItem() should return $pageIx",
             viewPager.currentItem, equalTo(pageIx)
         )
+        assertThat(viewPager.scrollState, equalTo(SCROLL_STATE_IDLE))
         onView(allOf<View>(withId(R.id.text_view), isDisplayed())).check(
             matches(withText(value))
         )
@@ -490,3 +498,19 @@
     get() {
         return this == fragmentAdapterProvider
     }
+
+fun scrollStateToString(@ViewPager2.ScrollState state: Int): String {
+    return when (state) {
+        SCROLL_STATE_IDLE -> "IDLE"
+        SCROLL_STATE_DRAGGING -> "DRAGGING"
+        SCROLL_STATE_SETTLING -> "SETTLING"
+        else -> throw IllegalArgumentException("Scroll state $state doesn't exist")
+    }
+}
+
+fun scrollStateGlossary(): String {
+    return "Scroll states: " +
+            "$SCROLL_STATE_IDLE=${scrollStateToString(SCROLL_STATE_IDLE)}, " +
+            "$SCROLL_STATE_DRAGGING=${scrollStateToString(SCROLL_STATE_DRAGGING)}, " +
+            "$SCROLL_STATE_SETTLING=${scrollStateToString(SCROLL_STATE_SETTLING)})"
+}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt
index e4ba599..5d2638c 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/DragWhileSmoothScrollTest.kt
@@ -95,8 +95,7 @@
                 // and check the result
                 callback.apply {
                     assertThat(
-                        "Unexpected sequence of state changes (0=IDLE, 1=DRAGGING, 2=SETTLING)" +
-                                dumpEvents(),
+                        "Unexpected sequence of state changes:" + dumpEvents(),
                         stateEvents.map { it.state },
                         equalTo(
                             if (expectIdleAfterDrag()) {
@@ -207,7 +206,7 @@
         }
 
         fun dumpEvents(): String {
-            return events.joinToString("\n- ", "\n- ")
+            return events.joinToString("\n- ", "\n(${scrollStateGlossary()})\n- ")
         }
     }
 }
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FakeDragTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FakeDragTest.kt
new file mode 100644
index 0000000..7b6e5df
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FakeDragTest.kt
@@ -0,0 +1,503 @@
+/*
+ * 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.Path
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.view.animation.LinearInterpolator
+import androidx.core.view.animation.PathInterpolatorCompat
+import androidx.test.filters.LargeTest
+import androidx.viewpager2.LocaleTestUtils
+import androidx.viewpager2.widget.BaseTest.Context.SwipeMethod
+import androidx.viewpager2.widget.FakeDragTest.Event.OnPageScrollStateChangedEvent
+import androidx.viewpager2.widget.FakeDragTest.Event.OnPageScrolledEvent
+import androidx.viewpager2.widget.FakeDragTest.Event.OnPageSelectedEvent
+import androidx.viewpager2.widget.FakeDragTest.TestConfig
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL
+import androidx.viewpager2.widget.swipe.PageSwiperFakeDrag
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.Matchers.greaterThan
+import org.junit.Assert.assertThat
+import org.junit.Assume.assumeThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors.newSingleThreadExecutor
+import java.util.concurrent.TimeUnit.MILLISECONDS
+import java.util.concurrent.TimeUnit.SECONDS
+import kotlin.math.roundToInt
+import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING as DRAGGING
+import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE as IDLE
+import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_SETTLING as SETTLING
+
+@RunWith(Parameterized::class)
+@LargeTest
+class FakeDragTest(private val config: TestConfig) : BaseTest() {
+    data class TestConfig(
+        @ViewPager2.Orientation val orientation: Int,
+        val rtl: Boolean,
+        val enableUserInput: Boolean
+    )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun spec(): List<TestConfig> = createTestSet()
+    }
+
+    private val pageCount = 10
+    private lateinit var test: Context
+    private lateinit var adapterProvider: AdapterProvider
+    private lateinit var fakeDragger: PageSwiperFakeDrag
+
+    override fun setUp() {
+        super.setUp()
+        if (config.rtl) {
+            localeUtil.resetLocale()
+            localeUtil.setLocale(LocaleTestUtils.RTL_LANGUAGE)
+        }
+        adapterProvider = viewAdapterProvider(stringSequence(pageCount))
+        test = setUpTest(config.orientation).also {
+            fakeDragger = PageSwiperFakeDrag(it.viewPager)
+            it.viewPager.isUserInputEnabled = config.enableUserInput
+            it.setAdapterSync(adapterProvider)
+            it.assertBasicState(0)
+        }
+    }
+
+    @Test
+    fun test_flingToNextPage() {
+        basicFakeDragTest(.2f, 100, 1)
+    }
+
+    @Test
+    fun test_peekNextPage() {
+        basicFakeDragTest(.1f, 200, 0, DecelerateInterpolator())
+    }
+
+    @Test
+    fun test_flingCompletelyToNextPage() {
+        basicFakeDragTest(1f, 100, 1)
+    }
+
+    @Test
+    fun test_peekNextAndMoveBack() {
+        // Roughly interpolates like this:
+        //   |
+        // 3 |   .-.
+        //   |  /   ',
+        // 1 | /      '-.___
+        //   |/
+        // 0 +--------------
+        //   0             1
+        basicFakeDragTest(.2f, 500, 0, PathInterpolatorCompat.create(Path().also {
+            it.moveTo(0f, 0f)
+            it.cubicTo(.4f, 6f, .5f, 1f, .8f, 1f)
+            it.lineTo(1f, 1f)
+        }))
+    }
+
+    @Test
+    fun test_dragAlmostToNextPageAndFlingBack() {
+        // Roughly interpolates like this:
+        //   |
+        //   |   .-.
+        // 1 |  /   '
+        //   | /
+        //   |/
+        // 0 +-------
+        //   0      1
+        basicFakeDragTest(.7f, 400, 0, PathInterpolatorCompat.create(Path().also {
+            it.moveTo(0f, 0f)
+            it.cubicTo(.4f, 1.3f, .7f, 1.5f, 1f, 1f)
+        }))
+    }
+
+    @Test
+    fun test_startFakeDragDuringManualDrag() {
+        // Skip tests where manual dragging is disabled
+        assumeThat(config.enableUserInput, equalTo(true))
+
+        // start manual drag
+        val latch = test.viewPager.addWaitForStateLatch(DRAGGING)
+        // Perform manual swipe in separate thread, because the SwipeMethod.MANUAL blocks while
+        // injecting events, and we need to interrupt it
+        newSingleThreadExecutor().execute { test.swipeForward(SwipeMethod.MANUAL) }
+        assertThat(latch.await(1, SECONDS), equalTo(true))
+
+        // start fake drag
+        assertThat(test.viewPager.beginFakeDrag(), equalTo(false))
+    }
+
+    @Test
+    fun test_startFakeDragToTargetPageWhileSettling() {
+        // Run the test two times to verify that state doesn't linger
+        repeat(2) {
+            val targetPage = test.viewPager.currentItem + 1
+            startFakeDragWhileSettling(targetPage, .2f, .2f, targetPage)
+        }
+    }
+
+    @Test
+    fun test_startFakeDragExactlyToTargetPageWhileSettling() {
+        // Run the test two times to verify that state doesn't linger
+        repeat(2) {
+            val tracker = PositionTracker().also { test.viewPager.registerOnPageChangeCallback(it) }
+            val targetPage = test.viewPager.currentItem + 1
+            startFakeDragWhileSettling(targetPage, .4f,
+                { targetPage - tracker.lastPosition }, targetPage, true)
+            test.viewPager.unregisterOnPageChangeCallback(tracker)
+        }
+    }
+
+    @Test
+    fun test_startFakeDragToNextPageWhileSettling() {
+        // Run the test two times to verify that state doesn't linger
+        repeat(2) {
+            val targetPage = test.viewPager.currentItem + 1
+            startFakeDragWhileSettling(targetPage, .5f, 1f, targetPage + 1)
+        }
+    }
+
+    @Test
+    fun test_startFakeDragExactlyToNextPageWhileSettling() {
+        // Run the test two times to verify that state doesn't linger
+        repeat(2) {
+            val tracker = PositionTracker().also { test.viewPager.registerOnPageChangeCallback(it) }
+            val targetPage = test.viewPager.currentItem + 1
+            val nextPage = targetPage + 1
+            startFakeDragWhileSettling(targetPage, .5f,
+                { nextPage - tracker.lastPosition }, nextPage, true)
+            test.viewPager.unregisterOnPageChangeCallback(tracker)
+        }
+    }
+
+    @Test
+    fun test_setCurrentItemDuringFakeDrag() {
+        setCurrentItemDuringFakeDrag(false)
+    }
+
+    @Test
+    fun test_smoothScrollDuringFakeDrag() {
+        setCurrentItemDuringFakeDrag(true)
+    }
+
+    @Test
+    fun test_startManualDragDuringFakeDrag() {
+        // Skip tests where manual dragging is disabled
+        assumeThat(config.enableUserInput, equalTo(true))
+
+        // Run the test two times to verify that state doesn't linger
+        repeat(2) {
+            val initialPage = test.viewPager.currentItem
+            val expectedFinalPage = initialPage + 1
+            val recorder = test.viewPager.addNewRecordingCallback()
+
+            // start fake drag
+            val fakeDragLatch = test.viewPager.addWaitForDistanceToTarget(expectedFinalPage, .9f)
+            val idleLatch = test.viewPager.addWaitForIdleLatch()
+            fakeDragger.fakeDrag(.5f, 500)
+            assertThat(fakeDragLatch.await(1, SECONDS), equalTo(true))
+
+            // start manual drag
+            test.swipeForward(SwipeMethod.MANUAL)
+            assertThat(idleLatch.await(2, SECONDS), equalTo(true))
+
+            // test assertions
+            test.assertBasicState(expectedFinalPage)
+            recorder.apply {
+                scrollEvents.assertValueSanity(0, pageCount - 1, test.viewPager.pageSize)
+                assertFirstEvents(DRAGGING)
+                assertLastEvents(expectedFinalPage)
+                assertPageSelectedEvents(initialPage, expectedFinalPage)
+                assertStateChanges(
+                    listOf(DRAGGING, SETTLING, IDLE),
+                    listOf(DRAGGING, IDLE)
+                )
+            }
+
+            test.viewPager.unregisterOnPageChangeCallback(recorder)
+        }
+    }
+
+    private fun basicFakeDragTest(
+        relativeDragDistance: Float,
+        duration: Long,
+        expectedFinalPage: Int,
+        interpolator: Interpolator = LinearInterpolator()
+    ) {
+        val startPage = test.viewPager.currentItem
+        // Run the test two times to verify that state doesn't linger
+        repeat(2) {
+            val initialPage = test.viewPager.currentItem
+            val expectedFinalPageWithOffset = expectedFinalPage + initialPage - startPage
+            val recorder = test.viewPager.addNewRecordingCallback()
+
+            val latch = test.viewPager.addWaitForIdleLatch()
+            fakeDragger.fakeDrag(relativeDragDistance, duration, interpolator)
+            latch.await(2000 + duration, MILLISECONDS)
+
+            // test assertions
+            test.assertBasicState(expectedFinalPageWithOffset)
+            recorder.apply {
+                scrollEvents.assertValueSanity(0, pageCount - 1, test.viewPager.pageSize)
+                assertFirstEvents(DRAGGING)
+                assertLastEvents(expectedFinalPageWithOffset)
+                assertPageSelectedEvents(initialPage, expectedFinalPageWithOffset)
+                assertStateChanges(
+                    listOf(DRAGGING, SETTLING, IDLE),
+                    listOf(DRAGGING, IDLE)
+                )
+            }
+
+            test.viewPager.unregisterOnPageChangeCallback(recorder)
+        }
+    }
+
+    private fun startFakeDragWhileSettling(
+        settleTarget: Int,
+        settleDistance: Float,
+        dragDistance: Float,
+        expectedFinalPage: Int
+    ) {
+        startFakeDragWhileSettling(settleTarget, settleDistance,
+            { dragDistance }, expectedFinalPage, false)
+    }
+
+    private fun startFakeDragWhileSettling(
+        settleTarget: Int,
+        settleDistance: Float,
+        dragDistance: () -> Float,
+        expectedFinalPage: Int,
+        fakeDragMustEndSnapped: Boolean
+    ) {
+        val initialPage = test.viewPager.currentItem
+        val recorder = test.viewPager.addNewRecordingCallback()
+
+        // start smooth scroll
+        val threshold = 1f - settleDistance
+        val scrollLatch = test.viewPager.addWaitForDistanceToTarget(settleTarget, threshold)
+        test.runOnUiThread { test.viewPager.setCurrentItem(settleTarget, true) }
+        assertThat(scrollLatch.await(1, SECONDS), equalTo(true))
+
+        // start fake drag
+        val idleLatch = test.viewPager.addWaitForIdleLatch()
+        fakeDragger.fakeDrag(dragDistance(), 200)
+        assertThat(idleLatch.await(2, SECONDS), equalTo(true))
+
+        // test assertions
+        test.assertBasicState(expectedFinalPage)
+        recorder.apply {
+            scrollEvents.assertValueSanity(0, pageCount - 1, test.viewPager.pageSize)
+            assertFirstEvents(SETTLING)
+            assertLastEvents(expectedFinalPage)
+            assertPageSelectedEvents(initialPage, settleTarget, expectedFinalPage)
+            if (fakeDragMustEndSnapped) {
+                assertThat("When a fake drag should end in a snapped position, we expect the last" +
+                        " scroll event after the FAKE_DRAG event to be snapped. ${dumpEvents()}",
+                    expectSettlingAfterState(DRAGGING), equalTo(false))
+            }
+            assertStateChanges(
+                listOf(SETTLING, DRAGGING, SETTLING, IDLE),
+                listOf(SETTLING, DRAGGING, IDLE)
+            )
+        }
+
+        test.viewPager.unregisterOnPageChangeCallback(recorder)
+    }
+
+    private fun setCurrentItemDuringFakeDrag(smoothScroll: Boolean) {
+        val initialPage = test.viewPager.currentItem
+        // start fake drag
+        val latch = test.viewPager.addWaitForStateLatch(DRAGGING)
+        fakeDragger.fakeDrag(.5f, 500)
+        assertThat(latch.await(1, SECONDS), equalTo(true))
+
+        // start smooth scroll
+        doIllegalAction("Cannot change current item when ViewPager2 is fake dragging") {
+            test.viewPager.setCurrentItem(initialPage + 1, smoothScroll)
+        }
+    }
+
+    private fun doIllegalAction(errorMessage: String, action: () -> Unit) {
+        val executionLatch = CountDownLatch(1)
+        var exception: IllegalStateException? = null
+        test.runOnUiThread {
+            try {
+                action()
+            } catch (e: IllegalStateException) {
+                exception = e
+            } finally {
+                executionLatch.countDown()
+            }
+        }
+        assertThat(executionLatch.await(1, SECONDS), equalTo(true))
+        assertThat(exception, notNullValue())
+        assertThat(exception!!.message, equalTo(errorMessage))
+    }
+
+    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 allEvents get() = events.toList()
+        val scrollEvents get() = events.mapNotNull { it as? OnPageScrolledEvent }
+        val stateEvents get() = events.mapNotNull { it as? OnPageScrollStateChangedEvent }
+        val selectEvents get() = events.mapNotNull { it as? OnPageSelectedEvent }
+
+        val eventCount get() = events.size
+        val firstEvent get() = events.firstOrNull()
+        val lastEvent get() = events.lastOrNull()
+
+        override fun onPageScrolled(
+            position: Int,
+            positionOffset: Float,
+            positionOffsetPixels: Int
+        ) {
+            synchronized(events) {
+                events.add(OnPageScrolledEvent(position, positionOffset, positionOffsetPixels))
+            }
+        }
+
+        override fun onPageSelected(position: Int) {
+            synchronized(events) {
+                events.add(OnPageSelectedEvent(position))
+            }
+        }
+
+        override fun onPageScrollStateChanged(state: Int) {
+            synchronized(events) {
+                events.add(OnPageScrollStateChangedEvent(state))
+            }
+        }
+
+        fun expectSettlingAfterState(state: Int): Boolean {
+            val changeToStateEvent = OnPageScrollStateChangedEvent(state)
+            val lastScrollEvent = events
+                .dropWhile { it != changeToStateEvent }
+                .dropWhile { it !is OnPageScrolledEvent }
+                .takeWhile { it is OnPageScrolledEvent }
+                .lastOrNull() as? OnPageScrolledEvent
+            return lastScrollEvent?.let { it.positionOffsetPixels != 0 } ?: true
+        }
+
+        fun dumpEvents(): String {
+            return events.joinToString("\n- ", "\n(${scrollStateGlossary()})\n- ")
+        }
+    }
+
+    private fun RecordingCallback.assertFirstEvents(expectedFirstState: Int) {
+        assertThat("There should be events", eventCount, greaterThan(0))
+        assertThat("First event should be state change to " +
+                "${scrollStateToString(expectedFirstState)}: ${dumpEvents()}",
+            firstEvent, equalTo(OnPageScrollStateChangedEvent(expectedFirstState) as Event))
+    }
+
+    private fun RecordingCallback.assertLastEvents(expectedFinalPage: Int) {
+        assertThat("Last event should be state change to IDLE: ${dumpEvents()}",
+            lastEvent, equalTo(OnPageScrollStateChangedEvent(IDLE) as Event))
+        assertThat("Scroll events don't end in snapped position: ${dumpEvents()}",
+            scrollEvents.last().positionOffsetPixels, equalTo(0))
+        assertThat("Scroll events don't end at page $expectedFinalPage: ${dumpEvents()}",
+            scrollEvents.last().position, equalTo(expectedFinalPage))
+    }
+
+    private fun RecordingCallback.assertPageSelectedEvents(vararg visitedPages: Int) {
+        val expectedPageSelects = visitedPages.toList().zipWithNext().mapNotNull { pair ->
+            // If visited page is same as previous page, no page selected event should be fired
+            if (pair.first == pair.second) null else pair.second
+        }
+        assertThat("Sequence of selected pages should be $expectedPageSelects: ${dumpEvents()}",
+            selectEvents.map { it.position }, equalTo(expectedPageSelects))
+
+        val settleEvent = OnPageScrollStateChangedEvent(SETTLING)
+        val idleEvent = OnPageScrollStateChangedEvent(IDLE)
+        val events = allEvents
+        events.forEachIndexed { i, event ->
+            if (event is OnPageSelectedEvent) {
+                assertThat("OnPageSelectedEvents cannot be the first or last event: " +
+                        dumpEvents(), i, isBetweenInEx(1, eventCount - 1))
+                val isAfterSettleEvent = events[i - 1] == settleEvent
+                val isBeforeIdleEvent = events[i + 1] == idleEvent
+                assertThat("OnPageSelectedEvent at index $i must follow a SETTLE event or precede" +
+                        " an IDLE event, but not both: ${dumpEvents()}",
+                    isAfterSettleEvent.xor(isBeforeIdleEvent), equalTo(true))
+            }
+        }
+    }
+
+    private fun RecordingCallback.assertStateChanges(
+        statesWithSettling: List<Int>,
+        statesWithoutSettling: List<Int>
+    ) {
+        assertThat(
+            "Unexpected sequence of state changes:" + dumpEvents(),
+            stateEvents.map { it.state },
+            equalTo(
+                if (expectSettlingAfterState(DRAGGING)) {
+                    statesWithSettling
+                } else {
+                    statesWithoutSettling
+                }
+            )
+        )
+    }
+
+    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 class PositionTracker : ViewPager2.OnPageChangeCallback() {
+        var lastPosition = 0f
+        override fun onPageScrolled(position: Int, offset: Float, offsetPx: Int) {
+            lastPosition = position + offset
+        }
+    }
+}
+
+private fun createTestSet(): List<TestConfig> {
+    return listOf(ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL).flatMap { orientation ->
+        listOf(false, true).flatMap { rtl ->
+            listOf(true, false).map { enableUserInput ->
+                TestConfig(orientation, rtl, enableUserInput)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
index b021caf..e1e7909 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
@@ -25,6 +25,7 @@
 import androidx.testutils.PollingCheck
 import androidx.viewpager.widget.ViewPager
 import androidx.viewpager2.LocaleTestUtils
+import androidx.viewpager2.widget.BaseTest.Context.SwipeMethod
 import androidx.viewpager2.widget.PageChangeCallbackTest.Event.MarkerEvent
 import androidx.viewpager2.widget.PageChangeCallbackTest.Event.OnPageScrollStateChangedEvent
 import androidx.viewpager2.widget.PageChangeCallbackTest.Event.OnPageScrolledEvent
@@ -46,6 +47,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
+import java.util.concurrent.Executors.newSingleThreadExecutor
 import java.util.concurrent.TimeUnit.MILLISECONDS
 import java.util.concurrent.TimeUnit.SECONDS
 import java.util.concurrent.atomic.AtomicBoolean
@@ -731,6 +733,52 @@
     }
 
     @Test
+    fun test_getScrollState() {
+        val test = setUpTest(config.orientation)
+        test.setAdapterSync(viewAdapterProvider(stringSequence(5)))
+
+        // Test SCROLL_STATE_SETTLING
+        test_getScrollState(test, SCROLL_STATE_SETTLING, 1) {
+            test.runOnUiThread { test.viewPager.setCurrentItem(1, true) }
+        }
+
+        // Test SCROLL_STATE_DRAGGING (real drag)
+        test_getScrollState(test, SCROLL_STATE_DRAGGING, 2, true) {
+            // Perform manual swipe in separate thread, because the SwipeMethod.MANUAL blocks while
+            // injecting events, and we need to check getScrollState() during the swipe.
+            newSingleThreadExecutor().execute { test.swipeForward(SwipeMethod.MANUAL) }
+        }
+
+        // Test SCROLL_STATE_DRAGGING (fake drag)
+        test_getScrollState(test, SCROLL_STATE_DRAGGING, 3, true) {
+            test.swipeForward(SwipeMethod.FAKE_DRAG)
+        }
+    }
+
+    private fun test_getScrollState(
+        test: Context,
+        @ViewPager2.ScrollState state: Int,
+        expectedTargetPage: Int,
+        checkSettling: Boolean = false,
+        viewPagerAction: () -> Unit
+    ) {
+        val stateLatch = test.viewPager.addWaitForStateLatch(state)
+        val settlingLatch = test.viewPager.addWaitForStateLatch(SCROLL_STATE_SETTLING)
+        val idleLatch = test.viewPager.addWaitForIdleLatch()
+        viewPagerAction()
+        // Wait for onScrollStateChanged
+        assertThat(stateLatch.await(1, SECONDS), equalTo(true))
+        // Check scrollState
+        assertThat(test.viewPager.scrollState, equalTo(state))
+        if (checkSettling) {
+            assertThat(settlingLatch.await(2, SECONDS), equalTo(true))
+        }
+        // Let the animation finish
+        assertThat(idleLatch.await(2, SECONDS), equalTo(true))
+        test.assertBasicState(expectedTargetPage)
+    }
+
+    @Test
     fun test_setCurrentItem_noAdapter() {
         val test = setUpTest(config.orientation)
         assertThat(test.viewPager.adapter, nullValue())
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/ManualSwipeInjector.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/ManualSwipeInjector.java
index 4a0f586..e041112 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/ManualSwipeInjector.java
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/ManualSwipeInjector.java
@@ -29,9 +29,14 @@
 import java.util.List;
 
 /**
- * Performs a swipe on a view from the center of that view to on of its edges.
+ * Performs a swipe on a view from the center of that view to on of its edges. Mostly the same as
+ * Espresso's swipe ViewActions, but since this is not a ViewAction, it is not performed on the UI
+ * thread. It is still synchronous though, with sleeps between the injection of each MotionEvent. If
+ * you need asynchronous injection, run it in a separate thread. Another difference is that this
+ * injector swipes from the center of the targeted View to the center of an edge, instead of from
+ * the center of one edge to the center of another edge.
  *
- * Obtain a new instance of this class for each swipe you want to perform, with one of the {@link
+ * <p>Obtain a new instance of this class for each swipe you want to perform, with one of the {@link
  * #swipeLeft() swipe methods}. Inject the motion events by calling {@link #perform(Instrumentation,
  * View)}.
  */
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageSwiperFakeDrag.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageSwiperFakeDrag.kt
new file mode 100644
index 0000000..c08b9cb
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageSwiperFakeDrag.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.swipe
+
+import android.os.SystemClock
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.Interpolator
+import android.view.animation.LinearInterpolator
+import androidx.core.view.ViewCompat
+import androidx.viewpager2.widget.ViewPager2
+import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+class PageSwiperFakeDrag(private val viewPager: ViewPager2) : PageSwiper {
+    companion object {
+        // 60 fps
+        private const val FRAME_LENGTH_MS = 1000L / 60
+        private const val FLING_DURATION_MS = 100L
+    }
+
+    private val ViewPager2.pageSize: Int
+        get() {
+            return if (orientation == ORIENTATION_HORIZONTAL) {
+                measuredWidth - paddingLeft - paddingRight
+            } else {
+                measuredHeight - paddingTop - paddingBottom
+            }
+        }
+
+    private val needsRtlModifier
+        get() = viewPager.orientation == ORIENTATION_HORIZONTAL &&
+                ViewCompat.getLayoutDirection(viewPager) == ViewCompat.LAYOUT_DIRECTION_RTL
+
+    override fun swipeNext() {
+        fakeDrag(.5f, interpolator = AccelerateInterpolator())
+    }
+
+    override fun swipePrevious() {
+        fakeDrag(-.5f, interpolator = AccelerateInterpolator())
+    }
+
+    fun fakeDrag(
+        relativeDragDistance: Float,
+        duration: Long = FLING_DURATION_MS,
+        interpolator: Interpolator = LinearInterpolator()
+    ) {
+        // Generate the deltas to feed to fakeDragBy()
+        val rtlModifier = if (needsRtlModifier) -1 else 1
+        val steps = max(1, (duration / FRAME_LENGTH_MS.toFloat()).roundToInt())
+        val distancePx = viewPager.pageSize * -relativeDragDistance * rtlModifier
+        val deltas = List(steps) { i ->
+            val currDistance = interpolator.getInterpolation((i + 1f) / steps) * distancePx
+            val prevDistance = interpolator.getInterpolation((i + 0f) / steps) * distancePx
+            currDistance - prevDistance
+        }
+
+        // Send the fakeDrag events
+        var eventTime = SystemClock.uptimeMillis()
+        val delayMs = { eventTime - SystemClock.uptimeMillis() }
+        viewPager.post { viewPager.beginFakeDrag() }
+        for (delta in deltas) {
+            eventTime += FRAME_LENGTH_MS
+            viewPager.postDelayed({ viewPager.fakeDragBy(delta) }, delayMs())
+        }
+        eventTime++
+        viewPager.postDelayed({ viewPager.endFakeDrag() }, delayMs())
+    }
+}
\ No newline at end of file
diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/FakeDrag.java b/viewpager2/src/main/java/androidx/viewpager2/widget/FakeDrag.java
new file mode 100644
index 0000000..ac9d3c4
--- /dev/null
+++ b/viewpager2/src/main/java/androidx/viewpager2/widget/FakeDrag.java
@@ -0,0 +1,138 @@
+/*
+ * 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 static androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL;
+
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.UiThread;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Provides fake dragging functionality to {@link ViewPager2}.
+ */
+final class FakeDrag {
+    private final ViewPager2 mViewPager;
+    private final ScrollEventAdapter mScrollEventAdapter;
+    private final RecyclerView mRecyclerView;
+
+    private VelocityTracker mVelocityTracker;
+    private int mMaximumVelocity;
+    private float mRequestedDragDistance;
+    private int mActualDraggedDistance;
+    private long mFakeDragBeginTime;
+
+    FakeDrag(ViewPager2 viewPager, ScrollEventAdapter scrollEventAdapter,
+            RecyclerView recyclerView) {
+        mViewPager = viewPager;
+        mScrollEventAdapter = scrollEventAdapter;
+        mRecyclerView = recyclerView;
+    }
+
+    boolean isFakeDragging() {
+        return mScrollEventAdapter.isFakeDragging();
+    }
+
+    @UiThread
+    boolean beginFakeDrag() {
+        if (mScrollEventAdapter.isDragging()) {
+            return false;
+        }
+        mRequestedDragDistance = mActualDraggedDistance = 0;
+        mFakeDragBeginTime = SystemClock.uptimeMillis();
+        beginFakeVelocityTracker();
+
+        mScrollEventAdapter.notifyBeginFakeDrag();
+        if (!mScrollEventAdapter.isIdle()) {
+            // Stop potentially running settling animation
+            mRecyclerView.stopScroll();
+        }
+        addFakeMotionEvent(mFakeDragBeginTime, MotionEvent.ACTION_DOWN, 0, 0);
+        return true;
+    }
+
+    @UiThread
+    boolean fakeDragBy(float offsetPxFloat) {
+        if (!mScrollEventAdapter.isFakeDragging()) {
+            // Can happen legitimately if user started dragging during fakeDrag and app is still
+            // sending fakeDragBy commands
+            return false;
+        }
+        // Subtract the offset, because content scrolls in the opposite direction of finger motion
+        mRequestedDragDistance -= offsetPxFloat;
+        // Calculate amount of pixels to scroll ...
+        int offsetPx = Math.round(mRequestedDragDistance - mActualDraggedDistance);
+        // ... and keep track of pixels scrolled so we don't get rounding errors
+        mActualDraggedDistance += offsetPx;
+        long time = SystemClock.uptimeMillis();
+
+        boolean isHorizontal = mViewPager.getOrientation() == ORIENTATION_HORIZONTAL;
+        // Scroll deltas use pixels:
+        final int offsetX = isHorizontal ? offsetPx : 0;
+        final int offsetY = isHorizontal ? 0 : offsetPx;
+        // Motion events get the raw float distance:
+        final float x = isHorizontal ? mRequestedDragDistance : 0;
+        final float y = isHorizontal ? 0 : mRequestedDragDistance;
+
+        mRecyclerView.scrollBy(offsetX, offsetY);
+        addFakeMotionEvent(time, MotionEvent.ACTION_MOVE, x, y);
+        return true;
+    }
+
+    @UiThread
+    boolean endFakeDrag() {
+        if (!mScrollEventAdapter.isFakeDragging()) {
+            // Happens legitimately if user started dragging during fakeDrag
+            return false;
+        }
+
+        mScrollEventAdapter.notifyEndFakeDrag();
+
+        // Compute the velocity of the fake drag
+        final int pixelsPerSecond = 1000;
+        final VelocityTracker velocityTracker = mVelocityTracker;
+        velocityTracker.computeCurrentVelocity(pixelsPerSecond, mMaximumVelocity);
+        int xVelocity = (int) velocityTracker.getXVelocity();
+        int yVelocity = (int) velocityTracker.getYVelocity();
+        // And fling or snap the ViewPager2 to its destination
+        if (!mRecyclerView.fling(xVelocity, yVelocity)) {
+            // Velocity too low, trigger snap to page manually
+            mViewPager.snapToPage();
+        }
+        return true;
+    }
+
+    private void beginFakeVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+            final ViewConfiguration configuration = ViewConfiguration.get(mViewPager.getContext());
+            mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void addFakeMotionEvent(long time, int action, float x, float y) {
+        final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, action, x, y, 0);
+        mVelocityTracker.addMovement(ev);
+        ev.recycle();
+    }
+}
diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java b/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java
index f493cb4..3e2b758 100644
--- a/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java
+++ b/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java
@@ -54,7 +54,7 @@
 
     @Retention(SOURCE)
     @IntDef({STATE_IDLE, STATE_IN_PROGRESS_MANUAL_DRAG, STATE_IN_PROGRESS_SMOOTH_SCROLL,
-            STATE_IN_PROGRESS_IMMEDIATE_SCROLL})
+            STATE_IN_PROGRESS_IMMEDIATE_SCROLL, STATE_IN_PROGRESS_FAKE_DRAG})
     private @interface AdapterState {
     }
 
@@ -62,6 +62,7 @@
     private static final int STATE_IN_PROGRESS_MANUAL_DRAG = 1;
     private static final int STATE_IN_PROGRESS_SMOOTH_SCROLL = 2;
     private static final int STATE_IN_PROGRESS_IMMEDIATE_SCROLL = 3;
+    private static final int STATE_IN_PROGRESS_FAKE_DRAG = 4;
 
     private static final int NO_POSITION = -1;
 
@@ -76,6 +77,7 @@
     private int mTarget;
     private boolean mDispatchSelected;
     private boolean mScrollHappened;
+    private boolean mFakeDragging;
 
     ScrollEventAdapter(@NonNull LinearLayoutManager layoutManager) {
         mLayoutManager = layoutManager;
@@ -91,6 +93,7 @@
         mTarget = NO_POSITION;
         mDispatchSelected = false;
         mScrollHappened = false;
+        mFakeDragging = false;
     }
 
     /**
@@ -102,26 +105,13 @@
         // User started a drag (not dragging -> dragging)
         if (mAdapterState != STATE_IN_PROGRESS_MANUAL_DRAG
                 && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
-            // Remember we're performing a drag
-            mAdapterState = STATE_IN_PROGRESS_MANUAL_DRAG;
-            if (mTarget != NO_POSITION) {
-                // Target was set means programmatic scroll was in progress
-                // Update "drag start page" to reflect the page that ViewPager2 thinks it is at
-                mDragStartPosition = mTarget;
-                // Reset target because drags have no target until released
-                mTarget = NO_POSITION;
-            } else {
-                // ViewPager2 was at rest, set "drag start page" to current page
-                mDragStartPosition = getPosition();
-            }
-            dispatchStateChanged(SCROLL_STATE_DRAGGING);
+            startDrag(false);
             return;
         }
 
         // Drag is released, RecyclerView is snapping to page (dragging -> settling)
         // Note that mAdapterState is not updated, to remember we were dragging when settling
-        if (mAdapterState == STATE_IN_PROGRESS_MANUAL_DRAG
-                && newState == RecyclerView.SCROLL_STATE_SETTLING) {
+        if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_SETTLING) {
             // Only go through the settling phase if the drag actually moved the page
             if (mScrollHappened) {
                 dispatchStateChanged(SCROLL_STATE_SETTLING);
@@ -132,8 +122,7 @@
         }
 
         // Drag is finished (dragging || settling -> idle)
-        if (mAdapterState == STATE_IN_PROGRESS_MANUAL_DRAG
-                && newState == RecyclerView.SCROLL_STATE_IDLE) {
+        if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_IDLE) {
             boolean dispatchIdle = false;
             updateScrollEventValues();
             if (!mScrollHappened) {
@@ -169,7 +158,7 @@
     @Override
     public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
         mScrollHappened = true;
-        ScrollEventValues values = updateScrollEventValues();
+        updateScrollEventValues();
 
         if (mDispatchSelected) {
             // Drag started settling, need to calculate target page and dispatch onPageSelected now
@@ -178,19 +167,19 @@
 
             // "&& values.mOffsetPx != 0": filters special case where we're scrolling forward and
             // the first scroll event after settling already got us at the target
-            mTarget = scrollingForward && values.mOffsetPx != 0
-                    ? values.mPosition + 1 : values.mPosition;
+            mTarget = scrollingForward && mScrollValues.mOffsetPx != 0
+                    ? mScrollValues.mPosition + 1 : mScrollValues.mPosition;
             if (mDragStartPosition != mTarget) {
                 dispatchSelected(mTarget);
             }
         }
 
-        dispatchScrolled(values.mPosition, values.mOffset, values.mOffsetPx);
+        dispatchScrolled(mScrollValues.mPosition, mScrollValues.mOffset, mScrollValues.mOffsetPx);
 
         // Dispatch idle in onScrolled instead of in onScrollStateChanged because RecyclerView
         // doesn't send IDLE event when using setCurrentItem(x, false)
-        if ((values.mPosition == mTarget || mTarget == NO_POSITION) && values.mOffsetPx == 0
-                && mScrollState != SCROLL_STATE_DRAGGING) {
+        if ((mScrollValues.mPosition == mTarget || mTarget == NO_POSITION)
+                && mScrollValues.mOffsetPx == 0 && !(mScrollState == SCROLL_STATE_DRAGGING)) {
             // When the target page is reached and the user is not dragging anymore, we're settled,
             // so go to idle.
             // Special case and a bit of a hack when mTarget == NO_POSITION: RecyclerView is being
@@ -206,16 +195,18 @@
      * Calculates the current position and the offset (as a percentage and in pixels) of that
      * position from the center.
      */
-    private ScrollEventValues updateScrollEventValues() {
+    private void updateScrollEventValues() {
         ScrollEventValues values = mScrollValues;
 
         values.mPosition = mLayoutManager.findFirstVisibleItemPosition();
         if (values.mPosition == RecyclerView.NO_POSITION) {
-            return values.reset();
+            values.reset();
+            return;
         }
         View firstVisibleView = mLayoutManager.findViewByPosition(values.mPosition);
         if (firstVisibleView == null) {
-            return values.reset();
+            values.reset();
+            return;
         }
 
         // TODO(123350297): automated test for this
@@ -244,7 +235,22 @@
                     + "positive amount, not by %d", values.mOffsetPx));
         }
         values.mOffset = sizePx == 0 ? 0 : (float) values.mOffsetPx / sizePx;
-        return values;
+    }
+
+    private void startDrag(boolean isFakeDrag) {
+        mFakeDragging = isFakeDrag;
+        mAdapterState = isFakeDrag ? STATE_IN_PROGRESS_FAKE_DRAG : STATE_IN_PROGRESS_MANUAL_DRAG;
+        if (mTarget != NO_POSITION) {
+            // Target was set means programmatic scroll was in progress
+            // Update "drag start page" to reflect the page that ViewPager2 thinks it is at
+            mDragStartPosition = mTarget;
+            // Reset target because drags have no target until released
+            mTarget = NO_POSITION;
+        } else {
+            // ViewPager2 was at rest, set "drag start page" to current page
+            mDragStartPosition = getPosition();
+        }
+        dispatchStateChanged(SCROLL_STATE_DRAGGING);
     }
 
     /**
@@ -263,7 +269,7 @@
     }
 
     /**
-     * Let the adapter know that mCurrentItem was restored in onRestoreInstanceState
+     * Let the adapter know that mCurrentItem was restored in onRestoreInstanceState.
      */
     void notifyRestoreCurrentItem(int currentItem) {
         // Don't send page selected event for page 0 for consistency with ViewPager
@@ -272,6 +278,37 @@
         }
     }
 
+    /**
+     * Let the adapter know that a fake drag has started.
+     */
+    void notifyBeginFakeDrag() {
+        mAdapterState = STATE_IN_PROGRESS_FAKE_DRAG;
+        startDrag(true);
+    }
+
+    /**
+     * Let the adapter know that a fake drag has ended.
+     */
+    void notifyEndFakeDrag() {
+        if (isDragging() && !mFakeDragging) {
+            // Real drag has already taken over, no need to post process the fake drag
+            return;
+        }
+        mFakeDragging = false;
+        updateScrollEventValues();
+        if (mScrollValues.mOffsetPx == 0) {
+            // We're snapped, so dispatch an IDLE event
+            if (mScrollValues.mPosition != mDragStartPosition) {
+                dispatchSelected(mScrollValues.mPosition);
+            }
+            dispatchStateChanged(SCROLL_STATE_IDLE);
+            resetState();
+        } else {
+            // We're not snapped, so dispatch a SETTLING event
+            dispatchStateChanged(SCROLL_STATE_SETTLING);
+        }
+    }
+
     private boolean isLayoutRTL() {
         return mLayoutManager.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
     }
@@ -280,11 +317,41 @@
         mCallback = callback;
     }
 
+    int getScrollState() {
+        return mScrollState;
+    }
+
     /**
-     * @return true if there is no known scroll in progress
+     * @return {@code true} if there is no known scroll in progress
      */
     boolean isIdle() {
-        return mAdapterState == STATE_IDLE;
+        return mScrollState == SCROLL_STATE_IDLE;
+    }
+
+    /**
+     * @return {@code true} if the ViewPager2 is being dragged. Returns {@code false} from the
+     *         moment the ViewPager2 starts settling or goes idle.
+     */
+    boolean isDragging() {
+        return mScrollState == SCROLL_STATE_DRAGGING;
+    }
+
+    /**
+     * @return {@code true} if a fake drag is ongoing. Returns {@code false} from the moment the
+     *         {@link ViewPager2#endFakeDrag()} is called.
+     */
+    boolean isFakeDragging() {
+        return mFakeDragging;
+    }
+
+    /**
+     * Checks if the adapter state (not the scroll state) is in the manual or fake dragging state.
+     * @return {@code true} if {@link #mAdapterState} is either {@link
+     *         #STATE_IN_PROGRESS_MANUAL_DRAG} or {@link #STATE_IN_PROGRESS_FAKE_DRAG}
+     */
+    private boolean isInAnyDraggingState() {
+        return mAdapterState == STATE_IN_PROGRESS_MANUAL_DRAG
+                || mAdapterState == STATE_IN_PROGRESS_FAKE_DRAG;
     }
 
     /**
@@ -346,11 +413,10 @@
         ScrollEventValues() {
         }
 
-        ScrollEventValues reset() {
+        void reset() {
             mPosition = RecyclerView.NO_POSITION;
             mOffset = 0f;
             mOffsetPx = 0;
-            return this;
         }
     }
 }
diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
index 9b3cb41..13a3cfd 100644
--- a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
+++ b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
@@ -86,7 +86,9 @@
     int mCurrentItem;
     private RecyclerView mRecyclerView;
     private LinearLayoutManager mLayoutManager;
+    private PagerSnapHelper mPagerSnapHelper;
     private ScrollEventAdapter mScrollEventAdapter;
+    private FakeDrag mFakeDragger;
     private PageTransformerAdapter mPageTransformerAdapter;
     private CompositeOnPageChangeCallback mPageChangeEventDispatcher;
     private boolean mUserInputEnabled = true;
@@ -124,9 +126,16 @@
         mRecyclerView.setLayoutParams(
                 new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
         mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());
-        new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
 
+        // Create ScrollEventAdapter before attaching PagerSnapHelper to RecyclerView, because the
+        // attach process calls PagerSnapHelperImpl.findSnapView, which uses the mScrollEventAdapter
         mScrollEventAdapter = new ScrollEventAdapter(mLayoutManager);
+        // Create FakeDrag before attaching PagerSnapHelper, same reason as above
+        mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
+        mPagerSnapHelper = new PagerSnapHelperImpl();
+        mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
+        // Add mScrollEventAdapter after attaching mPagerSnapHelper to mRecyclerView, because we
+        // don't want to respond on the events sent out during the attach process
         mRecyclerView.addOnScrollListener(mScrollEventAdapter);
 
         mPageChangeEventDispatcher = new CompositeOnPageChangeCallback(3);
@@ -421,6 +430,10 @@
      * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
      */
     public void setCurrentItem(int item, boolean smoothScroll) {
+        if (isFakeDragging()) {
+            throw new IllegalStateException("Cannot change current item when ViewPager2 is fake "
+                    + "dragging");
+        }
         Adapter adapter = getAdapter();
         if (adapter == null || adapter.getItemCount() <= 0) {
             return;
@@ -473,6 +486,109 @@
     }
 
     /**
+     * Returns the current scroll state of the ViewPager2. Returned value is one of can be one of
+     * {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
+     *
+     * @return The scroll state that was last dispatched to {@link
+     *         OnPageChangeCallback#onPageScrollStateChanged(int)}
+     */
+    @ScrollState
+    public int getScrollState() {
+        return mScrollEventAdapter.getScrollState();
+    }
+
+    /**
+     * Start a fake drag of the pager.
+     *
+     * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager2 with the
+     * touch scrolling of another view, while still letting the ViewPager2 control the snapping
+     * motion and fling behavior. (e.g. parallax-scrolling tabs.) Call {@link #fakeDragBy(float)} to
+     * simulate the actual drag motion. Call {@link #endFakeDrag()} to complete the fake drag and
+     * fling as necessary.
+     *
+     * <p>A fake drag can be interrupted by a real drag. From that point on, all calls to {@code
+     * fakeDragBy} and {@code endFakeDrag} will be ignored until the next fake drag is started by
+     * calling {@code beginFakeDrag}. If you need the ViewPager2 to ignore touch events and other
+     * user input during a fake drag, use {@link #setUserInputEnabled(boolean)}. If a real or fake
+     * drag is already in progress, this method will return {@code false}.
+     *
+     * @return {@code true} if the fake drag began successfully, {@code false} if it could not be
+     *         started
+     *
+     * @see #fakeDragBy(float)
+     * @see #endFakeDrag()
+     * @see #isFakeDragging()
+     */
+    public boolean beginFakeDrag() {
+        return mFakeDragger.beginFakeDrag();
+    }
+
+    /**
+     * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. Drag
+     * happens in the direction of the orientation. Positive offsets will drag to the previous page,
+     * negative values to the next page, with one exception: if layout direction is set to RTL and
+     * the ViewPager2's orientation is horizontal, then the behavior will be inverted. This matches
+     * the deltas of touch events that would cause the same real drag.
+     *
+     * <p>If the pager is not in the fake dragging state anymore, it ignores this call and returns
+     * {@code false}.
+     *
+     * @param offsetPxFloat Offset in pixels to drag by
+     * @return {@code true} if the fake drag was executed. If {@code false} is returned, it means
+     *         there was no fake drag to end.
+     *
+     * @see #beginFakeDrag()
+     * @see #endFakeDrag()
+     * @see #isFakeDragging()
+     */
+    public boolean fakeDragBy(float offsetPxFloat) {
+        return mFakeDragger.fakeDragBy(offsetPxFloat);
+    }
+
+    /**
+     * End a fake drag of the pager.
+     *
+     * @return {@code true} if the fake drag was ended. If {@code false} is returned, it means there
+     *         was no fake drag to end.
+     *
+     * @see #beginFakeDrag()
+     * @see #fakeDragBy(float)
+     * @see #isFakeDragging()
+     */
+    public boolean endFakeDrag() {
+        return mFakeDragger.endFakeDrag();
+    }
+
+    /**
+     * Returns {@code true} if a fake drag is in progress.
+     *
+     * @return {@code true} if currently in a fake drag, {@code false} otherwise.
+     * @see #beginFakeDrag()
+     * @see #fakeDragBy(float)
+     * @see #endFakeDrag()
+     */
+    public boolean isFakeDragging() {
+        return mFakeDragger.isFakeDragging();
+    }
+
+    /**
+     * Snaps the ViewPager2 to the closest page
+     */
+    void snapToPage() {
+        // Method copied from PagerSnapHelper#snapToTargetExistingView
+        // When fixing something here, make sure to update that method as well
+        View view = mPagerSnapHelper.findSnapView(mLayoutManager);
+        if (view == null) {
+            return;
+        }
+        int[] snapDistance = mPagerSnapHelper.calculateDistanceToFinalSnap(mLayoutManager, view);
+        //noinspection ConstantConditions
+        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
+            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
+        }
+    }
+
+    /**
      * Enable or disable user initiated scrolling. This includes touch input (scroll and fling
      * gestures) and accessibility input. Disabling keyboard input is not yet supported. When user
      * initiated scrolling is disabled, programmatic scrolls through {@link #setCurrentItem(int,
@@ -599,6 +715,21 @@
         }
     }
 
+    private class PagerSnapHelperImpl extends PagerSnapHelper {
+        PagerSnapHelperImpl() {
+        }
+
+        @Nullable
+        @Override
+        public View findSnapView(RecyclerView.LayoutManager layoutManager) {
+            // When interrupting a smooth scroll with a fake drag, we stop RecyclerView's scroll
+            // animation, which fires a scroll state change to IDLE. PagerSnapHelper then kicks in
+            // to snap to a page, which we need to prevent here.
+            // Simplifying that case: during a fake drag, no snapping should occur.
+            return isFakeDragging() ? null : super.findSnapView(layoutManager);
+        }
+    }
+
     private static class SmoothScrollToPosition implements Runnable {
         private final int mPosition;
         private final RecyclerView mRecyclerView;
@@ -641,9 +772,10 @@
         }
 
         /**
-         * Called when the scroll state changes. Useful for discovering when the user
-         * begins dragging, when the pager is automatically settling to the current page,
-         * or when it is fully stopped/idle.
+         * Called when the scroll state changes. Useful for discovering when the user begins
+         * dragging, when a fake drag is started, when the pager is automatically settling to the
+         * current page, or when it is fully stopped/idle. {@code state} can be one of {@link
+         * #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
          */
         public void onPageScrollStateChanged(@ScrollState int state) {
         }