Android application development (37) LTPO frame rate test based on Surfaceview

Android application development study notes – directory index

Refer to the android official website:

  1. Frame rate | Android media | Android Developers
  2. Multiple Refresh Rates | Android Open Source Project | Android Open Source Project
  3. WindowManager.LayoutParams | Android Developers

At present, the flagship mobile phones on the market are basically popularized with LTPO screens. In order to verify the hardware refreshing behavior of DDIC (display driver ID) of LTPO screens (screen hardware refresh can be detected by the AVDD current signal of the screen/Vsync signal inside the screen hardware) , Write a test program that can specify the frequency of app drawing (based on Android application development (35) SufaceView basic usage and Android application development (36) frame rate API test based on Surfaceview).

1. Get all frame rates supported by the screen

The application obtains the display refresh rate actually supported by the device, which can be obtained by calling Display.getSupportedModes(), and Mode.mRefreshRate is the frame rate information, so that it is safe to call setFrameRate()

// Display.java
Display.Mode[] mSupportedModes = getWindowManager().getDefaultDisplay().getSupportedModes();
for (Display. Mode mode : mSupportedModes) {
   Log.d(TAG, "getSupportedModes: " + mode.toString());
   Log.d(TAG, "getRefreshRate: " + mode.getRefreshRate());
}
 
 
public static final class Mode implements Parcelable {
        public static final Mode[] EMPTY_ARRAY = new Mode[0];
 
        private final int mModeId;
        private final int mWidth;
        private final int mHeight;
        private final float mRefreshRate;
        @NonNull
        private final float[] mAlternativeRefreshRates;
...
}

2. APP design ideas

1. APP interface design

  1. Dynamically display the frame rate and resolution information currently used by the system
  2. All frame rates supported by the system (using the Spinner component), can be selected from the drop-down list, and the current system frame rate can be changed through setFrameRate( ) or PreferredDisplayModeId
  3. Pull down to select the drawing frequency of the APP (using the Spinner component), providing common drawing frequency options
  4. Enter the APP drawing frequency (EditTextNumber): If the drawing frequency of the APP selected in the drop-down menu cannot meet the requirements, you can enter the drawing frequency, and the input takes precedence.
  5. Confirm button: start/stop drawing button
  6. Draw the display area: it is convenient to observe the phenomenon intuitively

2. How to set the frame rate

Use setFrameRate( )/PreferredDisplayModeId/PreferredRefreshRate as before

setFrameRate():

  • Surface. setFrameRate()
  • SurfaceControl.Transaction.setFrameRate()

surface.setFrameRate(contentFrameRate,
FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
// When calling setFrameRate(), it is better to pass in the exact frame rate instead of rounding to an integer. For example, when rendering video recorded at 29.97Hz, pass in 29.97 instead of rounding to 30.
Parameters: Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE applies only to video applications. For non-video use, use FRAME_RATE_COMPATIBILITY_DEFAULT.
Choose a strategy for changing the frame rate:
Google strongly recommends that apps call setFrameRate(fps, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, CHANGE_FRAME_RATE_ALWAYS) when displaying long-running videos such as movies, where fps is the frame rate of the video.
It is strongly recommended that you do not call setFrameRate() with CHANGE_FRAME_RATE_ALWAYS when you expect video playback to last a few minutes or less.

SurfaceControl.Transaction.setFrameRate()
The parameters are the same as surface.setFrameRate

PreferredDisplayModeId:

WindowManager.LayoutParams.preferredDisplayModeId is another way for applications to indicate their frame rate to the platform.

  • Use the preferredDisplayModeId if the application wants to change the resolution or other display mode settings.
  • setFrameRate()The platform will only switch display modes in response to calls if the mode switch is lightweight and unlikely to be noticed by the user. If an application prefers toggling display refresh rates even if it requires a lot of mode switching (for example, on Android TV devices), use preferredDisplayModeId.
  • Applications that cannot handle displays running at multiples of the application’s frame rate (which would require setting a presentation timestamp on each frame) should use the preferredDisplayModeId.

Display.Mode[] mSupportedModes;
mSupportedModes = getWindowManager().getDefaultDisplay().getSupportedModes();

WindowManager.LayoutParams params = getWindow().getAttributes();
params.preferredDisplayModeId = mSupportedModes[x].getModeId();
getWindow().setAttributes(params);

Log.d(TAG, “RefreshRate:” + mSupportedModes[x].getRefreshRate());

PreferredRefreshRate:

WindowManager.LayoutParams#preferredRefreshRate Sets the preferred frame rate on the application window and applies to all surfaces within the window. Regardless of the refresh rate supported by the device, the application should specify its preferred frame rate, similar to setFrameRate(), to give the scheduler a better hint of the application’s expected frame rate.

preferredRefreshRate will be ignored for surfaces using setFrameRate(). Generally use setFrameRate() if possible.

WindowManager.LayoutParams params = getWindow().getAttributes();
params.PreferredRefreshRate = preferredRefreshRate;

getWindow().setAttributes(params);

Log.d(TAG, “preferredRefreshRate:” + preferredRefreshRate);

3. Code implementation

MainActivity.java
public class MainActivity extends AppCompatActivity implements
        View. OnClickListener,
        AdapterView.OnItemSelectedListener,
        SurfaceHolder. Callback,
        DisplayManager. DisplayListener {
    private static final String TAG = "lzl-test-RefreshRateSurfaceViewTest";
    private Vibrator mVibrator;
    private TextView mTextViewInfo;
    private Spinner mSpinnerSystemSupportedRefreshRates, mSpinnerAppDrawFrequencySelect;
    private EditText mEditTextNumber;
    private Button mButton;

    private Display mDisplay;
    private Display.Mode mActiveMode;
    private Display.Mode[] mSupportedModes;
    private ArrayAdapter<Integer> mArrayAdapterSystemSelectableFps;
    private Integer[] mSystemSelectableFps;
    private int mSystemSelectedFps = 0;

    private ArrayAdapter<Integer> mArrayAdapterAppSelectableDrawFrequency;
    private Integer[] mAppSelectableDrawFrequency = {1, 5, 10, 24, 25, 30, 40, 50, 60, 90, 120, 144};
    private int mAppSelectedDrawFrequency = 0;
    private int mAppInputDrawFrequency = 0;
    private int mDoDrawFrequency = 0;

    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;
    private Paint mPaint = new Paint();
    private int mCircleRadius = 10;
    private boolean isRunning = false;
    private boolean isStart = false;
    private long mFrameCount = 0;
    private long mStartTimeMillis = 0, mStopTimeMillis = 0, mDrawCircleTimeMillis = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R. layout. activity_main);

        Log.d(TAG, "onCreate:----------------------------------------- ----------------start");

        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); //Set the screen not to rotate with the phone
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); //Set screen orientation display
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); //Set screen full screen
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); //Set the screen not to sleep

        mDisplay = getWindowManager().getDefaultDisplay();
        mActiveMode = mDisplay. getMode();
        mTextViewInfo = (TextView) findViewById(R.id.textViewInfo);
        mTextViewInfo.setText("The current frame rate of the system: " + (int) mActiveMode.getRefreshRate() + "Hz, resolution: " + mActiveMode.getPhysicalWidth() + " x " + mActiveMode.getPhysicalHeight());

        mSpinnerSystemSupportedRefreshRates = (Spinner) findViewById(R.id.spinnerSystemSupportedRefreshRates);
        mSpinnerSystemSupportedRefreshRates.setOnItemSelectedListener(this);
        mSpinnerAppDrawFrequencySelect = (Spinner) findViewById(R.id.spinnerAppDrawFrequencySelect);
        mSpinnerAppDrawFrequencySelect.setOnItemSelectedListener(this);

        mSupportedModes = getWindowManager().getDefaultDisplay().getSupportedModes();
        ArrayList<Integer> listFps = new ArrayList<>();
        listFps.add((int)mSupportedModes[0].getRefreshRate());
        for (int i = 0; i < mSupportedModes. length; i ++ ) {
            boolean found = false;
            Log.d(TAG, "getSupportedModes: " + mSupportedModes[i].toString());
            for (int index = 0; index < listFps. size(); index ++ ) {
                if ((int)mSupportedModes[i].getRefreshRate() == listFps.get(index)) {
                    found = true;
                    break;
                }
            }
            if (!found)
                listFps.add((int)mSupportedModes[i].getRefreshRate());
        }
        mSystemSelectableFps = new Integer[listFps. size()];
        for (int i = 0; i < listFps. size(); i ++ ) {
            mSystemSelectableFps[i] = (int)listFps.get(i);
        }
        mArrayAdapterSystemSelectableFps = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, mSystemSelectableFps);
        mArrayAdapterSystemSelectableFps.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // Set the option style of the drop-down menu
        mSpinnerSystemSupportedRefreshRates.setAdapter(mArrayAdapterSystemSelectableFps); // set to use the Adapter object
        for (int i = 0; i < mSystemSelectableFps. length; i ++ ) {
            if (mSystemSelectableFps[i].intValue() == (int)mActiveMode.getRefreshRate()) {
                mSpinnerSystemSupportedRefreshRates.setSelection(i);
                break;
            }
        }

        mArrayAdapterAppSelectableDrawFrequency = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, mAppSelectableDrawFrequency);
        mArrayAdapterAppSelectableDrawFrequency.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // Set the option style of the drop-down menu
        mSpinnerAppDrawFrequencySelect.setAdapter(mArrayAdapterAppSelectableDrawFrequency); // Set the Adapter object to be used
        for (int i = 0; i < mAppSelectableDrawFrequency. length; i ++ ) {
            if (mAppSelectableDrawFrequency[i].intValue() == 60) {
                mSpinnerAppDrawFrequencySelect.setSelection(i);
                break;
            }
        }

        mEditTextNumber = (EditText) findViewById(R.id.editTextNumber);

        mButton = (Button) findViewById(R.id.button);
        mButton.setOnClickListener(this);

        mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);
        mSurfaceHolder = mSurfaceView. getHolder();
        mSurfaceHolder.addCallback(this);

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            VibratorManager vibratorManager = (VibratorManager)getSystemService(VibratorManager.class);
            mVibrator = vibratorManager. getDefaultVibrator();
        } else {
            mVibrator = (Vibrator)getSystemService(Service. VIBRATOR_SERVICE);
        }

        /* Get DisplayManager */
        DisplayManager displayManager = (DisplayManager) getSystemService(Service. DISPLAY_SERVICE);
        /* Register the DisplayManager.DisplayListener of the DisplayManager to monitor and monitor the changes of the display, such as adding/deleting a display, display frame rate change, etc. */
        displayManager. registerDisplayListener(this, null);
    }

    @Override
    public void onClick(View view) {
        isStart = !isStart;
        if (isStart) {
            // Get the APP refresh rate input by the user
            if (!mEditTextNumber. getText(). toString(). isEmpty())
                mAppInputDrawFrequency = Integer. parseInt(mEditTextNumber. getText(). toString());
            else
                mAppInputDrawFrequency = 0;

            mDoDrawFrequency = (mAppInputDrawFrequency != 0) ? mAppInputDrawFrequency : mAppSelectedDrawFrequency;

            mEditTextNumber.setEnabled(false);
            mSpinnerSystemSupportedRefreshRates.setEnabled(false);
            mSpinnerAppDrawFrequencySelect.setEnabled(false);

            mButton.setText("Stop");
            Log.d(TAG, "Key press: start drawing");
            mVibrator. vibrate(80);
            start();
        } else {
            mButton.setText("start");
            Log.d(TAG, "Key press: stop drawing");
            stop();
            mVibrator. vibrate(80);

            mEditTextNumber.setEnabled(true);
            mSpinnerSystemSupportedRefreshRates.setEnabled(true);
            mSpinnerAppDrawFrequencySelect.setEnabled(true);
        }
    }

    // start drawing
    public void start() {
        isRunning = true;
        mFrameCount = 0;
        mDrawCircleTimeMillis = 0;
        mStartTimeMillis = System. currentTimeMillis();

        new Thread() {
            @Override
            public void run() {
                while (isRunning) {
                    drawCircle();
                    try {
                        long sleep_ms = (1000 / mDoDrawFrequency) - mDrawCircleTimeMillis;
                        Thread. sleep((sleep_ms > 0 )? sleep_ms : 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
    // stop drawing
    public void stop() {
        isRunning = false;
        mStopTimeMillis = System. currentTimeMillis();
        drawText();
    }

    // draw graphics
    private void drawCircle() {
        long now = System. currentTimeMillis();
        if (mSurfaceHolder != null) {
            Canvas canvas = mSurfaceHolder. lockCanvas();
            if (canvas != null) {
                mFrameCount++;
                // Set the canvas to gray background color
                canvas. drawARGB(255, 13, 61, 80);
                // draw circle
                canvas.drawCircle(canvas.getWidth() / 2, canvas.getWidth() / 2, mCircleRadius, mPaint);
                canvas.drawText("This is the first" + mFrameCount + "frame", 20, canvas.getWidth() + 40, mPaint);

                if (mCircleRadius < canvas. getWidth() / 2) {
                    mCircleRadius++;
                } else {
                    mCircleRadius = 10;
                }
                if (canvas != null & amp; & amp; mSurfaceHolder != null) {
                    mSurfaceHolder. unlockCanvasAndPost(canvas);
                }
            }
        }
        mDrawCircleTimeMillis = System. currentTimeMillis() - now;
    }

    private void drawText() {
        if (mSurfaceHolder != null) {
            Canvas canvas = mSurfaceHolder. lockCanvas();
            if (canvas != null) {
                long averageTimeNsV1 = 0, averageTimeNsV2 = 0;
                averageTimeNsV1 = 1000000 / mDoDrawFrequency;
                averageTimeNsV2 = (mStopTimeMillis - mStartTimeMillis) * 1000 / mFrameCount;
                canvas.drawText("Number of frames: " + mFrameCount + " Time-consuming: " + (mStopTimeMillis - mStartTimeMillis) + "(ms)",
                        20, canvas. getWidth() + 80, mPaint);
                canvas.drawText("Theory: " + averageTimeNsV1/1000 + "." + averageTimeNsV1 00 + "(ms)" +
                                ", Actual: " + averageTimeNsV2/1000 + "." + averageTimeNsV2 00 + "(ms)",
                        20, canvas. getWidth() + 120, mPaint);
                if (canvas != null & amp; & amp; mSurfaceHolder != null) {
                    mSurfaceHolder. unlockCanvasAndPost(canvas);
                }
            }
        }
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        Log.d(TAG, "surfaceCreated...");
        if (mSurfaceHolder == null) {
            // Call getHolder() method to get SurfaceHolder
            mSurfaceHolder = mSurfaceView. getHolder();
            // Set by SurfaceHolder.addCallback method: implement SurfaceHolder.Callback callback interface
            mSurfaceHolder.addCallback(this);
        }
        mPaint.setAntiAlias(true); // Set the brush to be anti-aliased
        mPaint.setColor(Color.RED); // Set the color of the brush
        mPaint.setStrokeWidth(10); // Set the line width of the brush
        mPaint.setStyle(Paint.Style.FILL); // Set the type of brush. STROK means hollow, FILL means solid
        mPaint.setTextSize(30);
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
        Log.d(TAG, "surfaceChanged...");
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
        Log.d(TAG, "surfaceDestroyed...");
        Log.d(TAG, "surfaceDestroyed: stop drawing");
        mSurfaceHolder = null;
        isStart = false;
        mButton.setText("start");
        stop();
    }

    @Override
    public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
        Log.d(TAG, "Spinner: onItemSelected: position = " + position + ", id = " + id);

        if (adapterView.getId() == R.id.spinnerSystemSupportedRefreshRates) {
            mSystemSelectedFps = mSystemSelectableFps[position].intValue();
            Log.d(TAG, "Spinner: mSystemSelectedFps:" + mSystemSelectedFps);
            int preferredDisplayModeId = 0;
            float preferredRefreshRate = 0f;
            for (Display. Mode mode : mSupportedModes) {
                if ((int)mode.getRefreshRate() == mSystemSelectedFps & amp; & amp;
                        mode.getPhysicalWidth() == mActiveMode.getPhysicalWidth() & &
                        mode.getPhysicalHeight() == mActiveMode.getPhysicalHeight()) {
                    Log.d(TAG, "find mode: " + mode.toString());
                    preferredDisplayModeId = mode. getModeId();
                    preferredRefreshRate = mode. getRefreshRate();
                }
            }
            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                WindowManager.LayoutParams params = getWindow().getAttributes();
                Log.d(TAG, "preferredDisplayModeId: old = " + params.preferredDisplayModeId + ", new = " + preferredDisplayModeId);
                Log.d(TAG, "preferredRefreshRate: old = " + params. preferredRefreshRate + ", new = " + preferredRefreshRate);

                /* WindowManager.LayoutParams#preferredDisplayModeId */
                params.preferredDisplayModeId = preferredDisplayModeId;
                getWindow().setAttributes(params);

                /* WindowManager.LayoutParams#preferredRefreshRate */
                /*
                params.preferredRefreshRate = preferredRefreshRate;
                getWindow().setAttributes(params);
                */

                /* setFrameRate() */
                /*
                if (mSurfaceHolder != null) {
                    mSurfaceHolder.getSurface().setFrameRate(mSystemSelectedFps,
                            Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                            Surface. CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
                }
                */
            }

        } else if (adapterView.getId() == R.id.spinnerAppDrawFrequencySelect) {
            mAppSelectedDrawFrequency = mAppSelectableDrawFrequency[position].intValue();
            Log.d(TAG, "Spinner: mAppSelectedDrawFrequency:" + mAppSelectedDrawFrequency);
        } else {
            Log.e(TAG, "Unknown spinner id!");
        }
    }

    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {
        Log.d(TAG, "onNothingSelected...");

    }

    @Override
    public void onDisplayAdded(int displayId) {
        Log.d(TAG, "onDisplayAdded...");

    }

    @Override
    public void onDisplayRemoved(int displayId) {
        Log.d(TAG, "onDisplayRemoved...");

    }

    @Override
    public void onDisplayChanged(int displayId) {
        Log.d(TAG, "onDisplayChanged...");

        mActiveMode = mDisplay. getMode();
        mTextViewInfo.setText("The current frame rate of the system: " + (int) mActiveMode.getRefreshRate() + "Hz, resolution: " + mActiveMode.getPhysicalWidth() + " x " + mActiveMode.getPhysicalHeight());

        for (int i = 0; i < mSystemSelectableFps. length; i ++ ) {
            if (mSystemSelectableFps[i].intValue() == (int) mActiveMode.getRefreshRate()) {
                mSpinnerSystemSupportedRefreshRates.setSelection(i);
                break;
            }
        }
    }
}

layout.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@ + id/textViewInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="The current frame rate of the system:"
        android:textColor="#FF0000"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@ + id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:text="The screen refresh rate supported by the system:"
        android:textSize="16sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@ + id/textViewInfo" />
    <Spinner
        android:id="@ + id/spinnerSystemSupportedRefreshRates"
        android:layout_width="100dp"
        android:layout_height="32dp"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="@ + id/textView1"
        app:layout_constraintStart_toStartOf="@ + id/textView1"
        app:layout_constraintTop_toBottomOf="@ + id/textView1" />

    <TextView
        android:id="@ + id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:text="Select the drawing frequency of APP:"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@ + id/textViewInfo" />
    <Spinner
        android:id="@ + id/spinnerAppDrawFrequencySelect"
        android:layout_width="100dp"
        android:layout_height="32dp"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="@ + id/textView2"
        app:layout_constraintStart_toStartOf="@ + id/textView2"
        app:layout_constraintTop_toBottomOf="@ + id/textView2" />

    <TextView
        android:id="@ + id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:text="Enter APP drawing frequency (Hz):"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="@ + id/spinnerSystemSupportedRefreshRates"
        app:layout_constraintStart_toStartOf="@ + id/textView1"
        app:layout_constraintTop_toBottomOf="@ + id/spinnerSystemSupportedRefreshRates" />
    <EditText
        android:id="@ + id/editTextNumber"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="4dp"
        android:ems="3"
        android:inputType="number"
        app:layout_constraintBottom_toBottomOf="@ + id/textView3"
        app:layout_constraintStart_toEndOf="@ + id/textView3"
        app:layout_constraintTop_toTopOf="@ + id/textView3" />
    
    <Button
        android:id="@ + id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="start"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@ + id/editTextNumber" />

    <SurfaceView
        android:id="@ + id/surfaceView"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@ + id/button" />

</androidx.constraintlayout.widget.ConstraintLayout>

3. APP running interface

Go to [Developer Options] to enable [Display Refresh Frequency]

4. The complete source code of the test program

Baidu network disk link: Baidu network disk Please enter the extraction code Extraction code: test

RefreshRateSurfaceViewTest directory

Click here to view the complete catalog of Android application development study notes