1. 说明
1.1 使用Mediaplayer和surfaceView进行视频播放,并实现:感应生命周期、支持无缝续播、宽高比适配以及全屏模式
1.2 创建一个播放控制View,并以ViewModel驱动
2. 配置信息
2.1 AndroidManifest.xml 添加网络权限
<uses-permission android:name="android.permission.INTERNET" />
2.2 http 明文请求设置
android:usesCleartextTraffic="true"
2.3 引用 lifecycle 库
def lifecycle_version = "2.6.0-alpha03"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// ViewModel utilities for Compose
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Saved state module for ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
2.4 矢量图标,添加系统自带矢量图
ic_baseline_play_arrow_24.xml,
ic_baseline_replay_24.xml,
ic_baseline_pause_24.xml
3. 布局文件
3.1 控制View,controller_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/controllerFrame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#55000000">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="bottom"
android:layout_margin="4dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/buttonControl"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
app:srcCompat="@drawable/ic_baseline_play_arrow_24" />
<SeekBar
android:id="@+id/seekBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="12"
android:progressBackgroundTint="#FFFFFF" />
</LinearLayout>
</FrameLayout>
3.2 竖屏布局,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">
<FrameLayout
android:id="@+id/playerFrame"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#000000"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
<include
layout="@layout/controller_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.3 横屏布局, 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">
<FrameLayout
android:id="@+id/playerFrame"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
<include
layout="@layout/controller_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
4. VM 层实现
4.1 自定义 MediaPlayer, MyMediaPlayer.kt
//LifecycleObserver
class MyMediaPlayer:MediaPlayer(), DefaultLifecycleObserver{
// @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
// fun pausePlayer(){
// pause()
// }
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
Log.e("MyTag","onPause");
pause()
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
Log.e("MyTag","onResume");
start()
}
}
4.2 实现 ViewModel 控制,PlayerViewModel.kt
//播放状态
enum class PlayerStatus{
Playing,Paused,Completed,NotReady
}
class PlayerViewModel(application: Application) : AndroidViewModel(application) {
private var controllerShowTime = 0L
val mediaPlayer = MyMediaPlayer()
private val _playerStatus = MutableLiveData(PlayerStatus.NotReady)
val playerStatus:LiveData<PlayerStatus> = _playerStatus
private var _bufferPercent = MutableLiveData(0)
val bufferPercent: LiveData<Int> = _bufferPercent
private val _controllerFrameVisibility = MutableLiveData(View.INVISIBLE)
val controllerFrameVisibility: LiveData<Int> = _controllerFrameVisibility;
private val _progressBarVisibility = MutableLiveData(View.VISIBLE)
val progressBarVisibility:LiveData<Int> = _progressBarVisibility
private val _videoResolution = MutableLiveData(Pair(0,0))
val videoResolution: LiveData<Pair<Int,Int>> = _videoResolution
init {
loadVideo()
}
private fun loadVideo(){
mediaPlayer.apply {
//https://media.w3.org/2010/05/sintel/trailer.mp4
//http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
//$packageName
//val videoPath = "android.resource://com.example.myplayer/${R.raw.redes}"
//android.resource://com.example.myplayer/2131623936
val videoPath = "https://media.w3.org/2010/05/sintel/trailer.mp4"
reset()
_progressBarVisibility.value = View.VISIBLE
_playerStatus.value = PlayerStatus.NotReady
setDataSource(videoPath)
//val fd = getApplication<Application>().getAssets().openFd("red.mp4");
//setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
setOnPreparedListener {
_progressBarVisibility.value = View.INVISIBLE;
//isLooping = true
it.start()
_playerStatus.value = PlayerStatus.Playing
Log.e("MyTag", "setOnPreparedListener")
}
//宽高
setOnVideoSizeChangedListener { _, width, height ->
_videoResolution.value = Pair(width, height)
}
//缓冲
setOnBufferingUpdateListener { _, percent ->
_bufferPercent.value = percent
}
//播放完成
setOnCompletionListener {
_playerStatus.value = PlayerStatus.Completed
}
//进度完成
setOnSeekCompleteListener {
mediaPlayer.start()
_playerStatus.value = PlayerStatus.Playing
_progressBarVisibility.value = View.INVISIBLE
}
prepareAsync()
}
}
//播放状态
fun togglePlayerStatus(){
when(_playerStatus.value){
PlayerStatus.Playing ->{
mediaPlayer.pause()
_playerStatus.value = PlayerStatus.Paused
}
PlayerStatus.Paused ->{
mediaPlayer.start()
_playerStatus.value = PlayerStatus.Playing
}
PlayerStatus.Completed ->{
mediaPlayer.start()
_playerStatus.value = PlayerStatus.Playing
}
else -> return
}
}
// 显示/隐藏 控制条
fun toggleControllerFrame(){
if(_controllerFrameVisibility.value == View.INVISIBLE){
_controllerFrameVisibility.value = View.VISIBLE
controllerShowTime = System.currentTimeMillis()
viewModelScope.launch {
delay(3000)
if(System.currentTimeMillis() - controllerShowTime > 3000){
_controllerFrameVisibility.value = View.INVISIBLE
}
}
}else{
_controllerFrameVisibility.value = View.INVISIBLE
}
}
//重新赋值
fun emmitVideoResolution(){
_videoResolution.value = _videoResolution.value
}
//设置 MediaPlayer 进度
fun playerSeekToProgress(progress: Int){
_progressBarVisibility.value = View.VISIBLE
mediaPlayer.seekTo(progress)
}
override fun onCleared() {
super.onCleared()
mediaPlayer.release()
Log.e("MyTag","mediaPlayer release");
}
}
4.3 调用view层, 使用ViewModel,MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var playerViewModel: PlayerViewModel
private lateinit var surfaceView: SurfaceView
private lateinit var playerFrameLayout: FrameLayout
private lateinit var seekBar: SeekBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// object :OrientationEventListener(this){
// override fun onOrientationChanged(orientation: Int) {
// }
// }
setContentView(R.layout.activity_main)
val progressBar: ProgressBar = findViewById(R.id.progressBar)
seekBar = findViewById(R.id.seekBar)
val controllerFrameLayout: FrameLayout = findViewById(R.id.controllerFrame)
val buttonControl: ImageView = findViewById(R.id.buttonControl)
playerFrameLayout = findViewById(R.id.playerFrame)
updatePlayerProgress()
playerViewModel = ViewModelProvider(this)[PlayerViewModel::class.java].apply {
progressBarVisibility.observe(this@MainActivity) {
progressBar.visibility = it
}
videoResolution.observe(this@MainActivity) {
seekBar.max = mediaPlayer.duration
//Log.e("MyTag","---- ${mediaPlayer.duration}");
playerFrameLayout.post {
reSizePlayer(it.first, it.second)
}
}
controllerFrameVisibility.observe(this@MainActivity) {
controllerFrameLayout.visibility = it
}
bufferPercent.observe(this@MainActivity, Observer {
//Log.e("MyTag","---- $it");
seekBar.secondaryProgress = seekBar.max * it / 100;
})
playerStatus.observe(this@MainActivity) {
buttonControl.isClickable = true
when (it) {
PlayerStatus.Paused -> buttonControl.setImageResource(R.drawable.ic_baseline_play_arrow_24)
PlayerStatus.Completed -> buttonControl.setImageResource(R.drawable.ic_baseline_replay_24)
PlayerStatus.NotReady -> buttonControl.isClickable = false
else -> buttonControl.setImageResource(R.drawable.ic_baseline_pause_24)
}
}
}
lifecycle.addObserver(playerViewModel.mediaPlayer)
buttonControl.setOnClickListener {
playerViewModel.togglePlayerStatus()
}
playerFrameLayout.setOnClickListener {
playerViewModel.toggleControllerFrame()
}
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
playerViewModel.playerSeekToProgress(progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
surfaceView = findViewById(R.id.surfaceView)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {}
override fun surfaceChanged(
holder: SurfaceHolder, format: Int, width: Int, height: Int
) {
playerViewModel.mediaPlayer.setDisplay(holder)
playerViewModel.mediaPlayer.setScreenOnWhilePlaying(true)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {}
})
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
hideSystemUI()
playerViewModel.emmitVideoResolution()
}
}
private fun reSizePlayer(width: Int, height: Int) {
if (width == 0 || height == 0) return
surfaceView.layoutParams = FrameLayout.LayoutParams(
playerFrameLayout.height * width / height,
FrameLayout.LayoutParams.MATCH_PARENT,
Gravity.CENTER
)
//1674 1908
//Log.e("MyTag","Size width: ${playerFrameLayout.height * width / height}")
}
private fun updatePlayerProgress() {
lifecycleScope.launch {
while (true) {
delay(500)
seekBar.progress = playerViewModel.mediaPlayer.currentPosition
}
}
}
private fun hideSystemUI() {
val decorView: View = window.decorView
// Set the content to appear under the system bars so that the
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN //Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
}
5. 效果图