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

news2024/11/24 6:05:56

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://www.coloradmin.cn/o/871984.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AirServer是什么软件,手机屏幕投屏电脑神器

什么是 AirServer&#xff1f; AirServer 是适用于 Mac 和 PC 的先进的屏幕镜像接收器。 它允许您接收 AirPlay 和 Google Cast 流&#xff0c;类似于 Apple TV 或 Chromecast 设备。AirServer 可以将一个简单的大屏幕或投影仪变成一个通用的屏幕镜像接收器 &#xff0c;是一款…

手势识别rtos小车(2)----蓝牙通信

在pycharm下面安装pybluez库 本人&#xff1a;win11python3.8pybluez2 第一步&#xff0c;直接在pycharm终端运行 pip install pybluez 一般都会直接报错 第二步&#xff0c;下载安装win11的SDK文件&#xff0c;Windows SDK - Windows 应用开发 | Microsoft Developer 第三步…

面试热题(验证二叉搜索树)

给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉树 二叉树满足以上3个条件&#xff0c…

SpringMVC关于SSM的整合配置步骤

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaweb 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 SSM整合 一、创建工程1.1创建Maven工程1.2工程命名1.3检查…

Ubuntu常用配置

文章目录 1. 安装VMware虚拟机软件2. 下载Ubuntu镜像3. 创建Ubuntu虚拟机4. 设置屏幕分辨率5. 更改系统语言为中文6. 切换中文输入法7. 修改系统时间8. 修改锁屏时间9. 通过系统自带的应用商店安装软件10. 安装JDK11. 安装 IntelliJ IDEA12. 将左侧任务栏自动隐藏13. 安装docke…

机器学习终极指南:特征工程(01/2) — 第 -2 部分

西姆兰吉特辛格 一、介绍 欢迎来到“机器学习终极指南”的第二部分。在第一部分中&#xff0c;我们讨论了探索性数据分析 &#xff08;EDA&#xff09;&#xff0c;这是机器学习管道中的关键步骤。在这一部分中&#xff0c;我们将深入研究特征工程&#xff0c;这是机器学习过程…

fiddler抓包工具的用法以及抓取手机报文定位bug

前言&#xff1a; fiddler抓包工具是日常测试中常用的一种bug定位工具 一 抓取https报文步骤 使用方法&#xff1a; 1 首先打开fiddler工具将证书导出 点击TOOLS------Options------Https-----Actions---选中第二个选项 2 把证书导出到桌面后 打开谷歌浏览器 设置---高级…

如何让你的图片服务也有类似OSS的图片处理功能

原文链接 前言 有自己机房的公司一般都有一套存储系统用于存储公司的图片、视频、音频、文件等数据&#xff0c;常见的存储系统有以NAS、FASTDFS为代表的传统文件存储&#xff0c;和以Minio为代表的对象存储系统&#xff0c;随着云服务的兴起很多公司逐渐将数据迁移到以阿里云…

MySql012——检索数据:创建计算字段(拼接字段、使用别名、执行算术计算)

准备工作1&#xff1a;在study库中创建表vendors&#xff0c;并插入数据 说明&#xff1a;vendors表包含供应商名和位置信息。 use study;CREATE TABLE vendors (vend_id int NOT NULL AUTO_INCREMENT,vend_name char(50) NOT NULL ,vend_address char(50) NULL ,…

SpringBootWeb案例

通过该综合案例,我们就可以知道,在开发一个Web程序时,前端程序、后端程序以及数据库这三者之间是如何交互、如何协作的,而通过这个综合案例也需要掌握根据接口文档开发服务端接口的能力。 而这个案例呢,就是Tlias智能学习辅助系统。 产品经理所绘制的页面原型: 在这个案…

postman测试后端增删改查

目录 一、本文介绍 二、准备工作 &#xff08;一&#xff09;新建测试 &#xff08;二&#xff09;默认url路径查看方法 三、增删改查 &#xff08;一&#xff09;查询全部 &#xff08;二&#xff09;增加数据 &#xff08;三&#xff09;删除数据 &#xff08;四&…

Faker库详解 - Python中的随机数据生成器

文章目录 Faker介绍Faker安装Faker使用基本使用方法随机生成人物相关的基础信息随机生成地理相关的信息随机生成网络相关的信息随机生成日期相关的信息随机生成数字/字符串/文本随机生成列表/元组/字典/集合/迭代器/json随机生成文件相关的信息随机生成颜色/表情每次请求获取相…

1.作用域

1.1局部作用域 局部作用域分为函数作用域和块作用域。 1.函数作用域: 在函数内部声明的变量只能在函数内部被访问&#xff0c;外部无法直接访问。 总结&#xff1a; (1)函数内部声明的变量&#xff0c;在函数外部无法被访问 (2)函数的参数也是函数内部的局部变量 (3)不同函数…

测试老鸟经验总结,Jmeter性能测试-重要指标与性能结果分析(超细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Aggregate Report …

【福建事业单位-数学运算】03经济利润-排列组合与概率

【福建事业单位-数学运算】03经济利润-排列组合与概率 一、经济利润1.1常规经济——考的多、难具体数值——方程无数值&#xff0c;给比例——赋值 1.2 分段计费1.3 函数最值&#xff08;销售总量 单价 * 销量 &#xff1b; 总利润 单利 * 销量&#xff09;总结 二、排列组合2…

腾讯云轻量应用服务器搭建WordPress网站教程

腾讯云百科分享使用腾讯云轻量应用服务器搭建WordPress网站教程流程&#xff0c;WordPress 是全球最流行的开源的博客和内容管理网站的建站平台&#xff0c;具备使用简单、功能强大、灵活可扩展的特点&#xff0c;提供丰富的主题插件。腾讯云轻量应用服务器提供 WordPress 应用…

基于注意力神经网络的深度强化学习探索方法:ARiADNE

ARiADNE:A Reinforcement learning approach using Attention-based Deep Networks for Exploration 文章目录 ARiADNE:A Reinforcement learning approach using Attention-based Deep Networks for Exploration机器人自主探索(ARE)ARE的传统边界法非短视路径深度强化学习的方…

leetcode 6914. 翻倍以链表形式表示的数字

给你一个 非空 链表的头节点 head &#xff0c;表示一个不含前导零的非负数整数。 将链表 翻倍 后&#xff0c;返回头节点 head 。 示例 1&#xff1a; 输入&#xff1a;head [1,8,9] 输出&#xff1a;[3,7,8] 解释&#xff1a;上图中给出的链表&#xff0c;表示数字 189 。返…

Linux网络基础(中)

目录&#xff1a; 再谈“协议” HTTP协议 认识URL&#xff1a; urlnecode和urldecode HTTP协议格式&#xff1a; HTTP的方法&#xff1a; 简易HTTP服务器&#xff1a; 传输层 再谈端口号&#xff1a; 端口号范围划分&#xff1a; netstat&#xff1a; pidof&…