feat: improve swipe back scale behavior across screens
This commit is contained in:
@@ -45,6 +45,7 @@ import com.rosetta.messenger.ui.chats.ConnectionLogsScreen
|
||||
import com.rosetta.messenger.ui.chats.RequestsListScreen
|
||||
import com.rosetta.messenger.ui.chats.SearchScreen
|
||||
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
||||
import com.rosetta.messenger.ui.components.SwipeBackBackgroundEffect
|
||||
import com.rosetta.messenger.ui.components.SwipeBackContainer
|
||||
import com.rosetta.messenger.ui.crashlogs.CrashLogsScreen
|
||||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||||
@@ -688,65 +689,67 @@ fun MainScreen(
|
||||
// 🔥 Простая навигация с swipe back
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Base layer - chats list (всегда видимый, чтобы его было видно при свайпе)
|
||||
ChatsListScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountName = accountName,
|
||||
accountUsername = accountUsername,
|
||||
accountVerified = accountVerified,
|
||||
accountPhone = accountPhone,
|
||||
accountPublicKey = accountPublicKey,
|
||||
accountPrivateKey = accountPrivateKey,
|
||||
privateKeyHash = privateKeyHash,
|
||||
onToggleTheme = onToggleTheme,
|
||||
onProfileClick = { pushScreen(Screen.Profile) },
|
||||
onNewGroupClick = {
|
||||
// TODO: Navigate to new group
|
||||
},
|
||||
onContactsClick = {
|
||||
// TODO: Navigate to contacts
|
||||
},
|
||||
onCallsClick = {
|
||||
// TODO: Navigate to calls
|
||||
},
|
||||
onSavedMessagesClick = {
|
||||
// Открываем чат с самим собой (Saved Messages)
|
||||
pushScreen(
|
||||
Screen.ChatDetail(
|
||||
SearchUser(
|
||||
title = "Saved Messages",
|
||||
username = "",
|
||||
publicKey = accountPublicKey,
|
||||
verified = 0,
|
||||
online = 1
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
onSettingsClick = { pushScreen(Screen.Profile) },
|
||||
onInviteFriendsClick = {
|
||||
// TODO: Share invite link
|
||||
},
|
||||
onSearchClick = { pushScreen(Screen.Search) },
|
||||
onRequestsClick = { pushScreen(Screen.Requests) },
|
||||
onNewChat = {
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onUserSelect = { selectedChatUser ->
|
||||
pushScreen(Screen.ChatDetail(selectedChatUser))
|
||||
},
|
||||
backgroundBlurColorId = backgroundBlurColorId,
|
||||
pinnedChats = pinnedChats,
|
||||
onTogglePin = { opponentKey ->
|
||||
mainScreenScope.launch { prefsManager.togglePinChat(opponentKey) }
|
||||
},
|
||||
chatsViewModel = chatsListViewModel,
|
||||
avatarRepository = avatarRepository,
|
||||
onAddAccount = {
|
||||
onAddAccount()
|
||||
},
|
||||
onSwitchAccount = onSwitchAccount,
|
||||
onDeleteAccountFromSidebar = onDeleteAccountFromSidebar
|
||||
)
|
||||
SwipeBackBackgroundEffect(modifier = Modifier.fillMaxSize()) {
|
||||
ChatsListScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountName = accountName,
|
||||
accountUsername = accountUsername,
|
||||
accountVerified = accountVerified,
|
||||
accountPhone = accountPhone,
|
||||
accountPublicKey = accountPublicKey,
|
||||
accountPrivateKey = accountPrivateKey,
|
||||
privateKeyHash = privateKeyHash,
|
||||
onToggleTheme = onToggleTheme,
|
||||
onProfileClick = { pushScreen(Screen.Profile) },
|
||||
onNewGroupClick = {
|
||||
// TODO: Navigate to new group
|
||||
},
|
||||
onContactsClick = {
|
||||
// TODO: Navigate to contacts
|
||||
},
|
||||
onCallsClick = {
|
||||
// TODO: Navigate to calls
|
||||
},
|
||||
onSavedMessagesClick = {
|
||||
// Открываем чат с самим собой (Saved Messages)
|
||||
pushScreen(
|
||||
Screen.ChatDetail(
|
||||
SearchUser(
|
||||
title = "Saved Messages",
|
||||
username = "",
|
||||
publicKey = accountPublicKey,
|
||||
verified = 0,
|
||||
online = 1
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
onSettingsClick = { pushScreen(Screen.Profile) },
|
||||
onInviteFriendsClick = {
|
||||
// TODO: Share invite link
|
||||
},
|
||||
onSearchClick = { pushScreen(Screen.Search) },
|
||||
onRequestsClick = { pushScreen(Screen.Requests) },
|
||||
onNewChat = {
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onUserSelect = { selectedChatUser ->
|
||||
pushScreen(Screen.ChatDetail(selectedChatUser))
|
||||
},
|
||||
backgroundBlurColorId = backgroundBlurColorId,
|
||||
pinnedChats = pinnedChats,
|
||||
onTogglePin = { opponentKey ->
|
||||
mainScreenScope.launch { prefsManager.togglePinChat(opponentKey) }
|
||||
},
|
||||
chatsViewModel = chatsListViewModel,
|
||||
avatarRepository = avatarRepository,
|
||||
onAddAccount = {
|
||||
onAddAccount()
|
||||
},
|
||||
onSwitchAccount = onSwitchAccount,
|
||||
onDeleteAccountFromSidebar = onDeleteAccountFromSidebar
|
||||
)
|
||||
}
|
||||
|
||||
SwipeBackContainer(
|
||||
isVisible = isRequestsVisible,
|
||||
@@ -935,7 +938,8 @@ fun MainScreen(
|
||||
isVisible = selectedUser != null,
|
||||
onBack = { popChatAndChildren() },
|
||||
isDarkTheme = isDarkTheme,
|
||||
swipeEnabled = !isChatSwipeLocked
|
||||
swipeEnabled = !isChatSwipeLocked,
|
||||
propagateBackgroundProgress = false
|
||||
) {
|
||||
selectedUser?.let { currentChatUser ->
|
||||
// Экран чата
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
@@ -32,6 +33,8 @@ private const val EDGE_ZONE_DP = 320
|
||||
private const val EDGE_ZONE_WIDTH_FRACTION = 0.85f
|
||||
private const val TOUCH_SLOP_FACTOR = 0.35f
|
||||
private const val HORIZONTAL_DOMINANCE_RATIO = 1.05f
|
||||
private const val BACKGROUND_MIN_SCALE = 0.94f
|
||||
private const val BACKGROUND_PARALLAX_DP = 18
|
||||
|
||||
/**
|
||||
* Telegram-style swipe back container (optimized)
|
||||
@@ -45,12 +48,43 @@ private const val HORIZONTAL_DOMINANCE_RATIO = 1.05f
|
||||
* - Shadow on left edge during swipe
|
||||
* - Low completion threshold for comfortable one-handed back swipe
|
||||
*/
|
||||
private object SwipeBackSharedProgress {
|
||||
var ownerId by mutableLongStateOf(Long.MIN_VALUE)
|
||||
var progress by mutableFloatStateOf(0f)
|
||||
var active by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwipeBackBackgroundEffect(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val progress = SwipeBackSharedProgress.progress.coerceIn(0f, 1f)
|
||||
val active = SwipeBackSharedProgress.active
|
||||
val parallaxPx = with(density) { BACKGROUND_PARALLAX_DP.dp.toPx() }
|
||||
val scale = if (active) BACKGROUND_MIN_SCALE + (1f - BACKGROUND_MIN_SCALE) * progress else 1f
|
||||
val backgroundTranslationX = if (active) -parallaxPx * (1f - progress) else 0f
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
modifier.graphicsLayer {
|
||||
transformOrigin = TransformOrigin(0f, 0.5f)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
translationX = backgroundTranslationX
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwipeBackContainer(
|
||||
isVisible: Boolean,
|
||||
onBack: () -> Unit,
|
||||
isDarkTheme: Boolean,
|
||||
swipeEnabled: Boolean = true,
|
||||
propagateBackgroundProgress: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
// 🚀 Lazy composition: skip ALL setup until the screen is opened for the first time.
|
||||
@@ -64,6 +98,8 @@ fun SwipeBackContainer(
|
||||
val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() }
|
||||
val edgeZonePx = with(density) { EDGE_ZONE_DP.dp.toPx() }
|
||||
val effectiveEdgeZonePx = maxOf(edgeZonePx, screenWidthPx * EDGE_ZONE_WIDTH_FRACTION)
|
||||
val backgroundParallaxPx = with(density) { BACKGROUND_PARALLAX_DP.dp.toPx() }
|
||||
val containerId = remember { System.identityHashCode(Any()).toLong() }
|
||||
|
||||
// Animation state for swipe (used only for swipe animations, not during drag)
|
||||
val offsetAnimatable = remember { Animatable(0f) }
|
||||
@@ -97,6 +133,38 @@ fun SwipeBackContainer(
|
||||
|
||||
// Scrim alpha based on swipe progress
|
||||
val scrimAlpha = (1f - (currentOffset / screenWidthPx).coerceIn(0f, 1f)) * 0.4f
|
||||
val sharedOwnerId = SwipeBackSharedProgress.ownerId
|
||||
val sharedProgress = SwipeBackSharedProgress.progress
|
||||
val sharedActive = SwipeBackSharedProgress.active
|
||||
val isBackgroundForActiveSwipe = sharedActive && sharedOwnerId != containerId
|
||||
val backgroundScale =
|
||||
if (isBackgroundForActiveSwipe) {
|
||||
BACKGROUND_MIN_SCALE + (1f - BACKGROUND_MIN_SCALE) * sharedProgress.coerceIn(0f, 1f)
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
val backgroundTranslationX =
|
||||
if (isBackgroundForActiveSwipe) {
|
||||
-backgroundParallaxPx * (1f - sharedProgress.coerceIn(0f, 1f))
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
fun updateSharedSwipeProgress(progress: Float, active: Boolean) {
|
||||
if (!propagateBackgroundProgress) return
|
||||
SwipeBackSharedProgress.ownerId = containerId
|
||||
SwipeBackSharedProgress.progress = progress.coerceIn(0f, 1f)
|
||||
SwipeBackSharedProgress.active = active
|
||||
}
|
||||
|
||||
fun clearSharedSwipeProgressIfOwner() {
|
||||
if (!propagateBackgroundProgress) return
|
||||
if (SwipeBackSharedProgress.ownerId == containerId) {
|
||||
SwipeBackSharedProgress.ownerId = Long.MIN_VALUE
|
||||
SwipeBackSharedProgress.progress = 0f
|
||||
SwipeBackSharedProgress.active = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle visibility changes
|
||||
// 🔥 FIX: try/finally ensures animation flags are ALWAYS reset even if
|
||||
@@ -134,13 +202,26 @@ fun SwipeBackContainer(
|
||||
shouldShow = false
|
||||
isAnimatingOut = false
|
||||
dragOffset = 0f
|
||||
clearSharedSwipeProgressIfOwner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) { onDispose { clearSharedSwipeProgressIfOwner() } }
|
||||
|
||||
if (!shouldShow && !isAnimatingIn && !isAnimatingOut) return
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize().graphicsLayer {
|
||||
if (isBackgroundForActiveSwipe) {
|
||||
transformOrigin = TransformOrigin(0f, 0.5f)
|
||||
scaleX = backgroundScale
|
||||
scaleY = backgroundScale
|
||||
translationX = backgroundTranslationX
|
||||
}
|
||||
}
|
||||
) {
|
||||
// Scrim (dimming layer behind the screen) - only when swiping
|
||||
if (currentOffset > 0f) {
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = scrimAlpha)))
|
||||
@@ -225,6 +306,12 @@ fun SwipeBackContainer(
|
||||
startedSwipe = true
|
||||
isDragging = true
|
||||
dragOffset = offsetAnimatable.value
|
||||
updateSharedSwipeProgress(
|
||||
progress =
|
||||
dragOffset /
|
||||
screenWidthPx,
|
||||
active = true
|
||||
)
|
||||
|
||||
val imm =
|
||||
context.getSystemService(
|
||||
@@ -251,6 +338,12 @@ fun SwipeBackContainer(
|
||||
0f,
|
||||
screenWidthPx
|
||||
)
|
||||
updateSharedSwipeProgress(
|
||||
progress =
|
||||
dragOffset /
|
||||
screenWidthPx,
|
||||
active = true
|
||||
)
|
||||
velocityTracker.addPosition(
|
||||
change.uptimeMillis,
|
||||
change.position
|
||||
@@ -281,6 +374,12 @@ fun SwipeBackContainer(
|
||||
|
||||
scope.launch {
|
||||
offsetAnimatable.snapTo(dragOffset)
|
||||
updateSharedSwipeProgress(
|
||||
progress =
|
||||
dragOffset /
|
||||
screenWidthPx,
|
||||
active = true
|
||||
)
|
||||
|
||||
if (shouldComplete) {
|
||||
offsetAnimatable.animateTo(
|
||||
@@ -291,7 +390,15 @@ fun SwipeBackContainer(
|
||||
ANIMATION_DURATION_EXIT,
|
||||
easing =
|
||||
TelegramEasing
|
||||
)
|
||||
),
|
||||
block = {
|
||||
updateSharedSwipeProgress(
|
||||
progress =
|
||||
value /
|
||||
screenWidthPx,
|
||||
active = true
|
||||
)
|
||||
}
|
||||
)
|
||||
// 🔥 FIX: Reset state BEFORE onBack() to prevent
|
||||
// redundant fade-out animation in LaunchedEffect.
|
||||
@@ -300,6 +407,7 @@ fun SwipeBackContainer(
|
||||
// overlay blocks all touches on the chat list.
|
||||
shouldShow = false
|
||||
dragOffset = 0f
|
||||
clearSharedSwipeProgressIfOwner()
|
||||
onBack()
|
||||
} else {
|
||||
offsetAnimatable.animateTo(
|
||||
@@ -310,9 +418,18 @@ fun SwipeBackContainer(
|
||||
ANIMATION_DURATION_EXIT,
|
||||
easing =
|
||||
TelegramEasing
|
||||
)
|
||||
),
|
||||
block = {
|
||||
updateSharedSwipeProgress(
|
||||
progress =
|
||||
value /
|
||||
screenWidthPx,
|
||||
active = true
|
||||
)
|
||||
}
|
||||
)
|
||||
dragOffset = 0f
|
||||
clearSharedSwipeProgressIfOwner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user