25
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GLOBISAdvent Calendar 2021

Day 15

AndroidでYouTubeの再生画面の回転仕様を再現する

Posted at

はじめに

本記事はGlobis Advent Calendar15日目の記事です。

業務で動画学習アプリを作っている中で、YouTubeの画面回転の仕様を真似しようとしたら一筋縄ではいかなかったので備忘録として残しておきます:pencil:

長いので要約するとrequestedOrientationとOrientationEventListenerを使って実現しましたという話です。

YouTube再生画面の画面回転仕様について

普段YouTubeで動画を視聴する中で特に違和感なく利用していたのですが、詳細仕様を意識して触っていなかったため、この機会にじっくり観察してみました。
試した環境はAndroid 11のPixel 3a XLで、YouTubeアプリのバージョンは16.46.37です。

画面表示の向き

言うまでもありませんが、YouTubeの再生画面には縦向きと横向きの表示があります。

縦向き

まずは基本の縦向きです。

このときプレイヤーの右下には全画面表示するボタンが表示され、押下すると横向きの全画面表示に切り替わります。

横向き(全画面表示)

プレイヤー右下の全画面表示ボタンを押下すると、画面表示が時計回りに90°回転し全画面表示になります。
このときプレイヤーの右下には全画面表示を終了するボタンが表示され、押下すると縦向きの表示に切り替わります。

厳密に言うと横向きは上記だけでは不十分で、更にそこから180°回転させたパターンが存在します。
上図では右側に表示されていた端末のボタン群が、下図だと左側に表示されています。

画面表示の向きのまとめ

縦向きの0°を始点として、時計回りに90°、または反時計周りに90°回した横向きが2パターンの合計3パターンの画面表示の向きがあります。
また、余談ですが縦向きを逆さまにした180°では画面は回転しませんでした。

端末を傾けたときの挙動

端末を傾けたときの挙動は、端末側で設定できる自動回転ON/OFFの設定値によって挙動が異なります。

自動回転OFFのとき

  • 縦向きのとき
    • 端末を90°左に傾けても、縦向きが継続する
    • 端末を90°右に傾けても、縦向きが継続する

ezgif-3-004a1925a2ee.gif

  • 横向きのとき
    • 端末を0°の位置に戻しても、横向きが継続する
    • 最初の横向きから180°傾けると、回転し反対の横向きに切り替わる

ezgif-3-693dfb463986.gif

自動回転ONのとき

  • 縦向きのとき
    • 端末を90°左に傾けると、横向きに切り替わる
    • 端末を90°右に傾けると、横向きに切り替わる
  • 横向きのとき
    • 端末を0°の位置に戻すと、縦向きに切り替わる

ezgif-3-c22c82132b6f.gif

端末を傾けたときの挙動のまとめ

  • 自動回転OFF
    • 縦向きのときは端末を左右に90°傾けても画面表示は切り替わらない
    • 横向きのときは端末を傾けるとその向きに適した横向きに切り替わる
  • 自動回転ON
    • 表示の向きに関わらず端末を傾けるとそれに沿った向きの表示に切り替わる

という、ここまでは至極真っ当な挙動かと思います。

全画面表示/解除ボタンが絡んだときの挙動

YouTubeはプレイヤーの右下に全画面表示を切り替えるボタンがあるのですが、これが絡むと少々話がややこしくなってきます。
なぜなら、全画面表示ボタンを押下するタイミングによっては、実際の端末の傾きと表示されている画面表示の向きが異なる場合があるからです。

また、更に厄介なことに、端末側の自動回転ON/OFFの設定値によっても挙動が異なってきます。

自動回転OFFのとき

  • 縦向きのとき
    • 全画面表示ボタンを押下すると、横向きに切り替わる
      • 表示されている横向きから見て180°回転させると、反対側の横向きに切り替わる
        • なお、端末を縦向きに戻してもいずれかの横向きが継続する

ezgif-3-fc93cb8b5a47.gif

  • 横向きのとき
    • 全画面解除ボタンを押下すると、縦向きに切り替わる
      • 上記から端末をいずれかの横向きに戻しても縦向きが継続する

ezgif-3-108fc22b93f1.gif

自動回転ONのとき

  • 縦向きのとき
    • 全画面表示ボタンを押下すると、横向きに切り替わる
      • 一度端末を横向きにするまでは、そのままの向きが継続する
        • 一度端末を横向きにすると、それ以降は端末の傾きに応じた画面回転をする

ezgif-3-ada3d174172e.gif

  • 横向きのとき
    • 全画面解除ボタンを押下すると、縦向きに切り替わる
      • 一度端末を縦向きにするまでは、そのまま縦向きが継続する
        • 一度端末を縦向きにすると、それ以降は端末の傾きに応じた画面回転をする

ezgif-3-824bd6d946cc.gif

全画面表示/解除ボタンが絡んだときの挙動のまとめ

  • 端末の自動回転がOFFのとき
    • 全画面表示に切り替えた際に、端末の傾きに応じた横向きになり、全画面表示を解除すると縦向きに固定される。
  • 端末の自動回転がONのとき
    • 全画面表示/解除ボタンを押下すると表示の向きが切り替わるが、一度端末の傾きを表示されている向きと揃えると、それ以降は端末の傾きに応じた画面回転をする

仕様のまとめ

整理すると下記の4つの概念が絡んで画面回転の挙動が決定されます。

  • 画面表示の向き(縦 or 横)
  • 端末の傾き(0~359°)
  • 全画面表示/解除ボタン押下
  • 自動回転の設定値(ON or OFF)

パターンを書き出すと少々膨れてしまいますが、下記のような挙動になります。

自動回転OFFのとき

  • 縦向きのとき
    • 端末の傾きを360°どの角度に変更しても縦向きが継続する
    • 全画面表示ボタンを押下すると横向きになる
  • 横向きのとき
    • 端末の傾きを左右いずれかに90°変更しても横向きが継続する
    • 端末の傾きを180°変更すると、画面が回転して反対側の横向きになる
    • 全画面解除ボタンを押下すると縦向きになる

自動回転がONのとき

  • 縦向きのとき
    • 端末を傾けると、その傾きに応じた画面回転をする(逆さまの縦向きを除く)
    • 全画面表示ボタンを押下すると横向きになる
      • 端末の傾きを横向きに揃えるまでは横向き表示が継続し、その後は端末の傾きに応じた画面回転をする
  • 横向きのとき
    • 端末を傾けると、その傾きに応じた画面回転をする(逆さまの縦向きを除く)
    • 全画面解除ボタンを押下すると縦向きになる
      • 端末の傾きを縦向きに揃えるまでは縦向き表示が継続し、その後は端末の傾きに応じた画面回転をする

実装方法

YouTubeの再生画面の画面回転仕様を把握したところで、同様の動作をするように実装していきます。
サンプルとして、縦向きと横向きに応じてボタンの文字と動作が変わるアプリを考えます。

縦向きのときはEXPANDというボタンが表示され、押下すると横画面表示に変わります。

横向きのときはCONTRACTというボタンが表示され、押下すると縦画面表示に変わります。

(アイコンを用意するのが面倒だったので文字のボタンでお茶を濁しています:pray:

全画面表示/解除ボタンの挙動を再現する

画面右下にあるボタンを再現し、押下されるごとに縦向き/横向きを切り替えられるようにしていきます。

レイアウトの用意

このサンプルではDataBindingを使用します。
また、ボタンは別個に用意しておきisPortraitの値に応じて表示/非表示を切り替えています。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="isPortrait"
            type="Boolean" />

        <import type="android.view.View" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/button_expand"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="expand"
            android:visibility="@{isPortrait ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <Button
            android:id="@+id/button_contract"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="contract"
            android:visibility="@{isPortrait ? View.GONE : View.VISIBLE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

ManifestへのconfigChanges属性の追加

構成の変更を自分で処理するに従い、画面の向きが変更された時にActivity#onConfigurationChangedが呼ばれるよう、MainActivityの属性にandroid:onConfigChanges=orientation|screenSizeを追加します。

AndroidManifest.xml
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|screenSize"
            android:exported="true"> 
                        // 中略
        </activity>

MainActivityの実装

onCreateでの初回読み込み時とonConfigurationChangedが呼び出されるタイミングでxml側のisPortraitを更新します。

画面の向きはAndroidManifest.xmlでActivityのandroid:screenOrientationに値を設定することで指定できるのですが、指定してしまうと画面の向きの設定が固定されてしまいます。
今回はボタン押下時に動的に画面を回転させたいため、そのような場合にはActivity#setRequestedOrientationを呼び出します。

注意点としてEXPANDボタンが押された時にリクエストするのはActivityInfo.SCREEN_ORIENTATION_LANDSCAPEではなくActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPEです:warning:
両者は一見似ているのですが前者は90°で固定されてしまうのに対し、後者はその名にSENSORが入っているようにセンサーの状態に準ずる横向きの表示になります。

「自動回転OFFの状態で全画面表示に切り替えた際に、端末の傾きに応じた横向きになる」という挙動を再現するためには後者が適切です:ok_woman:

MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(
            this,
            R.layout.activity_main
        )

        binding.buttonExpand.setOnClickListener {
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
        }
        binding.buttonContract.setOnClickListener {
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        }

        updateScreenOrientation(resources.configuration)
    }

    private fun updateScreenOrientation(config: Configuration) {
        binding.isPortrait = config.orientation == Configuration.ORIENTATION_PORTRAIT
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        updateScreenOrientation(newConfig)
    }

ここまでの実装における問題点

これで自動回転がOFFのときの全画面表示/解除ボタン押下時の動作仕様は満たせるようになりました。

  • 自動回転OFFのとき
    • 縦向きのとき
      • 全画面表示ボタンを押下すると横向きになる:ok:
    • 横向きのとき
      • 端末の傾きを180°変更すると、画面が回転して反対側の横向きになる:ok:
      • 全画面解除ボタンを押下すると縦向きになる:ok:
  • 自動回転がONのとき
    • 縦向きのとき
      • 全画面表示ボタンを押下すると横向きになる:ok:
        • 端末の傾きを横向きに揃えるまでは横向き表示が継続し、その後は端末の傾きに応じた画面回転をする:ng:
    • 横向きのとき
      • 全画面解除ボタンを押下すると縦向きになる:ok:
        • 端末の傾きを縦向きに揃えるまでは縦向き表示が継続し、その後は端末の傾きに応じた画面回転をする:ng:

しかし、ビルドして動作を確認してみるとこのままでは自動回転ONの時の仕様を満たせません。
具体的には一度ボタンを押してしまうと、端末の傾きによる画面回転がうまく動作しません。

その理由は、ボタン押下時に指定されるrequestedOrientationSCREEN_ORIENTATION_PORTRAITSCREEN_ORIENTATION_SENSOR_LANDSCAPEのどちらかだからです。
この値が指定されてしまった後だと、端末の傾きに応じた画面回転をしません。

画面の自動回転ONのときの挙動を再現する

requestedOrientationの指定だけでは完全な再現ができませんでした、ではどうすればよいのか。
最初に考えたのは自動回転がONの時、画面回転をした後に端末センサーによって表示の向きが決まるSCREEN_ORIENTATION_SENSORを指定することでした。

そこでonConfigurationChangedで指定することを試みたのですが、実行してみると回転した画面がすぐに元に戻ってしまいました:innocent:

この原因は画面表示の向きと、端末の傾きは必ずしも一致しないことによるものです。

例えば、端末の傾きが0°且つ縦向きの時に全画面表示ボタンを押すと、画面表示の向きは横向きになりますが、端末の傾きはまだ0°のままです。
端末の傾きが0°の状態でSCREEN_ORIENTATION_SENSORを指定すると、直ちに「端末は縦向きである」と判断されてしまうため、すぐに縦向きに戻されてしまいます。

ふりだしに戻り、別の手段での解決策を探していったところ、OrientationEventListenerによって求めている挙動を実現できそうということが分かりました:bulb:

OrientationEventListenerとは?

APIレベル3から存在している端末の傾きが変わった時にSensorManagerからの通知を受け取るためのヘルパークラスです。
端末の傾きが変わるたびにonOrientationChangedが呼び出され、0~359までの度数が分かります。

余談ですが私自身はこれまで使う場面がなかったため、今回調べるまでその存在を知りませんでした:rolling_eyes:

OrientationEventListenerを用いた実装

今回はまず、抽象クラスであるOrientationEventListenerを実装したOrientationHelperクラスを作成し、それをMainActivityから利用していきます。

OrientationHelper.kt

自動回転OFFのときに端末の傾きが必要になるのは全画面表示だけであり、なおかつSCREEN_ORIENTATION_SENSOR_LANDSCAPEが指定されていればこちらでハンドリングの必要はありません。
そのため、自動回転ON/OFFの設定値を取得し、もし設定がOFFだったら早期リターンをしています。

また、端末の傾きが変わるたびに逐一onOrientationChangedが呼ばれ通知回数が多くなりがちなので、このコードでは流量を減らすために500msの制限を設けています。

OrientationHelper.kt
class OrientationHelper(
    private val context: Context
) : OrientationEventListener(context) {

    interface OnOrientationChangeListener {
        fun onOrientationChanged(orientation: Int)
    }

    private var onOrientationChangeListener: OnOrientationChangeListener? = null

    private var lastTime: Long = 0

    override fun onOrientationChanged(orientation: Int) {
        if(!isAutoRotateEnabled()) return

        val currentTime = System.currentTimeMillis()
        if (currentTime - lastTime < 500) return

        onOrientationChangeListener?.onOrientationChanged(orientation)
        lastTime = currentTime
    }

    fun setListener(onOrientationChangeListener: OnOrientationChangeListener?) {
        this.onOrientationChangeListener = onOrientationChangeListener
    }

    private fun isAutoRotateEnabled(): Boolean =
        Settings.System.getInt(
            context.contentResolver,
            Settings.System.ACCELEROMETER_ROTATION
        ) == 1
}

MainActivity.kt

先述の画面回転ボタンに関連するコードは省略してあります。

onCreateでOrientationHelperのインスタンスを作成し、onResume/onPauseで有効/無効の切り替えをします。
端末の傾きが通知されるのがonOrientationChangedメソッドで、この中の処理が今回の肝です。

現在設定されているrequestedOrientationの値、最後の表示の向き、現在の端末の傾きを比較し、適切なタイミングでrequestedOrientationに値を設定します。

また、今回の実装ではSCREEN_ORIENTATION_SENSORを指定してセンサーに頼ることはなく、端末の傾きに応じた3種類いずれかの設定値を動的に指定していることに注意してください:warning:

設定値 効果
SCREEN_ORIENTATION_PORTRAIT 縦固定(0°)
SCREEN_ORIENTATION_REVERSE_LANDSCAPE 横固定(90°)
SCREEN_ORIENTATION_LANDSCAPE 横固定(270°)
MainActivity.kt
class MainActivity : AppCompatActivity(), OrientationHelper.OnOrientationChangeListener {

    private lateinit var orientationHelper: OrientationHelper
    private var currentOrientation: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        // 中略
        orientationHelper = OrientationHelper(this)
    }

    override fun onResume() {
        super.onResume()
        orientationHelper.apply {
            setListener(this@MainActivity)
            enable()
        }
    }

    override fun onPause() {
        super.onPause()
        orientationHelper.apply {
            setListener(null)
            orientationHelper.disable()
        }
    }

    override fun onOrientationChanged(orientation: Int) {
        val lastOrientation = currentOrientation

        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
            currentOrientation = -1
            return
        }

        if (orientation >= 350 || orientation <= 10) {
            if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE && lastOrientation == 0 ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE && lastOrientation == 0 ||
                requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE && lastOrientation == 0
            ) return
            if (currentOrientation == 0) return
            currentOrientation = 0
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        } else if (orientation in 80..100) {
            if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT && lastOrientation == 90) return
            if (currentOrientation == 90) return
            currentOrientation = 90
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
        } else if (orientation in 260..280) {
            if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT && lastOrientation == 270) return
            if (currentOrientation == 270) return
            currentOrientation = 270
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
        }
    }
}

これで実装は終わりです。

requestedOrientationに加えOrientationEventListenerを利用することによって、先程:ng:だった自動回転がONのときの動作仕様が:ok:になりました。

  • 自動回転がONのとき
    • 縦向きのとき
      • 全画面表示ボタンを押下すると横向きになる:ok:
        • 端末の傾きを横向きに揃えるまでは横向き表示が継続し、その後は端末の傾きに応じた画面回転をする:ng::ok::tada:
    • 横向きのとき
      • 全画面解除ボタンを押下すると縦向きになる:ok:
        • 端末の傾きを縦向きに揃えるまでは縦向き表示が継続し、その後は端末の傾きに応じた画面回転をする:ng::ok::tada:

完成品

出来上がったサンプルアプリの動作仕様とGIFアニメをまとめておきます。

自動回転OFFのとき

  • 縦向きのとき
    • 端末の傾きを360°どの角度に変更しても縦向きが継続する
    • 全画面表示ボタンを押下すると横向きになる
  • 横向きのとき
    • 端末の傾きを左右いずれかに90°変更しても横向きが継続する
    • 端末の傾きを180°変更すると、画面が回転して反対側の横向きになる
    • 全画面解除ボタンを押下すると縦向きになる

ezgif-7-301bf48ded1b.gif

自動回転がONのとき

  • 縦向きのとき
    • 端末を傾けると、その傾きに応じた画面回転をする(逆さまの縦向きを除く)
    • 全画面表示ボタンを押下すると横向きになる
      • 端末の傾きを横向きに揃えるまでは横向き表示が継続し、その後は端末の傾きに応じた画面回転をする
  • 横向きのとき
    • 端末を傾けると、その傾きに応じた画面回転をする(逆さまの縦向きを除く)
    • 全画面解除ボタンを押下すると縦向きになる
      • 端末の傾きを縦向きに揃えるまでは縦向き表示が継続し、その後は端末の傾きに応じた画面回転をする

ezgif-7-b01dfa0946bb.gif

おわりに

本家YouTubeの内部実装は全く異なるものかもしれませんが、再生画面の回転仕様はrequestedOrientationOrientationEventListenerを使うことで再現することができました。
もし再現したくなったときはこの記事のことを思い出すか、もっと良い方法を見つけたらこっそり教えて下さい。

参考

Androidプログラマへの道 ~ Moonlight 明日香 ~ 画面の向きを設定する
チラシの裏的備忘録 Androidで設定を参照, 操作するあれこれ -画面の設定編-
Change Screen Orientation programmatically using a Button

25
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?