Loading core/java/android/view/View.java +20 −8 Original line number Diff line number Diff line Loading @@ -4744,6 +4744,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.ExportedProperty(category = "layout") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) protected int mLeft; /** * The mLeft from the previous frame. Used for detecting movement for purposes of variable * refresh rate. */ private int mLastFrameLeft; /** * The distance in pixels from the left edge of this view's parent * to the right edge of this view. Loading @@ -4760,6 +4765,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.ExportedProperty(category = "layout") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) protected int mTop; /** * The mTop from the previous frame. Used for detecting movement for purposes of variable * refresh rate. */ private int mLastFrameTop; /** * The distance in pixels from the top edge of this view's parent * to the bottom edge of this view. Loading Loading @@ -19535,7 +19545,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setTop(int top) { if (top != mTop) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (mAttachInfo != null) { Loading Loading @@ -20427,7 +20436,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void offsetTopAndBottom(int offset) { if (offset != 0) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (isHardwareAccelerated()) { Loading Loading @@ -20479,7 +20487,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void offsetLeftAndRight(int offset) { if (offset != 0) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (isHardwareAccelerated()) { Loading Loading @@ -25523,7 +25530,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; changed = true; // Remember our drawn bit Loading Loading @@ -33976,8 +33982,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // The most common case is when nothing is set, so this special case is called // often. if (mAttachInfo.mViewVelocityApi && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) && ((mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) || mLastFrameLeft != mLeft || mLastFrameTop != mTop) && viewRootImpl.shouldCheckFrameRate(false) && parent instanceof View && ((View) parent).mFrameContentVelocity <= 0) { Loading @@ -33990,14 +33997,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, viewRootImpl.votePreferredFrameRateCategory(category, reason, this); mLastFrameRateCategory = frameRateCategory; } mLastFrameLeft = mLeft; mLastFrameTop = mTop; return; } if (viewRootImpl.shouldCheckFrameRate(frameRate > 0f)) { float velocityFrameRate = 0f; if (mAttachInfo.mViewVelocityApi) { if (velocity < 0f && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) && ((mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) || mLastFrameLeft != mLeft || mLastFrameTop != mTop) && mParent instanceof View && ((View) mParent).mFrameContentVelocity <= 0 ) { Loading Loading @@ -34062,6 +34072,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, viewRootImpl.votePreferredFrameRateCategory(category, reason, this); mLastFrameRateCategory = frameRateCategory; } mLastFrameLeft = mLeft; mLastFrameTop = mTop; } private float convertVelocityToFrameRate(float velocityPps) { core/java/android/view/ViewRootImpl.java +2 −1 Original line number Diff line number Diff line Loading @@ -12976,7 +12976,8 @@ public final class ViewRootImpl implements ViewParent, mMinusOneFrameIntervalMillis = timeIntervalMillis; mLastUpdateTimeMillis = currentTimeMillis; if (timeIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) { if (timeIntervalMillis + mMinusTwoFrameIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) { int infrequentUpdateCount = mInfrequentUpdateCount; mInfrequentUpdateCount = infrequentUpdateCount == INFREQUENT_UPDATE_COUNTS ? infrequentUpdateCount : infrequentUpdateCount + 1; core/tests/coretests/src/android/view/ViewFrameRateTest.java +75 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package android.view; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; 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.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_VELOCITY_MAPPING_READ_ONLY; import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; Loading Loading @@ -435,6 +436,80 @@ public class ViewFrameRateTest { waitForAfterDraw(); } /** * A common behavior is for two different views to be invalidated in succession, but * intermittently. We want to treat this as an intermittent invalidation. * * This test will only succeed on non-cuttlefish devices, so it is commented out * for potential manual testing. */ // @Test @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY}) public void intermittentDoubleInvalidate() throws Throwable { View parent = (View) mMovingView.getParent(); mActivityRule.runOnUiThread(() -> { parent.setWillNotDraw(false); // Make sure the View is large ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams(); layoutParams.width = parent.getWidth(); layoutParams.height = parent.getHeight(); mMovingView.setLayoutParams(layoutParams); }); waitForFrameRateCategoryToSettle(); for (int i = 0; i < 5; i++) { int expectedCategory; if (i < 4) { // not intermittent yet. // It takes 2 frames of intermittency before Views vote as intermittent. // It takes 4 more frames for the category to drop to the next category. expectedCategory = toolkitFrameRateDefaultNormalReadOnly() ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; } else { // intermittent expectedCategory = FRAME_RATE_CATEGORY_NORMAL; } mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); runAfterDraw(() -> assertEquals(expectedCategory, mViewRoot.getLastPreferredFrameRateCategory())); }); waitForAfterDraw(); mActivityRule.runOnUiThread(() -> { parent.invalidate(); runAfterDraw(() -> assertEquals(expectedCategory, mViewRoot.getLastPreferredFrameRateCategory())); }); waitForAfterDraw(); Thread.sleep(90); } } // When a view has two motions that offset each other, the overall motion // should be canceled and be considered unmoved. @Test @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY }) public void sameFrameMotion() throws Throwable { mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE); waitForFrameRateCategoryToSettle(); mActivityRule.runOnUiThread(() -> { mMovingView.offsetLeftAndRight(10); mMovingView.offsetLeftAndRight(-10); mMovingView.offsetTopAndBottom(100); mMovingView.offsetTopAndBottom(-100); mMovingView.invalidate(); runAfterDraw(() -> { assertEquals(0f, mViewRoot.getLastPreferredFrameRate(), 0f); assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE, mViewRoot.getLastPreferredFrameRateCategory()); }); }); waitForAfterDraw(); } private void runAfterDraw(@NonNull Runnable runnable) { Handler handler = new Handler(Looper.getMainLooper()); mAfterDrawLatch = new CountDownLatch(1); Loading Loading
core/java/android/view/View.java +20 −8 Original line number Diff line number Diff line Loading @@ -4744,6 +4744,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.ExportedProperty(category = "layout") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) protected int mLeft; /** * The mLeft from the previous frame. Used for detecting movement for purposes of variable * refresh rate. */ private int mLastFrameLeft; /** * The distance in pixels from the left edge of this view's parent * to the right edge of this view. Loading @@ -4760,6 +4765,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.ExportedProperty(category = "layout") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) protected int mTop; /** * The mTop from the previous frame. Used for detecting movement for purposes of variable * refresh rate. */ private int mLastFrameTop; /** * The distance in pixels from the top edge of this view's parent * to the bottom edge of this view. Loading Loading @@ -19535,7 +19545,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setTop(int top) { if (top != mTop) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (mAttachInfo != null) { Loading Loading @@ -20427,7 +20436,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void offsetTopAndBottom(int offset) { if (offset != 0) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (isHardwareAccelerated()) { Loading Loading @@ -20479,7 +20487,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void offsetLeftAndRight(int offset) { if (offset != 0) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (isHardwareAccelerated()) { Loading Loading @@ -25523,7 +25530,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { mPrivateFlags4 |= PFLAG4_HAS_MOVED; changed = true; // Remember our drawn bit Loading Loading @@ -33976,8 +33982,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // The most common case is when nothing is set, so this special case is called // often. if (mAttachInfo.mViewVelocityApi && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) && ((mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) || mLastFrameLeft != mLeft || mLastFrameTop != mTop) && viewRootImpl.shouldCheckFrameRate(false) && parent instanceof View && ((View) parent).mFrameContentVelocity <= 0) { Loading @@ -33990,14 +33997,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, viewRootImpl.votePreferredFrameRateCategory(category, reason, this); mLastFrameRateCategory = frameRateCategory; } mLastFrameLeft = mLeft; mLastFrameTop = mTop; return; } if (viewRootImpl.shouldCheckFrameRate(frameRate > 0f)) { float velocityFrameRate = 0f; if (mAttachInfo.mViewVelocityApi) { if (velocity < 0f && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) && ((mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == ( PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN) || mLastFrameLeft != mLeft || mLastFrameTop != mTop) && mParent instanceof View && ((View) mParent).mFrameContentVelocity <= 0 ) { Loading Loading @@ -34062,6 +34072,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, viewRootImpl.votePreferredFrameRateCategory(category, reason, this); mLastFrameRateCategory = frameRateCategory; } mLastFrameLeft = mLeft; mLastFrameTop = mTop; } private float convertVelocityToFrameRate(float velocityPps) {
core/java/android/view/ViewRootImpl.java +2 −1 Original line number Diff line number Diff line Loading @@ -12976,7 +12976,8 @@ public final class ViewRootImpl implements ViewParent, mMinusOneFrameIntervalMillis = timeIntervalMillis; mLastUpdateTimeMillis = currentTimeMillis; if (timeIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) { if (timeIntervalMillis + mMinusTwoFrameIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) { int infrequentUpdateCount = mInfrequentUpdateCount; mInfrequentUpdateCount = infrequentUpdateCount == INFREQUENT_UPDATE_COUNTS ? infrequentUpdateCount : infrequentUpdateCount + 1;
core/tests/coretests/src/android/view/ViewFrameRateTest.java +75 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package android.view; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; 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.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_VELOCITY_MAPPING_READ_ONLY; import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; Loading Loading @@ -435,6 +436,80 @@ public class ViewFrameRateTest { waitForAfterDraw(); } /** * A common behavior is for two different views to be invalidated in succession, but * intermittently. We want to treat this as an intermittent invalidation. * * This test will only succeed on non-cuttlefish devices, so it is commented out * for potential manual testing. */ // @Test @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY}) public void intermittentDoubleInvalidate() throws Throwable { View parent = (View) mMovingView.getParent(); mActivityRule.runOnUiThread(() -> { parent.setWillNotDraw(false); // Make sure the View is large ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams(); layoutParams.width = parent.getWidth(); layoutParams.height = parent.getHeight(); mMovingView.setLayoutParams(layoutParams); }); waitForFrameRateCategoryToSettle(); for (int i = 0; i < 5; i++) { int expectedCategory; if (i < 4) { // not intermittent yet. // It takes 2 frames of intermittency before Views vote as intermittent. // It takes 4 more frames for the category to drop to the next category. expectedCategory = toolkitFrameRateDefaultNormalReadOnly() ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; } else { // intermittent expectedCategory = FRAME_RATE_CATEGORY_NORMAL; } mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); runAfterDraw(() -> assertEquals(expectedCategory, mViewRoot.getLastPreferredFrameRateCategory())); }); waitForAfterDraw(); mActivityRule.runOnUiThread(() -> { parent.invalidate(); runAfterDraw(() -> assertEquals(expectedCategory, mViewRoot.getLastPreferredFrameRateCategory())); }); waitForAfterDraw(); Thread.sleep(90); } } // When a view has two motions that offset each other, the overall motion // should be canceled and be considered unmoved. @Test @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY }) public void sameFrameMotion() throws Throwable { mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE); waitForFrameRateCategoryToSettle(); mActivityRule.runOnUiThread(() -> { mMovingView.offsetLeftAndRight(10); mMovingView.offsetLeftAndRight(-10); mMovingView.offsetTopAndBottom(100); mMovingView.offsetTopAndBottom(-100); mMovingView.invalidate(); runAfterDraw(() -> { assertEquals(0f, mViewRoot.getLastPreferredFrameRate(), 0f); assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE, mViewRoot.getLastPreferredFrameRateCategory()); }); }); waitForAfterDraw(); } private void runAfterDraw(@NonNull Runnable runnable) { Handler handler = new Handler(Looper.getMainLooper()); mAfterDrawLatch = new CountDownLatch(1); Loading