Loading core/java/android/view/View.java +37 −2 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; Loading Loading @@ -5629,7 +5630,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Nullable private ViewTranslationCallback mViewTranslationCallback; private float mFrameContentVelocity = 0; private float mFrameContentVelocity = -1; @Nullable Loading Loading @@ -5660,6 +5661,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, protected long mMinusTwoFrameIntervalMillis = 0; private int mLastFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; private float mLastFrameX = Float.NaN; private float mLastFrameY = Float.NaN; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = Float.NaN; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) Loading Loading @@ -24597,7 +24601,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public void draw(@NonNull Canvas canvas) { final int privateFlags = mPrivateFlags; mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; mFrameContentVelocity = 0; mFrameContentVelocity = -1; mLastFrameX = mLeft + mRenderNode.getTranslationX(); mLastFrameY = mTop + mRenderNode.getTranslationY(); /* * Draw traversal performs several drawing steps which must be executed Loading Loading @@ -33673,6 +33680,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (sToolkitMetricsForFrameRateDecisionFlagValue) { viewRootImpl.recordViewPercentage(sizePercentage); } if (viewVelocityApi()) { float velocity = mFrameContentVelocity; if (velocity < 0f) { velocity = calculateVelocity(); } if (velocity > 0f) { float frameRate = convertVelocityToFrameRate(velocity); viewRootImpl.votePreferredFrameRate(frameRate, FRAME_RATE_COMPATIBILITY_GTE); return; } } if (!Float.isNaN(mPreferredFrameRate)) { if (mPreferredFrameRate < 0) { if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) { Loading @@ -33695,6 +33713,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } private float convertVelocityToFrameRate(float velocityPps) { float density = getResources().getDisplayMetrics().density; float velocityDps = velocityPps / density; // Choose a frame rate in increments of 10fps return Math.min(140f, 60f + (10f * (float) Math.floor(velocityDps / 300f))); } private float calculateVelocity() { // This current calculation is very simple. If something on the screen moved, then // it votes for the highest velocity. If it doesn't move, then return 0. float x = mLeft + mRenderNode.getTranslationX(); float y = mTop + mRenderNode.getTranslationY(); return (!Float.isNaN(mLastFrameX) && (x != mLastFrameX || y != mLastFrameY)) ? 100_000f : 0f; } /** * Set the current velocity of the View, we only track positive value. * We will use the velocity information to adjust the frame rate when applicable. core/java/android/view/ViewRootImpl.java +24 −4 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; Loading Loading @@ -7568,7 +7569,8 @@ public final class ViewRootImpl implements ViewParent, } // For the variable refresh rate project if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { if (handled && shouldTouchBoost(action & MotionEvent.ACTION_MASK, mWindowAttributes.type)) { // set the frame rate to the maximum value. mIsTouchBoosting = true; setPreferredFrameRateCategory(mPreferredFrameRateCategory); Loading Loading @@ -12395,6 +12397,17 @@ public final class ViewRootImpl implements ViewParent, mFrameRateCompatibility).applyAsyncUnsafe(); mLastPreferredFrameRate = preferredFrameRate; } if (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE && mIsTouchBoosting) { // We've received a velocity, so we'll let the velocity control the // frame rate unless we receive additional motion events. mIsTouchBoosting = false; if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.instant( Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate velocity used, no touch boost on next frame" ); } } } catch (Exception e) { Log.e(mTag, "Unable to set frame rate", e); } finally { Loading @@ -12420,9 +12433,8 @@ public final class ViewRootImpl implements ViewParent, } private boolean shouldTouchBoost(int motionEventAction, int windowType) { boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN || motionEventAction == MotionEvent.ACTION_MOVE || motionEventAction == MotionEvent.ACTION_UP; // boost for almost all input boolean desiredAction = motionEventAction != MotionEvent.ACTION_OUTSIDE; boolean undesiredType = windowType == TYPE_INPUT_METHOD && sToolkitFrameRateTypingReadOnlyFlagValue; // use toolkitSetFrameRate flag to gate the change Loading Loading @@ -12527,6 +12539,14 @@ public final class ViewRootImpl implements ViewParent, return mPreferredFrameRate >= 0 ? mPreferredFrameRate : mLastPreferredFrameRate; } /** * Returns whether touch boost is currently enabled. */ @VisibleForTesting public boolean getIsTouchBoosting() { return mIsTouchBoosting; } /** * Get the value of mFrameRateCompatibility */ core/tests/coretests/res/layout/view_velocity_test.xml 0 → 100644 +25 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 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. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/moving_view" android:layout_width="50dp" android:layout_height="50dp" /> </FrameLayout> core/tests/coretests/src/android/view/ViewVelocityTest.java 0 → 100644 +102 −0 Original line number Diff line number Diff line /* * Copyright (C) 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. */ package android.view; import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.app.Activity; import android.os.SystemClock; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.R; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class ViewVelocityTest { @Rule public ActivityTestRule<ViewCaptureTestActivity> mActivityRule = new ActivityTestRule<>( ViewCaptureTestActivity.class); private Activity mActivity; private View mMovingView; private ViewRootImpl mViewRoot; @Before public void setUp() throws Throwable { mActivity = mActivityRule.getActivity(); mActivityRule.runOnUiThread(() -> { mActivity.setContentView(R.layout.view_velocity_test); mMovingView = mActivity.findViewById(R.id.moving_view); }); ViewParent parent = mActivity.getWindow().getDecorView().getParent(); while (parent instanceof View) { parent = parent.getParent(); } mViewRoot = (ViewRootImpl) parent; } @UiThreadTest @Test public void frameRateChangesWhenContentMoves() { mMovingView.offsetLeftAndRight(100); float frameRate = mViewRoot.getPreferredFrameRate(); assertTrue(frameRate > 0); } @UiThreadTest @Test public void firstFrameNoMovement() { assertEquals(0f, mViewRoot.getPreferredFrameRate(), 0f); } @Test public void touchBoostDisable() throws Throwable { mActivityRule.runOnUiThread(() -> { long now = SystemClock.uptimeMillis(); MotionEvent down = MotionEvent.obtain( /* downTime */ now, /* eventTime */ now, /* action */ MotionEvent.ACTION_DOWN, /* x */ 0f, /* y */ 0f, /* metaState */ 0 ); mActivity.dispatchTouchEvent(down); mMovingView.offsetLeftAndRight(10); }); mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); }); mActivityRule.runOnUiThread(() -> { assertFalse(mViewRoot.getIsTouchBoosting()); }); } } Loading
core/java/android/view/View.java +37 −2 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; Loading Loading @@ -5629,7 +5630,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Nullable private ViewTranslationCallback mViewTranslationCallback; private float mFrameContentVelocity = 0; private float mFrameContentVelocity = -1; @Nullable Loading Loading @@ -5660,6 +5661,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, protected long mMinusTwoFrameIntervalMillis = 0; private int mLastFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; private float mLastFrameX = Float.NaN; private float mLastFrameY = Float.NaN; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = Float.NaN; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) Loading Loading @@ -24597,7 +24601,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public void draw(@NonNull Canvas canvas) { final int privateFlags = mPrivateFlags; mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; mFrameContentVelocity = 0; mFrameContentVelocity = -1; mLastFrameX = mLeft + mRenderNode.getTranslationX(); mLastFrameY = mTop + mRenderNode.getTranslationY(); /* * Draw traversal performs several drawing steps which must be executed Loading Loading @@ -33673,6 +33680,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (sToolkitMetricsForFrameRateDecisionFlagValue) { viewRootImpl.recordViewPercentage(sizePercentage); } if (viewVelocityApi()) { float velocity = mFrameContentVelocity; if (velocity < 0f) { velocity = calculateVelocity(); } if (velocity > 0f) { float frameRate = convertVelocityToFrameRate(velocity); viewRootImpl.votePreferredFrameRate(frameRate, FRAME_RATE_COMPATIBILITY_GTE); return; } } if (!Float.isNaN(mPreferredFrameRate)) { if (mPreferredFrameRate < 0) { if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) { Loading @@ -33695,6 +33713,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } private float convertVelocityToFrameRate(float velocityPps) { float density = getResources().getDisplayMetrics().density; float velocityDps = velocityPps / density; // Choose a frame rate in increments of 10fps return Math.min(140f, 60f + (10f * (float) Math.floor(velocityDps / 300f))); } private float calculateVelocity() { // This current calculation is very simple. If something on the screen moved, then // it votes for the highest velocity. If it doesn't move, then return 0. float x = mLeft + mRenderNode.getTranslationX(); float y = mTop + mRenderNode.getTranslationY(); return (!Float.isNaN(mLastFrameX) && (x != mLastFrameX || y != mLastFrameY)) ? 100_000f : 0f; } /** * Set the current velocity of the View, we only track positive value. * We will use the velocity information to adjust the frame rate when applicable.
core/java/android/view/ViewRootImpl.java +24 −4 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; Loading Loading @@ -7568,7 +7569,8 @@ public final class ViewRootImpl implements ViewParent, } // For the variable refresh rate project if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { if (handled && shouldTouchBoost(action & MotionEvent.ACTION_MASK, mWindowAttributes.type)) { // set the frame rate to the maximum value. mIsTouchBoosting = true; setPreferredFrameRateCategory(mPreferredFrameRateCategory); Loading Loading @@ -12395,6 +12397,17 @@ public final class ViewRootImpl implements ViewParent, mFrameRateCompatibility).applyAsyncUnsafe(); mLastPreferredFrameRate = preferredFrameRate; } if (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE && mIsTouchBoosting) { // We've received a velocity, so we'll let the velocity control the // frame rate unless we receive additional motion events. mIsTouchBoosting = false; if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.instant( Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate velocity used, no touch boost on next frame" ); } } } catch (Exception e) { Log.e(mTag, "Unable to set frame rate", e); } finally { Loading @@ -12420,9 +12433,8 @@ public final class ViewRootImpl implements ViewParent, } private boolean shouldTouchBoost(int motionEventAction, int windowType) { boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN || motionEventAction == MotionEvent.ACTION_MOVE || motionEventAction == MotionEvent.ACTION_UP; // boost for almost all input boolean desiredAction = motionEventAction != MotionEvent.ACTION_OUTSIDE; boolean undesiredType = windowType == TYPE_INPUT_METHOD && sToolkitFrameRateTypingReadOnlyFlagValue; // use toolkitSetFrameRate flag to gate the change Loading Loading @@ -12527,6 +12539,14 @@ public final class ViewRootImpl implements ViewParent, return mPreferredFrameRate >= 0 ? mPreferredFrameRate : mLastPreferredFrameRate; } /** * Returns whether touch boost is currently enabled. */ @VisibleForTesting public boolean getIsTouchBoosting() { return mIsTouchBoosting; } /** * Get the value of mFrameRateCompatibility */
core/tests/coretests/res/layout/view_velocity_test.xml 0 → 100644 +25 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 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. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/moving_view" android:layout_width="50dp" android:layout_height="50dp" /> </FrameLayout>
core/tests/coretests/src/android/view/ViewVelocityTest.java 0 → 100644 +102 −0 Original line number Diff line number Diff line /* * Copyright (C) 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. */ package android.view; import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.app.Activity; import android.os.SystemClock; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.R; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class ViewVelocityTest { @Rule public ActivityTestRule<ViewCaptureTestActivity> mActivityRule = new ActivityTestRule<>( ViewCaptureTestActivity.class); private Activity mActivity; private View mMovingView; private ViewRootImpl mViewRoot; @Before public void setUp() throws Throwable { mActivity = mActivityRule.getActivity(); mActivityRule.runOnUiThread(() -> { mActivity.setContentView(R.layout.view_velocity_test); mMovingView = mActivity.findViewById(R.id.moving_view); }); ViewParent parent = mActivity.getWindow().getDecorView().getParent(); while (parent instanceof View) { parent = parent.getParent(); } mViewRoot = (ViewRootImpl) parent; } @UiThreadTest @Test public void frameRateChangesWhenContentMoves() { mMovingView.offsetLeftAndRight(100); float frameRate = mViewRoot.getPreferredFrameRate(); assertTrue(frameRate > 0); } @UiThreadTest @Test public void firstFrameNoMovement() { assertEquals(0f, mViewRoot.getPreferredFrameRate(), 0f); } @Test public void touchBoostDisable() throws Throwable { mActivityRule.runOnUiThread(() -> { long now = SystemClock.uptimeMillis(); MotionEvent down = MotionEvent.obtain( /* downTime */ now, /* eventTime */ now, /* action */ MotionEvent.ACTION_DOWN, /* x */ 0f, /* y */ 0f, /* metaState */ 0 ); mActivity.dispatchTouchEvent(down); mMovingView.offsetLeftAndRight(10); }); mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); }); mActivityRule.runOnUiThread(() -> { assertFalse(mViewRoot.getIsTouchBoosting()); }); } }