参考android官网:
- Frame rate | Android media | Android Developers
- 多重刷新率 | Android 开源项目 | Android Open Source Project
- 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 界面设计
- 动态显示系统当前使用的帧率和分辨率信息
- 系统支持的所有帧率(使用Spinner组件),可下拉选择,通过setFrameRate( )或者PreferredDisplayModeId改变当前系统帧率
- 下拉选择APP的绘制频率(使用Spinner组件),提供常用的绘制频率选择项
- 输入APP绘制频率(EditTextNumber):如果下拉选择APP的绘制频率无法满足要求,可以输入绘制频率,输入优先。
- 确认按键:启动/停止绘制按键
- 绘制显示区域:方便直观观察现象
2.设置帧率的方法
同之前一样使用setFrameRate( )/PreferredDisplayModeId/PreferredRefreshRate
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目录