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