ExoPlayerのUI変更方法

以下のように、 ExoPlayer は特別な変更をしなくても良い感じの UI にしてくれるのですが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/7765e13f-11ce-21c1-5fac-7a9df24ff35c.png)

これらの UI をカスタマイズする事も出来ます。 :upside_down: 

# 動画再生UIの変更

ExoPlayer の UI変更 はちょっと特殊で、 `com.google.android.exoplayer2.ui.PlayerView` の `app:player_layout_id` に動画再生画面のレイアウトIDを指定します。

```xml
<com.google.android.exoplayer2.ui.PlayerView
        ~~~
        app:player_layout_id="{layout_id}" 
        />
```

UI を変更する時は適切な ID や Viewクラス を用いる必要があります。
それらは、[PlayerView (ExoPlayer library)](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerView.html)の `Overriding the layout file` の項目に使用出来る resource_id が列挙されているので、こちらを参考にレイアウトを構築してください。

ちなみに、デフォルトでは[`exo_simple_player_view.xml`](https://github.com/google/ExoPlayer/blob/release-v2/library/ui/src/main/res/layout/exo_simple_player_view.xml)というレイアウトが表示されております。

| res_id | 意味 | 型 |
| :-: | --- | :-- |
| `exo_content_frame` | 再生中のメディアやアルバムアートを表示するフレームです。| [`AspectRatioFrameLayout`](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.html) |
| `exo_buffering`  | プレイヤーがバッファリングしている時に表示される View 。| [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_subtitles` | 字幕の View です。| [`SubtitleView`](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/SubtitleView.html) |
| `exo_shutter`| ビデオを非表示にするときに表示される View 。応用的な方法として、再生開始時は動画再生画面を隠すことで、ビデオ再生時のちらつきをなくす事が出来ます。 | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_artwork`| アルバムのアートワークを表示します。| [`ImageView`](https://developer.android.com/reference/android/widget/ImageView.html) |
| `exo_error_message`| エラーが起きたときに表示されるテキスト。| [`TextView`](https://developer.android.com/reference/android/widget/TextView.html) |
| `exo_controller_placeholder` | インフレートされた[PlayerControlView](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerControlView.html)を置き換えられます。`exo_controller`が存在する時は無視されます。 | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_controller` | すでにインフレートされている[PlayerControlView](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerControlView.html)。 | [PlayerControlView](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerControlView.html) |
| `exo_ad_overlay` | 広告のUIを表示するためのオーバーレイです。 | [FrameLayout](https://developer.android.com/reference/android/widget/FrameLayout.html) |
| `exo_overlay` | プレイヤーの上部に表示されるオーバーレイです。 | [FrameLayout](https://developer.android.com/reference/android/widget/FrameLayout.html) |

（`exo_controller_placeholder` と `exo_controller` の違いはいまいちわかっていませんので、詳しい方は教えて頂けるとうれしいです。。。 :bow: ）

## 例

動画の読み込み中に Lottieアニメーション が表示されるようにします。
アニメーションは LottieSample に良い感じの物が落ちていたので、こちらを使わせて頂きます。
https://lottiefiles.com/1055-world-locations

### 事前準備

読み込み中に `exo_buffering` を表示させるには、 Layout に以下の設定が必要です。

```xml
<com.google.android.exoplayer2.ui.PlayerView
        ...
        app:show_buffering="always" 
        ...
        />
```

### UI変更

#### プレイヤーのUI

`exo_buffering` を画面全体を覆う半透明の黒い Layout の上に先ほどの LottieAnimation を表示する様にします。

```my_movie_view.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/exo_content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center">

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


        <TextView
                android:id="@+id/exo_error_message"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        <View
                android:id="@id/exo_controller_placeholder"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />

        <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/exo_buffering"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:background="#9E000000"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent">

            <com.airbnb.lottie.LottieAnimationView
                    android:layout_width="300dp"
                    android:layout_height="300dp"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:lottie_autoPlay="true"
                    app:lottie_loop="true"
                    app:lottie_rawRes="@raw/loading" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
```

#### UI指定

`activity_main.xml`で UI の レイアウトID を指定します。

```activity_main.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">

    <com.google.android.exoplayer2.ui.PlayerView
            android:id="@+id/player_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#00000000"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:player_layout_id="@layout/my_movie_view"
            app:show_buffering="always" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

### 完成

こんな感じになります。

![output-1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/2dd25b4c-51d1-14e6-a4e8-b439a45bd6a0.gif)


# コントロールバーの変更

コントロールバーも[動画再生UIの変更](#動画再生UIの変更)と同じような感じで、 `com.google.android.exoplayer2.ui.PlayerView` の `app:controller_layout_id` にコントロールバーの レイアウトID を指定します。

```xml
<com.google.android.exoplayer2.ui.PlayerView
        ~~~
        app:controller_layout_id="{layout_id}" 
        />
```

動画再生UI と同じく、 UI を変更する時は適切な ID や Viewクラス を用いる必要があります。
それらは、[PlayerControlView (ExoPlayer library)](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerControlView.html)の `Overriding the layout file` の項目に使用出来る resource_id が列挙されているので、こちらを参考にレイアウトを構築してください。

ちなみに、デフォルトでは[`exo_playback_control_view.xml`](https://github.com/google/ExoPlayer/blob/release-v2/library/ui/src/main/res/layout/exo_playback_control_view.xml)というレイアウトが表示されております。

| res_id | 意味 | 型 |
| :-: | --- | :-- |
| `exo_play` | 再生ボタン :arrow_forward:| [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_pause`  | 一時停止ボタン :pause_button:  | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_rew` | 巻き戻しボタン :rewind: | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_ffwd`| 早送りボタン :fast_forward: | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_prev` | 前に戻るボタン :track_previous: | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_next`| 次に進むボタン :track_next: | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_repeat_toggle` | リピートボタン :repeat: drawableのID指定によって、トグルの画像を変更します。<br>`exo_controls_repeat_off`、`exo_controls_repeat_one`、`exo_controls_repeat_all` | [`ImageView`](https://developer.android.com/reference/android/widget/ImageView.html) |
| `exo_shuffle` | シャッフルボタン :twisted_rightwards_arrows: drawableのID指定によって、トグルの画像を変更します。<br>`exo_controls_shuffle_off`、`exo_controls_shuffle_on` | [`ImageView`](https://developer.android.com/reference/android/widget/ImageView.html) |
| `exo_vr ` | VRモードボタン  | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_position` | 再生位置を表すテキスト。 | [TextView](https://developer.android.com/reference/android/widget/TextView.html) |
| `exo_duration` | 動画自体の長さを表すテキスト。 | [TextView](https://developer.android.com/reference/android/widget/TextView.html) |
| `exo_progress_placeholder` | インフレートされた[DefaultTimeBar](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/DefaultTimeBar.html)を置き換えられます。`exo_progress`が存在する時は無視されます。 | [`View`](https://developer.android.com/reference/android/view/View.html) |
| `exo_progress` | 再生中に更新され、シークできるプログレスバー。 [DefaultTimeBar](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/DefaultTimeBar.html)が表示されています。 | [TimeBar](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/TimeBar.html) |

（`exo_progress_placeholder` と `exo_progress` の違いはいまいちわかっていませんので、詳しいから教えて頂けるとうれしいです。。。 :bow: ）

## プログレスバーについて

ExoPlayerのプログレスバー、一見[SeekBar](https://developer.android.com/reference/android/widget/SeekBar)を使っていそうに見えますが、実は違うんです・・・。 :innocent: 

```java
public class DefaultTimeBar extends View implements TimeBar {
  ...
}
```

普通の Viewクラス ですね・・・。じゃあどうやってバーを描画しているの？と思ってコードを追っていると、毎回表示位置を計算して Canvas に描画している事がわかりました。 :innocent: 

```java
@Override
public void onDraw(Canvas canvas) {
  canvas.save();
  drawTimeBar(canvas);    // タイムバーをCanvasへ描画
  drawPlayhead(canvas);  // プレイヘッドをCanvasへ描画
  canvas.restore();
}
```

これの何が痛いかというと、プログレスバーの背景を画像にしたり、角丸にしたり等が容易に出来ないのです。 :innocent: 
[DefaultTimeBar](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/DefaultTimeBar.html)側で背景色の変更やプレイヘッドの Drawable変更 のインターフェースは設けられているのですが、背景変更は存在しません。。。

<details><summary>こうなった経緯</summary><div>

以下の Commit を確認すると、元々は[SeekBar](https://developer.android.com/reference/android/widget/SeekBar)で実装されていたことがわかります。

https://github.com/google/ExoPlayer/commit/9d20a8d41c0cc6cd01b20bd72f3bbfcf11e49cae#diff-e62855b4d9e67db5a4bbac0616213cc3L68

では、なぜわざわざ Canvas への描画方式へ変更したのかと言いますと、この変更がリリースされたのが、 `v2.4.0` になっており、この時のリリースノートには以下の様に記述されています。

>New time bar view, including support for displaying ad break markers.

広告の再生位置表示を行う様にしたという内容です。 YouTube のように広告再生位置をバー上に表示する、という内容ですね。
このようなバー上に何かを描画する場合は、 SeekBar でやるより、 Canvas へ描画した方が楽なので、このようにしたのだと推測出来ます。

</div></details>

### プログレスバーの背景変更の解決方

しばらく、どうすればいいのだ・・・？と途方に暮れていましたが、 TimeBar を実装した、 SeekBar のサブクラスを作成すれば良いという事に気づきました。
以下の様なクラスを作ると行けます。

```SeekTimeBar.kt
class SeekTimeBar @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null
) : SeekBar(context, attrs), TimeBar {
    private val listeners: CopyOnWriteArraySet<TimeBar.OnScrubListener> = CopyOnWriteArraySet()
    private var isSeeking = false

    init {
        setLayerType(View.LAYER_TYPE_SOFTWARE, null)
        setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                listeners.forEach {
                    it.onScrubMove(this@SeekTimeBar, progress.toLong())
                }
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {
                isSeeking = true
                listeners.forEach {
                    it.onScrubStart(this@SeekTimeBar, progress.toLong())
                }
            }

            override fun onStopTrackingTouch(seekBar: SeekBar?) {
                isSeeking = false
                listeners.forEach {
                    it.onScrubStop(this@SeekTimeBar, progress.toLong(), false)
                }
            }

        })
    }

    override fun setKeyCountIncrement(count: Int) {
        progress += count
    }

    override fun setBufferedPosition(bufferedPosition: Long) {
        secondaryProgress = bufferedPosition.toInt()
    }

    override fun addListener(listener: TimeBar.OnScrubListener?) {
        listeners.add(listener)
    }

    override fun setDuration(duration: Long) {
        max = duration.toInt()
    }

    override fun removeListener(listener: TimeBar.OnScrubListener?) {
        listeners.remove(listener)
    }

    override fun setKeyTimeIncrement(time: Long) {
        progress += time.toInt()
    }

    override fun setPosition(position: Long) {
        //シーク中は何もしない（ちらつき防止）
        if (isSeeking) return
        progress = position.toInt()
    }

    override fun setAdGroupTimesMs(adGroupTimesMs: LongArray?, playedAdGroups: BooleanArray?, adGroupCount: Int) {
    }

    override fun verifyDrawable(who: Drawable): Boolean {
        return super.verifyDrawable(who) || who == progressDrawable
    }
}
```

あとは、 XML側 で `android:progressDrawable` や `android:thumb` を指定すれば背景変更やプレイヘッド変更が可能になります。
(広告再生位置の表示は未対応です。それを実装したい場合は少々めんどくさい・・・。)

## 例

長らくプログレスバーについて説明していましたが、ここではコントロールバーの変更の例を示します。
再生ボタンと停止ボタンを前面に表示するだけのコントロールバー（バー・・・？）を実装します。

### 事前準備

今回はコントロールバーを全画面表示したいので、 `my_movie_view.xml` の `exo_controller_placeholder` のサイズを `0dp` に変更します。

```my_movie_view.xml
<View
        android:id="@id/exo_controller_placeholder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
```

### UI変更

#### コントロールバーのUI

背景を半透明にして、再生ボタンと停止ボタンを中央に表示します。
ややこしいですが、再生中は停止ボタンを表示するようにし、停止中は再生ボタンを表示するようにしています。（YouTubeと同じ方式）

```my_controller_view.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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#BE000000">

    <ImageButton
            android:id="@+id/exo_pause"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@null"
            android:scaleType="fitXY"
            android:src="@drawable/exo_controls_pause"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    <ImageButton
            android:id="@+id/exo_play"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@null"
            android:scaleType="fitXY"
            android:src="@drawable/exo_controls_play"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_goneMarginStart="25dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

#### UI指定

`activity_main.xml` で UI の レイアウトID を指定します。

```activity_main.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">

    <com.google.android.exoplayer2.ui.PlayerView
            android:id="@+id/player_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#00000000"
            app:controller_layout_id="@layout/my_controller_view"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:player_layout_id="@layout/my_movie_view"
            app:show_buffering="always" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

#### 完成

こんな感じになります。

![controller.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129095/6b34df60-cb4f-547c-c1ed-c1e1fb70557b.gif)

# 最後に

ExoPlayer の UI変更 についてまとめました。
UI変更方法自体はちょっと複雑ですが、マスターすればいろんな動画再生の UI を実現出来るような気がします。
（もっと楽に UI変更できて欲しいという気持ちはありますが・・・。 :innocent: ）

今回、 UI変更 について再調査している時に、まだ自分の把握していない設定がありましたので、次回触るときにはマスターしておきたいですね・・・。

# 参考

- [UI components - ExoPlayer](https://exoplayer.dev/ui-components.html)
- [ExoPlayerを使ってAndroidアプリに動画を入れた話 #GameWith #TechWith - GameWith Developer Blog](https://tech.gamewith.co.jp/entry/2019/03/25/103300)
- [PlayerView (ExoPlayer library)](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerView.html)