Browse Source

MediaViewer : introduce fullscreen and flick to dismiss behavior

pull/2438/head
ganfra 7 months ago
parent
commit
22676cc5eb
  1. 1
      changelog.d/2390.feature
  2. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt
  3. 4
      gradle/libs.versions.toml
  4. 1
      libraries/mediaviewer/api/build.gradle.kts
  5. 94
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt
  6. 24
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt
  7. 19
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt
  8. 199
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt

1
changelog.d/2390.feature

@ -0,0 +1 @@ @@ -0,0 +1 @@
MediaViewer : introduce fullscreen and flick to dismiss behavior.

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt

@ -132,7 +132,8 @@ private fun AttachmentPreviewContent( @@ -132,7 +132,8 @@ private fun AttachmentPreviewContent(
) {
when (attachment) {
is Attachment.Media -> LocalMediaView(
localMedia = attachment.localMedia
localMedia = attachment.localMedia,
onClick = {}
)
}
}

4
gradle/libs.versions.toml

@ -39,6 +39,7 @@ showkase = "1.0.2" @@ -39,6 +39,7 @@ showkase = "1.0.2"
appyx = "1.4.0"
sqldelight = "2.0.1"
wysiwyg = "2.29.0"
telephoto = "0.8.0"
# DI
dagger = "2.50"
@ -163,7 +164,8 @@ sqlite = "androidx.sqlite:sqlite-ktx:2.4.0" @@ -163,7 +164,8 @@ sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
vanniktech_blurhash = "com.vanniktech:blurhash:0.2.0"
telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.8.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.1"
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2"

1
libraries/mediaviewer/api/build.gradle.kts

@ -39,6 +39,7 @@ dependencies { @@ -39,6 +39,7 @@ dependencies {
implementation(libs.dagger)
implementation(libs.telephoto.zoomableimage)
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.flick)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)

94
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt

@ -18,14 +18,16 @@ package io.element.android.libraries.mediaviewer.api.local @@ -18,14 +18,16 @@ package io.element.android.libraries.mediaviewer.api.local
import android.annotation.SuppressLint
import android.net.Uri
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -35,7 +37,10 @@ import androidx.compose.material.icons.outlined.GraphicEq @@ -35,7 +37,10 @@ import androidx.compose.material.icons.outlined.GraphicEq
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -72,48 +77,45 @@ import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWra @@ -72,48 +77,45 @@ import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWra
import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer
import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState
import io.element.android.libraries.ui.strings.CommonStrings
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
import me.saket.telephoto.zoomable.rememberZoomableState
@SuppressLint("UnsafeOptInUsageError")
@Composable
fun LocalMediaView(
localMedia: LocalMedia?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
mediaInfo: MediaInfo? = localMedia?.info,
) {
val zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 5f)
)
val mimeType = mediaInfo?.mimeType
when {
mimeType.isMimeTypeImage() -> MediaImageView(
localMediaViewState = localMediaViewState,
localMedia = localMedia,
zoomableState = zoomableState,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
mimeType.isMimeTypeVideo() -> MediaVideoView(
localMediaViewState = localMediaViewState,
localMedia = localMedia,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
mimeType == MimeTypes.Pdf -> MediaPDFView(
localMediaViewState = localMediaViewState,
localMedia = localMedia,
zoomableState = zoomableState,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
// TODO handle audio with exoplayer
else -> MediaFileView(
localMediaViewState = localMediaViewState,
uri = localMedia?.uri,
info = mediaInfo,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
}
}
@ -122,24 +124,25 @@ fun LocalMediaView( @@ -122,24 +124,25 @@ fun LocalMediaView(
private fun MediaImageView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?,
zoomableState: ZoomableState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalInspectionMode.current) {
Image(
painter = painterResource(id = CommonDrawables.sample_background),
modifier = modifier.fillMaxSize(),
modifier = modifier,
contentDescription = null,
)
} else {
val zoomableImageState = rememberZoomableImageState(zoomableState)
val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState)
localMediaViewState.isReady = zoomableImageState.isImageDisplayed
ZoomableAsyncImage(
modifier = modifier.fillMaxSize(),
modifier = modifier,
state = zoomableImageState,
model = localMedia?.uri,
contentDescription = stringResource(id = CommonStrings.common_image),
contentScale = ContentScale.Fit,
onClick = { onClick() }
)
}
}
@ -149,8 +152,14 @@ private fun MediaImageView( @@ -149,8 +152,14 @@ private fun MediaImageView(
private fun MediaVideoView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
var playableState: PlayableState.Playable by remember {
mutableStateOf(PlayableState.Playable(isPlaying = false, isShowingControls = false))
}
localMediaViewState.playableState = playableState
val context = LocalContext.current
val playerListener = object : Player.Listener {
override fun onRenderedFirstFrame() {
@ -158,7 +167,7 @@ private fun MediaVideoView( @@ -158,7 +167,7 @@ private fun MediaVideoView(
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
localMediaViewState.isPlaying = isPlaying
playableState = playableState.copy(isPlaying = isPlaying)
}
}
val exoPlayer = remember {
@ -176,19 +185,34 @@ private fun MediaVideoView( @@ -176,19 +185,34 @@ private fun MediaVideoView(
} else {
exoPlayer.setMediaItems(emptyList())
}
KeepScreenOn(localMediaViewState.isPlaying)
KeepScreenOn(playableState.isPlaying)
AndroidView(
factory = {
PlayerView(context).apply {
player = exoPlayer
setShowPreviousButton(false)
setShowNextButton(false)
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
controllerShowTimeoutMs = 3000
setOnClickListener {
onClick()
}
setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { visibility ->
val isShowingControls = visibility == View.VISIBLE
playableState = playableState.copy(isShowingControls = isShowingControls)
})
controllerShowTimeoutMs = 1500
setShowPreviousButton(false)
setShowFastForwardButton(false)
setShowRewindButton(false)
setShowNextButton(false)
showController()
}
},
modifier = modifier.fillMaxSize()
onRelease = { playerView ->
playerView.setOnClickListener(null)
playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?)
playerView.player = null
},
modifier = modifier
)
OnLifecycleEvent { _, event ->
@ -208,15 +232,19 @@ private fun MediaVideoView( @@ -208,15 +232,19 @@ private fun MediaVideoView(
private fun MediaPDFView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?,
zoomableState: ZoomableState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val pdfViewerState = rememberPdfViewerState(
model = localMedia?.uri,
zoomableState = zoomableState
zoomableState = localMediaViewState.zoomableState,
)
localMediaViewState.isReady = pdfViewerState.isLoaded
PdfViewer(pdfViewerState = pdfViewerState, modifier = modifier)
PdfViewer(
pdfViewerState = pdfViewerState,
onClick = onClick,
modifier = modifier,
)
}
@Composable
@ -224,11 +252,23 @@ private fun MediaFileView( @@ -224,11 +252,23 @@ private fun MediaFileView(
localMediaViewState: LocalMediaViewState,
uri: Uri?,
info: MediaInfo?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val isAudio = info?.mimeType.isMimeTypeAudio().orFalse()
localMediaViewState.isReady = uri != null
Box(modifier = modifier.padding(horizontal = 8.dp), contentAlignment = Alignment.Center) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = modifier
.padding(horizontal = 8.dp)
.clickable(
onClick = onClick,
interactionSource = interactionSource,
indication = null
),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
modifier = Modifier

24
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt

@ -17,21 +17,35 @@ @@ -17,21 +17,35 @@
package io.element.android.libraries.mediaviewer.api.local
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.rememberZoomableState
@Stable
class LocalMediaViewState {
class LocalMediaViewState internal constructor(
val zoomableState: ZoomableState,
) {
var isReady: Boolean by mutableStateOf(false)
var isPlaying: Boolean by mutableStateOf(false)
var playableState: PlayableState by mutableStateOf(PlayableState.NotPlayable)
}
@Immutable
sealed interface PlayableState {
data object NotPlayable : PlayableState
data class Playable(
val isPlaying: Boolean,
val isShowingControls: Boolean
) : PlayableState
}
@Composable
fun rememberLocalMediaViewState(): LocalMediaViewState {
return remember {
LocalMediaViewState()
fun rememberLocalMediaViewState(zoomableState: ZoomableState = rememberZoomableState()): LocalMediaViewState {
return remember(zoomableState) {
LocalMediaViewState(zoomableState)
}
}

19
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt

@ -21,6 +21,7 @@ import androidx.compose.foundation.background @@ -21,6 +21,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -47,10 +48,15 @@ import me.saket.telephoto.zoomable.zoomable @@ -47,10 +48,15 @@ import me.saket.telephoto.zoomable.zoomable
@Composable
fun PdfViewer(
pdfViewerState: PdfViewerState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(
modifier = modifier.zoomable(pdfViewerState.zoomableState),
modifier = modifier
.zoomable(
state = pdfViewerState.zoomableState,
onClick = { onClick() }
),
contentAlignment = Alignment.Center
) {
val maxWidthInPx = maxWidth.roundToPx()
@ -61,7 +67,10 @@ fun PdfViewer( @@ -61,7 +67,10 @@ fun PdfViewer(
}
}
val pdfPages = pdfViewerState.getPages()
PdfPagesView(pdfPages.toImmutableList(), pdfViewerState.lazyListState)
PdfPagesView(
pdfPages = pdfPages.toImmutableList(),
lazyListState = pdfViewerState.lazyListState,
)
}
}
@ -74,8 +83,12 @@ private fun PdfPagesView( @@ -74,8 +83,12 @@ private fun PdfPagesView(
LazyColumn(
modifier = modifier.fillMaxSize(),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically)
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
) {
// Add a fake item to the top so that the first item is not at the top of the screen.
item {
Spacer(modifier = Modifier.height(80.dp))
}
items(pdfPages.size) { index ->
val pdfPage = pdfPages[index]
PdfPageView(pdfPage)

199
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt

@ -19,34 +19,36 @@ @@ -19,34 +19,36 @@
package io.element.android.libraries.mediaviewer.api.viewer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.mimetype.MimeTypes
@ -65,9 +67,19 @@ import io.element.android.libraries.mediaviewer.api.R @@ -65,9 +67,19 @@ import io.element.android.libraries.mediaviewer.api.R
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.PlayableState
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.delay
import me.saket.telephoto.flick.FlickToDismiss
import me.saket.telephoto.flick.FlickToDismissState
import me.saket.telephoto.flick.rememberFlickToDismissState
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
import me.saket.telephoto.zoomable.rememberZoomableState
import kotlin.time.Duration
@Composable
fun MediaViewerView(
@ -75,22 +87,25 @@ fun MediaViewerView( @@ -75,22 +87,25 @@ fun MediaViewerView(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
fun onRetry() {
state.eventSink(MediaViewerEvents.RetryLoading)
}
fun onDismissError() {
state.eventSink(MediaViewerEvents.ClearLoadingError)
}
val localMediaViewState = rememberLocalMediaViewState()
val showThumbnail = !localMediaViewState.isReady
val showProgress = rememberShowProgress(state.downloadedMedia)
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
var showOverlay by remember { mutableStateOf(true) }
Scaffold(
modifier,
topBar = {
containerColor = Color.Transparent,
snackbarHost = { SnackbarHost(snackbarHostState) },
) {
MediaViewerPage(
showOverlay = showOverlay,
state = state,
onDismiss = {
onBackPressed()
},
onShowOverlayChanged = {
showOverlay = it
}
)
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is AsyncData.Success,
mimeType = state.mediaInfo.mimeType,
@ -99,49 +114,127 @@ fun MediaViewerView( @@ -99,49 +114,127 @@ fun MediaViewerView(
canShare = state.canShare,
eventSink = state.eventSink
)
}
}
}
@Composable
fun MediaViewerPage(
showOverlay: Boolean,
state: MediaViewerState,
onDismiss: () -> Unit,
onShowOverlayChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
fun onRetry() {
state.eventSink(MediaViewerEvents.RetryLoading)
}
fun onDismissError() {
state.eventSink(MediaViewerEvents.ClearLoadingError)
}
val currentShowOverlay by rememberUpdatedState(showOverlay)
val currentOnShowOverlayChanged by rememberUpdatedState(onShowOverlayChanged)
val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false)
DismissFlickEffects(
flickState = flickState,
onDismissing = { animationDuration ->
delay(animationDuration / 3)
onDismiss()
},
snackbarHost = { SnackbarHost(snackbarHostState) },
onDragging = {
currentOnShowOverlayChanged(false)
}
)
FlickToDismiss(
state = flickState,
modifier = modifier.background(backgroundColorFor(flickState))
) {
Column(
val showProgress = rememberShowProgress(state.downloadedMedia)
Box(
modifier = Modifier
.fillMaxSize()
.padding(it),
.navigationBarsPadding()
) {
if (showProgress) {
LinearProgressIndicator(
Modifier
.fillMaxWidth()
.height(2.dp)
Box(contentAlignment = Alignment.Center) {
val zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false)
)
} else {
Spacer(Modifier.height(2.dp))
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (state.downloadedMedia is AsyncData.Failure) {
ErrorView(
errorMessage = stringResource(id = CommonStrings.error_unknown),
onRetry = ::onRetry,
onDismiss = ::onDismissError
)
val localMediaViewState = rememberLocalMediaViewState(zoomableState)
val showThumbnail = !localMediaViewState.isReady
val playableState = localMediaViewState.playableState
val showError = state.downloadedMedia is AsyncData.Failure
LaunchedEffect(playableState) {
if (playableState is PlayableState.Playable) {
currentOnShowOverlayChanged(playableState.isShowingControls)
}
}
LocalMediaView(
modifier = Modifier.fillMaxSize(),
localMediaViewState = localMediaViewState,
localMedia = state.downloadedMedia.dataOrNull(),
mediaInfo = state.mediaInfo,
onClick = {
if (playableState is PlayableState.NotPlayable) {
currentOnShowOverlayChanged(!currentShowOverlay)
}
},
)
ThumbnailView(
mediaInfo = state.mediaInfo,
thumbnailSource = state.thumbnailSource,
showThumbnail = showThumbnail,
isVisible = showThumbnail,
zoomableState = zoomableState
)
if (showError) {
ErrorView(
errorMessage = stringResource(id = CommonStrings.error_unknown),
onRetry = ::onRetry,
onDismiss = ::onDismissError
)
}
}
if (showProgress) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
)
}
}
}
}
@Composable
private fun DismissFlickEffects(
flickState: FlickToDismissState,
onDismissing: suspend (Duration) -> Unit,
onDragging: suspend () -> Unit,
) {
val currentOnDismissing by rememberUpdatedState(onDismissing)
val currentOnDragging by rememberUpdatedState(onDragging)
when (val gestureState = flickState.gestureState) {
is FlickToDismissState.GestureState.Dismissing -> {
LaunchedEffect(Unit) {
currentOnDismissing(gestureState.animationDuration)
}
}
is FlickToDismissState.GestureState.Dragging -> {
LaunchedEffect(Unit) {
currentOnDragging()
}
}
else -> Unit
}
}
@Composable
private fun rememberShowProgress(downloadedMedia: AsyncData<LocalMedia>): Boolean {
var showProgress by remember {
@ -175,6 +268,9 @@ private fun MediaViewerTopBar( @@ -175,6 +268,9 @@ private fun MediaViewerTopBar(
) {
TopAppBar(
title = {},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent.copy(0.6f),
),
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
IconButton(
@ -227,26 +323,28 @@ private fun MediaViewerTopBar( @@ -227,26 +323,28 @@ private fun MediaViewerTopBar(
@Composable
private fun ThumbnailView(
thumbnailSource: MediaSource?,
showThumbnail: Boolean,
isVisible: Boolean,
mediaInfo: MediaInfo,
zoomableState: ZoomableState,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = showThumbnail,
visible = isVisible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val mediaRequestData = MediaRequestData(
source = thumbnailSource,
kind = MediaRequestData.Kind.File(mediaInfo.name, mediaInfo.mimeType)
)
AsyncImage(
ZoomableAsyncImage(
state = rememberZoomableImageState(zoomableState),
modifier = Modifier.fillMaxSize(),
model = mediaRequestData,
alpha = 0.8f,
contentScale = ContentScale.Fit,
contentDescription = null,
)
@ -267,6 +365,21 @@ private fun ErrorView( @@ -267,6 +365,21 @@ private fun ErrorView(
)
}
@Composable
private fun backgroundColorFor(flickState: FlickToDismissState): Color {
val animatedAlpha by animateFloatAsState(
targetValue = when (flickState.gestureState) {
is FlickToDismissState.GestureState.Dismissed,
is FlickToDismissState.GestureState.Dismissing -> 0f
is FlickToDismissState.GestureState.Dragging,
is FlickToDismissState.GestureState.Idle,
is FlickToDismissState.GestureState.Resetting -> 1f - flickState.offsetFraction
},
label = "Background alpha",
)
return Color.Black.copy(alpha = animatedAlpha)
}
// Only preview in dark, dark theme is forced on the Node.
@Preview
@Composable

Loading…
Cancel
Save