[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);
+ }
+ }
}