From 6716b58f54bef127ea74982302be2d4f7036d5cb Mon Sep 17 00:00:00 2001 From: oxy Date: Fri, 6 Oct 2023 01:16:03 +0800 Subject: [PATCH] fix: improve landscape viewing and cover placeholder animations in player. --- .../java/com/m3u/androidApp/MainActivity.kt | 42 +++++++++++++--- .../java/com/m3u/features/live/LiveScreen.kt | 2 +- .../live/components/CoverPlaceholder.kt | 50 +++++++++++++++++++ .../features/live/fragments/LiveFragment.kt | 13 ++--- .../main/java/com/m3u/ui/components/AppBar.kt | 42 ++++++++-------- ui/src/main/java/com/m3u/ui/model/Helper.kt | 50 +++++++++++-------- 6 files changed, 143 insertions(+), 56 deletions(-) create mode 100644 features/live/src/main/java/com/m3u/features/live/components/CoverPlaceholder.kt diff --git a/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt b/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt index 660b8c376..f6bf34265 100644 --- a/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt +++ b/androidApp/src/main/java/com/m3u/androidApp/MainActivity.kt @@ -5,6 +5,7 @@ import android.content.res.Configuration import android.graphics.Color import android.graphics.Rect import android.os.Bundle +import android.util.Log import android.util.Rational import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle @@ -24,9 +25,11 @@ import kotlin.reflect.KMutableProperty0 @AndroidEntryPoint class MainActivity : ComponentActivity() { - private val controller = WindowInsetsControllerCompat(window, window.decorView).apply { - systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + private val controller by lazy { + WindowInsetsControllerCompat(window, window.decorView).apply { + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } } private var actualOnUserLeaveHint: (() -> Unit)? = null private var actualOnPipModeChanged: Consumer? = null @@ -35,6 +38,7 @@ class MainActivity : ComponentActivity() { installSplashScreen() enableEdgeToEdge() super.onCreate(savedInstanceState) + applyConfiguration(resources.configuration) setContent { App( connector = this::createHelper @@ -47,7 +51,6 @@ class MainActivity : ComponentActivity() { actions: Method>, fob: Method ): Helper = object : Helper { - override fun enterPipMode(size: Rect) { val params = PictureInPictureParams.Builder() .setAspectRatio(Rational(size.width(), size.height())) @@ -58,17 +61,19 @@ class MainActivity : ComponentActivity() { override var title: String by title override var actions: List by actions override var fob: Fob? by fob - override var systemUiVisibility: Boolean = true + override var statusBarsVisibility: Boolean = true set(value) { field = value controller.apply { if (value) { - show(WindowInsetsCompat.Type.systemBars()) + show(WindowInsetsCompat.Type.statusBars()) } else { - hide(WindowInsetsCompat.Type.systemBars()) + hide(WindowInsetsCompat.Type.statusBars()) } } } + override var navigationBarsVisibility: Boolean = + resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT override var darkMode: Boolean = resources.configuration.uiMode == Configuration.UI_MODE_NIGHT_YES @@ -96,6 +101,29 @@ class MainActivity : ComponentActivity() { super.onUserLeaveHint() actualOnUserLeaveHint?.invoke() } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + applyConfiguration(newConfig) + } + + private fun applyConfiguration(configuration: Configuration) { + Log.d( + "MainActivity", + "applyConfiguration: ${configuration.orientation == Configuration.ORIENTATION_PORTRAIT}" + ) + when (configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> { + controller.show(WindowInsetsCompat.Type.navigationBars()) + } + + Configuration.ORIENTATION_LANDSCAPE -> { + controller.hide(WindowInsetsCompat.Type.navigationBars()) + } + + else -> {} + } + } } internal typealias Method = KMutableProperty0 \ No newline at end of file diff --git a/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt b/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt index c5322939e..1f96266b4 100644 --- a/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt +++ b/features/live/src/main/java/com/m3u/features/live/LiveScreen.kt @@ -83,7 +83,7 @@ internal fun LiveRoute( helper.enterPipMode(state.playerState.videoSize) } } - systemUiVisibility = false + statusBarsVisibility = false onPipModeChanged = Consumer { info -> isPipMode = info.isInPictureInPictureMode } diff --git a/features/live/src/main/java/com/m3u/features/live/components/CoverPlaceholder.kt b/features/live/src/main/java/com/m3u/features/live/components/CoverPlaceholder.kt new file mode 100644 index 000000000..a36f77ed1 --- /dev/null +++ b/features/live/src/main/java/com/m3u/features/live/components/CoverPlaceholder.kt @@ -0,0 +1,50 @@ +package com.m3u.features.live.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import com.m3u.ui.components.Image +import com.m3u.ui.model.LocalDuration + +@Composable +internal fun CoverPlaceholder( + visible: Boolean, + cover: String, + modifier: Modifier = Modifier +) { + val configuration = LocalConfiguration.current + val duration = LocalDuration.current + val size = remember(configuration.screenHeightDp) { + configuration.screenHeightDp.dp + } + AnimatedVisibility( + visible = visible, + enter = scaleIn( + initialScale = 0.65f, + animationSpec = tween(duration.slow) + ) + fadeIn( + animationSpec = tween(duration.slow) + ), + exit = scaleOut( + targetScale = 0.65f, + animationSpec = tween(duration.slow) + ) + fadeOut( + animationSpec = tween(duration.slow) + ), + modifier = modifier + ) { + Image( + model = cover, + modifier = Modifier.size(size) + ) + } +} \ No newline at end of file diff --git a/features/live/src/main/java/com/m3u/features/live/fragments/LiveFragment.kt b/features/live/src/main/java/com/m3u/features/live/fragments/LiveFragment.kt index 32ae13a81..b455786d4 100644 --- a/features/live/src/main/java/com/m3u/features/live/fragments/LiveFragment.kt +++ b/features/live/src/main/java/com/m3u/features/live/fragments/LiveFragment.kt @@ -40,6 +40,7 @@ import androidx.media3.common.Player import com.m3u.core.annotation.ClipMode import com.m3u.core.util.basic.isNotEmpty import com.m3u.features.live.R +import com.m3u.features.live.components.CoverPlaceholder import com.m3u.features.live.components.LiveMask import com.m3u.ui.components.Background import com.m3u.ui.components.ExoPlayer @@ -99,12 +100,12 @@ internal fun LiveFragment( ) val shouldShowPlaceholder = cover.isNotEmpty() && videoSize.isEmpty - if (shouldShowPlaceholder) { - Image( - model = cover, - modifier = Modifier.fillMaxSize() - ) - } + + CoverPlaceholder( + visible = shouldShowPlaceholder, + cover = cover, + modifier = Modifier.align(Alignment.Center) + ) LiveMask( state = maskState, diff --git a/ui/src/main/java/com/m3u/ui/components/AppBar.kt b/ui/src/main/java/com/m3u/ui/components/AppBar.kt index f316e1f90..b43dce63b 100644 --- a/ui/src/main/java/com/m3u/ui/components/AppBar.kt +++ b/ui/src/main/java/com/m3u/ui/components/AppBar.kt @@ -63,25 +63,6 @@ import com.m3u.ui.model.LocalDuration import com.m3u.ui.model.LocalSpacing import com.m3u.ui.model.LocalTheme -@Suppress("unused") -interface AppTopBarConsumer { - fun Float.value(min: Float, max: Float): Boolean - - object Never : AppTopBarConsumer { - override fun Float.value(min: Float, max: Float): Boolean = false - } - - object Edges : AppTopBarConsumer { - override fun Float.value(min: Float, max: Float): Boolean = (this == min) || (this == max) - } - - object Always : AppTopBarConsumer { - override fun Float.value(min: Float, max: Float): Boolean = (this != min) && (this != max) - } -} - -fun AppTopBarConsumer.consume(value: Float, min: Float, max: Float): Boolean = value.value(min, max) - @Composable fun AppTopBar( modifier: Modifier = Modifier, @@ -237,7 +218,7 @@ internal object AppTopBarDefaults { @OptIn(ExperimentalLayoutApi::class) val windowInsets: WindowInsets - @Composable get() = WindowInsets.systemBarsIgnoringVisibility.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top) + @Composable get() = WindowInsets.systemBarsIgnoringVisibility.only(WindowInsetsSides.Top) /** * Linear interpolator through point (1, 1). @@ -245,4 +226,23 @@ internal object AppTopBarDefaults { * @param input the x value between 0~1f. */ fun interpolator(slope: Float, input: Float): Float = lerp(input, 1f, 1 - slope) -} \ No newline at end of file +} + +@Suppress("unused") +interface AppTopBarConsumer { + fun Float.value(min: Float, max: Float): Boolean + + object Never : AppTopBarConsumer { + override fun Float.value(min: Float, max: Float): Boolean = false + } + + object Edges : AppTopBarConsumer { + override fun Float.value(min: Float, max: Float): Boolean = (this == min) || (this == max) + } + + object Always : AppTopBarConsumer { + override fun Float.value(min: Float, max: Float): Boolean = (this != min) && (this != max) + } +} + +fun AppTopBarConsumer.consume(value: Float, min: Float, max: Float): Boolean = value.value(min, max) diff --git a/ui/src/main/java/com/m3u/ui/model/Helper.kt b/ui/src/main/java/com/m3u/ui/model/Helper.kt index a68467473..382b786f4 100644 --- a/ui/src/main/java/com/m3u/ui/model/Helper.kt +++ b/ui/src/main/java/com/m3u/ui/model/Helper.kt @@ -19,7 +19,8 @@ interface Helper { var title: String var actions: List var fob: Fob? - var systemUiVisibility: Boolean + var statusBarsVisibility: Boolean + var navigationBarsVisibility: Boolean var onUserLeaveHint: (() -> Unit)? var onPipModeChanged: Consumer? var darkMode: Boolean @@ -38,16 +39,26 @@ private data class HelperBundle( val darkMode: Boolean ) -private fun Helper.loadBundle(properties: HelperBundle) { - title = properties.title - actions = properties.actions - fob = properties.fob - systemUiVisibility = properties.systemUiVisibility - onUserLeaveHint = properties.onUserLeaveHint - onPipModeChanged = properties.onPipModeChanged - darkMode = properties.darkMode +private fun Helper.restore(bundle: HelperBundle) { + title = bundle.title + actions = bundle.actions + fob = bundle.fob + statusBarsVisibility = bundle.systemUiVisibility + onUserLeaveHint = bundle.onUserLeaveHint + onPipModeChanged = bundle.onPipModeChanged + darkMode = bundle.darkMode } +private fun Helper.backup(): HelperBundle = HelperBundle( + title = title, + actions = actions, + fob = fob, + systemUiVisibility = statusBarsVisibility, + onUserLeaveHint = onUserLeaveHint, + onPipModeChanged = onPipModeChanged, + darkMode = darkMode +) + @Composable @SuppressLint("ComposableNaming") fun Helper.repeatOnLifecycle( @@ -57,24 +68,16 @@ fun Helper.repeatOnLifecycle( check(state != Lifecycle.State.CREATED && state != Lifecycle.State.INITIALIZED) { "state cannot be CREATED or INITIALIZED!" } - var properties: HelperBundle? = null + var bundle: HelperBundle? = null LifecycleEffect { event -> when (event) { Lifecycle.Event.upTo(state) -> { - properties = HelperBundle( - title = title, - actions = actions, - fob = fob, - systemUiVisibility = systemUiVisibility, - onUserLeaveHint = onUserLeaveHint, - onPipModeChanged = onPipModeChanged, - darkMode = darkMode - ) + bundle = backup() block() } Lifecycle.Event.downFrom(state) -> { - properties?.let(::loadBundle) + bundle?.let(::restore) } else -> {} @@ -100,11 +103,16 @@ val EmptyHelper = object : Helper { error("Cannot set fob") } - override var systemUiVisibility: Boolean + override var statusBarsVisibility: Boolean get() = error("Cannot get systemUiVisibility") set(_) { error("Cannot set systemUiVisibility") } + override var navigationBarsVisibility: Boolean + get() = error("Cannot get navigationBarsVisibility") + set(_) { + error("Cannot set navigationBarsVisibility") + } override var darkMode: Boolean get() = error("Cannot get darkMode")