1、前言
前段时间,PM提了一个需求,在APP内直播观众和回放页面,需要增加一个横竖屏切换的按钮,和当前众多视频APP的横竖屏切换行为保持一致。
当拿到这个需求的时候,觉得是easy模式,无非就是setRequestedOrientation(横屏/竖屏)
就ok了。但交互评审和查看B站、虎牙、钉钉等APP之后,并不是自己想得那么easy。因为忽略了一个很重要的设置——屏幕自动旋转。
图片存放在github的图库,如果无法正常展示,可以路由至github上 Android横竖屏切换实践
2、需求分析
先将需求分析清楚,其实从系统锁定(下拉菜单里面是否打开了自动旋转)的角度分为两种情况:
-
系统关闭自动旋转。
-
系统打开自动旋转。
这里先不要考虑Activity的模式是
ActivityInfo.SCREEN_ORIENTATION_SENSOR
, 即使是,也是一样处理,这点后面会讲。
对于这两种情况,我们来看看延生出来的情况
image
【说明】
-
对于屏幕自动旋转关闭的情况下,情况比较简单,用户手动切换了横竖屏后,保持不动即可。
-
如果屏幕自动旋转打开的情况,用户手动切换了横竖屏,此时需要增加一个策略,如果用户旋转屏幕到对应的方向,需要恢复自动旋转。
- 举个栗子:在竖屏观看状态下,用户点击切换,此时屏幕切换到横屏,手机仍然是竖屏状态。用户旋转90度到手机也是横屏时,随后用户再竖直回手机,屏幕能够自动旋转回竖屏。(读起来有点饶)
3、功能实现
经过上面的分析,我们可以得出两个需要去解决的关键点
【关键点1】监听系统自动旋转设置。
【关键点2】监听用户旋转到用户设置的方向。
3.1 监听系统自动旋转设置
首先,我们要知道如何读取系统设置的有关屏幕自动旋转的值
// 0 off / 1 on
Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION)
我们可以封一个方法,表示系统自动旋转是否打开,可以考虑放到某个Util工具类中。
private boolean canActivityAutoRotate() {
try {
int value = Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION);
return value == 1;
} catch (Settings.SettingNotFoundException e) {
LogUtils.INSTANCE.error(TAG, "canActivityAutoRotate error", e);
}
return false;
}
我的知道了如何获取设置值,那么什么时候去读取呢,当然是系统监听到设置的值发生了变化。
可以使用内容观察者ContentObserver
来监听设置值的变化。我们新建一个类,继承自ContentObserver
, 并在内部定义一个接口,将观测结果回调。这样的封装,可以提供多处使用。
/**
* Created by Numen_fan on 2023/2/11
* Desc: 监听系统设置中是否打开自动选择,部分手机厂商叫方向锁定
*/
public class ScreenRotationSettingObserver extends ContentObserver {
private static final String TAG = "RotationObserver";
final ContentResolver mResolver;
private ScreenRotationSettingListener listener;
public ScreenRotationSettingObserver(Handler handler, ContentResolver resolver) {
super(handler);
mResolver = resolver;
}
public void setSystemOrientationSettingListener(ScreenRotationSettingListener l) {
this.listener = l;
}
/**
* 屏幕旋转设置改变时调用
*/
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
if (listener != null) {
listener.onRotationSettingChanged();
}
}
public void startObserver() {
if (mResolver == null) {
LogUtils.INSTANCE.warn(TAG, "mResolver is null");
return;
}
mResolver.registerContentObserver(Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
false, this);
}
public void stopObserver() {
if (mResolver == null) {
LogUtils.INSTANCE.warn(TAG, "mResolver is null");
return;
}
mResolver.unregisterContentObserver(this);
}
/**
* 监听回调
*/
public interface ScreenRotationSettingListener {
void onRotationSettingChanged();
}
}
3.2 监听屏幕旋转
这里的监听屏幕旋转,并不是简单的onConfigChanged()
,准确来说是监听屏幕的旋转角度。
上面提到,我们需要知道用户手动设置了横竖屏后,手机何时旋转到对应的位置,此时需要恢复Activity到默认的旋转行为。
我们可以借助OrientationEventListener
,实时的获取当前手机的旋转角度,从而计算出当前手机的横竖状态。这里可以补一下旋转角度的知识。
-
正常竖直状态(信号电量状态栏朝上),orientation = 0。
-
横屏状态1(信号电量状态栏朝左),orientation = 270。
-
反向竖直状态(信号电量状态栏朝下),orientation = 180。
-
横屏状态2 (信号电量状态栏朝右),orientation = 90。
同样我们可以封装一下
/**
* Created by Numen_fan on 2023/2/11
* Desc: 时刻检测屏幕的旋转角度,并计算当前的横竖屏状态
*/
public class ScreenOrientationDetector extends OrientationEventListener {
private int mCurrentOrientation;
public static final int ORIENTATION_UNDEFINED = 0;
public static final int ORIENTATION_PORTRAIT = 1;
public static final int ORIENTATION_LANDSCAPE = 2;
private int currentOrientation = ORIENTATION_UNDEFINED;
private OrientationChangeListener listener;
public ScreenOrientationDetector(Context context, int rate) {
super(context, rate);
}
public void setOrientationChangeListener(OrientationChangeListener l) {
this.listener = l;
}
private int getOrientation() {
if (this.mCurrentOrientation != 0 && this.mCurrentOrientation != 180) {
return this.mCurrentOrientation != 90 && this.mCurrentOrientation != 270
? ORIENTATION_UNDEFINED : ORIENTATION_LANDSCAPE;
} else {
return ORIENTATION_PORTRAIT;
}
}
@Override
public void onOrientationChanged(int orientation) {
if (orientation != ORIENTATION_UNKNOWN) {
if (orientation >= 45 && orientation <= 315) {
if (orientation > 45 && orientation < 135) {
this.mCurrentOrientation = 90;
} else if (orientation > 135 && orientation < 225) {
this.mCurrentOrientation = 180;
} else if (orientation > 225 && orientation < 315) {
this.mCurrentOrientation = 270;
}
} else {
this.mCurrentOrientation = 0;
}
int newOrientation = getOrientation();
if (ORIENTATION_UNDEFINED != newOrientation && newOrientation != currentOrientation) {
currentOrientation = newOrientation;
if (listener != null) {
listener.onOrientationChanged(currentOrientation);
}
}
}
}
public void initOrientation() {
currentOrientation = ORIENTATION_UNDEFINED;
}
/**
* 计算出屏幕发生旋转,就会触发
*/
public interface OrientationChangeListener {
void onOrientationChanged(int orientation);
}
}
注意这里有一个initOrientation
方法,这个后面会讲,它在什么场景下会被调用。
3.3 使用
这里我们用一个Activity来实现一下 ,功能很简单,在页面上放了一个按钮,点击后,切换横竖屏就好了。
public class OrientationActivity extends BaseActivity implements ScreenRotationSettingObserver.ScreenRotationSettingListener,
ScreenOrientationDetector.OrientationChangeListener {
private static final String TAG = "OrientationActivity";
private static final int CHANGE_ORIENTATION = 10086;
private ScreenRotationSettingObserver mScreenRotationSettingObserver;
private ScreenOrientationDetector mScreenOrientationDetector;
private Handler mHandler;
@Override
public int getContentResId() {
return R.layout.activity_orientation;
}
@Override
public void initUI() {
}
@Override
public void initParam() {
mScreenRotationSettingObserver = new ScreenRotationSettingObserver(mHandler, getContentResolver());
mScreenOrientationDetector = new ScreenOrientationDetector(this, SensorManager.SENSOR_DELAY_NORMAL);
mScreenRotationSettingObserver.setSystemOrientationSettingListener(this);
mScreenOrientationDetector.setOrientationChangeListener(this);
// 初始化Handler
mHandler = new Handler(getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == CHANGE_ORIENTATION) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
mScreenOrientationDetector.disable(); // 恢复设置后,结束检测
}
}
};
}
@Override
public void initListener() {
findViewById(R.id.btn_change_orientation).setOnClickListener(v -> changeOrientation());
// 开启屏幕自动旋转开关的监听
mScreenRotationSettingObserver.startObserver();
}
@Override
protected void onDestroy() {
super.onDestroy();
mScreenRotationSettingObserver.setSystemOrientationSettingListener(null);
mScreenOrientationDetector.setOrientationChangeListener(null);
mScreenOrientationDetector.disable();
mScreenRotationSettingObserver.stopObserver();
mHandler.removeMessages(CHANGE_ORIENTATION);
mHandler = null;
}
/**
* 屏幕自动旋转开关发生变化
*/
@Override
public void onRotationSettingChanged() {
LogUtils.INSTANCE.warn(TAG, "系统自动选择设置发生变化");
startScreenOrientationListen();
}
/**
* 实时计算的横竖屏发生了变化
*
* @param orientation 当前横屏还是竖屏
*/
@Override
public void onOrientationChanged(int orientation) {
LogUtils.INSTANCE.warn(TAG, "横竖屏计算发生变化,当前状态 = " + orientation);
if (canActivityAutoRotate() && getResources().getConfiguration().orientation == orientation) {
// 当手机旋转到和手动设置的同一个方向,恢复默认的设置。
mHandler.sendEmptyMessageDelayed(CHANGE_ORIENTATION, 500);
}
}
/**
* 手动改变横竖屏
*/
@SuppressLint("SourceLockedOrientationActivity")
private void changeOrientation() {
mHandler.removeMessages(CHANGE_ORIENTATION); // 手动切换,移除之前的延迟任务,避免快速点击带来的问题。
if (isLandscape()) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
startScreenOrientationListen();
}
/**
* 打开屏幕旋转监听
*/
private void startScreenOrientationListen() {
// 如果系统自动旋转打开,则开启横竖屏切换检测
if (canActivityAutoRotate()) {
LogUtils.INSTANCE.warn(TAG, "开启屏幕旋转检测");
mHandler.postDelayed(() -> {
mScreenOrientationDetector.initOrientation();
mScreenOrientationDetector.enable();
}, 500);
} else {
LogUtils.INSTANCE.warn(TAG, "关闭了自动旋转");
mScreenOrientationDetector.disable(); // 如果关闭了自动旋转,取消一次横竖屏监听
}
}
private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
private boolean canActivityAutoRotate() {
try {
int value = Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION);
return value == 1;
} catch (Settings.SettingNotFoundException e) {
LogUtils.INSTANCE.error(TAG, "canActivityAutoRotate error", e);
}
return false;
}
}
【说明】
-
各个初始化的中,不需要关注太多,在initParam中
startScreenOrientationListen
开启监听,监听系统中自动旋转的变化。 -
Handler用处1:用于延迟生效屏幕旋转监听。因为
OrientationEventListener
的回调是很频繁的,频率大概是200ms。如果我们手动切换后立刻开启,当用户在旋转的过程中,可能Sensor回调偏差,导致orientation计算出错,就会导致恢复默认,画面来回切换,这也是为何在开启检测和恢复默认的时候都有延迟执行。 -
Handler用户2:延迟执行恢复默认行为,注意由于有延迟机制,每次手动切换都要记得移除前一次的事件,否则在快速点击切换的时候会有问题,这点不难理解。(其实这里可以优化一下,orientation的判断应该拿在延迟事件中一起判断)
-
mScreenOrientationDetector.initOrientation();
是为了确保每次开启监听一定会收到回调,解决在竖屏状态下,手动点击切换横屏,保持不动的情况下,继续切回竖屏之后能够收到回调,恢复默认行为。 -
因为
OrientationEventListener
的执行频繁,所以要做好disable
的处理。
3.4 流程总结
依据3.3节中的实现,总结如下流程
image
网友评论