1、前言
前段時間,PM提了一個需求,在APP內(nèi)直播觀眾和回放頁面,需要增加一個橫豎屏切換的按鈕,和當(dāng)前眾多視頻APP的橫豎屏切換行為保持一致。
當(dāng)拿到這個需求的時候,覺得是easy模式,無非就是setRequestedOrientation(橫屏/豎屏)就ok了。但交互評審和查看B站、虎牙、釘釘?shù)華PP之后,并不是自己想得那么easy。因為忽略了一個很重要的設(shè)置——屏幕自動旋轉(zhuǎn)。
圖片存放在github的圖庫,如果無法正常展示,可以路由至github上 Android橫豎屏切換實踐
2、需求分析
先將需求分析清楚,其實從系統(tǒng)鎖定(下拉菜單里面是否打開了自動旋轉(zhuǎn))的角度分為兩種情況:
系統(tǒng)關(guān)閉自動旋轉(zhuǎn)。
系統(tǒng)打開自動旋轉(zhuǎn)。
這里先不要考慮Activity的模式是
ActivityInfo.SCREEN_ORIENTATION_SENSOR, 即使是,也是一樣處理,這點后面會講。
對于這兩種情況,我們來看看延生出來的情況

【說明】
對于屏幕自動旋轉(zhuǎn)關(guān)閉的情況下,情況比較簡單,用戶手動切換了橫豎屏后,保持不動即可。
-
如果屏幕自動旋轉(zhuǎn)打開的情況,用戶手動切換了橫豎屏,此時需要增加一個策略,如果用戶旋轉(zhuǎn)屏幕到對應(yīng)的方向,需要恢復(fù)自動旋轉(zhuǎn)。
- 舉個栗子:在豎屏觀看狀態(tài)下,用戶點擊切換,此時屏幕切換到橫屏,手機仍然是豎屏狀態(tài)。用戶旋轉(zhuǎn)90度到手機也是橫屏?xí)r,隨后用戶再豎直回手機,屏幕能夠自動旋轉(zhuǎn)回豎屏。(讀起來有點饒)
3、功能實現(xiàn)
經(jīng)過上面的分析,我們可以得出兩個需要去解決的關(guān)鍵點
【關(guān)鍵點1】監(jiān)聽系統(tǒng)自動旋轉(zhuǎn)設(shè)置。
【關(guān)鍵點2】監(jiān)聽用戶旋轉(zhuǎn)到用戶設(shè)置的方向。
3.1 監(jiān)聽系統(tǒng)自動旋轉(zhuǎn)設(shè)置
首先,我們要知道如何讀取系統(tǒng)設(shè)置的有關(guān)屏幕自動旋轉(zhuǎn)的值
// 0 off / 1 on
Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION)
我們可以封一個方法,表示系統(tǒng)自動旋轉(zhuǎn)是否打開,可以考慮放到某個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;
}
我的知道了如何獲取設(shè)置值,那么什么時候去讀取呢,當(dāng)然是系統(tǒng)監(jiān)聽到設(shè)置的值發(fā)生了變化。
可以使用內(nèi)容觀察者ContentObserver來監(jiān)聽設(shè)置值的變化。我們新建一個類,繼承自ContentObserver, 并在內(nèi)部定義一個接口,將觀測結(jié)果回調(diào)。這樣的封裝,可以提供多處使用。
/**
* Created by Numen_fan on 2023/2/11
* Desc: 監(jiān)聽系統(tǒng)設(shè)置中是否打開自動選擇,部分手機廠商叫方向鎖定
*/
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;
}
/**
* 屏幕旋轉(zhuǎn)設(shè)置改變時調(diào)用
*/
@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);
}
/**
* 監(jiān)聽回調(diào)
*/
public interface ScreenRotationSettingListener {
void onRotationSettingChanged();
}
}
3.2 監(jiān)聽屏幕旋轉(zhuǎn)
這里的監(jiān)聽屏幕旋轉(zhuǎn),并不是簡單的onConfigChanged(),準確來說是監(jiān)聽屏幕的旋轉(zhuǎn)角度。
上面提到,我們需要知道用戶手動設(shè)置了橫豎屏后,手機何時旋轉(zhuǎn)到對應(yīng)的位置,此時需要恢復(fù)Activity到默認的旋轉(zhuǎn)行為。
我們可以借助OrientationEventListener,實時的獲取當(dāng)前手機的旋轉(zhuǎn)角度,從而計算出當(dāng)前手機的橫豎狀態(tài)。這里可以補一下旋轉(zhuǎn)角度的知識。
正常豎直狀態(tài)(信號電量狀態(tài)欄朝上),orientation = 0。
橫屏狀態(tài)1(信號電量狀態(tài)欄朝左),orientation = 270。
反向豎直狀態(tài)(信號電量狀態(tài)欄朝下),orientation = 180。
橫屏狀態(tài)2 (信號電量狀態(tài)欄朝右),orientation = 90。
同樣我們可以封裝一下
/**
* Created by Numen_fan on 2023/2/11
* Desc: 時刻檢測屏幕的旋轉(zhuǎn)角度,并計算當(dāng)前的橫豎屏狀態(tài)
*/
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;
}
/**
* 計算出屏幕發(fā)生旋轉(zhuǎn),就會觸發(fā)
*/
public interface OrientationChangeListener {
void onOrientationChanged(int orientation);
}
}
注意這里有一個initOrientation 方法,這個后面會講,它在什么場景下會被調(diào)用。
3.3 使用
這里我們用一個Activity來實現(xiàn)一下 ,功能很簡單,在頁面上放了一個按鈕,點擊后,切換橫豎屏就好了。
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(); // 恢復(fù)設(shè)置后,結(jié)束檢測
}
}
};
}
@Override
public void initListener() {
findViewById(R.id.btn_change_orientation).setOnClickListener(v -> changeOrientation());
// 開啟屏幕自動旋轉(zhuǎn)開關(guān)的監(jiān)聽
mScreenRotationSettingObserver.startObserver();
}
@Override
protected void onDestroy() {
super.onDestroy();
mScreenRotationSettingObserver.setSystemOrientationSettingListener(null);
mScreenOrientationDetector.setOrientationChangeListener(null);
mScreenOrientationDetector.disable();
mScreenRotationSettingObserver.stopObserver();
mHandler.removeMessages(CHANGE_ORIENTATION);
mHandler = null;
}
/**
* 屏幕自動旋轉(zhuǎn)開關(guān)發(fā)生變化
*/
@Override
public void onRotationSettingChanged() {
LogUtils.INSTANCE.warn(TAG, "系統(tǒng)自動選擇設(shè)置發(fā)生變化");
startScreenOrientationListen();
}
/**
* 實時計算的橫豎屏發(fā)生了變化
*
* @param orientation 當(dāng)前橫屏還是豎屏
*/
@Override
public void onOrientationChanged(int orientation) {
LogUtils.INSTANCE.warn(TAG, "橫豎屏計算發(fā)生變化,當(dāng)前狀態(tài) = " + orientation);
if (canActivityAutoRotate() && getResources().getConfiguration().orientation == orientation) {
// 當(dāng)手機旋轉(zhuǎn)到和手動設(shè)置的同一個方向,恢復(fù)默認的設(shè)置。
mHandler.sendEmptyMessageDelayed(CHANGE_ORIENTATION, 500);
}
}
/**
* 手動改變橫豎屏
*/
@SuppressLint("SourceLockedOrientationActivity")
private void changeOrientation() {
mHandler.removeMessages(CHANGE_ORIENTATION); // 手動切換,移除之前的延遲任務(wù),避免快速點擊帶來的問題。
if (isLandscape()) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
startScreenOrientationListen();
}
/**
* 打開屏幕旋轉(zhuǎn)監(jiān)聽
*/
private void startScreenOrientationListen() {
// 如果系統(tǒng)自動旋轉(zhuǎn)打開,則開啟橫豎屏切換檢測
if (canActivityAutoRotate()) {
LogUtils.INSTANCE.warn(TAG, "開啟屏幕旋轉(zhuǎn)檢測");
mHandler.postDelayed(() -> {
mScreenOrientationDetector.initOrientation();
mScreenOrientationDetector.enable();
}, 500);
} else {
LogUtils.INSTANCE.warn(TAG, "關(guān)閉了自動旋轉(zhuǎn)");
mScreenOrientationDetector.disable(); // 如果關(guān)閉了自動旋轉(zhuǎn),取消一次橫豎屏監(jiān)聽
}
}
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;
}
}
【說明】
各個初始化的中,不需要關(guān)注太多,在initParam中
startScreenOrientationListen開啟監(jiān)聽,監(jiān)聽系統(tǒng)中自動旋轉(zhuǎn)的變化。Handler用處1:用于延遲生效屏幕旋轉(zhuǎn)監(jiān)聽。因為
OrientationEventListener的回調(diào)是很頻繁的,頻率大概是200ms。如果我們手動切換后立刻開啟,當(dāng)用戶在旋轉(zhuǎn)的過程中,可能Sensor回調(diào)偏差,導(dǎo)致orientation計算出錯,就會導(dǎo)致恢復(fù)默認,畫面來回切換,這也是為何在開啟檢測和恢復(fù)默認的時候都有延遲執(zhí)行。Handler用戶2:延遲執(zhí)行恢復(fù)默認行為,注意由于有延遲機制,每次手動切換都要記得移除前一次的事件,否則在快速點擊切換的時候會有問題,這點不難理解。(其實這里可以優(yōu)化一下,orientation的判斷應(yīng)該拿在延遲事件中一起判斷)
mScreenOrientationDetector.initOrientation();是為了確保每次開啟監(jiān)聽一定會收到回調(diào),解決在豎屏狀態(tài)下,手動點擊切換橫屏,保持不動的情況下,繼續(xù)切回豎屏之后能夠收到回調(diào),恢復(fù)默認行為。因為
OrientationEventListener的執(zhí)行頻繁,所以要做好disable的處理。
3.4 流程總結(jié)
依據(jù)3.3節(jié)中的實現(xiàn),總結(jié)如下流程
