pref: 完善视频播放器

This commit is contained in:
HuanCheng65 2023-07-16 14:10:25 +08:00
parent 3a5d601fd2
commit 4acd2d1852
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
11 changed files with 271 additions and 176 deletions

View File

@ -1,14 +1,10 @@
package com.huanchengfly.tieba.post.ui.common
import android.content.pm.ActivityInfo
import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent
@ -16,11 +12,8 @@ import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@ -42,23 +35,16 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.panpf.sketch.compose.AsyncImage
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.UserActivity
import com.huanchengfly.tieba.post.activities.WebViewActivity
import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass
import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.findActivity
import com.huanchengfly.tieba.post.models.protos.PhotoViewData
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
import com.huanchengfly.tieba.post.ui.widgets.compose.EmoticonText
import com.huanchengfly.tieba.post.ui.widgets.compose.FullScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage
import com.huanchengfly.tieba.post.ui.widgets.compose.VoicePlayer
import com.huanchengfly.tieba.post.ui.widgets.compose.video.OnFullScreenModeChangedListener
import com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayer
import com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayerSource
import com.huanchengfly.tieba.post.ui.widgets.compose.video.rememberVideoPlayerController
import com.huanchengfly.tieba.post.utils.appPreferences
import com.huanchengfly.tieba.post.utils.launchUrl
@ -179,72 +165,22 @@ data class VideoContentRender(
val context = LocalContext.current
if (picUrl.isNotBlank()) {
if (videoUrl.isNotBlank()) {
var fullScreen by remember { mutableStateOf(false) }
val systemUiController = rememberSystemUiController()
val videoPlayerController = rememberVideoPlayerController(
source = VideoPlayerSource.Network(videoUrl),
fullScreenModeChangedListener = object : OnFullScreenModeChangedListener {
override fun onFullScreenModeChanged(isFullScreen: Boolean) {
Log.i("VideoPlayer", "onFullScreenModeChanged $isFullScreen")
fullScreen = isFullScreen
systemUiController.isStatusBarVisible = !isFullScreen
systemUiController.isNavigationBarVisible = !isFullScreen
if (isFullScreen) {
context.findActivity()?.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
context.findActivity()?.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
}
}
)
// DisposableEffect(Unit) {
// onDispose {
// Log.i("VideoContentRender", "onDispose")
// videoPlayerController.release()
// }
// }
val videoPlayerContent =
movableContentOf { isFullScreen: Boolean, modifier: Modifier ->
VideoPlayer(
videoPlayerController = videoPlayerController,
modifier = modifier,
backgroundColor = if (isFullScreen) Color.Black else Color.Transparent
)
}
val picModifier = Modifier
.clip(RoundedCornerShape(context.appPreferences.radius.dp))
.fillMaxWidth(widthFraction)
.aspectRatio(width * 1f / height)
if (fullScreen) {
Spacer(
modifier = Modifier
.clip(RoundedCornerShape(context.appPreferences.radius.dp))
.fillMaxWidth(widthFraction)
.aspectRatio(width * 1f / height)
)
FullScreen {
videoPlayerContent(
true,
Modifier.fillMaxSize()
)
}
} else {
videoPlayerContent(
false,
Modifier
.clip(RoundedCornerShape(context.appPreferences.radius.dp))
.fillMaxWidth(widthFraction)
.aspectRatio(width * 1f / height)
)
}
if (videoUrl.isNotBlank()) {
com.huanchengfly.tieba.post.ui.widgets.compose.VideoPlayer(
videoUrl = videoUrl,
thumbnailUrl = picUrl,
modifier = picModifier
)
} else {
AsyncImage(
imageUri = picUrl,
contentDescription = stringResource(id = R.string.desc_video),
modifier = Modifier
.clip(RoundedCornerShape(context.appPreferences.radius.dp))
.fillMaxWidth(widthFraction)
.aspectRatio(width * 1f / height)
modifier = picModifier
.clickable {
WebViewActivity.launch(context, webUrl)
},

View File

@ -1,5 +1,7 @@
package com.huanchengfly.tieba.post.ui.widgets.compose
import android.content.pm.ActivityInfo
import android.util.Log
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -30,8 +33,8 @@ import androidx.compose.material.icons.rounded.FavoriteBorder
import androidx.compose.material.icons.rounded.PhotoSizeSelectActual
import androidx.compose.material.icons.rounded.SwapCalls
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -51,12 +54,10 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import cn.jzvd.Jzvd
import com.github.panpf.sketch.displayImage
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.fade
import com.google.accompanist.placeholder.material.placeholder
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.UserActivity
import com.huanchengfly.tieba.post.api.abstractText
@ -65,9 +66,13 @@ import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.User
import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.findActivity
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.utils.getImmutablePhotoViewData
import com.huanchengfly.tieba.post.ui.widgets.VideoPlayerStandard
import com.huanchengfly.tieba.post.ui.widgets.compose.video.DefaultVideoPlayerController
import com.huanchengfly.tieba.post.ui.widgets.compose.video.OnFullScreenModeChangedListener
import com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayerSource
import com.huanchengfly.tieba.post.ui.widgets.compose.video.rememberVideoPlayerController
import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.EmoticonUtil.emoticonString
import com.huanchengfly.tieba.post.utils.ImageUtil
@ -593,19 +598,51 @@ fun VideoPlayer(
modifier: Modifier = Modifier,
title: String = ""
) {
AndroidView(
factory = { context ->
VideoPlayerStandard(context)
},
modifier = modifier
) {
it.setUp(videoUrl, title)
it.posterImageView.displayImage(thumbnailUrl)
}
DisposableEffect(videoUrl) {
onDispose {
Jzvd.releaseAllVideos()
val context = LocalContext.current
val systemUiController = rememberSystemUiController()
val videoPlayerController = rememberVideoPlayerController(
source = VideoPlayerSource.Network(videoUrl),
thumbnailUrl = thumbnailUrl,
fullScreenModeChangedListener = object : OnFullScreenModeChangedListener {
override fun onFullScreenModeChanged(isFullScreen: Boolean) {
Log.i("VideoPlayer", "onFullScreenModeChanged $isFullScreen")
systemUiController.isStatusBarVisible = !isFullScreen
systemUiController.isNavigationBarVisible = !isFullScreen
if (isFullScreen) {
context.findActivity()?.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
context.findActivity()?.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
}
}
)
val fullScreen by (videoPlayerController as DefaultVideoPlayerController).collect { isFullScreen }
val videoPlayerContent =
movableContentOf { isFullScreen: Boolean, playerModifier: Modifier ->
com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayer(
videoPlayerController = videoPlayerController,
modifier = playerModifier,
backgroundColor = if (isFullScreen) Color.Black else Color.Transparent
)
}
if (fullScreen) {
Spacer(
modifier = modifier
)
FullScreen {
videoPlayerContent(
true,
Modifier.fillMaxSize()
)
}
} else {
videoPlayerContent(
false,
modifier
)
}
}

View File

@ -2,6 +2,7 @@ package com.huanchengfly.tieba.post.ui.widgets.compose.video
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
@ -9,6 +10,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.VideoSize
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSource
@ -29,7 +33,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
interface OnFullScreenModeChangedListener {
fun onFullScreenModeChanged(isFullScreen: Boolean)
@ -42,7 +45,6 @@ internal class DefaultVideoPlayerController(
private val fullScreenModeChangedListener: OnFullScreenModeChangedListener? = null
) : VideoPlayerController {
private val released = AtomicBoolean(false)
val releaseCounter = AtomicInteger(0)
private val _state = MutableStateFlow(initialState)
override val state: StateFlow<VideoPlayerState>
@ -86,6 +88,7 @@ internal class DefaultVideoPlayerController(
private var playerView: PlayerView? = null
private var updateDurationAndPositionJob: Job? = null
private var autoHideControllerJob: Job? = null
private val playerListener = object : Player.Listener {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@ -107,7 +110,8 @@ internal class DefaultVideoPlayerController(
_state.set {
copy(
playbackState = PlaybackState.of(playbackState)
playbackState = PlaybackState.of(playbackState),
startedPlay = playbackState != STATE_IDLE
)
}
}
@ -175,7 +179,18 @@ internal class DefaultVideoPlayerController(
}
fun initialize() {
releaseCounter.incrementAndGet()
Log.i("VideoPlayerController", "$this initialize")
val currentState = _state.value
exoPlayer.playWhenReady = currentState.isPlaying
initialStateRunner = {
exoPlayer.seekTo(currentState.currentPosition)
}
if (this::source.isInitialized) {
setSource(source)
}
if (playerView != null) {
playerViewAvailable(playerView!!)
}
}
/**
@ -185,17 +200,19 @@ internal class DefaultVideoPlayerController(
private val waitPlayerViewToPrepare = AtomicBoolean(false)
override fun play() {
if (exoPlayer.playbackState == Player.STATE_ENDED) {
_state.set { copy(startedPlay = true) }
if (exoPlayer.playbackState == STATE_ENDED) {
exoPlayer.seekTo(0)
}
exoPlayer.playWhenReady = true
autoHideControls()
}
override fun pause() {
exoPlayer.playWhenReady = false
}
override fun playPauseToggle() {
override fun togglePlaying() {
if (exoPlayer.isPlaying) pause()
else play()
}
@ -228,9 +245,6 @@ internal class DefaultVideoPlayerController(
}
override fun setSource(source: VideoPlayerSource) {
if (released.get()) {
initialize()
}
this.source = source
if (playerView == null) {
waitPlayerViewToPrepare.set(true)
@ -247,8 +261,27 @@ internal class DefaultVideoPlayerController(
_state.set { copy(controlsEnabled = enabled) }
}
fun showControls() {
fun showControls(autoHide: Boolean = true) {
_state.set { copy(controlsVisible = true) }
if (autoHide) {
autoHideControls()
} else {
cancelAutoHideControls()
}
}
private fun cancelAutoHideControls() {
Log.i("VideoPlayerController", "cancelAutoHideControls")
autoHideControllerJob?.cancel()
}
private fun autoHideControls() {
cancelAutoHideControls()
Log.i("VideoPlayerController", "autoHideControls")
autoHideControllerJob = coroutineScope.launch {
delay(5000)
hideControls()
}
}
fun hideControls() {
@ -264,12 +297,15 @@ internal class DefaultVideoPlayerController(
}
private fun updateDurationAndPosition() {
_state.set {
copy(
duration = exoPlayer.duration.coerceAtLeast(0),
currentPosition = exoPlayer.currentPosition.coerceAtLeast(0),
secondaryProgress = exoPlayer.bufferedPosition.coerceAtLeast(0)
)
if (exoPlayer.playbackState == STATE_READY || exoPlayer.playbackState == STATE_ENDED) {
_state.set {
copy(
duration = exoPlayer.duration.coerceAtLeast(0),
currentPosition = exoPlayer.currentPosition.coerceAtLeast(0),
secondaryProgress = exoPlayer.bufferedPosition.coerceAtLeast(0),
isPlaying = exoPlayer.isPlaying
)
}
}
}
@ -334,12 +370,12 @@ internal class DefaultVideoPlayerController(
}
override fun release() {
if (releaseCounter.decrementAndGet() <= 0 && released.compareAndSet(false, true)) {
Log.i("VideoPlayerController", "$this release")
if (released.compareAndSet(false, true)) {
exoPlayer.release()
previewExoPlayer.release()
_exoPlayer = null
_previewExoPlayer = null
playerView = null
}
}

View File

@ -92,7 +92,7 @@ fun PlayPauseButton(modifier: Modifier = Modifier) {
val playbackState by controller.collect { playbackState }
IconButton(
onClick = { controller.playPauseToggle() },
onClick = { controller.togglePlaying() },
modifier = modifier
) {
if (isPlaying) {

View File

@ -163,6 +163,21 @@ suspend fun PointerInputScope.detectMediaPlayerGesture(
onDrag: (Float) -> Unit
) {
coroutineScope {
// launch {
// detectVerticalDragGestures(
// onDragStart = {
// Log.i("MediaControlGestures", "Vertical onDragStart $it")
// },
// onDragEnd = {
// Log.i("MediaControlGestures", "Vertical onDragEnd")
// },
// onVerticalDrag = { change, dragAmount ->
// Log.i("MediaControlGestures", "Vertical onVerticalDrag $change $dragAmount")
// if (change.positionChange() != Offset.Zero) change.consume()
// },
// )
// }
launch {
detectHorizontalDragGestures(
onDragStart = onDragStart,

View File

@ -23,9 +23,11 @@ fun ProgressIndicator(
max = duration,
enabled = controlsVisible && controlsEnabled,
onSeek = {
controller.showControls(autoHide = false)
controller.previewSeekTo(it)
},
onSeekStopped = {
controller.showControls(autoHide = true)
controller.seekTo(it)
},
secondaryProgress = secondaryProgress,

View File

@ -4,8 +4,9 @@ import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -14,12 +15,17 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentColor
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Fullscreen
import androidx.compose.material.icons.rounded.FullscreenExit
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@ -36,10 +42,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.github.panpf.sketch.compose.AsyncImage
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.ui.widgets.compose.video.util.getDurationString
@ -49,6 +57,7 @@ internal val LocalVideoPlayerController =
@Composable
fun rememberVideoPlayerController(
source: VideoPlayerSource? = null,
thumbnailUrl: String? = null,
fullScreenModeChangedListener: OnFullScreenModeChangedListener? = null,
playWhenReady: Boolean = false,
): VideoPlayerController {
@ -62,8 +71,11 @@ fun rememberVideoPlayerController(
return DefaultVideoPlayerController(
context = context,
initialState = value,
coroutineScope = coroutineScope
)
coroutineScope = coroutineScope,
fullScreenModeChangedListener = fullScreenModeChangedListener
).apply {
source?.let { setSource(it) }
}
}
override fun SaverScope.save(value: DefaultVideoPlayerController): VideoPlayerState {
@ -73,7 +85,10 @@ fun rememberVideoPlayerController(
init = {
DefaultVideoPlayerController(
context = context,
initialState = VideoPlayerState(isPlaying = playWhenReady),
initialState = VideoPlayerState(
thumbnailUrl = thumbnailUrl,
isPlaying = playWhenReady
),
coroutineScope = coroutineScope,
fullScreenModeChangedListener = fullScreenModeChangedListener
).apply {
@ -101,10 +116,18 @@ fun VideoPlayer(
videoPlayerController.enableGestures(gesturesEnabled)
}
DisposableEffect(Unit) {
videoPlayerController.initialize()
onDispose {
videoPlayerController.release()
}
}
CompositionLocalProvider(
LocalContentColor provides Color.White,
LocalVideoPlayerController provides videoPlayerController
) {
val startedPlay by videoPlayerController.collect { startedPlay || playbackState != PlaybackState.IDLE }
val aspectRatio by videoPlayerController.collect { videoSize.first / videoSize.second }
val supportFullScreen =
remember(videoPlayerController) { videoPlayerController.supportFullScreen() }
@ -124,72 +147,98 @@ fun VideoPlayer(
.fillMaxSize()
.then(modifier)
) {
PlayerSurface(
modifier = Modifier
.aspectRatio(aspectRatio.takeUnless { it.isNaN() || it == 0f } ?: 2f)
.align(Alignment.Center)
) {
videoPlayerController.playerViewAvailable(it)
}
DisposableEffect(Unit) {
videoPlayerController.initialize()
Log.i(
"VideoPlayer",
"${videoPlayerController.releaseCounter.get()} init $videoPlayerController"
)
onDispose {
videoPlayerController.release()
Log.i(
"VideoPlayer",
"${videoPlayerController.releaseCounter.get()} release $videoPlayerController"
)
}
}
MediaControlGestures(modifier = Modifier.matchParentSize())
MediaControlButtons(
modifier = Modifier.matchParentSize()
)
val controlsVisible by videoPlayerController.collect { controlsVisible }
if (controlsVisible) {
Column(
if (startedPlay) {
PlayerSurface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.align(Alignment.BottomCenter),
verticalArrangement = Arrangement.spacedBy(8.dp)
.aspectRatio(aspectRatio.takeUnless { it.isNaN() || it == 0f } ?: 2f)
.align(Alignment.Center)
) {
ProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
Row(verticalAlignment = Alignment.CenterVertically) {
PositionAndDuration()
Spacer(modifier = Modifier.weight(1f))
if (videoPlayerController.supportFullScreen()) {
FullScreenButton()
}
}
videoPlayerController.playerViewAvailable(it)
}
MediaController()
} else {
val thumbnailUrl by videoPlayerController.collect { thumbnailUrl }
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = 12.dp)
.fillMaxSize()
.align(Alignment.Center),
contentAlignment = Alignment.Center
) {
ProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
if (thumbnailUrl != null) {
AsyncImage(
imageUri = thumbnailUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
IconButton(onClick = { videoPlayerController.play() }) {
Icon(
imageVector = Icons.Rounded.PlayArrow,
contentDescription = stringResource(id = R.string.btn_play),
modifier = Modifier.size(48.dp)
)
}
}
}
}
}
}
@Composable
fun BoxScope.MediaController() {
val videoPlayerController = LocalVideoPlayerController.current
MediaControlButtons(
modifier = Modifier.matchParentSize()
)
val controlsVisible by videoPlayerController.collect { controlsVisible }
val isFullScreen by videoPlayerController.collect { isFullScreen }
if (controlsVisible) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.align(Alignment.BottomCenter),
) {
if (isFullScreen) {
ProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = Modifier.width(16.dp))
PositionAndDuration()
Spacer(modifier = Modifier.weight(1f))
if (videoPlayerController.supportFullScreen()) {
FullScreenButton()
}
}
}
}
if (!isFullScreen || !controlsVisible) {
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = 12.dp)
) {
ProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
MediaControlGestures(modifier = Modifier.matchParentSize())
}
@Composable
fun PositionAndDuration(
modifier: Modifier = Modifier
@ -225,9 +274,14 @@ private fun FullScreenButton() {
Icons.Rounded.Fullscreen
}
Box(
modifier = Modifier.clickable {
videoPlayerController.toggleFullScreen()
}
modifier = Modifier
.padding(8.dp)
.clickable(
indication = rememberRipple(bounded = false),
interactionSource = remember { MutableInteractionSource() }
) {
videoPlayerController.toggleFullScreen()
}
) {
Icon(
imageVector = icon,

View File

@ -1,7 +1,9 @@
package com.huanchengfly.tieba.post.ui.widgets.compose.video
import androidx.compose.runtime.Stable
import kotlinx.coroutines.flow.StateFlow
@Stable
interface VideoPlayerController {
fun setSource(source: VideoPlayerSource)
@ -9,7 +11,7 @@ interface VideoPlayerController {
fun pause()
fun playPauseToggle()
fun togglePlaying()
fun quickSeekForward()

View File

@ -1,9 +1,19 @@
package com.huanchengfly.tieba.post.ui.widgets.compose.video
import android.os.Parcelable
import androidx.annotation.RawRes
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
sealed class VideoPlayerSource {
@Immutable
@Parcelize
sealed class VideoPlayerSource : Parcelable {
@Immutable
data class Raw(@RawRes val resId: Int) : VideoPlayerSource()
data class Network(val url: String, val headers: Map<String, String> = mapOf()) :
VideoPlayerSource()
@Immutable
data class Network(
val url: String,
val headers: Map<String, String> = mapOf()
) : VideoPlayerSource()
}

View File

@ -5,7 +5,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class VideoPlayerState(
val isPlaying: Boolean = true,
val thumbnailUrl: String? = null,
val startedPlay: Boolean = false,
val isPlaying: Boolean = false,
val controlsVisible: Boolean = true,
val controlsEnabled: Boolean = true,
val gesturesEnabled: Boolean = true,

View File

@ -676,4 +676,5 @@
<string name="tip_blocked_post">由于你的屏蔽设置,第 %d 楼已被屏蔽</string>
<string name="settings_block_video">屏蔽视频贴子</string>
<string name="btn_full_screen">全屏</string>
<string name="btn_play">播放</string>
</resources>