以下のように、 ExoPlayer は特別な変更をしなくても良い感じの UI にしてくれるのですが、
これらの UI をカスタマイズする事も出来ます。
動画再生UIの変更
ExoPlayer の UI変更 はちょっと特殊で、 com.google.android.exoplayer2.ui.PlayerView
の app: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_placeholder
と exo_controller
の違いはいまいちわかっていませんので、詳しい方は教えて頂けるとうれしいです。。。 )
例
動画の読み込み中に 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 を表示する様にします。
<?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 を指定します。
<?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>
完成
こんな感じになります。
コントロールバーの変更
コントロールバーも動画再生UIの変更と同じような感じで、 com.google.android.exoplayer2.ui.PlayerView
の app: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 |
再生ボタン | View |
exo_pause |
一時停止ボタン | View |
exo_rew |
巻き戻しボタン | View |
exo_ffwd |
早送りボタン | View |
exo_prev |
前に戻るボタン | View |
exo_next |
次に進むボタン | View |
exo_repeat_toggle |
リピートボタン drawableのID指定によって、トグルの画像を変更します。exo_controls_repeat_off 、exo_controls_repeat_one 、exo_controls_repeat_all
|
ImageView |
exo_shuffle |
シャッフルボタン drawableのID指定によって、トグルの画像を変更します。exo_controls_shuffle_off 、exo_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_placeholder
と exo_progress
の違いはいまいちわかっていませんので、詳しいから教えて頂けるとうれしいです。。。 )
プログレスバーについて
ExoPlayerのプログレスバー、一見SeekBarを使っていそうに見えますが、実は違うんです・・・。
public class DefaultTimeBar extends View implements TimeBar {
...
}
普通の Viewクラス ですね・・・。じゃあどうやってバーを描画しているの?と思ってコードを追っていると、毎回表示位置を計算して Canvas に描画している事がわかりました。
@Override
public void onDraw(Canvas canvas) {
canvas.save();
drawTimeBar(canvas); // タイムバーをCanvasへ描画
drawPlayhead(canvas); // プレイヘッドをCanvasへ描画
canvas.restore();
}
これの何が痛いかというと、プログレスバーの背景を画像にしたり、角丸にしたり等が容易に出来ないのです。
DefaultTimeBar側で背景色の変更やプレイヘッドの Drawable変更 のインターフェースは設けられているのですが、背景変更は存在しません。。。
こうなった経緯
以下の Commit を確認すると、元々はSeekBarで実装されていたことがわかります。
では、なぜわざわざ Canvas への描画方式へ変更したのかと言いますと、この変更がリリースされたのが、 v2.4.0
になっており、この時のリリースノートには以下の様に記述されています。
New time bar view, including support for displaying ad break markers.
広告の再生位置表示を行う様にしたという内容です。 YouTube のように広告再生位置をバー上に表示する、という内容ですね。
このようなバー上に何かを描画する場合は、 SeekBar でやるより、 Canvas へ描画した方が楽なので、このようにしたのだと推測出来ます。
プログレスバーの背景変更の解決方
しばらく、どうすればいいのだ・・・?と途方に暮れていましたが、 TimeBar を実装した、 SeekBar のサブクラスを作成すれば良いという事に気づきました。
以下の様なクラスを作ると行けます。
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
に変更します。
<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と同じ方式)
<?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 を指定します。
<?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>
完成
こんな感じになります。
最後に
ExoPlayer の UI変更 についてまとめました。
UI変更方法自体はちょっと複雑ですが、マスターすればいろんな動画再生の UI を実現出来るような気がします。
(もっと楽に UI変更できて欲しいという気持ちはありますが・・・。 )
今回、 UI変更 について再調査している時に、まだ自分の把握していない設定がありましたので、次回触るときにはマスターしておきたいですね・・・。