Переработан fullscreen фото из медиапикера под поведение Telegram
This commit is contained in:
@@ -75,6 +75,7 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -162,6 +163,9 @@ fun ChatDetailScreen(
|
|||||||
) {
|
) {
|
||||||
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
|
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val hasNativeNavigationBar = remember(context) {
|
||||||
|
com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)
|
||||||
|
}
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
@@ -290,6 +294,8 @@ fun ChatDetailScreen(
|
|||||||
var imageViewerInitialIndex by remember { mutableStateOf(0) }
|
var imageViewerInitialIndex by remember { mutableStateOf(0) }
|
||||||
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
|
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
|
||||||
var imageViewerImages by remember { mutableStateOf<List<ViewableImage>>(emptyList()) }
|
var imageViewerImages by remember { mutableStateOf<List<ViewableImage>>(emptyList()) }
|
||||||
|
var simplePickerPreviewUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
var simplePickerPreviewSourceThumb by remember { mutableStateOf<ThumbnailPosition?>(null) }
|
||||||
|
|
||||||
// 🎨 Управление статус баром — ВСЕГДА чёрные иконки в светлой теме
|
// 🎨 Управление статус баром — ВСЕГДА чёрные иконки в светлой теме
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
@@ -364,7 +370,8 @@ fun ChatDetailScreen(
|
|||||||
showEmojiPicker,
|
showEmojiPicker,
|
||||||
pendingCameraPhotoUri,
|
pendingCameraPhotoUri,
|
||||||
pendingGalleryImages,
|
pendingGalleryImages,
|
||||||
showInAppCamera
|
showInAppCamera,
|
||||||
|
simplePickerPreviewUri
|
||||||
) {
|
) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
showImageViewer ||
|
showImageViewer ||
|
||||||
@@ -372,7 +379,8 @@ fun ChatDetailScreen(
|
|||||||
showEmojiPicker ||
|
showEmojiPicker ||
|
||||||
pendingCameraPhotoUri != null ||
|
pendingCameraPhotoUri != null ||
|
||||||
pendingGalleryImages.isNotEmpty() ||
|
pendingGalleryImages.isNotEmpty() ||
|
||||||
showInAppCamera
|
showInAppCamera ||
|
||||||
|
simplePickerPreviewUri != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1801,7 +1809,10 @@ fun ChatDetailScreen(
|
|||||||
bottom =
|
bottom =
|
||||||
16.dp
|
16.dp
|
||||||
)
|
)
|
||||||
.navigationBarsPadding()
|
.then(
|
||||||
|
if (hasNativeNavigationBar) Modifier.navigationBarsPadding()
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX =
|
scaleX =
|
||||||
buttonScale
|
buttonScale
|
||||||
@@ -2168,7 +2179,9 @@ fun ChatDetailScreen(
|
|||||||
inputFocusTrigger =
|
inputFocusTrigger =
|
||||||
inputFocusTrigger,
|
inputFocusTrigger,
|
||||||
suppressKeyboard =
|
suppressKeyboard =
|
||||||
showInAppCamera
|
showInAppCamera,
|
||||||
|
hasNativeNavigationBar =
|
||||||
|
hasNativeNavigationBar
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3005,7 +3018,13 @@ fun ChatDetailScreen(
|
|||||||
onAvatarClick = {
|
onAvatarClick = {
|
||||||
viewModel.sendAvatarMessage()
|
viewModel.sendAvatarMessage()
|
||||||
},
|
},
|
||||||
recipientName = user.title
|
recipientName = user.title,
|
||||||
|
onPhotoPreviewRequested = { uri, sourceThumb ->
|
||||||
|
hideInputOverlays()
|
||||||
|
showMediaPicker = false
|
||||||
|
simplePickerPreviewSourceThumb = sourceThumb
|
||||||
|
simplePickerPreviewUri = uri
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MediaPickerBottomSheet(
|
MediaPickerBottomSheet(
|
||||||
@@ -3049,7 +3068,13 @@ fun ChatDetailScreen(
|
|||||||
onAvatarClick = {
|
onAvatarClick = {
|
||||||
viewModel.sendAvatarMessage()
|
viewModel.sendAvatarMessage()
|
||||||
},
|
},
|
||||||
recipientName = user.title
|
recipientName = user.title,
|
||||||
|
onPhotoPreviewRequested = { uri, sourceThumb ->
|
||||||
|
hideInputOverlays()
|
||||||
|
showMediaPicker = false
|
||||||
|
simplePickerPreviewSourceThumb = sourceThumb
|
||||||
|
simplePickerPreviewUri = uri
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} // Закрытие Box wrapper для Scaffold content
|
} // Закрытие Box wrapper для Scaffold content
|
||||||
@@ -3314,6 +3339,18 @@ fun ChatDetailScreen(
|
|||||||
|
|
||||||
} // Закрытие Scaffold content lambda
|
} // Закрытие Scaffold content lambda
|
||||||
|
|
||||||
|
simplePickerPreviewUri?.let { previewUri ->
|
||||||
|
SimpleFullscreenPhotoOverlay(
|
||||||
|
imageUri = previewUri,
|
||||||
|
sourceThumbnail = simplePickerPreviewSourceThumb,
|
||||||
|
modifier = Modifier.fillMaxSize().zIndex(100f),
|
||||||
|
onDismiss = {
|
||||||
|
simplePickerPreviewUri = null
|
||||||
|
simplePickerPreviewSourceThumb = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// <20> Image Viewer Overlay — FULLSCREEN поверх Scaffold
|
// <20> Image Viewer Overlay — FULLSCREEN поверх Scaffold
|
||||||
if (showImageViewer && imageViewerImages.isNotEmpty()) {
|
if (showImageViewer && imageViewerImages.isNotEmpty()) {
|
||||||
ImageViewerScreen(
|
ImageViewerScreen(
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.core.view.WindowCompat
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.rosetta.messenger.ui.chats.components.ImageEditorScreen
|
import com.rosetta.messenger.ui.chats.components.ImageEditorScreen
|
||||||
import com.rosetta.messenger.ui.chats.components.PhotoPreviewWithCaptionScreen
|
import com.rosetta.messenger.ui.chats.components.PhotoPreviewWithCaptionScreen
|
||||||
|
import com.rosetta.messenger.ui.chats.components.SimpleFullscreenPhotoViewer
|
||||||
import com.rosetta.messenger.ui.chats.components.ThumbnailPosition
|
import com.rosetta.messenger.ui.chats.components.ThumbnailPosition
|
||||||
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
||||||
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
||||||
@@ -146,6 +147,7 @@ fun ChatAttachAlert(
|
|||||||
currentUserPublicKey: String = "",
|
currentUserPublicKey: String = "",
|
||||||
maxSelection: Int = 10,
|
maxSelection: Int = 10,
|
||||||
recipientName: String? = null,
|
recipientName: String? = null,
|
||||||
|
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?) -> Unit)? = null,
|
||||||
viewModel: AttachAlertViewModel = viewModel()
|
viewModel: AttachAlertViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -679,8 +681,8 @@ fun ChatAttachAlert(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(shouldShow) {
|
LaunchedEffect(shouldShow, state.editingItem) {
|
||||||
if (!shouldShow) return@LaunchedEffect
|
if (!shouldShow || state.editingItem != null) return@LaunchedEffect
|
||||||
val window = (view.context as? Activity)?.window ?: return@LaunchedEffect
|
val window = (view.context as? Activity)?.window ?: return@LaunchedEffect
|
||||||
snapshotFlow { Triple(scrimAlpha, isPickerFullScreen, isDarkTheme) }
|
snapshotFlow { Triple(scrimAlpha, isPickerFullScreen, isDarkTheme) }
|
||||||
.collect { (alpha, fullScreen, dark) ->
|
.collect { (alpha, fullScreen, dark) ->
|
||||||
@@ -710,7 +712,7 @@ fun ChatAttachAlert(
|
|||||||
// POPUP RENDERING
|
// POPUP RENDERING
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
if (shouldShow) {
|
if (shouldShow && state.editingItem == null) {
|
||||||
Popup(
|
Popup(
|
||||||
alignment = Alignment.TopStart,
|
alignment = Alignment.TopStart,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
@@ -976,8 +978,13 @@ fun ChatAttachAlert(
|
|||||||
},
|
},
|
||||||
onItemClick = { item, position ->
|
onItemClick = { item, position ->
|
||||||
if (!item.isVideo) {
|
if (!item.isVideo) {
|
||||||
|
hideKeyboard()
|
||||||
|
if (onPhotoPreviewRequested != null) {
|
||||||
|
onPhotoPreviewRequested(item.uri, position)
|
||||||
|
} else {
|
||||||
thumbnailPosition = position
|
thumbnailPosition = position
|
||||||
viewModel.setEditingItem(item)
|
viewModel.setEditingItem(item)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModel.toggleSelection(item.id, maxSelection)
|
viewModel.toggleSelection(item.id, maxSelection)
|
||||||
}
|
}
|
||||||
@@ -1173,45 +1180,14 @@ fun ChatAttachAlert(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
state.editingItem?.let { item ->
|
state.editingItem?.let { item ->
|
||||||
ImageEditorScreen(
|
SimpleFullscreenPhotoViewer(
|
||||||
imageUri = item.uri,
|
imageUri = item.uri,
|
||||||
|
sourceThumbnail = thumbnailPosition,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
viewModel.setEditingItem(null)
|
viewModel.setEditingItem(null)
|
||||||
thumbnailPosition = null
|
thumbnailPosition = null
|
||||||
shouldShow = true
|
shouldShow = true
|
||||||
},
|
|
||||||
onSave = { editedUri ->
|
|
||||||
viewModel.setEditingItem(null)
|
|
||||||
thumbnailPosition = null
|
|
||||||
if (onMediaSelectedWithCaption == null) {
|
|
||||||
previewPhotoUri = editedUri
|
|
||||||
} else {
|
|
||||||
val mediaItem = MediaItem(
|
|
||||||
id = System.currentTimeMillis(),
|
|
||||||
uri = editedUri,
|
|
||||||
mimeType = "image/png",
|
|
||||||
dateModified = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
onMediaSelected(listOf(mediaItem), "")
|
|
||||||
onDismiss()
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onSaveWithCaption = if (onMediaSelectedWithCaption != null) { editedUri, caption ->
|
|
||||||
viewModel.setEditingItem(null)
|
|
||||||
thumbnailPosition = null
|
|
||||||
val mediaItem = MediaItem(
|
|
||||||
id = System.currentTimeMillis(),
|
|
||||||
uri = editedUri,
|
|
||||||
mimeType = "image/png",
|
|
||||||
dateModified = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
onMediaSelectedWithCaption(mediaItem, caption)
|
|
||||||
onDismiss()
|
|
||||||
} else null,
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
showCaptionInput = onMediaSelectedWithCaption != null,
|
|
||||||
recipientName = recipientName,
|
|
||||||
thumbnailPosition = thumbnailPosition
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ fun MediaPickerBottomSheet(
|
|||||||
onAvatarClick: () -> Unit = {},
|
onAvatarClick: () -> Unit = {},
|
||||||
currentUserPublicKey: String = "",
|
currentUserPublicKey: String = "",
|
||||||
maxSelection: Int = 10,
|
maxSelection: Int = 10,
|
||||||
recipientName: String? = null
|
recipientName: String? = null,
|
||||||
|
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -567,8 +568,8 @@ fun MediaPickerBottomSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reactive updates — single snapshotFlow drives ALL system bar colors
|
// Reactive updates — single snapshotFlow drives ALL system bar colors
|
||||||
LaunchedEffect(shouldShow) {
|
LaunchedEffect(shouldShow, editingItem) {
|
||||||
if (!shouldShow) return@LaunchedEffect
|
if (!shouldShow || editingItem != null) return@LaunchedEffect
|
||||||
val window = (view.context as? android.app.Activity)?.window ?: return@LaunchedEffect
|
val window = (view.context as? android.app.Activity)?.window ?: return@LaunchedEffect
|
||||||
|
|
||||||
snapshotFlow { Triple(scrimAlpha, isPickerFullScreen, isDarkTheme) }
|
snapshotFlow { Triple(scrimAlpha, isPickerFullScreen, isDarkTheme) }
|
||||||
@@ -593,7 +594,7 @@ fun MediaPickerBottomSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Используем Popup для показа поверх клавиатуры
|
// Используем Popup для показа поверх клавиатуры
|
||||||
if (shouldShow) {
|
if (shouldShow && editingItem == null) {
|
||||||
// BackHandler для закрытия по back
|
// BackHandler для закрытия по back
|
||||||
BackHandler {
|
BackHandler {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
@@ -986,8 +987,13 @@ fun MediaPickerBottomSheet(
|
|||||||
},
|
},
|
||||||
onItemClick = { item, position ->
|
onItemClick = { item, position ->
|
||||||
if (!item.isVideo) {
|
if (!item.isVideo) {
|
||||||
|
hideKeyboard()
|
||||||
|
if (onPhotoPreviewRequested != null) {
|
||||||
|
onPhotoPreviewRequested(item.uri, position)
|
||||||
|
} else {
|
||||||
thumbnailPosition = position
|
thumbnailPosition = position
|
||||||
editingItem = item
|
editingItem = item
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Videos don't have photo editor in this flow.
|
// Videos don't have photo editor in this flow.
|
||||||
toggleSelection(item.id)
|
toggleSelection(item.id)
|
||||||
@@ -1279,48 +1285,16 @@ fun MediaPickerBottomSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image Editor FULLSCREEN overlay для фото из галереи
|
// Fullscreen preview для выбранной фото из галереи (чистый экран без тулбаров).
|
||||||
// ImageEditorScreen wraps itself in a Dialog internally — no external wrapper needed
|
|
||||||
editingItem?.let { item ->
|
editingItem?.let { item ->
|
||||||
ImageEditorScreen(
|
SimpleFullscreenPhotoViewer(
|
||||||
imageUri = item.uri,
|
imageUri = item.uri,
|
||||||
|
sourceThumbnail = thumbnailPosition,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
editingItem = null
|
editingItem = null
|
||||||
thumbnailPosition = null
|
thumbnailPosition = null
|
||||||
shouldShow = true
|
shouldShow = true
|
||||||
},
|
|
||||||
onSave = { editedUri ->
|
|
||||||
editingItem = null
|
|
||||||
thumbnailPosition = null
|
|
||||||
if (onMediaSelectedWithCaption == null) {
|
|
||||||
previewPhotoUri = editedUri
|
|
||||||
} else {
|
|
||||||
val mediaItem = MediaItem(
|
|
||||||
id = System.currentTimeMillis(),
|
|
||||||
uri = editedUri,
|
|
||||||
mimeType = "image/png",
|
|
||||||
dateModified = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
onMediaSelected(listOf(mediaItem), "")
|
|
||||||
onDismiss()
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onSaveWithCaption = if (onMediaSelectedWithCaption != null) { editedUri, caption ->
|
|
||||||
editingItem = null
|
|
||||||
thumbnailPosition = null
|
|
||||||
val mediaItem = MediaItem(
|
|
||||||
id = System.currentTimeMillis(),
|
|
||||||
uri = editedUri,
|
|
||||||
mimeType = "image/png",
|
|
||||||
dateModified = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
onMediaSelectedWithCaption(mediaItem, caption)
|
|
||||||
onDismiss()
|
|
||||||
} else null,
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
showCaptionInput = onMediaSelectedWithCaption != null,
|
|
||||||
recipientName = recipientName,
|
|
||||||
thumbnailPosition = thumbnailPosition
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package com.rosetta.messenger.ui.chats.components
|
||||||
|
|
||||||
|
import android.graphics.Color as AndroidColor
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.CubicBezierEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private val ViewerExpandEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
|
||||||
|
|
||||||
|
private data class SimpleViewerTransform(
|
||||||
|
val scaleX: Float,
|
||||||
|
val scaleY: Float,
|
||||||
|
val translationX: Float,
|
||||||
|
val translationY: Float,
|
||||||
|
val cornerRadiusDp: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun lerpFloat(start: Float, stop: Float, fraction: Float): Float {
|
||||||
|
return start + (stop - start) * fraction
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFullscreenWindow(window: Window?) {
|
||||||
|
window ?: return
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
window.setLayout(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
window.setBackgroundDrawable(ColorDrawable(AndroidColor.TRANSPARENT))
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||||
|
window.decorView.setPadding(0, 0, 0, 0)
|
||||||
|
|
||||||
|
val attrs = window.attributes
|
||||||
|
attrs.width = WindowManager.LayoutParams.MATCH_PARENT
|
||||||
|
attrs.height = WindowManager.LayoutParams.MATCH_PARENT
|
||||||
|
attrs.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
window.attributes = attrs
|
||||||
|
|
||||||
|
val decorView = window.decorView
|
||||||
|
val telegramLikeFlags =
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
if (decorView.systemUiVisibility != telegramLikeFlags) {
|
||||||
|
decorView.systemUiVisibility = telegramLikeFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
window.statusBarColor = AndroidColor.TRANSPARENT
|
||||||
|
window.navigationBarColor = AndroidColor.TRANSPARENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isStatusBarContrastEnforced = false
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
val controller = WindowCompat.getInsetsController(window, decorView)
|
||||||
|
controller.isAppearanceLightStatusBars = false
|
||||||
|
controller.isAppearanceLightNavigationBars = false
|
||||||
|
controller.show(WindowInsetsCompat.Type.statusBars())
|
||||||
|
controller.show(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
window.setWindowAnimations(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleFullscreenPhotoViewer(
|
||||||
|
imageUri: android.net.Uri,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
sourceThumbnail: ThumbnailPosition? = null
|
||||||
|
) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties =
|
||||||
|
DialogProperties(
|
||||||
|
dismissOnBackPress = false,
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
decorFitsSystemWindows = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
SideEffect {
|
||||||
|
val dialogWindow = (view.parent as? DialogWindowProvider)?.window
|
||||||
|
setupFullscreenWindow(dialogWindow)
|
||||||
|
}
|
||||||
|
SimpleFullscreenPhotoContent(
|
||||||
|
imageUri = imageUri,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
sourceThumbnail = sourceThumbnail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleFullscreenPhotoOverlay(
|
||||||
|
imageUri: android.net.Uri,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
sourceThumbnail: ThumbnailPosition? = null,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
SimpleFullscreenPhotoContent(
|
||||||
|
imageUri = imageUri,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
sourceThumbnail = sourceThumbnail,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SimpleFullscreenPhotoContent(
|
||||||
|
imageUri: android.net.Uri,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
sourceThumbnail: ThumbnailPosition? = null,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var isClosing by remember { mutableStateOf(false) }
|
||||||
|
var screenSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
val progress = remember(imageUri, sourceThumbnail) {
|
||||||
|
Animatable(if (sourceThumbnail != null) 0f else 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(imageUri, sourceThumbnail) {
|
||||||
|
if (progress.value < 1f) {
|
||||||
|
progress.animateTo(
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = 230, easing = ViewerExpandEasing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeViewer() {
|
||||||
|
if (isClosing) return
|
||||||
|
isClosing = true
|
||||||
|
scope.launch {
|
||||||
|
progress.animateTo(
|
||||||
|
targetValue = 0f,
|
||||||
|
animationSpec = tween(durationMillis = 210, easing = ViewerExpandEasing)
|
||||||
|
)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler { closeViewer() }
|
||||||
|
|
||||||
|
val transform by remember(sourceThumbnail, screenSize, progress.value) {
|
||||||
|
derivedStateOf {
|
||||||
|
val p = progress.value
|
||||||
|
if (sourceThumbnail != null && screenSize != IntSize.Zero) {
|
||||||
|
val screenW = screenSize.width.toFloat().coerceAtLeast(1f)
|
||||||
|
val screenH = screenSize.height.toFloat().coerceAtLeast(1f)
|
||||||
|
val srcW = sourceThumbnail.width.coerceAtLeast(1f)
|
||||||
|
val srcH = sourceThumbnail.height.coerceAtLeast(1f)
|
||||||
|
|
||||||
|
val sourceScaleX = srcW / screenW
|
||||||
|
val sourceScaleY = srcH / screenH
|
||||||
|
|
||||||
|
val targetCenterX = screenW / 2f
|
||||||
|
val targetCenterY = screenH / 2f
|
||||||
|
val sourceCenterX = sourceThumbnail.x + srcW / 2f
|
||||||
|
val sourceCenterY = sourceThumbnail.y + srcH / 2f
|
||||||
|
|
||||||
|
SimpleViewerTransform(
|
||||||
|
scaleX = lerpFloat(sourceScaleX, 1f, p),
|
||||||
|
scaleY = lerpFloat(sourceScaleY, 1f, p),
|
||||||
|
translationX = lerpFloat(sourceCenterX - targetCenterX, 0f, p),
|
||||||
|
translationY = lerpFloat(sourceCenterY - targetCenterY, 0f, p),
|
||||||
|
cornerRadiusDp = lerpFloat(sourceThumbnail.cornerRadius, 0f, p)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SimpleViewerTransform(
|
||||||
|
scaleX = lerpFloat(0.94f, 1f, p),
|
||||||
|
scaleY = lerpFloat(0.94f, 1f, p),
|
||||||
|
translationX = 0f,
|
||||||
|
translationY = 0f,
|
||||||
|
cornerRadiusDp = 0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
modifier.fillMaxSize()
|
||||||
|
.onSizeChanged { screenSize = it }
|
||||||
|
.background(Color.Black)
|
||||||
|
.pointerInput(imageUri) { detectTapGestures(onTap = { closeViewer() }) },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUri,
|
||||||
|
contentDescription = "Photo",
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = transform.scaleX
|
||||||
|
scaleY = transform.scaleY
|
||||||
|
translationX = transform.translationX
|
||||||
|
translationY = transform.translationY
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
if (transform.cornerRadiusDp > 0f) {
|
||||||
|
Modifier.clip(RoundedCornerShape(transform.cornerRadiusDp.dp))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user