Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7d68d625 authored by George Mount's avatar George Mount
Browse files

Automatically detect when Views move and apply velocity.

Bug: 328283855
Bug: 328410701

When a View moves, the View will automatically update with
a velocity-based refresh rate rather than with size-based
refresh rate. This should help moving Views refresh at
high refresh rate and provide smooth scrolling when the
default refresh rate is 60Hz, even if they don't support
the velocity-based refresh rate. It also helps drop the
refresh rate as soon as scrolling ends rather than
waiting for the touch boost timeout.

Test: new tests, some manual testing, ran ViewTest
Change-Id: I192cb132d72c6853ee4ab2b7290ae6eabdfd1b61
parent 8faf871b
Loading
Loading
Loading
Loading
+38 −3
Original line number Diff line number Diff line
@@ -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;
    }
+24 −4
Original line number Diff line number Diff line
@@ -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
@@ -12530,6 +12542,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
     */
+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>
+108 −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 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());
        });
    }
}