[VRR] Support content velocity in RecyclerView

Fixes: 337283684

RecyclerView now calls setFrameContentVelocity when it is scrolling
with OverScroller.

Test: new test
Change-Id: I8f8a4eb527c0d933563d372a7a16c08781e70f57
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index fda546c..d9c0c48 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -57,6 +57,7 @@
     }
 
     defaultConfig {
+        compileSdkPreview "VanillaIceCream"
         multiDexEnabled = true
         testInstrumentationRunner "androidx.recyclerview.test.TestRunner"
         multiDexEnabled true
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
new file mode 100644
index 0000000..5b4c2f7
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 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.
+ */
+@file:Suppress("DEPRECATION")
+
+package androidx.recyclerview.widget
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.TextView
+import androidx.core.os.BuildCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+// TODO: change to VANILLA_ICE_CREAM when it is ready
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(AndroidJUnit4::class)
+class RecyclerViewScrollFrameRateTest {
+    @get:Rule
+    val rule = ActivityTestRule(TestContentViewActivity::class.java)
+
+    @Test
+    fun smoothScrollFrameRateBoost() {
+        // TODO: Remove when VANILLA_ICE_CREAM is ready and the SdkSuppress is modified
+        if (!BuildCompat.isAtLeastV()) {
+            return
+        }
+        val rv = RecyclerView(rule.activity)
+        rule.runOnUiThread {
+            rv.layoutManager =
+                LinearLayoutManager(rule.activity, LinearLayoutManager.VERTICAL, false)
+            rv.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+                override fun onCreateViewHolder(
+                    parent: ViewGroup,
+                    viewType: Int
+                ): RecyclerView.ViewHolder {
+                    val view = TextView(parent.context)
+                    view.textSize = 40f
+                    view.setTextColor(Color.WHITE)
+                    return object : RecyclerView.ViewHolder(view) {}
+                }
+
+                override fun getItemCount(): Int = 10000
+
+                override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+                    val view = holder.itemView as TextView
+                    view.text = "Text $position"
+                    val color = if (position % 2 == 0) Color.BLACK else 0xFF000080.toInt()
+                    view.setBackgroundColor(color)
+                }
+            }
+            rule.activity.contentView.addView(rv)
+        }
+        runOnDraw(rv, { rv.smoothScrollBy(0, 1000) }) {
+            // First Frame
+            assertThat(rv.frameContentVelocity).isGreaterThan(0f)
+        }
+
+        // Second frame
+        runOnDraw(rv) {
+            assertThat(rv.frameContentVelocity).isGreaterThan(0f)
+        }
+
+        // Third frame
+        runOnDraw(rv) {
+            assertThat(rv.frameContentVelocity).isGreaterThan(0f)
+        }
+    }
+
+    private fun runOnDraw(view: View, setup: () -> Unit = { }, onDraw: () -> Unit) {
+        val latch = CountDownLatch(1)
+        val onDrawListener = ViewTreeObserver.OnDrawListener {
+            latch.countDown()
+            onDraw()
+        }
+        rule.runOnUiThread {
+            view.viewTreeObserver.addOnDrawListener(onDrawListener)
+            setup()
+        }
+        assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
+        rule.runOnUiThread {
+            view.viewTreeObserver.removeOnDrawListener(onDrawListener)
+        }
+    }
+}
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 80be2f0..65be0f1 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -68,8 +68,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.core.os.TraceCompat;
 import androidx.core.util.Preconditions;
 import androidx.core.view.AccessibilityDelegateCompat;
@@ -6005,6 +6007,10 @@
                         mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
                     }
                 }
+                if (BuildCompat.isAtLeastV()) {
+                    Api35Impl.setFrameContentVelocity(RecyclerView.this,
+                            Math.abs(scroller.getCurrVelocity()));
+                }
             }
 
             SmoothScroller smoothScroller = mLayout.mSmoothScroller;
@@ -14627,4 +14633,11 @@
         }
         return mScrollingChildHelper;
     }
+
+    @RequiresApi(35)
+    private static final class Api35Impl {
+        public static void setFrameContentVelocity(View view, float velocity) {
+            view.setFrameContentVelocity(velocity);
+        }
+    }
 }