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"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
2.4 矢量图标,添加系统自带矢量图
ic_baseline_play_arrow_24.xml,
ic_baseline_replay_24.xml,
3. 布局文件
3.1 控制View,controller_layout.xml
<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">
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="bottom"
android:layout_margin="4dp"
android:orientation="horizontal">
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" />
android:id="@+id/seekBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="12"
android:progressBackgroundTint="#FFFFFF" />
3.2 竖屏布局,activity_main.xml
<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">
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">
android:id="@+id/surfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
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
<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">
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">
android:id="@+id/surfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
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
class MyMediaPlayer:MediaPlayer(), DefaultLifecycleObserver{
override fun onPause(owner: LifecycleOwner) {
Log.e("MyTag","onPause");
override fun onResume(owner: LifecycleOwner) {
Log.e("MyTag","onResume");
4.2 实现 ViewModel 控制,PlayerViewModel.kt
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 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
Int,Int>> = _videoResolution val videoPath = "https://media.w3.org/2010/05/sintel/trailer.mp4"
_progressBarVisibility.value = View.VISIBLE
_playerStatus.value = PlayerStatus.NotReady
_progressBarVisibility.value = View.INVISIBLE;
_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 {
_playerStatus.value = PlayerStatus.Playing
_progressBarVisibility.value = View.INVISIBLE
fun togglePlayerStatus(){
when(_playerStatus.value){
_playerStatus.value = PlayerStatus.Paused
_playerStatus.value = PlayerStatus.Playing
PlayerStatus.Completed ->{
_playerStatus.value = PlayerStatus.Playing
fun toggleControllerFrame(){
if(_controllerFrameVisibility.value == View.INVISIBLE){
_controllerFrameVisibility.value = View.VISIBLE
controllerShowTime = System.currentTimeMillis()
if(System.currentTimeMillis() - controllerShowTime > 3000){
_controllerFrameVisibility.value = View.INVISIBLE
_controllerFrameVisibility.value = View.INVISIBLE
fun emmitVideoResolution(){
_videoResolution.value = _videoResolution.value
fun playerSeekToProgress(progress: Int){
_progressBarVisibility.value = View.VISIBLE
mediaPlayer.seekTo(progress)
override fun onCleared() {
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)
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)
playerViewModel = ViewModelProvider(this)[PlayerViewModel::class.java].apply {
progressBarVisibility.observe(this@MainActivity) {
progressBar.visibility = it
videoResolution.observe(this@MainActivity) {
seekBar.max = mediaPlayer.duration
reSizePlayer(it.first, it.second)
controllerFrameVisibility.observe(this@MainActivity) {
controllerFrameLayout.visibility = it
bufferPercent.observe(this@MainActivity, Observer {
seekBar.secondaryProgress = seekBar.max * it / 100;
playerStatus.observe(this@MainActivity) {
buttonControl.isClickable = true
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) {
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) {
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,
private fun updatePlayerProgress() {
seekBar.progress = playerViewModel.mediaPlayer.currentPosition
private fun hideSystemUI() {
val decorView: View = window.decorView
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN)

5. 效果图
