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.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.VideoPlayerViewModel
private const val TAG = "ActivityBrainSlug"
class ActivityBrainSlug(val viewModel: VideoPlayerViewModel) {
val brainSlugScope = CoroutineScope(Dispatchers.Main + Job())
@ -78,10 +79,16 @@ class ActivityBrainSlug(val viewModel: VideoPlayerViewModel) {
removeSystemInsets()
viewsToHideOnFullscreen.forEach { it.visibility = View.GONE }
fullscreenPlayerView?.visibility = View.VISIBLE
embeddedPlayerView?.visibility = View.GONE
fullscreenPlayerView?.viewModel = viewModel
embeddedPlayerView?.viewModel = null
} else {
addSystemInsets()
viewsToHideOnFullscreen.forEach { it.visibility = View.VISIBLE }
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
) : super(context, attrs, defStyleAttr) {
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()
}

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
import net.newpipe.newplayer.PlayMode

View File

@ -38,9 +38,8 @@ data class VideoPlayerUIState(
val playbackPositionInMs: Long,
val fastSeekSeconds: Int,
val soundVolume: Float,
// when null use system value
val brightness: Float?
val brightness: Float?, // when null use system value
val embeddedUiConfig: EmbeddedUiConfig?
) : Parcelable {
companion object {
val DEFAULT = VideoPlayerUIState(
@ -59,7 +58,8 @@ data class VideoPlayerUIState(
playbackPositionInMs = 0,
fastSeekSeconds = 0,
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.ui.ContentScale
interface VideoPlayerViewModel {
var newPlayer: NewPlayer?
val internalPlayer: Player?
@ -53,4 +52,5 @@ interface VideoPlayerViewModel {
fun finishFastSeek()
fun brightnessChange(changeRate: Float, systemBrightness: 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.util.Log
import androidx.annotation.RequiresApi
import androidx.collection.mutableFloatFloatMapOf
import androidx.core.content.ContextCompat.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.asStateFlow
@ -52,6 +49,7 @@ val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
private const val TAG = "VideoPlayerViewModel"
@HiltViewModel
class VideoPlayerViewModelImpl @Inject constructor(
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()
private fun installNewPlayer() {
@ -194,7 +192,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
)
else instanceState.getParcelable(VIDEOPLAYER_UI_STATE)
if(recoveredUiState != null) {
if (recoveredUiState != null) {
mutableUiState.update {
recoveredUiState
}
@ -317,6 +315,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
override fun brightnessChange(changeRate: Float, systemBrightness: Float) {
if (mutableUiState.value.uiMode.fullscreen) {
val currentBrightness = mutableUiState.value.brightness
?: 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() {
uiVisibilityJob?.cancel()
finishFastSeek()
@ -356,13 +370,14 @@ class VideoPlayerViewModelImpl @Inject constructor(
override fun switchToFullscreen() {
uiVisibilityJob?.cancel()
finishFastSeek()
updateUiMode(UIModeState.FULLSCREEN_VIDEO)
}
private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()
if(newPlayMode != currentPlayMode) {
if (newPlayMode != currentPlayMode) {
newPlayer?.setPlayMode(newPlayMode!!)
} else {
mutableUiState.update {

View File

@ -4,7 +4,6 @@ import android.os.Bundle
import androidx.media3.common.Player
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale
@ -62,7 +61,7 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl")
}
override fun brightnessChange(changeRate: Float, currentValue: Float) {
override fun brightnessChange(changeRate: Float, systemBrightness: Float) {
println("dummy impl")
}
@ -70,6 +69,10 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl")
}
override fun onReportEmbeddedConfig(embeddedUiConfig: EmbeddedUiConfig?) {
println("dummy impl")
}
override fun 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.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@ -25,13 +26,15 @@ fun VideoPlayerLoadingPlaceholder(aspectRatio: Float = 3F / 1F) {
.aspectRatio(aspectRatio),
color = Color.Black
) {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(
modifier = Modifier
.width(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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -52,6 +53,7 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.common.Player
import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@ -86,21 +88,65 @@ fun VideoPlayerUI(
val lifecycleOwner = LocalLifecycleOwner.current
val defaultBrightness = getDefaultBrightness(activity)
val screenOrientation = activity.requestedOrientation
// Setup fullscreen
var embeddedUiConfig: EmbeddedUiConfig? by rememberSaveable {
mutableStateOf(null)
}
LaunchedEffect(uiState.uiMode.fullscreen) {
if (uiState.uiMode.fullscreen) {
LaunchedEffect(key1 = true) {
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false
viewModel.onReportEmbeddedConfig(
EmbeddedUiConfig(
WindowCompat.getInsetsController(
window,
view
).isAppearanceLightStatusBars,
defaultBrightness,
screenOrientation,
)
)
} else {
viewModel.onReportEmbeddedConfig(null)
}
}
// Setup immersive mode
if (uiState.uiMode.systemUiVisible) {
LaunchedEffect(key1 = false) {
LaunchedEffect(uiState.uiMode.fullscreen) {
if (uiState.uiMode.fullscreen) {
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())
}
}
if (uiState.uiMode.fullscreen) {
if (uiState.contentRatio < 1) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
} else {
LaunchedEffect(key1 = true) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
}
} 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 screenRatio =
displayMetrics.widthPixels.toFloat() / displayMetrics.heightPixels.toFloat()
val defaultBrightness = getDefaultBrightness(activity)
LaunchedEffect(key1 = uiState.brightness) {
Log.d(TAG, "New Brightnes: ${uiState.brightness}")

View File

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