📅  最后修改于: 2023-12-03 15:11:56.297000             🧑  作者: Mango
Jetpack Compose 是 Android 官方推出的一款全新的 UI 工具包,它使用 Kotlin 语言、采用声明式、函数式的方式来构建界面,使得 UI 代码更加简洁易懂。在 Jetpack Compose 的帮助下,我们可以创建出一款优秀的视频播放器。
使用 Jetpack Compose 构建视频播放器的架构设计如下:
┌──────────────────┐
│ Player │
└──────────────────┘
│
│ 视频状态变化
▼
┌──────────────────┐
│ Composable UI │
└──────────────────┘
│
│ UI 事件
▼
┌──────────────────┐
│ Composable VM │
└──────────────────┘
界面有点简陋,为了便于演示。此处用图片代替。
@Composable
fun VideoPlayerScreen(viewModel: VideoPlayerViewModel) {
val videoState by viewModel.videoState.observeAsState(VideoPlayerState())
Box(modifier = Modifier.fillMaxSize()) {
if (videoState.isLandScape) {
// 横屏状态
LandscapePlayerView(videoState)
} else {
// 竖屏状态
PortraitPlayerView(videoState)
}
PlayerSurface(videoState, viewModel)
if (videoState.videoUrl.isNotEmpty()) {
ControlView(videoState, viewModel)
}
}
}
class VideoPlayerViewModel : ViewModel() {
var videoState = mutableStateOf(VideoPlayerState())
private var currentPosition = 0L
private var resumePosition = 0L
/**
* 播放指定的本地视频文件
*/
fun playLocalVideo(context: Context, assetName: String) {
val fileDescriptor = context.assets.openFd(assetName)
val dataSourceFactory = DefaultDataSourceFactory(context, "exoplayer-sample")
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(fileDescriptor.uri)
play(mediaSource)
}
/**
* 播放指定的网络视频
*/
fun playOnlineVideo(context: Context, url: String) {
val dataSourceFactory = DefaultDataSourceFactory(context, "exoplayer-sample")
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(url))
play(mediaSource)
}
/**
* 开始播放视频
*/
private fun play(mediaSource: MediaSource) {
videoState.value = videoState.value.copy(isPlaying = true)
val player = SimpleExoPlayer.Builder(appContext).build()
player.prepare(mediaSource)
player.playWhenReady = true
player.seekTo(currentPosition)
videoState.value = videoState.value.copy(player = player, videoUrl = mediaSource.toString())
player.addListener(object : Player.EventListener {
override fun onPlaybackStateChanged(@Player.State playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
when (playbackState) {
Player.STATE_READY -> {
videoState.value = videoState.value.copy(duration = player.duration)
}
Player.STATE_ENDED -> {
videoState.value = videoState.value.copy(isPlaying = false)
}
}
}
override fun onPositionDiscontinuity(reason: Int) {
super.onPositionDiscontinuity(reason)
currentPosition = player.currentPosition
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
videoState.value = videoState.value.copy(isPlaying = true)
} else {
videoState.value = videoState.value.copy(isPlaying = false)
}
}
})
}
/**
* 开始播放下一条视频
*/
fun playNextVideo(context: Context, urlList: List<String>) {
val currentIndex = urlList.indexOf(videoState.value.videoUrl)
if (currentIndex == urlList.size - 1) {
context.toast("已经是最后一条视频了")
return
}
currentPosition = 0L
resumePosition = 0L
val nextIndex = currentIndex + 1
val nextUrl = urlList[nextIndex]
playOnlineVideo(context, nextUrl)
}
/**
* 暂停视频播放
*/
fun pause() {
videoState.value = videoState.value.copy(isPlaying = false)
resumePosition = videoState.value.player?.currentPosition ?: 0L
videoState.value.player?.playWhenReady = false
}
/**
* 继续视频播放
*/
fun resume() {
videoState.value = videoState.value.copy(isPlaying = true)
videoState.value.player?.playWhenReady = true
}
/**
* 停止视频播放
*/
fun stop() {
videoState.value = videoState.value.copy(isPlaying = false)
videoState.value.player?.stop()
}
/**
* 跳转至指定时间
*/
fun seekTo(timeMs: Long) {
videoState.value.player?.seekTo(timeMs)
}
@RequiresApi(Build.VERSION_CODES.N)
fun isPortrait(activity: Activity): Boolean {
return activity.display?.rotation == Surface.ROTATION_0 || activity.display?.rotation == Surface.ROTATION_180
}
/**
* 切换横竖屏
*/
fun toggleScreen(activity: Activity) {
if (isPortrait(activity)) {
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
videoState.value = videoState.value.copy(isLandScape = true)
} else {
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
videoState.value = videoState.value.copy(isLandScape = false)
}
}
}
data class VideoPlayerState(
val isPlaying: Boolean = false, // 是否正在播放
val isLandScape: Boolean = false,// 是否横屏
val videoUrl: String = "", // 视频URL
val player: SimpleExoPlayer? = null, // 播放器
val duration: Long = 0L, // 视频总时长
val currentTime: Long = 0L // 视频当前播放进度
)
@Composable
fun ControlView(
videoState: VideoPlayerState,
viewModel: VideoPlayerViewModel
) {
Column(
Modifier
.background(Color.Black.copy(alpha = 0.3f))
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(modifier = Modifier.weight(1f)) // 占位符
ControlMenu(
videoState,
viewModel,
Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
)
}
}
@Composable
fun ControlMenu(
videoState: VideoPlayerState,
viewModel: VideoPlayerViewModel,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
IconButton(
modifier = Modifier.size(56.dp),
onClick = { viewModel.playNextVideo(appContext, urlList) }) {
Icon(
imageVector = Icons.Default.SkipNext,
contentDescription = null
)
}
if (videoState.isPlaying) {
IconButton(
modifier = Modifier.size(56.dp),
onClick = { viewModel.pause() }) {
Icon(
imageVector = Icons.Default.Pause,
contentDescription = null
)
}
} else {
IconButton(
modifier = Modifier.size(56.dp),
onClick = { viewModel.resume() }) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null
)
}
}
IconButton(
modifier = Modifier.size(56.dp),
onClick = { viewModel.stop() }) {
Icon(
painter = painterResource(id = R.drawable.ic_stop),
contentDescription = null
)
}
}
}
@Composable
fun PlayerSurface(
videoState: VideoPlayerState,
viewModel: VideoPlayerViewModel
) {
val surfaceView = remember { SurfaceView(appContext) }
AndroidView({ surfaceView }) { view ->
view.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
videoState.player?.setVideoSurface(holder.surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// no-op
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
// no-op
}
})
}
DisposableEffect(videoState.player) {
onDispose {
videoState.player?.release()
}
videoState.player?.playbackState?.let { playbackState ->
if (playbackState == Player.STATE_IDLE) {
if (videoState.videoUrl.startsWith("http")) {
viewModel.playOnlineVideo(appContext, videoState.videoUrl)
} else {
viewModel.playLocalVideo(appContext, videoState.videoUrl)
}
}
}
videoState.player?.videoComponent?.addVideoListener(object : VideoListener {
override fun onRenderedFirstFrame() {
viewModel.seekTo(viewModel.resumePosition)
}
override fun onVideoSizeChanged(width: Int, height: Int, _unused_: Int, _unused2_: Float) {
videoState.player?.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING)
}
})
// The onPositionChanged callback is called whenever the position changes,
// but we only want to re-compose when the _displayed_ position changes. This
// ensures that we don't cause unnecessary re-compose calls, since they
// are very expensive.
var lastDisplayedPosition = -1L
val updatePosition = {
val newPosition = videoState.player?.currentPosition ?: 0L
if (newPosition != lastDisplayedPosition) {
lastDisplayedPosition = newPosition
viewModel.videoState.value = videoState.copy(currentTime = newPosition)
}
}
val handler = Handler(Looper.getMainLooper())
val updatePositionRunnable = object : Runnable {
override fun run() {
updatePosition()
handler.postDelayed(this, 16)
}
}
videoState.player?.addListener(object : Player.EventListener {
override fun onPlaybackStateChanged(@Player.State playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_READY) {
handler.postDelayed(updatePositionRunnable, 16)
updatePosition()
} else {
handler.removeCallbacks(updatePositionRunnable)
}
}
})
// When view composition is complete, start playback if necessary
if (videoState.isPlaying) {
viewModel.resume()
}
onDispose {
videoState.player?.stop()
}
}
}
完整代码示例见 GitHub。
随着 Jetpack Compose 的不断完善,它将会成为未来 Android UI 开发的主流方式。通过本文,我们了解了如何使用 Jetpack Compose 构建一个简单的视频播放器,利用 Compose 的优势,让界面代码更加简洁易懂,提升了开发效率,同时又可以更好地维护代码,是一个非常棒的开发方式。