Android橫豎屏切換實踐

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, 即使是,也是一樣處理,這點后面會講。

對于這兩種情況,我們來看看延生出來的情況


image

【說明】

  1. 對于屏幕自動旋轉(zhuǎn)關(guān)閉的情況下,情況比較簡單,用戶手動切換了橫豎屏后,保持不動即可。

  2. 如果屏幕自動旋轉(zhuǎn)打開的情況,用戶手動切換了橫豎屏,此時需要增加一個策略,如果用戶旋轉(zhuǎn)屏幕到對應(yīng)的方向,需要恢復(fù)自動旋轉(zhuǎn)。

    1. 舉個栗子:在豎屏觀看狀態(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;
    }
}

【說明】

  1. 各個初始化的中,不需要關(guān)注太多,在initParam中startScreenOrientationListen開啟監(jiān)聽,監(jiān)聽系統(tǒng)中自動旋轉(zhuǎn)的變化。

  2. 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í)行。

  3. Handler用戶2:延遲執(zhí)行恢復(fù)默認行為,注意由于有延遲機制,每次手動切換都要記得移除前一次的事件,否則在快速點擊切換的時候會有問題,這點不難理解。(其實這里可以優(yōu)化一下,orientation的判斷應(yīng)該拿在延遲事件中一起判斷)

  4. mScreenOrientationDetector.initOrientation(); 是為了確保每次開啟監(jiān)聽一定會收到回調(diào),解決在豎屏狀態(tài)下,手動點擊切換橫屏,保持不動的情況下,繼續(xù)切回豎屏之后能夠收到回調(diào),恢復(fù)默認行為。

  5. 因為OrientationEventListener 的執(zhí)行頻繁,所以要做好disable 的處理。

3.4 流程總結(jié)

依據(jù)3.3節(jié)中的實現(xiàn),總結(jié)如下流程


image

資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容