fix: adjust padding and status bar colors in ImageEditorScreen for improved UI experience
This commit is contained in:
@@ -84,6 +84,8 @@ import android.provider.MediaStore
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.delay
|
||||
import android.app.Activity
|
||||
import androidx.core.view.WindowCompat
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -128,6 +130,9 @@ fun ChatDetailScreen(
|
||||
val coordinator = remember { KeyboardTransitionCoordinator() }
|
||||
val view = LocalView.current
|
||||
|
||||
// 🎨 Window reference для управления статус баром
|
||||
val window = remember { (view.context as? Activity)?.window }
|
||||
|
||||
// 🔥 Focus state for input
|
||||
val inputFocusRequester = remember { FocusRequester() }
|
||||
|
||||
@@ -189,6 +194,24 @@ fun ChatDetailScreen(
|
||||
var imageViewerInitialIndex by remember { mutableStateOf(0) }
|
||||
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
|
||||
|
||||
// 🎨 Управление статус баром - чёрный при просмотре фото
|
||||
DisposableEffect(isDarkTheme, showImageViewer) {
|
||||
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
|
||||
|
||||
if (showImageViewer) {
|
||||
// 📸 При просмотре фото - чёрный статус бар
|
||||
window?.statusBarColor = 0xFF000000.toInt()
|
||||
insetsController?.isAppearanceLightStatusBars = false
|
||||
} else {
|
||||
// Обычный режим - цвет хедера
|
||||
val headerColor = if (isDarkTheme) 0xFF212121.toInt() else 0xFFFFFFFF.toInt()
|
||||
window?.statusBarColor = headerColor
|
||||
insetsController?.isAppearanceLightStatusBars = !isDarkTheme
|
||||
}
|
||||
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
// 📷 Camera: URI для сохранения фото
|
||||
var cameraImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
|
||||
@@ -532,6 +555,8 @@ fun ChatDetailScreen(
|
||||
else Color.White
|
||||
} else headerBackground
|
||||
)
|
||||
// 🎨 statusBarsPadding ПОСЛЕ background = хедер начинается под статус баром
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
// Контент хедера с Crossfade для плавной смены - ускоренная
|
||||
// анимация
|
||||
@@ -545,7 +570,6 @@ fun ChatDetailScreen(
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.height(56.dp)
|
||||
.padding(
|
||||
horizontal =
|
||||
@@ -723,7 +747,6 @@ fun ChatDetailScreen(
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.height(56.dp)
|
||||
.padding(
|
||||
horizontal =
|
||||
|
||||
@@ -65,6 +65,7 @@ import ja.burhanrashid52.photoeditor.PhotoEditorView
|
||||
import ja.burhanrashid52.photoeditor.SaveSettings
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import androidx.core.view.WindowCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -163,14 +164,23 @@ fun ImageEditorScreen(
|
||||
val originalStatusBarColor = window?.statusBarColor ?: 0
|
||||
val originalNavigationBarColor = window?.navigationBarColor ?: 0
|
||||
|
||||
// Устанавливаем черный цвет
|
||||
// 🔥 Сохраняем оригинальное состояние иконок статус бара
|
||||
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
|
||||
val originalLightStatusBars = insetsController?.isAppearanceLightStatusBars ?: false
|
||||
val originalLightNavigationBars = insetsController?.isAppearanceLightNavigationBars ?: false
|
||||
|
||||
// Устанавливаем черный цвет и светлые иконки
|
||||
window?.statusBarColor = android.graphics.Color.BLACK
|
||||
window?.navigationBarColor = android.graphics.Color.BLACK
|
||||
insetsController?.isAppearanceLightStatusBars = false // Светлые иконки на черном фоне
|
||||
insetsController?.isAppearanceLightNavigationBars = false
|
||||
|
||||
onDispose {
|
||||
// Восстанавливаем оригинальные цвета
|
||||
// Восстанавливаем оригинальные цвета и состояние иконок
|
||||
window?.statusBarColor = originalStatusBarColor
|
||||
window?.navigationBarColor = originalNavigationBarColor
|
||||
insetsController?.isAppearanceLightStatusBars = originalLightStatusBars
|
||||
insetsController?.isAppearanceLightNavigationBars = originalLightNavigationBars
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,10 +596,10 @@ fun ImageEditorScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// 🔥 imePadding СНАЧАЛА - поднимает над клавиатурой
|
||||
.then(if (shouldUseImePadding) Modifier.imePadding() else Modifier)
|
||||
// 🔥 Затем добавляем отступ для toolbar когда клавиатура закрыта
|
||||
// 🔥 Отступ для toolbar ТОЛЬКО когда клавиатура/emoji закрыты
|
||||
.padding(bottom = bottomPaddingForCaption)
|
||||
// 🔥 imePadding поднимает над клавиатурой (применяется только когда emoji не показан)
|
||||
.then(if (shouldUseImePadding) Modifier.imePadding() else Modifier)
|
||||
) {
|
||||
TelegramCaptionBar(
|
||||
caption = caption,
|
||||
@@ -1414,6 +1424,33 @@ fun MultiImageEditorScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val view = LocalView.current
|
||||
|
||||
// 🎨 Черный статус бар и навигационный бар для редактора
|
||||
val window = remember { (view.context as? Activity)?.window }
|
||||
DisposableEffect(Unit) {
|
||||
val originalStatusBarColor = window?.statusBarColor ?: 0
|
||||
val originalNavigationBarColor = window?.navigationBarColor ?: 0
|
||||
|
||||
// 🔥 Сохраняем оригинальное состояние иконок статус бара
|
||||
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
|
||||
val originalLightStatusBars = insetsController?.isAppearanceLightStatusBars ?: false
|
||||
val originalLightNavigationBars = insetsController?.isAppearanceLightNavigationBars ?: false
|
||||
|
||||
// Устанавливаем черный цвет и светлые иконки
|
||||
window?.statusBarColor = android.graphics.Color.BLACK
|
||||
window?.navigationBarColor = android.graphics.Color.BLACK
|
||||
insetsController?.isAppearanceLightStatusBars = false // Светлые иконки на черном фоне
|
||||
insetsController?.isAppearanceLightNavigationBars = false
|
||||
|
||||
onDispose {
|
||||
// Восстанавливаем оригинальные цвета и состояние иконок
|
||||
window?.statusBarColor = originalStatusBarColor
|
||||
window?.navigationBarColor = originalNavigationBarColor
|
||||
insetsController?.isAppearanceLightStatusBars = originalLightStatusBars
|
||||
insetsController?.isAppearanceLightNavigationBars = originalLightNavigationBars
|
||||
}
|
||||
}
|
||||
|
||||
val imagesWithCaptions = remember {
|
||||
mutableStateListOf<ImageWithCaption>().apply {
|
||||
|
||||
@@ -155,23 +155,22 @@ fun ImageViewerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Close with animation
|
||||
// Close with animation - красиво возвращаемся к пузырьку
|
||||
fun closeWithAnimation() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
|
||||
if (sourceBounds != null) {
|
||||
scope.launch {
|
||||
scope.launch {
|
||||
if (sourceBounds != null) {
|
||||
// Плавно возвращаемся к исходному положению пузырька
|
||||
animationProgress.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = TelegramEasing
|
||||
durationMillis = 280,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
@@ -192,17 +191,61 @@ fun ImageViewerScreen(
|
||||
// UI visibility state
|
||||
var showControls by remember { mutableStateOf(true) }
|
||||
|
||||
// Drag to dismiss
|
||||
var offsetY by remember { mutableStateOf(0f) }
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎬 ПРОСТОЙ FADE-OUT при свайпе
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
var dragOffsetY by remember { mutableFloatStateOf(0f) }
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
val dismissThreshold = 200f
|
||||
var dragVelocity by remember { mutableFloatStateOf(0f) }
|
||||
val dismissThreshold = 100f
|
||||
val velocityThreshold = 500f
|
||||
|
||||
// Animated background alpha based on animation progress and drag
|
||||
val baseAlpha = animationProgress.value
|
||||
val dragAlpha = if (isDragging) {
|
||||
(1f - (offsetY.absoluteValue / 500f)).coerceIn(0.3f, 1f)
|
||||
} else 1f
|
||||
val backgroundAlpha = baseAlpha * dragAlpha
|
||||
// Анимированные значения
|
||||
val animatedOffsetY = remember { Animatable(0f) }
|
||||
val dismissAlpha = remember { Animatable(1f) }
|
||||
|
||||
// Синхронизируем drag с анимацией
|
||||
LaunchedEffect(isDragging, dragOffsetY) {
|
||||
if (isDragging) {
|
||||
animatedOffsetY.snapTo(dragOffsetY)
|
||||
}
|
||||
}
|
||||
|
||||
// 🎬 Простой fade-out dismiss
|
||||
fun smoothDismiss() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
|
||||
scope.launch {
|
||||
// Плавно исчезаем - более длинная анимация
|
||||
dismissAlpha.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 350,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// Плавный возврат на место
|
||||
fun snapBack() {
|
||||
scope.launch {
|
||||
animatedOffsetY.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 250,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Alpha на основе drag прогресса
|
||||
val dismissProgress = (animatedOffsetY.value.absoluteValue / 300f).coerceIn(0f, 1f)
|
||||
val backgroundAlpha = animationProgress.value * dismissAlpha.value * (1f - dismissProgress * 0.5f)
|
||||
|
||||
// Current image info
|
||||
val currentImage = images.getOrNull(pagerState.currentPage)
|
||||
@@ -212,37 +255,6 @@ fun ImageViewerScreen(
|
||||
closeWithAnimation()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎬 CALCULATE ANIMATED TRANSFORMS (Telegram style)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val progress = animationProgress.value
|
||||
|
||||
// Calculate interpolated values for shared element transition
|
||||
val animatedScale: Float
|
||||
val animatedTranslationX: Float
|
||||
val animatedTranslationY: Float
|
||||
val animatedCornerRadius: Float
|
||||
|
||||
if (sourceBounds != null && screenSize.width > 0 && screenSize.height > 0) {
|
||||
// Source state (thumbnail in chat)
|
||||
val sourceScale = sourceBounds.width / screenSize.width.toFloat()
|
||||
val sourceCenterX = sourceBounds.left + sourceBounds.width / 2f
|
||||
val sourceCenterY = sourceBounds.top + sourceBounds.height / 2f
|
||||
val screenCenterX = screenSize.width / 2f
|
||||
val screenCenterY = screenSize.height / 2f
|
||||
|
||||
// Interpolate between source and fullscreen
|
||||
animatedScale = sourceScale + (1f - sourceScale) * progress
|
||||
animatedTranslationX = (sourceCenterX - screenCenterX) * (1f - progress)
|
||||
animatedTranslationY = (sourceCenterY - screenCenterY) * (1f - progress)
|
||||
animatedCornerRadius = sourceBounds.cornerRadius * (1f - progress)
|
||||
} else {
|
||||
animatedScale = 1f
|
||||
animatedTranslationX = 0f
|
||||
animatedTranslationY = 0f
|
||||
animatedCornerRadius = 0f
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -250,63 +262,86 @@ fun ImageViewerScreen(
|
||||
.background(Color.Black.copy(alpha = backgroundAlpha))
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📸 HORIZONTAL PAGER with shared element animation
|
||||
// 📸 HORIZONTAL PAGER с Telegram-style эффектом наслаивания
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
// Apply Telegram-style shared element transform
|
||||
scaleX = animatedScale
|
||||
scaleY = animatedScale
|
||||
translationX = animatedTranslationX
|
||||
translationY = animatedTranslationY + offsetY
|
||||
// Clip with animated corner radius
|
||||
clip = animatedCornerRadius > 0f
|
||||
shape = if (animatedCornerRadius > 0f) {
|
||||
RoundedCornerShape(animatedCornerRadius.dp)
|
||||
} else {
|
||||
RectangleShape
|
||||
}
|
||||
alpha = dismissAlpha.value
|
||||
translationY = animatedOffsetY.value
|
||||
},
|
||||
pageSpacing = 30.dp, // Telegram: dp(30) между фото
|
||||
key = { images[it].attachmentId }
|
||||
) { page ->
|
||||
val image = images[page]
|
||||
|
||||
ZoomableImage(
|
||||
// 🎬 Telegram-style наслаивание: правая страница уменьшается и затемняется
|
||||
val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
|
||||
val isRightPage = pageOffset < 0
|
||||
|
||||
// Scale эффект только для правой страницы (1.0 → 0.7)
|
||||
val scaleFactor = if (isRightPage) {
|
||||
val progress = (-pageOffset).coerceIn(0f, 1f)
|
||||
1f - (progress * 0.3f)
|
||||
} else 1f
|
||||
|
||||
// Alpha для правой страницы
|
||||
val pageAlpha = if (isRightPage) {
|
||||
1f - ((-pageOffset).coerceIn(0f, 1f) * 0.3f)
|
||||
} else 1f
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = scaleFactor
|
||||
scaleY = scaleFactor
|
||||
alpha = pageAlpha
|
||||
}
|
||||
) {
|
||||
ZoomableImage(
|
||||
image = image,
|
||||
privateKey = privateKey,
|
||||
onTap = { showControls = !showControls },
|
||||
onVerticalDrag = { dragAmount ->
|
||||
offsetY += dragAmount
|
||||
onVerticalDrag = { dragAmount, velocity ->
|
||||
dragOffsetY += dragAmount
|
||||
dragVelocity = velocity
|
||||
isDragging = true
|
||||
},
|
||||
onDragEnd = {
|
||||
isDragging = false
|
||||
if (offsetY.absoluteValue > dismissThreshold) {
|
||||
closeWithAnimation()
|
||||
val shouldDismiss = dragOffsetY.absoluteValue > dismissThreshold ||
|
||||
dragVelocity.absoluteValue > velocityThreshold
|
||||
if (shouldDismiss) {
|
||||
smoothDismiss()
|
||||
} else {
|
||||
offsetY = 0f
|
||||
dragOffsetY = 0f
|
||||
dragVelocity = 0f
|
||||
snapBack()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
} // Закрытие Box wrapper
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎛️ TOP BAR (показываем только когда анимация завершена)
|
||||
// 🎛️ TOP BAR - Telegram-style (200ms, 24dp slide)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
AnimatedVisibility(
|
||||
visible = showControls && animationState == 1 && !isClosing,
|
||||
enter = fadeIn(tween(150)) + slideInVertically { -it },
|
||||
exit = fadeOut(tween(100)) + slideOutVertically { -it },
|
||||
enter = fadeIn(tween(200, easing = FastOutSlowInEasing)) +
|
||||
slideInVertically(tween(200, easing = FastOutSlowInEasing)) { -96 }, // ~24dp
|
||||
exit = fadeOut(tween(200, easing = FastOutSlowInEasing)) +
|
||||
slideOutVertically(tween(200, easing = FastOutSlowInEasing)) { -96 },
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.6f)
|
||||
Color.Black.copy(alpha = 0.5f)
|
||||
)
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp)
|
||||
@@ -348,13 +383,15 @@ fun ImageViewerScreen(
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📍 PAGE INDICATOR (если больше 1 фото, показываем когда анимация завершена)
|
||||
// 📍 PAGE INDICATOR - Telegram-style (200ms, 24dp slide снизу)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (images.size > 1) {
|
||||
AnimatedVisibility(
|
||||
visible = showControls && animationState == 1 && !isClosing,
|
||||
enter = fadeIn(tween(150)),
|
||||
exit = fadeOut(tween(100)),
|
||||
enter = fadeIn(tween(200, easing = FastOutSlowInEasing)) +
|
||||
slideInVertically(tween(200, easing = FastOutSlowInEasing)) { 96 }, // +24dp снизу
|
||||
exit = fadeOut(tween(200, easing = FastOutSlowInEasing)) +
|
||||
slideOutVertically(tween(200, easing = FastOutSlowInEasing)) { 96 },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 32.dp)
|
||||
@@ -406,14 +443,14 @@ fun ImageViewerScreen(
|
||||
* - Double tap to zoom 2.5x / reset
|
||||
* - Pinch to zoom 1x-5x
|
||||
* - Pan when zoomed
|
||||
* - Vertical drag to dismiss when not zoomed
|
||||
* - Vertical drag to dismiss when not zoomed (with velocity tracking)
|
||||
*/
|
||||
@Composable
|
||||
private fun ZoomableImage(
|
||||
image: ViewableImage,
|
||||
privateKey: String,
|
||||
onTap: () -> Unit,
|
||||
onVerticalDrag: (Float) -> Unit = {},
|
||||
onVerticalDrag: (Float, Float) -> Unit = { _, _ -> }, // dragAmount, velocity
|
||||
onDragEnd: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -564,84 +601,100 @@ private fun ZoomableImage(
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
var isVerticalDragging = false
|
||||
var lastDragTime = 0L
|
||||
var lastDragY = 0f
|
||||
var currentVelocity = 0f
|
||||
val touchSlopValue = 20f // Минимальное смещение для определения направления
|
||||
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
// Wait for first down
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
awaitEachGesture {
|
||||
// Wait for first down
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
lastDragTime = System.currentTimeMillis()
|
||||
lastDragY = down.position.y
|
||||
currentVelocity = 0f
|
||||
|
||||
var zoom = 1f
|
||||
var pastTouchSlop = false
|
||||
val touchSlop = viewConfiguration.touchSlop
|
||||
var lockedToDismiss = false
|
||||
var zoom = 1f
|
||||
var pastTouchSlop = false
|
||||
var lockedToDismiss = false
|
||||
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
val canceled = event.changes.any { it.isConsumed }
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
val canceled = event.changes.any { it.isConsumed }
|
||||
|
||||
if (!canceled) {
|
||||
val zoomChange = event.calculateZoom()
|
||||
val panChange = event.calculatePan()
|
||||
if (!canceled) {
|
||||
val zoomChange = event.calculateZoom()
|
||||
val panChange = event.calculatePan()
|
||||
|
||||
if (!pastTouchSlop) {
|
||||
zoom *= zoomChange
|
||||
val centroidSize = event.calculateCentroidSize(useCurrent = false)
|
||||
val touchMoved = abs(panChange.x) > touchSlop || abs(panChange.y) > touchSlop
|
||||
val zoomMotion = abs(1 - zoom) * centroidSize > touchSlop
|
||||
if (!pastTouchSlop) {
|
||||
zoom *= zoomChange
|
||||
val centroidSize = event.calculateCentroidSize(useCurrent = false)
|
||||
val touchMoved = abs(panChange.x) > touchSlopValue || abs(panChange.y) > touchSlopValue
|
||||
val zoomMotion = abs(1 - zoom) * centroidSize > touchSlopValue
|
||||
|
||||
if (touchMoved || zoomMotion) {
|
||||
pastTouchSlop = true
|
||||
if (touchMoved || zoomMotion) {
|
||||
pastTouchSlop = true
|
||||
|
||||
// Decide: vertical dismiss or zoom/pan?
|
||||
if (scale <= 1.05f && zoomChange == 1f &&
|
||||
abs(panChange.y) > abs(panChange.x) * 1.5f) {
|
||||
lockedToDismiss = true
|
||||
isVerticalDragging = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pastTouchSlop) {
|
||||
if (lockedToDismiss) {
|
||||
// Vertical drag for dismiss
|
||||
onVerticalDrag(panChange.y)
|
||||
event.changes.forEach { it.consume() }
|
||||
} else {
|
||||
// Zoom and pan
|
||||
val newScale = (scale * zoomChange).coerceIn(minScale, maxScale)
|
||||
|
||||
// Calculate max offsets based on zoom
|
||||
val maxX = (containerSize.width * (newScale - 1) / 2f).coerceAtLeast(0f)
|
||||
val maxY = (containerSize.height * (newScale - 1) / 2f).coerceAtLeast(0f)
|
||||
|
||||
val newOffsetX = (offsetX + panChange.x).coerceIn(-maxX, maxX)
|
||||
val newOffsetY = (offsetY + panChange.y).coerceIn(-maxY, maxY)
|
||||
|
||||
scale = newScale
|
||||
offsetX = newOffsetX
|
||||
offsetY = newOffsetY
|
||||
|
||||
// Consume if zoomed to prevent pager swipe
|
||||
if (scale > 1.05f) {
|
||||
event.changes.forEach { it.consume() }
|
||||
}
|
||||
// Decide: vertical dismiss or zoom/pan?
|
||||
if (scale <= 1.05f && zoomChange == 1f &&
|
||||
abs(panChange.y) > abs(panChange.x) * 1.5f) {
|
||||
lockedToDismiss = true
|
||||
isVerticalDragging = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (event.changes.any { it.pressed })
|
||||
|
||||
// Pointer up - end drag
|
||||
if (isVerticalDragging) {
|
||||
isVerticalDragging = false
|
||||
onDragEnd()
|
||||
}
|
||||
if (pastTouchSlop) {
|
||||
if (lockedToDismiss) {
|
||||
// Calculate velocity for smooth dismiss
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val currentY = event.changes.firstOrNull()?.position?.y ?: lastDragY
|
||||
val timeDelta = (currentTime - lastDragTime).coerceAtLeast(1L)
|
||||
val positionDelta = currentY - lastDragY
|
||||
|
||||
// Snap back if scale is close to 1
|
||||
if (scale < 1.05f) {
|
||||
scale = 1f
|
||||
offsetX = 0f
|
||||
offsetY = 0f
|
||||
// Velocity in px/second
|
||||
currentVelocity = (positionDelta / timeDelta) * 1000f
|
||||
|
||||
lastDragTime = currentTime
|
||||
lastDragY = currentY
|
||||
|
||||
// Vertical drag for dismiss with velocity
|
||||
onVerticalDrag(panChange.y, currentVelocity)
|
||||
event.changes.forEach { it.consume() }
|
||||
} else {
|
||||
// Zoom and pan
|
||||
val newScale = (scale * zoomChange).coerceIn(minScale, maxScale)
|
||||
|
||||
// Calculate max offsets based on zoom
|
||||
val maxX = (containerSize.width * (newScale - 1) / 2f).coerceAtLeast(0f)
|
||||
val maxY = (containerSize.height * (newScale - 1) / 2f).coerceAtLeast(0f)
|
||||
|
||||
val newOffsetX = (offsetX + panChange.x).coerceIn(-maxX, maxX)
|
||||
val newOffsetY = (offsetY + panChange.y).coerceIn(-maxY, maxY)
|
||||
|
||||
scale = newScale
|
||||
offsetX = newOffsetX
|
||||
offsetY = newOffsetY
|
||||
|
||||
// Consume if zoomed to prevent pager swipe
|
||||
if (scale > 1.05f) {
|
||||
event.changes.forEach { it.consume() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (event.changes.any { it.pressed })
|
||||
|
||||
// Pointer up - end drag
|
||||
if (isVerticalDragging) {
|
||||
isVerticalDragging = false
|
||||
onDragEnd()
|
||||
}
|
||||
|
||||
// Snap back if scale is close to 1
|
||||
if (scale < 1.05f) {
|
||||
scale = 1f
|
||||
offsetX = 0f
|
||||
offsetY = 0f
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user