本節(jié)教程我們將來(lái)介紹下ExoPlayer的視頻播放功能选调。
我們?cè)诒竟?jié)將主要介紹以下知識(shí)點(diǎn):
- ExoPlayer高級(jí)自定義的實(shí)現(xiàn)
- 視頻的全屏播放和退出全屏播放
- ExoPlayer在RecyclerView中的復(fù)用
ExoPlayer介紹
MediaPlayer和ExoPlayer是Google官方支持的兩種播放器,但是ExoPlayer比MediaPlayer多了支持基于 HTTP 的動(dòng)態(tài)自適應(yīng)流 (DASH)、SmoothStreaming 和通用加密等功能吨灭。
并且重要的是它獨(dú)立于Android代碼框架,以一個(gè)開(kāi)源代碼庫(kù)的形式存在,所以在自定義上更有優(yōu)勢(shì)。
ExoPlayer簡(jiǎn)單的使用方法
- 引入依賴庫(kù)
implementation 'com.google.android.exoplayer:exoplayer:2.12.0'
- 布局中引入PlayerView
播放視頻我們需要使用PlayerView泣特,我們簡(jiǎn)單來(lái)看下PlayerView的源碼,其繼承于FrameLayout挑随,其中有三個(gè)重要的屬性状您,
public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
@Nullable private final View surfaceView;
@Nullable private final PlayerControlView controller;
private Player player;
}
-
surfaceView
是呈現(xiàn)視頻的View,可以是TextureView兜挨,SurfaceView, 默認(rèn)是SurfaceView膏孟。 -
controller
是播放控制的View,上面提供一些控件可以控制視頻的播放暑劝,暫停骆莹,顯示當(dāng)前進(jìn)度等颗搂。默認(rèn)是PlayerControlView担猛。 -
player
是視頻的播放器,在構(gòu)造函數(shù)初始化的時(shí)候沒(méi)有賦值丢氢,需要單獨(dú)設(shè)置傅联。
總結(jié):PlayerView通過(guò)
player
播放視頻顯示在surfaceView
上,用戶可以通過(guò)提供的controller
進(jìn)行播放的控制疚察。
介紹了基本的知識(shí)點(diǎn)后蒸走,我們?cè)诓季治募幸?strong>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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".DefaultViewActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_buffering="always"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
- 設(shè)置播放器
val player: SimpleExoPlayer = SimpleExoPlayer.Builder(this@MainActivity).build().also { it.playWhenReady = true }
video_player.player = player
我們前面提到PlayerView的兩個(gè)屬性在構(gòu)造函數(shù)調(diào)用時(shí)賦值了,但是player沒(méi)有貌嫡,需要主動(dòng)設(shè)置比驻。這里我們?cè)O(shè)置成SimpleExoPlayer對(duì)象。
SimpleExoPlayer是庫(kù)中提供的播放器岛抄,可以直接使用别惦。
- 設(shè)置播放源
// play item
val uri = Uri.parse("https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4")
val dataSourceFactory = DefaultHttpDataSourceFactory()
val videoSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
// prepare
player.prepare(videoSource)
- 監(jiān)聽(tīng)播放器的狀態(tài)
我們可以監(jiān)聽(tīng)播放器的狀態(tài),代碼如下:
player.addListener(object: Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
Log.d("JJMusic","playWhenReady: $playWhenReady playbackState: $playbackState")
when (playbackState) {
Player.STATE_BUFFERING ->
Log.d("JJMusic","加載中")
Player.STATE_READY ->
Log.d("JJMusic","準(zhǔn)備完畢")
Player.STATE_ENDED ->
Log.d("JJMusic","播放完成")
}
}
override fun onPlayerError(error: ExoPlaybackException) {
Log.e("JJMusic","ExoPlaybackException: $error")
}
})
最后得到的效果如下所示:
ExoPlayer簡(jiǎn)單自定義
我們目前使用的是默認(rèn)的播放控制布局文件夫椭,我們可以修改播放的布局文件達(dá)到自定義效果掸掸。
- 自定義播放控制的布局文件
假設(shè)我們把布局文件設(shè)計(jì)如下所示:
<!-- layout_video_simple.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:id="@+id/constraint"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/exo_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@mipmap/exo_btn_play" />
<ImageView
android:id="@+id/exo_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@mipmap/exo_btn_pause" />
<TextView
android:id="@+id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginBottom="12dp"
android:contentDescription="@null"
android:text="1"
android:textColor="@color/colorPrimary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/splash_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="1dp"
android:layout_marginBottom="12dp"
android:contentDescription="@null"
android:text="/"
android:textColor="@color/colorPrimary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/exo_position"
tools:text="/" />
<TextView
android:id="@+id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="1dp"
android:layout_marginBottom="12dp"
android:contentDescription="@null"
android:text="1"
android:textColor="@color/colorPrimary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/splash_tv" />
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@+id/exo_progress"
android:layout_width="0dp"
android:layout_height="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:bar_height="2dp"
app:unplayed_color="@color/exo_gray_ripple"
app:played_color="@color/colorAccent"
app:scrubber_color="@color/colorAccent"
app:buffered_color="@color/colorPrimary"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
-
id為
exo_play
的按鈕和id為exo_pause
的按鈕在屏幕正中間位置 -
id為
exo_position
的文本和id為exo_duration
的文本在左下角 -
id為
exo_progress
的進(jìn)度條在最底部。進(jìn)度條的類是DefaultTimeBar蹭秋,可以設(shè)置一些屬性扰付。譬如上面的bar_height
(進(jìn)度條的高度),unplayed_color
(未緩沖部分的顏色),played_color
(已播放部分的顏色)和buffered_color
(已緩沖完部分的顏色)等等。
注意:這些id是PlayerControlView源代碼中能找到的id,否則是沒(méi)有效果的仁讨。
- 修改PlayerView布局文件
<com.google.android.exoplayer2.ui.PlayerView
...
app:controller_layout_id="@layout/layout_video_simple"
/>
其他的和前面的類似羽莺,只是加了個(gè)屬性controller_layout_id
,值為我們剛才設(shè)計(jì)的布局文件layout_video_simple洞豁。
簡(jiǎn)單自定義得到的效果如下所示:
ExoPlayer高級(jí)自定義
簡(jiǎn)單的自定義我們只是更改了PlayerControlView的布局文件盐固,復(fù)用了其中的id
屠橄,能修改的很有限,沒(méi)有涉及到源代碼的修改闰挡。
高級(jí)自定義就需要修改源代碼了锐墙。其實(shí)就是修改PlayerView,PlayerControlView长酗,甚至是TimeBar的源代碼溪北。
接下來(lái)我們就用高級(jí)自定義來(lái)實(shí)現(xiàn)下網(wǎng)易云音樂(lè)的全屏播放功能夺脾,需要的效果如下:
- 修改PlayerControlView
新建一個(gè)JJPlayerControlView類之拨,然后將PlayerControlView所有源代碼拷貝在這個(gè)類中。
public class JJPlayerControlView extends FrameLayout {
// PlayerControlView內(nèi)容
}
接下來(lái)在JJPlayerControlView中加入一個(gè)全屏按鈕屬性咧叭。
public class JJPlayerControlView extends FrameLayout {
// 全屏按鈕
private final ImageButton maxButton;
// PlayerControlView內(nèi)容
public JJPlayerControlView(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet playbackAttrs) {
...
maxButton = findViewById(R.id.exo_max_btn);
if (maxButton != null) {
maxButton.setOnClickListener(componentListener);
}
...
}
}
- 修改PlayerView
新建一個(gè)JJPlayerView類蚀乔,然后將PlayerView所有源代碼拷貝在這個(gè)類中。
public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
// PlayerView的內(nèi)容
}
將JJPlayerView的controller
指定為JJPlayerControlView菲茬,即:
public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
@Nullable private final JJPlayerControlView controller;
// PlayerView的其他內(nèi)容
}
- 修改TimeBar
如果需要修改進(jìn)度條吉挣,新建一個(gè)JJTimeBar類,然后將DefaultTimeBar所有源代碼拷貝在這個(gè)類中婉弹。
public class JJTimeBar extends View implements TimeBar {
...
}
當(dāng)然修改將JJPlayerControlView中的timeBar
改為JJTimeBar類睬魂。
public class JJPlayerControlView extends FrameLayout {
// 全屏按鈕
private final ImageButton maxButton;
// 自定義進(jìn)度條
@Nullable private JJTimeBar timeBar;
// PlayerControlView內(nèi)容
public JJPlayerControlView(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet playbackAttrs) {
...
maxButton = findViewById(R.id.exo_max_btn);
if (maxButton != null) {
maxButton.setOnClickListener(componentListener);
}
...
}
}
- 修改JJPlayerControlView布局文件
layout_video_recyclerview.xml相對(duì)前面,我們多添加了一個(gè)id
為exo_max_btn的按鈕镀赌。
為了看的更加明顯氯哮,我把其他的按鈕或者文本的id都改了,不再使用默認(rèn)的id商佛,這時(shí)候?yàn)榱苏业綄?duì)應(yīng)的控件喉钢,就需要修改對(duì)應(yīng)的源代碼了。譬如我把播放按鈕的id改為了exo_play_btn
良姆。
public class JJPlayerControlView extends FrameLayout {
// 代碼修改
playButton = findViewById(R.id.exo_play_btn);
if (playButton != null) {
playButton.setOnClickListener(componentListener);
}
}
- JJPlayerView布局文件
JJPlayerView使用JJPlayerControlView自定義的布局文件
<com.johnny.jjmusic.exoplayer.JJPlayerView
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"
app:show_buffering="always"
app:controller_layout_id="@layout/layout_video_recyclerview"
>
</com.johnny.jjmusic.exoplayer.JJPlayerView>
- 全屏和退出全屏的實(shí)現(xiàn)邏輯
我們先來(lái)看一張圖就能很清晰的了解全屏和退出全屏的邏輯了:
全屏的時(shí)候JJPlayerView放在Activity的R.id.content上肠虽,隱藏ActionBar,切換成橫屏顯示歇盼,退出全屏的時(shí)候就重新放在RecyclerView的ItemView上舔痕,顯示ActionBar,切換成豎屏顯示豹缀。
所以最后很簡(jiǎn)單伯复,只要處理maxButton點(diǎn)擊事件時(shí)實(shí)現(xiàn)這個(gè)功能就可以了。
進(jìn)入全屏播放
fun enterFullScreen() {
// 橫豎屏狀態(tài)判斷
if (viewModel.playMode == VideoPlayMode.MODE_FULL_SCREEN) return
// 隱藏ActionBar
playerView.context.hideActionBar()
// 旋轉(zhuǎn)屏幕
playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
// 將JJPlayerView從RecyclerView移除邢笙,加入Activity的R.id.content下
playerView.context.activity?.let {
val contentView = it.findViewById<ViewGroup>(android.R.id.content)
// remove
removePlayerView()
viewModel.isVideoViewAdded = true
// add
val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
contentView.addView(playerView, params)
val frameParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
playerView.controller?.timeBarContainer?.addView(timeBar, frameParams)
viewModel.playMode = VideoPlayMode.MODE_FULL_SCREEN
}
}
退出全屏播放
/* 退出全屏 */
fun exitFullScreen() {
// 橫豎屏狀態(tài)判斷
if (viewModel.playMode == VideoPlayMode.MODE_NORMAL) return
// 顯示ActionBar
playerView.context.showActionBar()
// 旋轉(zhuǎn)屏幕
playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
// 將JJPlayerView從Activity的R.id.content移除啸如,加入RecyclerView的ItemView下
playerView.context.activity?.let {
// remove
val contentView = it.findViewById<ViewGroup>(android.R.id.content)
contentView.removeView(playerView)
playerView.controller?.timeBarContainer?.removeView(timeBar)
// add
viewModel.viewModelScope.launch {
delay(100)
addPlayerView()
}
viewModel.playMode = VideoPlayMode.MODE_NORMAL
}
}
上面代碼中涉及到的幾個(gè)擴(kuò)展方法,也一同貼出來(lái):
//----------Activity----------
val Context.activity: Activity?
get() {
return when (this) {
is Activity -> {
this
}
is ContextWrapper -> {
this.baseContext.activity
}
else -> {
null
}
}
}
val Context.appCompActivity: AppCompatActivity?
get() {
return when (this) {
is AppCompatActivity -> {
this
}
is ContextThemeWrapper -> {
this.baseContext.appCompActivity
}
else -> {
null
}
}
}
//---------- ActionBar ----------
@SuppressLint("RestrictedApi")
fun Context.showActionBar() {
this.appCompActivity?.supportActionBar?.let {
it.setShowHideAnimationEnabled(false)
it.show()
}
this.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
@SuppressLint("RestrictedApi")
fun Context.hideActionBar() {
this.appCompActivity?.supportActionBar?.let {
it.setShowHideAnimationEnabled(false)
it.hide()
}
this.activity?.window?.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
至此ExoPlayer的高級(jí)自定義就到此為止了氮惯。
由于可以修改源碼叮雳,所以進(jìn)行高度自定義就變得可實(shí)現(xiàn)了想暗。當(dāng)然是在熟悉源碼的前提下進(jìn)行修改。
ExoPlayer在RecyclerView中的復(fù)用
上面的實(shí)現(xiàn)效果中帘不,我們點(diǎn)擊RecyclerView不同的Item说莫,都能播放視頻,如果每個(gè)ItemView都有一個(gè)PlayerView那是非常不合適的寞焙。對(duì)PlayerView是一個(gè)非常合適的解決方案储狭。
其實(shí)這個(gè)解決方案和全屏的方案也非常相似,就是將PlayerView在不同的Item中移除和加入捣郊。然后播放新的視頻辽狈。
其中有一些細(xì)節(jié)需要處理,譬如播放的進(jìn)度需要記錄下來(lái)呛牲,下次再點(diǎn)擊的時(shí)候從上次停止的地方進(jìn)行播放刮萌。還譬如需要監(jiān)聽(tīng)RecyclerView.OnChildAttachStateChangeListener,當(dāng)執(zhí)行onChildViewDetachedFromWindow時(shí)候娘扩,如果在播放需要將播放器停止着茸。等等
有了思路,解決起來(lái)也就很簡(jiǎn)單了畜侦。這里不再貼代碼了元扔。