feat:视频播放效果优化
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
||||
applicationId = "com.memory.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 53
|
||||
versionName = "1.6.9"
|
||||
versionCode = 67
|
||||
versionName = "1.8.3"
|
||||
|
||||
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
|
||||
buildConfigField("int", "VERSION_CODE", "53")
|
||||
buildConfigField("int", "VERSION_CODE", "67")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -3,20 +3,35 @@ package com.memory.app.ui.components
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FastForward
|
||||
import androidx.compose.material.icons.filled.FastRewind
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VideoPlayer(
|
||||
videoUrl: String,
|
||||
@@ -24,6 +39,10 @@ fun VideoPlayer(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var isPlaying by remember { mutableStateOf(true) }
|
||||
var currentPosition by remember { mutableStateOf(0L) }
|
||||
var duration by remember { mutableStateOf(0L) }
|
||||
var showControls by remember { mutableStateOf(true) }
|
||||
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
@@ -35,27 +54,46 @@ fun VideoPlayer(
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
isLoading = playbackState == Player.STATE_BUFFERING
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(playing: Boolean) {
|
||||
isPlaying = playing
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
LaunchedEffect(exoPlayer) {
|
||||
while (isActive) {
|
||||
currentPosition = exoPlayer.currentPosition
|
||||
duration = exoPlayer.duration.coerceAtLeast(0L)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动隐藏控制器
|
||||
LaunchedEffect(showControls, isPlaying) {
|
||||
if (showControls && isPlaying) {
|
||||
delay(3000)
|
||||
showControls = false
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
exoPlayer.release()
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clickable { showControls = !showControls }
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PlayerView(ctx).apply {
|
||||
player = exoPlayer
|
||||
useController = true
|
||||
// 设置控制器显示时间和行为
|
||||
controllerShowTimeoutMs = 3000
|
||||
controllerHideOnTouch = true
|
||||
controllerAutoShow = true
|
||||
// 使用默认控制器,ExoPlayer 的默认控制器已经在底部
|
||||
useController = false
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
@@ -75,5 +113,133 @@ fun VideoPlayer(
|
||||
CircularProgressIndicator(color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义控制器
|
||||
if (showControls) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.3f))
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.padding(bottom = 40.dp),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
// 播放控制按钮
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
exoPlayer.seekTo((exoPlayer.currentPosition - 15000).coerceAtLeast(0))
|
||||
},
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FastRewind,
|
||||
contentDescription = "后退15秒",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (isPlaying) {
|
||||
exoPlayer.pause()
|
||||
} else {
|
||||
exoPlayer.play()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(64.dp)
|
||||
) {
|
||||
Icon(
|
||||
if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
||||
contentDescription = if (isPlaying) "暂停" else "播放",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
exoPlayer.seekTo((exoPlayer.currentPosition + 15000).coerceAtMost(duration))
|
||||
},
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FastForward,
|
||||
contentDescription = "前进15秒",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 进度条
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = formatTime(currentPosition),
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Slider(
|
||||
value = if (duration > 0) currentPosition.toFloat() / duration else 0f,
|
||||
onValueChange = { value ->
|
||||
val newPosition = (value * duration).toLong()
|
||||
exoPlayer.seekTo(newPosition)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(20.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = Color.White,
|
||||
activeTrackColor = Color.White,
|
||||
inactiveTrackColor = Color.White.copy(alpha = 0.3f)
|
||||
),
|
||||
track = { sliderState ->
|
||||
SliderDefaults.Track(
|
||||
sliderState = sliderState,
|
||||
modifier = Modifier.height(2.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formatTime(duration),
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTime(timeMs: Long): String {
|
||||
if (timeMs <= 0) return "0:00"
|
||||
val totalSeconds = timeMs / 1000
|
||||
val minutes = totalSeconds / 60
|
||||
val seconds = totalSeconds % 60
|
||||
return String.format("%d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="#CC000000"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="20dp">
|
||||
|
||||
<!-- 播放控制按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="20dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_rew"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="后退15秒"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/exo_icon_rewind"
|
||||
android:tint="#FFFFFF" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_play_pause"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="播放/暂停"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/exo_icon_play"
|
||||
android:tint="#FFFFFF" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_ffwd"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="前进15秒"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/exo_icon_fastforward"
|
||||
android:tint="#FFFFFF" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 进度条区域 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="10dp"
|
||||
android:text="00:00"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.media3.ui.DefaultTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="10dp"
|
||||
android:text="00:00"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
10
android/app/src/main/res/layout/exo_player_control_view.xml
Normal file
10
android/app/src/main/res/layout/exo_player_control_view.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:controller_layout_id="@layout/exo_custom_playback_control"
|
||||
app:show_buffering="when_playing"
|
||||
app:show_timeout="3000"
|
||||
app:use_controller="true" />
|
||||
Reference in New Issue
Block a user