5
4

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.

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

image.png

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

動画再生UIの変更

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

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

UI を変更する時は適切な ID や Viewクラス を用いる必要があります。
それらは、PlayerView (ExoPlayer library)Overriding the layout file の項目に使用出来る resource_id が列挙されているので、こちらを参考にレイアウトを構築してください。

ちなみに、デフォルトではexo_simple_player_view.xmlというレイアウトが表示されております。

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

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

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

事前準備

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

<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

コントロールバーの変更

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

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

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

ちなみに、デフォルトではexo_playback_control_view.xmlというレイアウトが表示されております。

res_id 意味
exo_play 再生ボタン :arrow_forward: View
exo_pause 一時停止ボタン :pause_button: View
exo_rew 巻き戻しボタン :rewind: View
exo_ffwd 早送りボタン :fast_forward: View
exo_prev 前に戻るボタン :track_previous: View
exo_next 次に進むボタン :track_next: View
exo_repeat_toggle リピートボタン :repeat: drawableのID指定によって、トグルの画像を変更します。
exo_controls_repeat_offexo_controls_repeat_oneexo_controls_repeat_all
ImageView
exo_shuffle シャッフルボタン :twisted_rightwards_arrows: drawableのID指定によって、トグルの画像を変更します。
exo_controls_shuffle_offexo_controls_shuffle_on
ImageView
exo_vr VRモードボタン View
exo_position 再生位置を表すテキスト。 TextView
exo_duration 動画自体の長さを表すテキスト。 TextView
exo_progress_placeholder インフレートされたDefaultTimeBarを置き換えられます。exo_progressが存在する時は無視されます。 View
exo_progress 再生中に更新され、シークできるプログレスバー。 DefaultTimeBarが表示されています。 TimeBar

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

プログレスバーについて

ExoPlayerのプログレスバー、一見SeekBarを使っていそうに見えますが、実は違うんです・・・。 :innocent:

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

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

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

これの何が痛いかというと、プログレスバーの背景を画像にしたり、角丸にしたり等が容易に出来ないのです。 :innocent:
DefaultTimeBar側で背景色の変更やプレイヘッドの Drawable変更 のインターフェースは設けられているのですが、背景変更は存在しません。。。

こうなった経緯

以下の Commit を確認すると、元々は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 へ描画した方が楽なので、このようにしたのだと推測出来ます。

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

しばらく、どうすればいいのだ・・・?と途方に暮れていましたが、 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:progressDrawableandroid:thumb を指定すれば背景変更やプレイヘッド変更が可能になります。
(広告再生位置の表示は未対応です。それを実装したい場合は少々めんどくさい・・・。)

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

事前準備

今回はコントロールバーを全画面表示したいので、 my_movie_view.xmlexo_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

最後に

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

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

参考

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?