[feat] Gestures for volume and brightness control

This commit is contained in:
acite
2025-08-27 03:43:52 +08:00
parent fc76e6995f
commit 76054da910
3 changed files with 261 additions and 24 deletions

View File

@@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
import android.graphics.drawable.Icon
import android.net.http.SslCertificate.saveState
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -56,6 +57,9 @@ import com.acitelight.aether.view.VideoScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.attributes = window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {

View File

@@ -1,8 +1,10 @@
package com.acitelight.aether.view
import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.media.AudioManager
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -12,7 +14,6 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
@@ -50,7 +51,10 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Brightness4
import androidx.compose.material.icons.filled.FastForward
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.Info
@@ -58,6 +62,7 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.DividerDefaults
@@ -68,6 +73,8 @@ import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
@@ -95,6 +102,7 @@ import com.acitelight.aether.Global
import com.acitelight.aether.ToggleFullScreen
import com.acitelight.aether.model.KeyImage
import com.acitelight.aether.model.Video
import kotlin.math.abs
fun formatTime(ms: Long): String {
if (ms <= 0) return "00:00:00"
@@ -223,6 +231,18 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
val context = LocalContext.current
val activity = context as? Activity
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
var volFactor by remember { mutableFloatStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()) }
fun setVolume(value: Int) {
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
value.coerceIn(0, maxVolume),
AudioManager.FLAG_PLAY_SOUND
)
}
Box(modifier)
{
AndroidView(
@@ -239,19 +259,53 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
.fillMaxWidth()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
videoPlayerViewModel.dragging = true
videoPlayerViewModel.planeVisibility = true
exoPlayer.pause()
onDragStart = { offset ->
if(offset.x < size.width / 2)
{
videoPlayerViewModel.draggingPurpose = -1;
}else{
videoPlayerViewModel.draggingPurpose = -2;
}
},
onDragEnd = {
videoPlayerViewModel.dragging = false
if (videoPlayerViewModel.isPlaying)
if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0)
exoPlayer.play()
videoPlayerViewModel.draggingPurpose = -1;
},
onDrag = { change, dragAmount ->
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
if(abs(dragAmount.x) > abs(dragAmount.y) &&
(videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2))
{
videoPlayerViewModel.draggingPurpose = 0
videoPlayerViewModel.planeVisibility = true
exoPlayer.pause()
}
else if(videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = 1
else if(videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = 2
if(videoPlayerViewModel.draggingPurpose == 0)
{
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
}else if(videoPlayerViewModel.draggingPurpose == 2)
{
val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
volFactor = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()
if(dragAmount.y < 0)
setVolume( cu + 1);
else if(dragAmount.y > 0)
setVolume( cu - 1);
}else if(videoPlayerViewModel.draggingPurpose == 1)
{
videoPlayerViewModel.brit = (videoPlayerViewModel.brit - dragAmount.y * 0.002f).coerceIn(0f, 1f);
activity?.window?.attributes = activity.window.attributes.apply {
screenBrightness = videoPlayerViewModel.brit.coerceIn(0f, 1f)
}
activity?.window?.setAttributes(activity.window.attributes)
}
}
)
}
@@ -312,7 +366,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.dragging,
visible = videoPlayerViewModel.draggingPurpose == 0,
enter = fadeIn(
initialAlpha = 0f,
),
@@ -320,7 +374,8 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
) {
)
{
Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime(
@@ -328,12 +383,74 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
)
}",
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(bottom = 12.dp),
modifier = Modifier.padding(bottom = 12.dp),
fontSize = 18.sp
)
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 2,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.AutoMirrored.Filled.VolumeUp,
contentDescription = "Vol",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = volFactor,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 1,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.Default.Brightness4,
contentDescription = "Brightness",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = videoPlayerViewModel.brit,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
if(cover > 0.0f)
Spacer(Modifier.background(Color(0x00FF6699 - 0x00222222 + ((0x000000FF * cover).toLong() shl 24) )).fillMaxSize())
@@ -713,6 +830,18 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
val activity = context as? Activity
val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!;
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
var volFactor by remember { mutableFloatStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()) }
fun setVolume(value: Int) {
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
value.coerceIn(0, maxVolume),
AudioManager.FLAG_PLAY_SOUND
)
}
BackHandler {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
@@ -738,19 +867,53 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
.fillMaxWidth()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
videoPlayerViewModel.planeVisibility = true
videoPlayerViewModel.dragging = true;
exoPlayer.pause()
onDragStart = { offset ->
if(offset.x < size.width / 2)
{
videoPlayerViewModel.draggingPurpose = -1;
}else{
videoPlayerViewModel.draggingPurpose = -2;
}
},
onDragEnd = {
videoPlayerViewModel.dragging = false;
if (videoPlayerViewModel.isPlaying)
if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0)
exoPlayer.play()
videoPlayerViewModel.draggingPurpose = -1;
},
onDrag = { change, dragAmount ->
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
if(abs(dragAmount.x) > abs(dragAmount.y) &&
(videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2))
{
videoPlayerViewModel.draggingPurpose = 0
videoPlayerViewModel.planeVisibility = true
exoPlayer.pause()
}
else if(videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = 1
else if(videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = 2
if(videoPlayerViewModel.draggingPurpose == 0)
{
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
}else if(videoPlayerViewModel.draggingPurpose == 2)
{
val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
volFactor = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()
if(dragAmount.y < 0)
setVolume( cu + 1);
else if(dragAmount.y > 0)
setVolume( cu - 1);
}else if(videoPlayerViewModel.draggingPurpose == 1)
{
videoPlayerViewModel.brit = (videoPlayerViewModel.brit - dragAmount.y * 0.002f).coerceIn(0f, 1f);
activity?.window?.attributes = activity.window.attributes.apply {
screenBrightness = videoPlayerViewModel.brit.coerceIn(0f, 1f)
}
activity?.window?.setAttributes(activity.window.attributes)
}
}
)
}
@@ -782,7 +945,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
)
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.dragging,
visible = videoPlayerViewModel.draggingPurpose == 0,
enter = fadeIn(
initialAlpha = 0f,
),
@@ -790,7 +953,8 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
) {
)
{
Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime(
@@ -803,6 +967,68 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
)
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 2,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.AutoMirrored.Filled.VolumeUp,
contentDescription = "Vol",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = volFactor,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 1,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.Default.Brightness4,
contentDescription = "Brightness",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = videoPlayerViewModel.brit,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
AnimatedVisibility(
visible = videoPlayerViewModel.isLongPressing,
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),

View File

@@ -1,5 +1,6 @@
package com.acitelight.aether.viewModel
import android.app.Activity
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -40,7 +41,12 @@ class VideoPlayerViewModel() : ViewModel()
var playProcess by mutableFloatStateOf(0.0f)
var planeVisibility by mutableStateOf(true)
var isLongPressing by mutableStateOf(false)
var dragging by mutableStateOf(false)
// -1 : Not dragging
// 0 : Seek
// 1 : Volume
// 2 : Brightness
var draggingPurpose by mutableIntStateOf(-1)
var thumbUp by mutableIntStateOf(0)
var thumbDown by mutableIntStateOf(0)
@@ -53,6 +59,7 @@ class VideoPlayerViewModel() : ViewModel()
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
var imageLoader: ImageLoader? = null;
var brit by mutableFloatStateOf(0.5f)
@OptIn(UnstableApi::class)
@Composable