安卓给app提供了:
MediaPlayer:播放视频或音频功能,详见谷歌MediaPlayer文档
借助MediaPlayer,我们可以轻松的实现一个简单的播放器app。一般来说app显示内容放在Activity中。但是试想一般的播放器要求app进入后台后可以继续播放声音,app回到前台后可以继续播放视频。因此其实MediaPlayer更适合放在Service中来播放视频,而我们的Activity仅显示视频即可。接下来我们依次实现MediaService和MediaActivity。
首先实现我们的核心类MediaService:
class MediaService : Service() {
val mMediaPlayer = MediaPlayer() // 播放媒体文件的对象
val mMediaBinder = MediaBinder() // 传给Activity,使得Activity可以与我们这个Service通信
var mIsForeground = false // 为true时变成前台服务,为false时停止前台服务成为一般的后台服务
var mMediaListener: MediaListener? = null // mMediaPlayer的相关回调
var mTimer: Timer? = null // 用来定时调用mMediaListener.onProgress回调方法的定时器
var mTimerTask = object : TimerTask() { // 定时调用mMediaListener.onProgress回调方法的任务
override fun run() {
mMediaListener?.onProgress(mMediaPlayer.currentPosition, mMediaPlayer.duration)
}
}
// 此类的方法都是从Activity调用的
inner class MediaBinder : Binder() {
// 设置显示视频的surfaceHolder
fun setDisplay(surfaceHolder: SurfaceHolder?) {
mMediaPlayer.setDisplay(surfaceHolder)
}
// 设置一个额外的媒体播放完毕时回调
fun setMediaListener(mediaListener: MediaListener?) {
mMediaListener = mediaListener
mMediaPlayer.setOnVideoSizeChangedListener(mMediaListener)
if (mMediaListener == null) {
mTimer?.cancel()
mTimer = null
} else if (mTimer == null) {
mTimer = Timer().apply { schedule(mTimerTask, 0, 100) }
}
}
// 打开媒体文件
fun open(uri: Uri) {
mMediaPlayer.reset()
mMediaPlayer.setDataSource(this@MediaService, uri)
mMediaPlayer.prepareAsync()
}
// 继续或暂停播放媒体,返回true代表继续播放了,返回false代表暂停播放了
fun playOrPause(): Boolean {
if (mMediaPlayer.isPlaying) mMediaPlayer.pause()
else mMediaPlayer.start()
updateServiceState()
return mMediaPlayer.isPlaying
}
// 停止播放媒体
fun stop() {
mMediaPlayer.stop()
updateServiceState()
}
// 调整播放进度
fun seekTo(millisecond: Int) {
mMediaPlayer.seekTo(millisecond)
}
}
override fun onBind(intent: Intent): IBinder {
return mMediaBinder
}
override fun onCreate() {
super.onCreate()
mMediaPlayer.setOnPreparedListener {
mMediaPlayer.start()
updateServiceState()
}
mMediaPlayer.setOnCompletionListener {
mMediaListener?.onCompletion(it)
updateServiceState()
}
mMediaPlayer.setOnErrorListener { mp, what, extra ->
Log.e(TAG, "MediaPlayer error! mediaPlayer=$mp, what=$what, extra=$extra")
updateServiceState()
true
}
}
// 如果正在播放媒体文件,则变成前台服务,否则变成一般的后台服务
private fun updateServiceState() {
if (mMediaPlayer.isPlaying != mIsForeground) {
mIsForeground = !mIsForeground
if (mIsForeground) {
startForegroundNotification()
} else {
stopForeground(true)
}
}
}
// 创建notification,变成前台服务
private fun startForegroundNotification() {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("Media", "Media", NotificationManager.IMPORTANCE_HIGH)
notificationManager.createNotificationChannel(channel)
val intent = Intent(this, MediaActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val notification = Notification.Builder(this, "Media")
.setSmallIcon(R.drawable.ic_media)
.setContentTitle("Media")
.setContentText("Playing...")
.setContentIntent(pendingIntent)
.build()
startForeground(123, notification)
}
override fun onDestroy() {
super.onDestroy()
mMediaPlayer.release()
}
companion object {
private val TAG = MediaService::class.simpleName!!
}
}
MediaService可以在播放视频时成为前台Service,这样保证我们的MediaService播放视频时不会被突然杀掉,并且也创建了相应的通知,用户只要点击通知即可进入MediaActivity观看视频。
在MediaService中还有MediaBinder,其提供了一些方法供MediaActivity那边调用。
接下来实现我们的布局文件activity_media.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/surface_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.sc.media.MediaActivity">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<RelativeLayout
android:id="@+id/media_control"
android:layout_width="match_parent"
android:layout_height="84dp"
android:layout_gravity="bottom">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:paddingVertical="10dp"
android:background="#80000000">
<ImageButton
android:id="@+id/open_media_file_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/play_pause_media_button"
android:src="@drawable/ic_file"
android:scaleType="fitCenter"
android:background="@android:color/transparent"
android:contentDescription="@string/open_media_file" />
<ImageButton
android:id="@+id/play_pause_media_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/ic_play_white"
android:scaleType="fitCenter"
android:background="@android:color/transparent"
android:contentDescription="@string/play_or_pause_media" />
<ImageButton
android:id="@+id/stop_media_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/play_pause_media_button"
android:src="@drawable/ic_stop"
android:scaleType="fitCenter"
android:background="@android:color/transparent"
android:contentDescription="@string/stop_play_media" />
</RelativeLayout>
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@android:color/transparent"
android:stepSize="1.0"
android:valueFrom="0.0"
android:valueTo="1.0"
android:value="0.0" />
</RelativeLayout>
</FrameLayout>
我们主要是使用了一个SurfaceView来显示视频,并且使用ImageButton实现我们的打开媒体文件、暂停/播放和停止按钮,还使用Slider实现播放进度条的显示与控制。
最后来实现我们的MediaActivity:
class MediaActivity : AppCompatActivity() {
lateinit var mSurfaceContainer: FrameLayout // SurfaceView的全屏父View
lateinit var mSurfaceView: SurfaceView // 用来播放视频的SurfaceView
lateinit var mMediaControlView: View // 媒体播放工具栏
lateinit var mPlayPauseButton: ImageButton // 控制媒体播放暂停的按钮
lateinit var mSlider: Slider // 媒体播放进度条
lateinit var mMediaBinder: MediaService.MediaBinder // 用来与MediaService通信的Binder对象
var mVideoRatio = 0f // 视频的宽高比
var mSliderTrackingTouch = false // mSlider是否正在被拖
val mMediaListener = object : MediaListener {
// 视频尺寸改变时调用
override fun onVideoSizeChanged(mp: MediaPlayer?, width: Int, height: Int) {
mVideoRatio = if (height == 0) 0f else width.toFloat() / height.toFloat()
updateSurfaceSize()
}
// 媒体播放完毕时调用
override fun onCompletion(mp: MediaPlayer?) {
mPlayPauseButton.setImageResource(R.drawable.ic_play_white)
}
// 播放进度改变时调用
override fun onProgress(position: Int, duration: Int) {
if (mSliderTrackingTouch) return
mSlider.valueTo = duration.toFloat().coerceAtLeast(1f)
mSlider.value = position.toFloat().coerceAtLeast(mSlider.valueFrom).coerceAtMost(mSlider.valueTo)
}
}
// 连接MediaService的对象
val mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
mMediaBinder = service as MediaService.MediaBinder
mMediaBinder.setMediaListener(mMediaListener)
mSurfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
mMediaBinder.setDisplay(holder)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.d(TAG, "Surface changed! width=$width, height=$height")
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
mMediaBinder.setDisplay(null)
}
})
}
override fun onServiceDisconnected(name: ComponentName?) { }
}
......
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media)
// 隐藏SystemUI,包括导航栏、状态栏等
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
mSurfaceContainer = findViewById(R.id.surface_container)
mSurfaceView = findViewById(R.id.surface_view)
mMediaControlView = findViewById(R.id.media_control)
mPlayPauseButton = findViewById(R.id.play_pause_media_button)
mSlider = findViewById(R.id.slider)
// 保证转屏时可以正确更新SurfaceView的尺寸
mSurfaceView.viewTreeObserver.addOnGlobalLayoutListener { updateSurfaceSize() }
// 连接绑定MediaService
val bindIntent = Intent(this, MediaService::class.java)
bindService(bindIntent, mServiceConnection, BIND_AUTO_CREATE)
// 打开媒体文件按钮
findViewById<ImageButton>(R.id.open_media_file_button).setOnClickListener {
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
addCategory(Intent.CATEGORY_OPENABLE)
startActivityForResult(this, 0)
}
}
// 播放/暂停按钮
mPlayPauseButton.setOnClickListener {
if (mMediaBinder.playOrPause()) mPlayPauseButton.setImageResource(R.drawable.ic_pause_white)
else mPlayPauseButton.setImageResource(R.drawable.ic_play_white)
}
// 停止按钮
findViewById<ImageButton>(R.id.stop_media_button).setOnClickListener {
mMediaBinder.stop()
mPlayPauseButton.setImageResource(R.drawable.ic_play_white)
}
// 按住进度条时,让进度条显示格式为(分:秒.毫秒)的时间
mSlider.setLabelFormatter { value: Float ->
val millis = value.toInt()
val seconds = millis / 1000
val minutes = seconds / 60
"$minutes:${seconds % 60}.${millis % 1000}"
}
// 更新mSliderTrackingTouch,使得我们通过这个变量可以得知当前进度条是否正在被拖
mSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) { mSliderTrackingTouch = true }
override fun onStopTrackingTouch(slider: Slider) { mSliderTrackingTouch = false }
})
// 通过进度条调整播放进度
mSlider.addOnChangeListener { slider, value, fromUser ->
if (mSliderTrackingTouch and fromUser) {
mMediaBinder.seekTo(value.toInt())
}
}
}
// 得到打开媒体文件结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val uri: Uri = data?.data ?: return
mMediaBinder.open(uri) // 要MediaService开始播放媒体文件
mPlayPauseButton.setImageResource(R.drawable.ic_pause_white)
}
// 根据视频宽高比mVideoRatio和mSurfaceContainer的尺寸更新mSurfaceView的尺寸
private fun updateSurfaceSize() {
if (mVideoRatio <= 0) return
val playerRatio = mSurfaceContainer.width.toFloat() / mSurfaceContainer.height.toFloat()
val params = mSurfaceView.layoutParams
if (playerRatio > mVideoRatio) {
params.width = (mSurfaceContainer.height.toFloat() * mVideoRatio).toInt()
params.height = mSurfaceContainer.height
} else if (playerRatio < mVideoRatio) {
params.width = mSurfaceContainer.width
params.height = (mSurfaceContainer.width.toFloat() / mVideoRatio).toInt()
} else {
params.width = mSurfaceContainer.width
params.height = mSurfaceContainer.height
}
mSurfaceView.layoutParams = params
}
override fun onDestroy() {
super.onDestroy()
mMediaBinder.setMediaListener(null)
unbindService(mServiceConnection)
}
companion object {
private val TAG = MediaActivity::class.simpleName!!
fun actionStart(context: Context) {
val intent = Intent(context, MediaActivity::class.java)
context.startActivity(intent)
}
}
}
在MediaActivity中,我们在onCreate中首先使用WindowInsetsControllerCompat类隐藏导航栏和状态栏,防止这些系统窗口影响我们看视频的体验。然后调用bindService方法与我们的MediaService建立连接。最后设置打开媒体文件、播放/暂停等按钮设置按键监听器,实现这些按键的功能。
源代码地址:https://github.com/SSSxCCC/SCApp
总结
最好在前台服务中播放视频,这样不管我们的Activity是什么状态都可以继续播放视频。