AndroidでYoutube等でよく見かけるシークバーを移動させる時に、
シークバーの上部にプレビューを表示させるあれを試して見たいと思います。
以下のメジャーな2つのライブラリを使って実装を進めて行きます。
PreviewSeekBar
SeekBarの上部にPreview画面を表示させていい感じに表示させるライブラリです。
ExoPlayer
ご存知Google製のAndroid用動画Playerです。
環境構築
新規にプロジェクトを作成し、必要なライブラリを [module]/build.gradle
に追加します。
android {
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
// ExoPlayer
def exoplayer_version = '2.8.1'
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
// PreviewSeekBar
implementation 'com.github.rubensousa:previewseekbar:2.0.0'
implementation 'com.github.rubensousa:previewseekbar-exoplayer:2.8.1.0'
}
実装
まずはExoPlayerで普通に動画再生できるまで実装
今回はAndroidスマートフォンの横画面フルスクリーンで動画を視聴する想定で実装していきます。
動画のコンテンツに関してはこちらを利用させて頂きました
- ActionBar付きのstyleを
NoActionBar
に変更する
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
...
</style>
</resources>
- レイアウトファイルに
PlayerView
を配置
<?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:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
app:layout_constraintDimensionRatio
を 16:9
に設定する事で縦のサイズをいい感じに 横との比率が9
で調整してくれます。
- Activityの実装
const val SAMPLE_MP4_URL = "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4"
class MainActivity : AppCompatActivity() {
private var player: ExoPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// フルスクリーンにする
window.decorView.apply {
systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
)
}
}
override fun onStart() {
super.onStart()
player = createPlayer()
playerView.player = player
}
override fun onStop() {
super.onStop()
player?.release()
}
private fun createPlayer(): ExoPlayer {
val dataSourceFactory = DefaultDataSourceFactory(
this@MainActivity, DefaultBandwidthMeter(),
DefaultHttpDataSourceFactory(Util.getUserAgent(this@MainActivity, packageName))
)
val videoSource = ExtractorMediaSource.Factory(dataSourceFactory)
.createMediaSource(Uri.parse(SAMPLE_MP4_URL))
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(DefaultBandwidthMeter())
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
return ExoPlayerFactory.newSimpleInstance(this@MainActivity, trackSelector).apply {
prepare(videoSource)
playWhenReady = true
}
}
}
あとは AndroidManifest.xml
に
<uses-permission android:name="android.permission.INTERNET"/>
を忘れずに追加して実行して、動画が再生されればOKです
シークバー動かした時にプレビュー表示させる
本題のプレビュー表示をやっていこうと思います。
まずはプレイヤーのコントローラ部分のレイアウトを作成します。
<?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"
android:layout_gravity="bottom"
android:layoutDirection="ltr">
<LinearLayout
android:id="@+id/controlsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#CC000000"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">
<ImageButton
android:id="@id/exo_prev"
style="@style/ExoMediaButton.Previous" />
<ImageButton
android:id="@id/exo_rew"
style="@style/ExoMediaButton.Rewind" />
<ImageButton
android:id="@id/exo_play"
style="@style/ExoMediaButton.Play" />
<ImageButton
android:id="@id/exo_pause"
style="@style/ExoMediaButton.Pause" />
<ImageButton
android:id="@id/exo_ffwd"
style="@style/ExoMediaButton.FastForward" />
<ImageButton
android:id="@id/exo_next"
style="@style/ExoMediaButton.Next" />
</LinearLayout>
<TextView
android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/controlsLayout"
app:layout_constraintStart_toStartOf="parent"
tools:text="18:20" />
<com.github.rubensousa.previewseekbar.exoplayer.PreviewTimeBar
android:id="@+id/exo_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="@id/exo_position"
app:layout_constraintEnd_toStartOf="@id/exo_duration"
app:layout_constraintStart_toEndOf="@+id/exo_position"
app:layout_constraintTop_toTopOf="@+id/exo_position"
app:previewFrameLayout="@id/previewFrameLayout" />
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="8dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="parent"
tools:text="25:23" />
<FrameLayout
android:id="@+id/previewFrameLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:padding="5dp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.25"
tools:visibility="visible">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
作成したコントローラー用のViewを com.google.android.exoplayer2.ui.PlayerView
の
controller_layout_id
で指定してやります。
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:controller_layout_id="@layout/controller_view"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
シークバーをDrag&Dropしている時にプレビューを表示させる
今のままだとプレビューが表示されないので、実装を追加します。
最終的なプレビューのイメージは以下の様になります。
実際はシークバーの位置に応じたサムネイルの画像を用意したり、バックグラウンドでレンダリングした画像を表示したりする事になるかと思いますが、
今回は背景色が異なる画像を4枚用意して、シークバーの位置に応じてそれぞれの画像を表示させています。
PreviewView.OnPreviewChangeListener
の実装
シークバーをDrag&Dropした時に呼ばれるListenerを実装してセットしてあげます。
const val SAMPLE_MP4_URL = "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4"
const val TOTAL_VIDEO_MSEC = 117000
val DUMMY_THUMBNAILS = listOf(
"https://placehold.jp/a6eaf5/0a0909/150x150.png",
"https://placehold.jp/f5baa6/0a0909/150x150.png",
"https://placehold.jp/abf5a6/0a0909/150x150.png",
"https://placehold.jp/e0a6f5/0a0909/150x150.png"
)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
// PreviewTimeBarを取得
val previewTimeBar = playerView.findViewById<PreviewTimeBar>(R.id.exo_progress)
previewTimeBar.addOnPreviewChangeListener(onPreviewChangeListener)
}
// シークバーをDrag&Dropした時に呼ばれるListener
private val onPreviewChangeListener = object : PreviewView.OnPreviewChangeListener {
override fun onPreview(previewView: PreviewView?, progress: Int, fromUser: Boolean) {
val eachTime = TOTAL_VIDEO_MSEC / DUMMY_THUMBNAILS.size
val index = progress / eachTime
val url = DUMMY_THUMBNAILS.elementAtOrNull(index) ?: return
// progressの位置に応じたサムネイルを表示
Glide.with(this@MainActivity)
.load(url)
.into(imageView)
}
override fun onStartPreview(previewView: PreviewView?, progress: Int) {
}
override fun onStopPreview(previewView: PreviewView?, progress: Int) {
}
}
}
サムネイルの表示に関しては自前でprogressの位置から計算してどのサムネイルを表示するか
制御する必要があります。
上記の実装は以下のリポジトリにアップしてます。何かの参考になれば幸いです
その他
コントローラーの表示/非表示を制御したい場合
デフォルトの動作としては、初回表示されしばらく経過するとコントローラーが消えます。
消えた際に何かキー入力すると再び表示されますが、常に表示させる場合は以下の設定を行います。
android - How do I make the Exoplayer controls always visible? - Stack Overflow
↑こちらにある通り、show_timeout
を 0
で設定すると常に表示された状態になります。
あとは PlayerView#showController
/ PlayerView#hideController
で表示を切り替える事ができます。
バッドノウハウ
-
app:controller_layout_id
にレイアウトファイルを指定してもSeekBarとExoPlayerが同期しない- レイアウトファイル内のidをExoPlayerが指定するid名に合わせないといけない
-
getDefaultScrubberColor
メソッドが無いと怒られる
Caused by: java.lang.NoSuchMethodError: No static method getDefaultScrubberColor(I)I in class Lcom/github/rubensousa/previewseekbar/exoplayer/PreviewTimeBar;
or its super classes (declaration of 'com.github.rubensousa.previewseekbar.exoplayer.PreviewTimeBar' appears in ...
at com.github.rubensousa.previewseekbar.exoplayer.PreviewTimeBar.<init>(PreviewTimeBar.java:57)
手元の環境だとExoPlayerのVerが 2.10.8
の場合に上記エラーが発生しました。
ExoPlyerのVerをダウングレードすると正しく表示されました。'2.10.8 => 2.8.1'