Синхронизированы анимации чата и Requests с Telegram, добавлен shimmer и исправлено копирование текста
This commit is contained in:
@@ -49,6 +49,7 @@ 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.SwipeBackBackgroundEffect
|
||||||
import com.rosetta.messenger.ui.components.SwipeBackContainer
|
import com.rosetta.messenger.ui.components.SwipeBackContainer
|
||||||
|
import com.rosetta.messenger.ui.components.SwipeBackEnterAnimation
|
||||||
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
|
||||||
import com.rosetta.messenger.ui.settings.BackupScreen
|
import com.rosetta.messenger.ui.settings.BackupScreen
|
||||||
@@ -1029,6 +1030,7 @@ fun MainScreen(
|
|||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
layer = 1,
|
layer = 1,
|
||||||
swipeEnabled = !isChatSwipeLocked,
|
swipeEnabled = !isChatSwipeLocked,
|
||||||
|
enterAnimation = SwipeBackEnterAnimation.SlideFromRight,
|
||||||
propagateBackgroundProgress = false
|
propagateBackgroundProgress = false
|
||||||
) {
|
) {
|
||||||
selectedUser?.let { currentChatUser ->
|
selectedUser?.let { currentChatUser ->
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ import androidx.compose.animation.AnimatedContent
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.SizeTransform
|
import androidx.compose.animation.SizeTransform
|
||||||
@@ -53,6 +58,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
@@ -119,6 +125,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private val groupMembersCountCache = java.util.concurrent.ConcurrentHashMap<String, Int>()
|
||||||
|
|
||||||
private data class IncomingRunAvatarAccumulator(
|
private data class IncomingRunAvatarAccumulator(
|
||||||
val senderPublicKey: String,
|
val senderPublicKey: String,
|
||||||
val senderDisplayName: String,
|
val senderDisplayName: String,
|
||||||
@@ -515,11 +523,15 @@ fun ChatDetailScreen(
|
|||||||
val chatsListViewModel: ChatsListViewModel = viewModel()
|
val chatsListViewModel: ChatsListViewModel = viewModel()
|
||||||
val dialogsList by chatsListViewModel.dialogs.collectAsState()
|
val dialogsList by chatsListViewModel.dialogs.collectAsState()
|
||||||
val groupRepository = remember { GroupRepository.getInstance(context) }
|
val groupRepository = remember { GroupRepository.getInstance(context) }
|
||||||
|
val groupMembersCacheKey =
|
||||||
|
remember(user.publicKey, currentUserPublicKey) {
|
||||||
|
"${currentUserPublicKey.trim()}::${user.publicKey.trim()}"
|
||||||
|
}
|
||||||
var groupAdminKeys by remember(user.publicKey, currentUserPublicKey) {
|
var groupAdminKeys by remember(user.publicKey, currentUserPublicKey) {
|
||||||
mutableStateOf<Set<String>>(emptySet())
|
mutableStateOf<Set<String>>(emptySet())
|
||||||
}
|
}
|
||||||
var groupMembersCount by remember(user.publicKey, currentUserPublicKey) {
|
var groupMembersCount by remember(groupMembersCacheKey) {
|
||||||
mutableStateOf<Int?>(null)
|
mutableStateOf<Int?>(groupMembersCountCache[groupMembersCacheKey])
|
||||||
}
|
}
|
||||||
var mentionCandidates by remember(user.publicKey, currentUserPublicKey) {
|
var mentionCandidates by remember(user.publicKey, currentUserPublicKey) {
|
||||||
mutableStateOf<List<MentionCandidate>>(emptyList())
|
mutableStateOf<List<MentionCandidate>>(emptyList())
|
||||||
@@ -540,14 +552,25 @@ fun ChatDetailScreen(
|
|||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cachedMembersCount = groupMembersCountCache[groupMembersCacheKey]
|
||||||
|
groupMembersCount = cachedMembersCount
|
||||||
|
|
||||||
val members = withContext(Dispatchers.IO) {
|
val members = withContext(Dispatchers.IO) {
|
||||||
groupRepository.requestGroupMembers(user.publicKey).orEmpty()
|
groupRepository.requestGroupMembers(user.publicKey)
|
||||||
|
}
|
||||||
|
if (members == null) {
|
||||||
|
groupAdminKeys = emptySet()
|
||||||
|
mentionCandidates = emptyList()
|
||||||
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
val normalizedMembers =
|
val normalizedMembers =
|
||||||
members.map { it.trim() }
|
members.map { it.trim() }
|
||||||
.filter { it.isNotBlank() }
|
.filter { it.isNotBlank() }
|
||||||
.distinct()
|
.distinct()
|
||||||
groupMembersCount = normalizedMembers.size
|
groupMembersCount = normalizedMembers.size
|
||||||
|
if (normalizedMembers.isNotEmpty()) {
|
||||||
|
groupMembersCountCache[groupMembersCacheKey] = normalizedMembers.size
|
||||||
|
}
|
||||||
|
|
||||||
val adminKey = normalizedMembers.firstOrNull().orEmpty()
|
val adminKey = normalizedMembers.firstOrNull().orEmpty()
|
||||||
groupAdminKeys =
|
groupAdminKeys =
|
||||||
@@ -1006,9 +1029,13 @@ fun ChatDetailScreen(
|
|||||||
val isRosettaOfficial = user.title.equals("Rosetta", ignoreCase = true) ||
|
val isRosettaOfficial = user.title.equals("Rosetta", ignoreCase = true) ||
|
||||||
user.username.equals("rosetta", ignoreCase = true) ||
|
user.username.equals("rosetta", ignoreCase = true) ||
|
||||||
isSystemAccount
|
isSystemAccount
|
||||||
val groupMembersSubtitleCount = groupMembersCount ?: 0
|
val groupMembersSubtitleCount = groupMembersCount
|
||||||
val groupMembersSubtitle =
|
val groupMembersSubtitle =
|
||||||
"$groupMembersSubtitleCount member${if (groupMembersSubtitleCount == 1) "" else "s"}"
|
if (groupMembersSubtitleCount == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"$groupMembersSubtitleCount member${if (groupMembersSubtitleCount == 1) "" else "s"}"
|
||||||
|
}
|
||||||
val chatSubtitle =
|
val chatSubtitle =
|
||||||
when {
|
when {
|
||||||
isSavedMessages -> "Notes"
|
isSavedMessages -> "Notes"
|
||||||
@@ -1249,19 +1276,10 @@ fun ChatDetailScreen(
|
|||||||
extractCopyableMessageText(
|
extractCopyableMessageText(
|
||||||
msg
|
msg
|
||||||
)
|
)
|
||||||
if (messageText.isBlank()) {
|
messageText
|
||||||
null
|
.takeIf {
|
||||||
} else {
|
it.isNotBlank()
|
||||||
val time =
|
}
|
||||||
SimpleDateFormat(
|
|
||||||
"HH:mm",
|
|
||||||
Locale.getDefault()
|
|
||||||
)
|
|
||||||
.format(
|
|
||||||
msg.timestamp
|
|
||||||
)
|
|
||||||
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n$messageText"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.joinToString(
|
.joinToString(
|
||||||
"\n\n"
|
"\n\n"
|
||||||
@@ -1582,6 +1600,10 @@ fun ChatDetailScreen(
|
|||||||
isDarkTheme =
|
isDarkTheme =
|
||||||
isDarkTheme
|
isDarkTheme
|
||||||
)
|
)
|
||||||
|
} else if (isGroupChat &&
|
||||||
|
groupMembersCount == null
|
||||||
|
) {
|
||||||
|
GroupMembersSubtitleSkeleton()
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
@@ -3597,6 +3619,53 @@ fun ChatDetailScreen(
|
|||||||
} // Закрытие outer Box
|
} // Закрытие outer Box
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GroupMembersSubtitleSkeleton() {
|
||||||
|
val transition = rememberInfiniteTransition(label = "groupMembersSkeleton")
|
||||||
|
val shimmerProgress by
|
||||||
|
transition.animateFloat(
|
||||||
|
initialValue = -1f,
|
||||||
|
targetValue = 2f,
|
||||||
|
animationSpec =
|
||||||
|
infiniteRepeatable(
|
||||||
|
animation = tween(1200, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
),
|
||||||
|
label = "groupMembersSkeletonShimmer"
|
||||||
|
)
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(top = 2.dp)
|
||||||
|
.width(76.dp)
|
||||||
|
.height(12.dp)
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val widthPx = with(density) { maxWidth.toPx() }.coerceAtLeast(1f)
|
||||||
|
val gradientWidthPx = with(density) { 44.dp.toPx() }
|
||||||
|
val shimmerX = shimmerProgress * widthPx
|
||||||
|
|
||||||
|
val shimmerBrush =
|
||||||
|
Brush.linearGradient(
|
||||||
|
colorStops =
|
||||||
|
arrayOf(
|
||||||
|
0f to Color.White.copy(alpha = 0.22f),
|
||||||
|
0.5f to Color.White.copy(alpha = 0.56f),
|
||||||
|
1f to Color.White.copy(alpha = 0.22f)
|
||||||
|
),
|
||||||
|
start = Offset(shimmerX - gradientWidthPx, 0f),
|
||||||
|
end = Offset(shimmerX, 0f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(shimmerBrush)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TiledChatWallpaper(
|
private fun TiledChatWallpaper(
|
||||||
wallpaperResId: Int,
|
wallpaperResId: Int,
|
||||||
|
|||||||
@@ -2171,39 +2171,102 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestsCount > 0) {
|
item(key = "requests_section") {
|
||||||
item(
|
val isRequestsSectionVisible =
|
||||||
key =
|
requestsCount > 0 &&
|
||||||
"requests_section"
|
isRequestsVisible
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible =
|
||||||
|
isRequestsSectionVisible,
|
||||||
|
enter =
|
||||||
|
slideInVertically(
|
||||||
|
initialOffsetY = {
|
||||||
|
fullHeight ->
|
||||||
|
-fullHeight /
|
||||||
|
3
|
||||||
|
},
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
260,
|
||||||
|
easing =
|
||||||
|
FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
) +
|
||||||
|
expandVertically(
|
||||||
|
expandFrom =
|
||||||
|
Alignment
|
||||||
|
.Top,
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
260,
|
||||||
|
easing =
|
||||||
|
FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
) +
|
||||||
|
fadeIn(
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
180
|
||||||
|
),
|
||||||
|
initialAlpha =
|
||||||
|
0.7f
|
||||||
|
),
|
||||||
|
exit =
|
||||||
|
slideOutVertically(
|
||||||
|
targetOffsetY = {
|
||||||
|
fullHeight ->
|
||||||
|
-fullHeight /
|
||||||
|
3
|
||||||
|
},
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
220,
|
||||||
|
easing =
|
||||||
|
FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
) +
|
||||||
|
shrinkVertically(
|
||||||
|
shrinkTowards =
|
||||||
|
Alignment
|
||||||
|
.Top,
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
220,
|
||||||
|
easing =
|
||||||
|
FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
) +
|
||||||
|
fadeOut(
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis =
|
||||||
|
140
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
Column {
|
||||||
visible = isRequestsVisible,
|
RequestsSection(
|
||||||
enter = expandVertically(
|
count =
|
||||||
animationSpec = tween(250, easing = FastOutSlowInEasing)
|
requestsCount,
|
||||||
) + fadeIn(animationSpec = tween(200)),
|
requests =
|
||||||
exit = shrinkVertically(
|
requests,
|
||||||
animationSpec = tween(250, easing = FastOutSlowInEasing)
|
isDarkTheme =
|
||||||
) + fadeOut(animationSpec = tween(200))
|
isDarkTheme,
|
||||||
) {
|
onClick = {
|
||||||
Column {
|
openRequestsRouteSafely()
|
||||||
RequestsSection(
|
}
|
||||||
count =
|
)
|
||||||
requestsCount,
|
Divider(
|
||||||
requests =
|
color =
|
||||||
requests,
|
dividerColor,
|
||||||
isDarkTheme =
|
thickness =
|
||||||
isDarkTheme,
|
0.5.dp
|
||||||
onClick = {
|
)
|
||||||
openRequestsRouteSafely()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Divider(
|
|
||||||
color =
|
|
||||||
dividerColor,
|
|
||||||
thickness =
|
|
||||||
0.5.dp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.rosetta.messenger.ui.components
|
package com.rosetta.messenger.ui.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -19,15 +20,21 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
// Telegram's CubicBezierInterpolator(0.25, 0.1, 0.25, 1.0)
|
// Telegram's CubicBezierInterpolator(0.25, 0.1, 0.25, 1.0)
|
||||||
private val TelegramEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
|
private val TelegramEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
|
||||||
|
private val TelegramOpenDecelerateEasing =
|
||||||
|
Easing { input -> DecelerateInterpolator(1.5f).getInterpolation(input) }
|
||||||
|
|
||||||
// Swipe-back thresholds tuned for ultra-light navigation.
|
// Swipe-back thresholds tuned for ultra-light navigation.
|
||||||
private const val COMPLETION_THRESHOLD = 0.05f // 5% of screen width
|
private const val COMPLETION_THRESHOLD = 0.05f // 5% of screen width
|
||||||
private const val FLING_VELOCITY_THRESHOLD = 35f // px/s
|
private const val FLING_VELOCITY_THRESHOLD = 35f // px/s
|
||||||
private const val ANIMATION_DURATION_ENTER = 300
|
private const val ANIMATION_DURATION_ENTER = 300
|
||||||
|
private const val ANIMATION_DURATION_ENTER_TELEGRAM_CHAT = 150
|
||||||
private const val ANIMATION_DURATION_EXIT = 200
|
private const val ANIMATION_DURATION_EXIT = 200
|
||||||
private const val EDGE_ZONE_DP = 320
|
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
|
||||||
@@ -36,6 +43,42 @@ private const val HORIZONTAL_DOMINANCE_RATIO = 1.05f
|
|||||||
private const val BACKGROUND_MIN_SCALE = 0.97f
|
private const val BACKGROUND_MIN_SCALE = 0.97f
|
||||||
private const val BACKGROUND_PARALLAX_DP = 4
|
private const val BACKGROUND_PARALLAX_DP = 4
|
||||||
|
|
||||||
|
enum class SwipeBackEnterAnimation {
|
||||||
|
Fade,
|
||||||
|
SlideFromRight
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun runTelegramChatEnterAnimation(
|
||||||
|
alphaAnimatable: Animatable<Float, AnimationVector1D>,
|
||||||
|
enterOffsetAnimatable: Animatable<Float, AnimationVector1D>,
|
||||||
|
startOffsetPx: Float
|
||||||
|
) {
|
||||||
|
var progress = 0f
|
||||||
|
var lastFrameTimeMs = withFrameNanos { it } / 1_000_000L
|
||||||
|
|
||||||
|
while (coroutineContext.isActive && progress < 1f) {
|
||||||
|
val nowMs = withFrameNanos { it } / 1_000_000L
|
||||||
|
var dtMs = nowMs - lastFrameTimeMs
|
||||||
|
if (dtMs > 40L && progress == 0f) {
|
||||||
|
dtMs = 0L
|
||||||
|
} else if (dtMs > 18L) {
|
||||||
|
dtMs = 18L
|
||||||
|
}
|
||||||
|
lastFrameTimeMs = nowMs
|
||||||
|
|
||||||
|
progress =
|
||||||
|
(progress + (dtMs / ANIMATION_DURATION_ENTER_TELEGRAM_CHAT.toFloat()))
|
||||||
|
.coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
val interpolated = TelegramOpenDecelerateEasing.transform(progress).coerceIn(0f, 1f)
|
||||||
|
alphaAnimatable.snapTo(interpolated)
|
||||||
|
enterOffsetAnimatable.snapTo(startOffsetPx * (1f - interpolated))
|
||||||
|
}
|
||||||
|
|
||||||
|
alphaAnimatable.snapTo(1f)
|
||||||
|
enterOffsetAnimatable.snapTo(0f)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Telegram-style swipe back container (optimized)
|
* Telegram-style swipe back container (optimized)
|
||||||
*
|
*
|
||||||
@@ -89,6 +132,7 @@ fun SwipeBackContainer(
|
|||||||
swipeEnabled: Boolean = true,
|
swipeEnabled: Boolean = true,
|
||||||
propagateBackgroundProgress: Boolean = true,
|
propagateBackgroundProgress: Boolean = true,
|
||||||
deferToChildren: Boolean = false,
|
deferToChildren: Boolean = false,
|
||||||
|
enterAnimation: SwipeBackEnterAnimation = SwipeBackEnterAnimation.Fade,
|
||||||
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.
|
||||||
@@ -103,10 +147,12 @@ fun SwipeBackContainer(
|
|||||||
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 backgroundParallaxPx = with(density) { BACKGROUND_PARALLAX_DP.dp.toPx() }
|
||||||
|
val chatEnterOffsetPx = with(density) { 48.dp.toPx() }
|
||||||
val containerId = remember { System.identityHashCode(Any()).toLong() }
|
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) }
|
||||||
|
val enterOffsetAnimatable = remember { Animatable(0f) }
|
||||||
|
|
||||||
// Alpha animation for fade-in entry
|
// Alpha animation for fade-in entry
|
||||||
val alphaAnimatable = remember { Animatable(0f) }
|
val alphaAnimatable = remember { Animatable(0f) }
|
||||||
@@ -129,8 +175,15 @@ fun SwipeBackContainer(
|
|||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
// Current offset: use drag offset during drag, animatable otherwise
|
// Current offset: use drag offset during drag, animatable otherwise + optional enter slide
|
||||||
val currentOffset = if (isDragging) dragOffset else offsetAnimatable.value
|
val baseOffset = if (isDragging) dragOffset else offsetAnimatable.value
|
||||||
|
val enterOffset =
|
||||||
|
if (isAnimatingIn && enterAnimation == SwipeBackEnterAnimation.SlideFromRight) {
|
||||||
|
enterOffsetAnimatable.value
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
val currentOffset = baseOffset + enterOffset
|
||||||
|
|
||||||
// Current alpha: use animatable during fade animations, otherwise 1
|
// Current alpha: use animatable during fade animations, otherwise 1
|
||||||
val currentAlpha = if (isAnimatingIn || isAnimatingOut) alphaAnimatable.value else 1f
|
val currentAlpha = if (isAnimatingIn || isAnimatingOut) alphaAnimatable.value else 1f
|
||||||
@@ -186,17 +239,29 @@ fun SwipeBackContainer(
|
|||||||
isAnimatingIn = true
|
isAnimatingIn = true
|
||||||
try {
|
try {
|
||||||
offsetAnimatable.snapTo(0f) // No slide for entry
|
offsetAnimatable.snapTo(0f) // No slide for entry
|
||||||
|
dragOffset = 0f
|
||||||
alphaAnimatable.snapTo(0f)
|
alphaAnimatable.snapTo(0f)
|
||||||
alphaAnimatable.animateTo(
|
if (enterAnimation == SwipeBackEnterAnimation.SlideFromRight) {
|
||||||
targetValue = 1f,
|
enterOffsetAnimatable.snapTo(chatEnterOffsetPx)
|
||||||
animationSpec =
|
runTelegramChatEnterAnimation(
|
||||||
tween(
|
alphaAnimatable = alphaAnimatable,
|
||||||
durationMillis = ANIMATION_DURATION_ENTER,
|
enterOffsetAnimatable = enterOffsetAnimatable,
|
||||||
easing = FastOutSlowInEasing
|
startOffsetPx = chatEnterOffsetPx
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
|
enterOffsetAnimatable.snapTo(0f)
|
||||||
|
alphaAnimatable.animateTo(
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec =
|
||||||
|
tween(
|
||||||
|
durationMillis = ANIMATION_DURATION_ENTER,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isAnimatingIn = false
|
isAnimatingIn = false
|
||||||
|
enterOffsetAnimatable.snapTo(0f)
|
||||||
}
|
}
|
||||||
} else if (!isVisible && shouldShow) {
|
} else if (!isVisible && shouldShow) {
|
||||||
// Animate out: fade-out (when triggered by button, not swipe)
|
// Animate out: fade-out (when triggered by button, not swipe)
|
||||||
@@ -231,7 +296,7 @@ fun SwipeBackContainer(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// Scrim (dimming layer behind the screen) - only when swiping
|
// Scrim (dimming layer behind the screen) - only when swiping
|
||||||
if (currentOffset > 0f) {
|
if (!isAnimatingIn && currentOffset > 0f) {
|
||||||
Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = scrimAlpha)))
|
Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = scrimAlpha)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user