make activity brainslug also access uiState

This operation introduces a glitch since the composable and
the views are updated simultaniously. However this leads to a situation
where the embedded view thinkgs its fullscreen and thus renders alike.
Due to this reason the embbedded view breafly jumps up.
This commit is contained in:
Christian Schabesberger 2024-08-26 16:49:03 +02:00
parent 16b43aa89a
commit 8f78d72a13
11 changed files with 160 additions and 45 deletions

View File

@ -26,10 +26,11 @@ import androidx.core.view.WindowInsetsCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
private const val TAG = "ActivityBrainSlug"
class ActivityBrainSlug(val viewModel: VideoPlayerViewModel) { class ActivityBrainSlug(val viewModel: VideoPlayerViewModel) {
val brainSlugScope = CoroutineScope(Dispatchers.Main + Job()) val brainSlugScope = CoroutineScope(Dispatchers.Main + Job())
@ -78,10 +79,16 @@ class ActivityBrainSlug(val viewModel: VideoPlayerViewModel) {
removeSystemInsets() removeSystemInsets()
viewsToHideOnFullscreen.forEach { it.visibility = View.GONE } viewsToHideOnFullscreen.forEach { it.visibility = View.GONE }
fullscreenPlayerView?.visibility = View.VISIBLE fullscreenPlayerView?.visibility = View.VISIBLE
embeddedPlayerView?.visibility = View.GONE
fullscreenPlayerView?.viewModel = viewModel
embeddedPlayerView?.viewModel = null
} else { } else {
addSystemInsets() addSystemInsets()
viewsToHideOnFullscreen.forEach { it.visibility = View.VISIBLE } viewsToHideOnFullscreen.forEach { it.visibility = View.VISIBLE }
fullscreenPlayerView?.visibility = View.GONE fullscreenPlayerView?.visibility = View.GONE
embeddedPlayerView?.visibility = View.VISIBLE
fullscreenPlayerView?.viewModel = null
embeddedPlayerView?.viewModel = viewModel
} }
} }
} }

View File

@ -49,7 +49,7 @@ class VideoPlayerView : FrameLayout {
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : super(context, attrs, defStyleAttr) { ) : super(context, attrs, defStyleAttr) {
val view = LayoutInflater.from(context).inflate(R.layout.video_player_view, this) val view = LayoutInflater.from(context).inflate(R.layout.video_player_view, this)
composeView = view.findViewById<ComposeView>(R.id.video_player_compose_view) composeView = view.findViewById(R.id.video_player_compose_view)
applyViewModel() applyViewModel()
} }

View File

@ -0,0 +1,34 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
/**
* Restores the embedded mode UI config when returning from fullscreen
*/
@Parcelize
data class EmbeddedUiConfig(
val systemBarInLightMode: Boolean,
val brightness: Float,
val screenOrientation: Int
) : Parcelable

View File

@ -1,3 +1,23 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.model package net.newpipe.newplayer.model
import net.newpipe.newplayer.PlayMode import net.newpipe.newplayer.PlayMode

View File

@ -38,9 +38,8 @@ data class VideoPlayerUIState(
val playbackPositionInMs: Long, val playbackPositionInMs: Long,
val fastSeekSeconds: Int, val fastSeekSeconds: Int,
val soundVolume: Float, val soundVolume: Float,
val brightness: Float?, // when null use system value
// when null use system value val embeddedUiConfig: EmbeddedUiConfig?
val brightness: Float?
) : Parcelable { ) : Parcelable {
companion object { companion object {
val DEFAULT = VideoPlayerUIState( val DEFAULT = VideoPlayerUIState(
@ -59,7 +58,8 @@ data class VideoPlayerUIState(
playbackPositionInMs = 0, playbackPositionInMs = 0,
fastSeekSeconds = 0, fastSeekSeconds = 0,
soundVolume = 0f, soundVolume = 0f,
brightness = null brightness = null,
embeddedUiConfig = null
) )
} }
} }

View File

@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.StateFlow
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
interface VideoPlayerViewModel { interface VideoPlayerViewModel {
var newPlayer: NewPlayer? var newPlayer: NewPlayer?
val internalPlayer: Player? val internalPlayer: Player?
@ -53,4 +52,5 @@ interface VideoPlayerViewModel {
fun finishFastSeek() fun finishFastSeek()
fun brightnessChange(changeRate: Float, systemBrightness: Float) fun brightnessChange(changeRate: Float, systemBrightness: Float)
fun volumeChange(changeRate: Float) fun volumeChange(changeRate: Float)
fun onReportEmbeddedConfig(embeddedUiConfig: EmbeddedUiConfig?)
} }

View File

@ -26,19 +26,16 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.collection.mutableFloatFloatMapOf
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.Player import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -52,6 +49,7 @@ val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
private const val TAG = "VideoPlayerViewModel" private const val TAG = "VideoPlayerViewModel"
@HiltViewModel @HiltViewModel
class VideoPlayerViewModelImpl @Inject constructor( class VideoPlayerViewModelImpl @Inject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
@ -121,7 +119,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow<Float> () var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow<Float>()
override val embeddedPlayerDraggedDownBy = mutableEmbeddedPlayerDraggedDownBy.asSharedFlow() override val embeddedPlayerDraggedDownBy = mutableEmbeddedPlayerDraggedDownBy.asSharedFlow()
private fun installNewPlayer() { private fun installNewPlayer() {
@ -194,7 +192,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
) )
else instanceState.getParcelable(VIDEOPLAYER_UI_STATE) else instanceState.getParcelable(VIDEOPLAYER_UI_STATE)
if(recoveredUiState != null) { if (recoveredUiState != null) {
mutableUiState.update { mutableUiState.update {
recoveredUiState recoveredUiState
} }
@ -317,6 +315,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
override fun brightnessChange(changeRate: Float, systemBrightness: Float) { override fun brightnessChange(changeRate: Float, systemBrightness: Float) {
if (mutableUiState.value.uiMode.fullscreen) { if (mutableUiState.value.uiMode.fullscreen) {
val currentBrightness = mutableUiState.value.brightness val currentBrightness = mutableUiState.value.brightness
?: if (systemBrightness < 0f) 0.5f else systemBrightness ?: if (systemBrightness < 0f) 0.5f else systemBrightness
@ -347,6 +346,21 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
override fun onReportEmbeddedConfig(embeddedUiConfig: EmbeddedUiConfig?) {
if (embeddedUiConfig == null) {
mutableUiState.update {
it.copy(embeddedUiConfig = null)
}
} else {
if (uiState.value.embeddedUiConfig == null) {
println("gurken: ${embeddedUiConfig}")
mutableUiState.update {
it.copy(embeddedUiConfig = embeddedUiConfig)
}
}
}
}
override fun switchToEmbeddedView() { override fun switchToEmbeddedView() {
uiVisibilityJob?.cancel() uiVisibilityJob?.cancel()
finishFastSeek() finishFastSeek()
@ -356,13 +370,14 @@ class VideoPlayerViewModelImpl @Inject constructor(
override fun switchToFullscreen() { override fun switchToFullscreen() {
uiVisibilityJob?.cancel() uiVisibilityJob?.cancel()
finishFastSeek() finishFastSeek()
updateUiMode(UIModeState.FULLSCREEN_VIDEO) updateUiMode(UIModeState.FULLSCREEN_VIDEO)
} }
private fun updateUiMode(newState: UIModeState) { private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode() val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode() val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()
if(newPlayMode != currentPlayMode) { if (newPlayMode != currentPlayMode) {
newPlayer?.setPlayMode(newPlayMode!!) newPlayer?.setPlayMode(newPlayMode!!)
} else { } else {
mutableUiState.update { mutableUiState.update {

View File

@ -4,7 +4,6 @@ import android.os.Bundle
import androidx.media3.common.Player import androidx.media3.common.Player
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
@ -62,7 +61,7 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl") println("dummy impl")
} }
override fun brightnessChange(changeRate: Float, currentValue: Float) { override fun brightnessChange(changeRate: Float, systemBrightness: Float) {
println("dummy impl") println("dummy impl")
} }
@ -70,6 +69,10 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl") println("dummy impl")
} }
override fun onReportEmbeddedConfig(embeddedUiConfig: EmbeddedUiConfig?) {
println("dummy impl")
}
override fun pause() { override fun pause() {
println("dummy pause") println("dummy pause")
} }

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -25,13 +26,15 @@ fun VideoPlayerLoadingPlaceholder(aspectRatio: Float = 3F / 1F) {
.aspectRatio(aspectRatio), .aspectRatio(aspectRatio),
color = Color.Black color = Color.Black
) { ) {
Box(contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier CircularProgressIndicator(
modifier = Modifier
.width(64.dp) .width(64.dp)
.height(64.dp) .height(64.dp)
.align((Alignment.Center))) .align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurface
)
} }
} }
} }

View File

@ -37,6 +37,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -52,6 +53,7 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.common.Player import androidx.media3.common.Player
import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@ -86,21 +88,65 @@ fun VideoPlayerUI(
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val defaultBrightness = getDefaultBrightness(activity)
val screenOrientation = activity.requestedOrientation
// Setup fullscreen // Setup fullscreen
var embeddedUiConfig: EmbeddedUiConfig? by rememberSaveable {
mutableStateOf(null)
}
LaunchedEffect(uiState.uiMode.fullscreen) {
if (uiState.uiMode.fullscreen) { if (uiState.uiMode.fullscreen) {
LaunchedEffect(key1 = true) { viewModel.onReportEmbeddedConfig(
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false EmbeddedUiConfig(
WindowCompat.getInsetsController(
window,
view
).isAppearanceLightStatusBars,
defaultBrightness,
screenOrientation,
)
)
} else {
viewModel.onReportEmbeddedConfig(null)
} }
} }
// Setup immersive mode LaunchedEffect(uiState.uiMode.fullscreen) {
if (uiState.uiMode.systemUiVisible) { if (uiState.uiMode.fullscreen) {
LaunchedEffect(key1 = false) { WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
false
} else {
uiState.embeddedUiConfig?.let {
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
it.systemBarInLightMode
}
}
}
// setup immersive mode
LaunchedEffect(
key1 = uiState.uiMode.controllerUiVisible,
key2 = uiState.uiMode.fullscreen
) {
if (uiState.uiMode.fullscreen && !uiState.uiMode.systemUiVisible) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
} else {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
} }
}
if (uiState.uiMode.fullscreen) {
if (uiState.contentRatio < 1) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
} else { } else {
LaunchedEffect(key1 = true) { LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) }
} else {
uiState.embeddedUiConfig?.let {
LockScreenOrientation(orientation = it.screenOrientation)
} }
} }
@ -116,20 +162,11 @@ fun VideoPlayerUI(
} }
} }
// Set Screen Rotation
if (uiState.uiMode.fullscreen) {
if (uiState.contentRatio < 1) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
} else {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
}
}
val displayMetrics = activity.resources.displayMetrics val displayMetrics = activity.resources.displayMetrics
val screenRatio = val screenRatio =
displayMetrics.widthPixels.toFloat() / displayMetrics.heightPixels.toFloat() displayMetrics.widthPixels.toFloat() / displayMetrics.heightPixels.toFloat()
val defaultBrightness = getDefaultBrightness(activity)
LaunchedEffect(key1 = uiState.brightness) { LaunchedEffect(key1 = uiState.brightness) {
Log.d(TAG, "New Brightnes: ${uiState.brightness}") Log.d(TAG, "New Brightnes: ${uiState.brightness}")

View File

@ -27,6 +27,7 @@ import android.content.ContextWrapper
import android.view.WindowManager import android.view.WindowManager
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -36,14 +37,9 @@ import java.util.Locale
@Composable @Composable
fun LockScreenOrientation(orientation: Int) { fun LockScreenOrientation(orientation: Int) {
val context = LocalContext.current val context = LocalContext.current
DisposableEffect(orientation) { LaunchedEffect(orientation) {
val activity = context.findActivity() ?: return@DisposableEffect onDispose {} val activity = context.findActivity() ?: return@LaunchedEffect
val originalOrientation = activity.requestedOrientation
activity.requestedOrientation = orientation activity.requestedOrientation = orientation
onDispose {
// restore original orientation when view disappears
activity.requestedOrientation = originalOrientation
}
} }
} }