Android应用开发(37)LTPO帧率测试基于Surfaceview

news/2024/7/5 3:26:28

Android应用开发学习笔记——目录索引

 参考android官网:

  1. Frame rate  |  Android media  |  Android Developers
  2. 多重刷新率  |  Android 开源项目  |  Android Open Source Project
  3. WindowManager.LayoutParams  |  Android Developers

目前市面上旗舰手机基本都是普及了LTPO屏幕,为了验证LTPO屏幕的DDIC(display driver ID)的硬件刷帧行为(屏幕硬件的刷新可以通过屏幕的AVDD电流信号/屏幕硬件内部的Vsync信号检测),写一个可以指定app出图频率的测试程序(基于Android应用开发(35)SufaceView基本用法 与 Android应用开发(36)帧率API测试基于Surfaceview)。

一、获取屏幕支持的所有帧率

应用程序获取设备实际支持的显示刷新率,可以通过调用 Display.getSupportedModes()获取,Mode.mRefreshRate就是帧率信息,以便安全调用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;
...
}

二、APP设计思路

1. APP 界面设计

  1.  动态显示系统当前使用的帧率和分辨率信息
  2. 系统支持的所有帧率(使用Spinner组件),可下拉选择,通过setFrameRate( )或者PreferredDisplayModeId改变当前系统帧率
  3. 下拉选择APP的绘制频率(使用Spinner组件),提供常用的绘制频率选择项
  4. 输入APP绘制频率(EditTextNumber):如果下拉选择APP的绘制频率无法满足要求,可以输入绘制频率,输入优先。
  5. 确认按键:启动/停止绘制按键
  6. 绘制显示区域:方便直观观察现象

2.设置帧率的方法

同之前一样使用setFrameRate( )/PreferredDisplayModeId/PreferredRefreshRate

setFrameRate()

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

surface.setFrameRate(contentFrameRate,
    FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
    CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS)
;
// 调用setFrameRate()时,最好传入准确的帧速率,而不是四舍五入为整数。例如,在渲染以 29.97Hz 录制的视频时,传入 29.97 而不是四舍五入为 30。
参数:Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE仅适用于视频应用程序。对于非视频用途,请使用FRAME_RATE_COMPATIBILITY_DEFAULT.
选择改变帧速率的策略:
google强烈建议应用在显示电影等长时间运行的视频时调用setFrameRate(fps , FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, CHANGE_FRAME_RATE_ALWAYS) ,其中 fps 是视频的帧速率。
当您预计视频播放持续几分钟或更短时间时,强烈建议您不要调用setFrameRate()带参数CHANGE_FRAME_RATE_ALWAYS。

SurfaceControl.Transaction.setFrameRate()
参数和surface.setFrameRate是一样的

PreferredDisplayModeId:

WindowManager.LayoutParams.preferredDisplayModeId 是应用程序向平台指示其帧速率的另一种方式。

  • 如果应用程序想要更改分辨率或其他显示模式设置,请使用preferredDisplayModeId
  • setFrameRate()如果模式切换是轻量级的并且不太可能被用户注意到,则平台只会响应调用来切换显示模式 。如果应用程序更喜欢切换显示刷新率,即使它需要大量模式切换(例如,在 Android TV 设备上),请使用preferredDisplayModeId.
  • 无法处理以应用程序帧速率倍数运行的显示的应用程序(这需要在每个帧上设置演示时间戳)应使用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 在应用程序窗口上设置首选帧速率,并且该速率适用于窗口内的所有表面。无论设备支持的刷新率如何,应用程序都应指定其首选帧速率,类似于 setFrameRate(),以便为调度程序更好地提示应用程序的预期帧速率。

preferredRefreshRate对于使用 的表面将被忽略setFrameRate()。如果可能的话一般使用setFrameRate()

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

getWindow().setAttributes(params);

Log.d(TAG, "preferredRefreshRate:" + preferredRefreshRate);

 3.代码实现

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); //设置屏幕不随手机旋转
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); //设置屏幕直向显示
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); //设置屏幕全屏
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); //设置屏幕不进入休眠

        mDisplay = getWindowManager().getDefaultDisplay();
        mActiveMode = mDisplay.getMode();
        mTextViewInfo = (TextView) findViewById(R.id.textViewInfo);
        mTextViewInfo.setText("系统当前帧率:" + (int) mActiveMode.getRefreshRate() + "Hz, 分辨率: " + 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); // 设置下拉选单的选项样式
        mSpinnerSystemSupportedRefreshRates.setAdapter(mArrayAdapterSystemSelectableFps); // 设置使用 Adapter 对象
        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); // 设置下拉选单的选项样式
        mSpinnerAppDrawFrequencySelect.setAdapter(mArrayAdapterAppSelectableDrawFrequency); // 设置使用 Adapter 对象
        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);
        }

        /* 获取DisplayManager */
        DisplayManager displayManager = (DisplayManager) getSystemService(Service.DISPLAY_SERVICE);
        /* 注册DisplayManager 的DisplayManager.DisplayListener 监听,监听display的变化,如添加/删除display,display帧率变化等 */
        displayManager.registerDisplayListener(this, null);
    }

    @Override
    public void onClick(View view) {
        isStart = !isStart;
        if (isStart) {
            // 获取用户输入的APP刷新率
            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("停止");
            Log.d(TAG, "按键按下:开始绘制");
            mVibrator.vibrate(80);
            start();
        } else {
            mButton.setText("启动");
            Log.d(TAG, "按键按下:停止绘制");
            stop();
            mVibrator.vibrate(80);

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

    // 开始绘制
    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();
    }
    // 停止绘制
    public void stop() {
        isRunning = false;
        mStopTimeMillis = System.currentTimeMillis();
        drawText();
    }

    // 绘制图形
    private void drawCircle() {
        long now = System.currentTimeMillis();
        if (mSurfaceHolder != null) {
            Canvas canvas = mSurfaceHolder.lockCanvas();
            if (canvas != null) {
                mFrameCount++;
                // 设置画布为灰色背景色
                canvas.drawARGB(255, 13, 61, 80);
                // 画圆
                canvas.drawCircle(canvas.getWidth() / 2, canvas.getWidth() / 2, mCircleRadius, mPaint);
                canvas.drawText("这是第" + mFrameCount + "帧", 20, canvas.getWidth() + 40, mPaint);

                if (mCircleRadius < canvas.getWidth() / 2) {
                    mCircleRadius++;
                } else {
                    mCircleRadius = 10;
                }
                if (canvas != null && 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("帧数:" + mFrameCount + " 耗时:" + (mStopTimeMillis - mStartTimeMillis) + "(ms)",
                        20, canvas.getWidth() + 80, mPaint);
                canvas.drawText("理论:" + averageTimeNsV1/1000 + "." + averageTimeNsV1%1000 + "(ms)" +
                                ", 实际:" + averageTimeNsV2/1000 + "." + averageTimeNsV2%1000 + "(ms)",
                        20, canvas.getWidth() + 120, mPaint);
                if (canvas != null && mSurfaceHolder != null) {
                    mSurfaceHolder.unlockCanvasAndPost(canvas);
                }
            }
        }
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        Log.d(TAG, "surfaceCreated...");
        if (mSurfaceHolder == null) {
            // 调用getHolder()方法获取SurfaceHolder
            mSurfaceHolder = mSurfaceView.getHolder();
            // 通过 SurfaceHolder.addCallback方法设置:实现SurfaceHolder.Callback回调接口
            mSurfaceHolder.addCallback(this);
        }
        mPaint.setAntiAlias(true); // 设置画笔为无锯齿
        mPaint.setColor(Color.RED); // 设置画笔的颜色
        mPaint.setStrokeWidth(10); // 设置画笔的线宽
        mPaint.setStyle(Paint.Style.FILL); // 设置画笔的类型。STROK表示空心,FILL表示实心
        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:停止绘制");
        mSurfaceHolder = null;
        isStart = false;
        mButton.setText("启动");
        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 &&
                        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("系统当前帧率:" + (int) mActiveMode.getRefreshRate() + "Hz, 分辨率: " + 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="系统当前帧率:"
        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="系统支持的屏幕刷新率:"
        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="选择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="输入APP绘制频率(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="启动"
        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>

三、APP运行界面

到【开发者选项】去开启【显示刷新频率】

        


 

四、测试程序完整源码

百度网盘链接:百度网盘 请输入提取码 提取码:test

RefreshRateSurfaceViewTest目录

点此查看Android应用开发学习笔记的完整目录


http://lihuaxi.xjx100.cn/news/1441427.html

相关文章

什么是CSS中的渐变(gradient)?如何使用CSS创建线性渐变和径向渐变?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 渐变&#xff08;Gradient&#xff09;在CSS中的应用⭐ 线性渐变&#xff08;Linear Gradient&#xff09;语法&#xff1a;示例&#xff1a; ⭐ 径向渐变&#xff08;Radial Gradient&#xff09;语法&#xff1a;示例&#xff1a; ⭐ 写…

16收16发ARINC429模块

6通道发送&#xff0c; 16通道接收 发送通道: 每路发送通道FIFO大小为&#xff1a;511 x 32bit(CHR32216/32316) &#xff0c;缓存256条发送消息(CHR32216-EX/32316-EX) 发送FIFO可设置复位 可设置消息间隔&#xff0c; 字间隔和发送帧的预定数呈 发送波特率100Kbps、50Kbps、…

LeetCode 36题:有效的数独

题目 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff…

PAT(Advanced Level)刷题指南 —— 第九弹【Dijkstra + DFS】

一、1018 Public Bike Management 1. 问题重述 杭州市设有公共自行车服务,为来自世界各地的游客提供了极大的便利。可以在任何车站租用一辆自行车,然后将其还给城市中的其他车站。 公共自行车管理中心(PBMC)不断监视所有站点的实时容量。如果工作站刚满一半,则称其处于理…

免费开源的多种人工智能项目,比如:训练一个模型,让人工智能玩王者荣耀

免费开源的多种人工智能项目&#xff0c;比如&#xff1a;训练一个模型&#xff0c;让人工智能玩王者荣耀。 全文大纲 PULSE - 该开源项目可以通过给图片增加像素点来实现去马赛克或高清化。 Depix - 给打了马赛克的文字去码。 TecoGAN - 给视频去马赛克或者进行超分辨率。 Sk…

linux0.95(VFS重点)源码通俗解读(施工中)

文件系统在磁盘中的体现 下面是磁盘的内容&#xff0c;其中i节点就是一个inode数组&#xff0c;逻辑块就是数据块可用于存放数据 操作系统通过将磁盘数据读入到内存中指定的缓冲区块来与磁盘交互&#xff0c;对内存中的缓冲区块修改后写回磁盘。 进程(task_struct * task[N…

【深入浅出程序设计竞赛(基础篇)第二章 算法从0开始】

深入浅出程序设计竞赛&#xff08;基础篇&#xff09;第二章 算法从0开始 第二章 例题例2-1例2-2例2-3例2-4例2-5例2-6例2-7例2-8例2-9例2-10例2-11例2-12 第二章 课后习题2-12-22-32-42-52-62-7 第二章 例题 例2-1 #include<iostream> using namespace std;int main()…

Nacos AP架构集群搭建(Windows)

手写SpringCloud项目地址&#xff0c;求个star github:https://github.com/huangjianguo2000/spring-cloud-lightweight gitee:https://gitee.com/huangjianguo2000/spring-cloud-lightweigh 目录&#xff1a; 一&#xff1a;初始化MySQL 二&#xff1a;复制粘贴三份Nacos文…