diff options
| -rw-r--r-- | core/java/android/view/View.java | 41 | ||||
| -rw-r--r-- | core/java/android/view/ViewRootImpl.java | 28 | ||||
| -rw-r--r-- | core/tests/coretests/res/layout/view_velocity_test.xml | 25 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/ViewVelocityTest.java | 108 |
4 files changed, 195 insertions, 7 deletions
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 828004b6b235..a6f380d4d483 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -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; @@ -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 @@ -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) @@ -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 @@ -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) { @@ -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. @@ -33725,7 +33760,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @FlaggedApi(FLAG_VIEW_VELOCITY_API) public float getFrameContentVelocity() { if (viewVelocityApi()) { - return mFrameContentVelocity; + return (mFrameContentVelocity < 0f) ? 0f : mFrameContentVelocity; } return 0; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 02f8e6e9b810..42f64052d987 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -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; @@ -7571,7 +7572,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); @@ -12398,6 +12400,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 { @@ -12423,9 +12436,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 @@ -12531,6 +12543,14 @@ public final class ViewRootImpl implements ViewParent, } /** + * Returns whether touch boost is currently enabled. + */ + @VisibleForTesting + public boolean getIsTouchBoosting() { + return mIsTouchBoosting; + } + + /** * Get the value of mFrameRateCompatibility */ @VisibleForTesting diff --git a/core/tests/coretests/res/layout/view_velocity_test.xml b/core/tests/coretests/res/layout/view_velocity_test.xml new file mode 100644 index 000000000000..98154a4b6b78 --- /dev/null +++ b/core/tests/coretests/res/layout/view_velocity_test.xml @@ -0,0 +1,25 @@ +<?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> diff --git a/core/tests/coretests/src/android/view/ViewVelocityTest.java b/core/tests/coretests/src/android/view/ViewVelocityTest.java new file mode 100644 index 000000000000..d437f7bc4060 --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewVelocityTest.java @@ -0,0 +1,108 @@ +/* + * 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 android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; + +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 android.platform.test.annotations.RequiresFlagsEnabled; + +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 + @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API) + public void frameRateChangesWhenContentMoves() { + mMovingView.offsetLeftAndRight(100); + float frameRate = mViewRoot.getPreferredFrameRate(); + assertTrue(frameRate > 0); + } + + @UiThreadTest + @Test + @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API) + public void firstFrameNoMovement() { + assertEquals(0f, mViewRoot.getPreferredFrameRate(), 0f); + } + + @Test + @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API) + 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()); + }); + } +} |