fix: enhance avatar expansion and snapping behavior for smoother interactions
This commit is contained in:
@@ -546,52 +546,52 @@ fun ProfileMetaballOverlay(
|
|||||||
val avatarCenterY = avatarState.centerY
|
val avatarCenterY = avatarState.centerY
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// UNIFIED SCALE для всех режимов:
|
// 🔥 UNIFIED SCALE - БЕЗ резких переключений when
|
||||||
// - Normal (expansion=0, collapse=0): scale = 1.0
|
// Всегда вычисляем scale на основе avatarState.radius
|
||||||
// - Expansion (expansion>0): scale растёт до screenWidth/baseSizePx
|
// При expansion - дополнительно интерполируем к screenWidth
|
||||||
// - Collapse (collapse>0): scale = avatarState.radius * 2 / baseSizePx
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val uniformScale: Float
|
|
||||||
val currentCenterX: Float
|
|
||||||
val currentCenterY: Float
|
|
||||||
val cornerRadiusPx: Float
|
|
||||||
val applyBlur: Boolean
|
|
||||||
|
|
||||||
when {
|
// Базовый scale из avatarState.radius (работает для collapse И normal)
|
||||||
expansionProgress > 0f -> {
|
val baseScale = (avatarState.radius * 2f) / baseSizePx
|
||||||
// EXPANSION MODE
|
|
||||||
val targetSize = screenWidthPx
|
|
||||||
uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
|
|
||||||
|
|
||||||
// Центр смещается к центру header
|
// При expansion: дополнительно масштабируем к screenWidth
|
||||||
|
val targetExpansionScale = screenWidthPx / baseSizePx
|
||||||
|
val uniformScale = if (expansionProgress > 0f) {
|
||||||
|
// Плавный переход от baseScale к targetExpansionScale
|
||||||
|
lerpFloat(baseScale, targetExpansionScale, expansionProgress)
|
||||||
|
} else {
|
||||||
|
baseScale
|
||||||
|
}.coerceAtLeast(0.01f) // Защита от деления на 0
|
||||||
|
|
||||||
|
// Центр: при expansion двигается к центру header
|
||||||
val targetCenterX = screenWidthPx / 2f
|
val targetCenterX = screenWidthPx / 2f
|
||||||
val targetCenterY = headerHeightPx / 2f
|
val targetCenterY = headerHeightPx / 2f
|
||||||
currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
val currentCenterX = if (expansionProgress > 0f) {
|
||||||
currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
||||||
|
} else {
|
||||||
|
avatarCenterX
|
||||||
|
}
|
||||||
|
val currentCenterY = if (expansionProgress > 0f) {
|
||||||
|
lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
||||||
|
} else {
|
||||||
|
avatarCenterY
|
||||||
|
}
|
||||||
|
|
||||||
// Corner radius: круг → квадрат
|
// Corner radius: для base size (120dp)
|
||||||
cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
// В normal/collapse: из avatarState, но пересчитанный для base size
|
||||||
applyBlur = false
|
// При expansion: круг → квадрат
|
||||||
}
|
val cornerRadiusPx: Float = if (expansionProgress > 0f) {
|
||||||
collapseProgress > 0f -> {
|
// Expansion: плавно круг → квадрат
|
||||||
// COLLAPSE MODE - используем avatarState.radius через scale
|
lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
||||||
uniformScale = (avatarState.radius * 2f) / baseSizePx
|
} else {
|
||||||
currentCenterX = avatarCenterX
|
// Normal/Collapse: используем avatarState.cornerRadius
|
||||||
currentCenterY = avatarCenterY
|
// Пересчитываем для base size: cornerRadius_base = cornerRadius_current / baseScale
|
||||||
// cornerRadius нужен относительно BASE size, поэтому делим на scale
|
(avatarState.cornerRadius / baseScale).coerceAtMost(baseSizePx / 2f)
|
||||||
cornerRadiusPx = avatarState.cornerRadius / uniformScale
|
|
||||||
applyBlur = avatarState.blurRadius > 0.5f
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// NORMAL MODE
|
|
||||||
uniformScale = 1f
|
|
||||||
currentCenterX = avatarCenterX
|
|
||||||
currentCenterY = avatarCenterY
|
|
||||||
cornerRadiusPx = baseSizePx / 2f // Полный круг
|
|
||||||
applyBlur = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blur только при collapse близко к notch
|
||||||
|
val applyBlur = expansionProgress == 0f && avatarState.blurRadius > 0.5f
|
||||||
|
|
||||||
// Offset: Box имеет ФИКСИРОВАННЫЙ размер, scale применяется от центра
|
// Offset: Box имеет ФИКСИРОВАННЫЙ размер, scale применяется от центра
|
||||||
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
||||||
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
|
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
|
||||||
@@ -698,37 +698,34 @@ fun ProfileMetaballOverlayCompat(
|
|||||||
val avatarCenterX = avatarState.centerX
|
val avatarCenterX = avatarState.centerX
|
||||||
val avatarCenterY = avatarState.centerY
|
val avatarCenterY = avatarState.centerY
|
||||||
|
|
||||||
// UNIFIED SCALE для всех режимов
|
// 🔥 UNIFIED SCALE - БЕЗ резких переключений when
|
||||||
val uniformScale: Float
|
val baseScale = (avatarState.radius * 2f) / baseSizePx
|
||||||
val currentCenterX: Float
|
val targetExpansionScale = screenWidthPx / baseSizePx
|
||||||
val currentCenterY: Float
|
val uniformScale = if (expansionProgress > 0f) {
|
||||||
val cornerRadiusPx: Float
|
lerpFloat(baseScale, targetExpansionScale, expansionProgress)
|
||||||
|
} else {
|
||||||
|
baseScale
|
||||||
|
}.coerceAtLeast(0.01f)
|
||||||
|
|
||||||
when {
|
// Центр: при expansion двигается к центру header
|
||||||
expansionProgress > 0f -> {
|
|
||||||
// EXPANSION MODE
|
|
||||||
val targetSize = screenWidthPx
|
|
||||||
uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
|
|
||||||
val targetCenterX = screenWidthPx / 2f
|
val targetCenterX = screenWidthPx / 2f
|
||||||
val targetCenterY = headerHeightPx / 2f
|
val targetCenterY = headerHeightPx / 2f
|
||||||
currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
val currentCenterX = if (expansionProgress > 0f) {
|
||||||
currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
||||||
cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
} else {
|
||||||
|
avatarCenterX
|
||||||
}
|
}
|
||||||
collapseProgress > 0f -> {
|
val currentCenterY = if (expansionProgress > 0f) {
|
||||||
// COLLAPSE MODE - используем avatarState.radius через scale
|
lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
||||||
uniformScale = (avatarState.radius * 2f) / baseSizePx
|
} else {
|
||||||
currentCenterX = avatarCenterX
|
avatarCenterY
|
||||||
currentCenterY = avatarCenterY
|
|
||||||
cornerRadiusPx = avatarState.cornerRadius / uniformScale
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// NORMAL MODE
|
|
||||||
uniformScale = 1f
|
|
||||||
currentCenterX = avatarCenterX
|
|
||||||
currentCenterY = avatarCenterY
|
|
||||||
cornerRadiusPx = baseSizePx / 2f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Corner radius
|
||||||
|
val cornerRadiusPx: Float = if (expansionProgress > 0f) {
|
||||||
|
lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
||||||
|
} else {
|
||||||
|
(avatarState.cornerRadius / baseScale).coerceAtMost(baseSizePx / 2f)
|
||||||
}
|
}
|
||||||
|
|
||||||
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import androidx.activity.compose.BackHandler
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -192,7 +194,7 @@ fun OtherProfileScreen(
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
var overscrollOffset by remember { mutableFloatStateOf(0f) }
|
var overscrollOffset by remember { mutableFloatStateOf(0f) }
|
||||||
val maxOverscroll = with(density) { 250.dp.toPx() } // Максимальное расширение
|
val maxOverscroll = with(density) { 250.dp.toPx() } // Максимальное расширение
|
||||||
val snapThreshold = maxOverscroll * 0.33f // Telegram: 33%
|
val snapThreshold = maxOverscroll * 0.05f // 🔥 5% - мгновенный snap!
|
||||||
|
|
||||||
// Track dragging state
|
// Track dragging state
|
||||||
var isDragging by remember { mutableStateOf(false) }
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
@@ -216,44 +218,51 @@ fun OtherProfileScreen(
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// SNAP ANIMATION - как Telegram's expandAnimator
|
// SNAP ANIMATION - как Telegram's expandAnimator
|
||||||
// При отпускании пальца: snap к 0 или к max в зависимости от порога
|
// При отпускании пальца: snap к 0 или к max в зависимости от порога
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// 🔥 FIX: isPulledDown имеет ВЫСШИЙ ПРИОРИТЕТ
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
val targetOverscroll = when {
|
val targetOverscroll = when {
|
||||||
isDragging -> overscrollOffset // Во время drag - напрямую следуем за пальцем
|
isPulledDown -> maxOverscroll // 🔥 ВЫСШИЙ ПРИОРИТЕТ: snap сработал!
|
||||||
isPulledDown -> maxOverscroll // После snap - держим раскрытым
|
isDragging -> overscrollOffset // Во время drag (до порога)
|
||||||
overscrollOffset > snapThreshold -> maxOverscroll // Перешли порог - snap к max
|
overscrollOffset > snapThreshold -> maxOverscroll
|
||||||
else -> 0f // Не дотянули - snap обратно
|
else -> 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telegram: duration = (1 - value) * 250ms при expand, value * 250ms при collapse
|
// 🔥 FIX: Когда isPulledDown=true - анимация должна быть МГНОВЕННОЙ
|
||||||
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
||||||
val snapDuration = if (targetOverscroll == maxOverscroll) {
|
// 🔥 Плавная spring анимация для snap
|
||||||
((1f - currentProgress) * 150).toInt().coerceIn(50, 150)
|
|
||||||
} else {
|
|
||||||
(currentProgress * 150).toInt().coerceIn(50, 150)
|
|
||||||
}
|
|
||||||
|
|
||||||
val animatedOverscroll by animateFloatAsState(
|
val animatedOverscroll by animateFloatAsState(
|
||||||
targetValue = targetOverscroll,
|
targetValue = targetOverscroll,
|
||||||
animationSpec = tween(
|
animationSpec = if (isDragging && !isPulledDown) {
|
||||||
durationMillis = if (isDragging) 0 else snapDuration,
|
// Без анимации во время drag (до snap)
|
||||||
easing = LinearOutSlowInEasing
|
spring(stiffness = Spring.StiffnessHigh)
|
||||||
),
|
} else {
|
||||||
|
// Плавная анимация для snap
|
||||||
|
spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy, // Лёгкий bounce
|
||||||
|
stiffness = Spring.StiffnessMediumLow // Плавное движение
|
||||||
|
)
|
||||||
|
},
|
||||||
label = "overscroll"
|
label = "overscroll"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExpansionProgress для передачи в overlay
|
// ExpansionProgress для передачи в overlay
|
||||||
|
// 🔥 FIX: Когда isPulledDown=true - ВСЕГДА используем animatedOverscroll
|
||||||
val expansionProgress = when {
|
val expansionProgress = when {
|
||||||
collapseProgress > 0.1f -> 0f // Не расширяем при collapse
|
collapseProgress > 0.1f -> 0f
|
||||||
|
isPulledDown -> (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f) // 🔥 AUTO-EXPAND!
|
||||||
isDragging -> (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
isDragging -> (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
||||||
else -> (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f)
|
else -> (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Haptic при достижении порога (как Telegram)
|
// Haptic при достижении порога (как Telegram)
|
||||||
LaunchedEffect(expansionProgress) {
|
// 🔥 FIX: Также автоматически snap к expanded
|
||||||
if (expansionProgress >= 0.33f && !hasTriggeredExpandHaptic && isDragging) {
|
LaunchedEffect(expansionProgress, isDragging) {
|
||||||
|
if (expansionProgress >= 0.10f && !hasTriggeredExpandHaptic && isDragging) {
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
hasTriggeredExpandHaptic = true
|
hasTriggeredExpandHaptic = true
|
||||||
} else if (expansionProgress < 0.2f) {
|
// 🔥 AUTO-SNAP!
|
||||||
|
isPulledDown = true
|
||||||
|
} else if (expansionProgress < 0.02f) {
|
||||||
hasTriggeredExpandHaptic = false
|
hasTriggeredExpandHaptic = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,19 +493,6 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
// Avatar font size for placeholder
|
// Avatar font size for placeholder
|
||||||
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 12.sp, collapseProgress)
|
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 12.sp, collapseProgress)
|
||||||
|
|
||||||
// Haptic feedback при достижении полного квадрата (ТОЛЬКО С АВАТАРКОЙ)
|
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
|
||||||
var hasTriggeredHaptic by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(expansionProgress, hasAvatar) {
|
|
||||||
if (hasAvatar && expansionProgress >= 0.95f && !hasTriggeredHaptic) {
|
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
hasTriggeredHaptic = true
|
|
||||||
} else if (expansionProgress < 0.5f) {
|
|
||||||
hasTriggeredHaptic = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text animation - always centered
|
// Text animation - always centered
|
||||||
val textDefaultY = expandedHeight - 48.dp
|
val textDefaultY = expandedHeight - 48.dp
|
||||||
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT_OTHER / 2
|
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT_OTHER / 2
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ fun ProfileScreen(
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
var overscrollOffset by remember { mutableFloatStateOf(0f) }
|
var overscrollOffset by remember { mutableFloatStateOf(0f) }
|
||||||
val maxOverscroll = with(density) { 250.dp.toPx() } // Максимальное расширение
|
val maxOverscroll = with(density) { 250.dp.toPx() } // Максимальное расширение
|
||||||
val snapThreshold = maxOverscroll * 0.33f // Telegram: 33%
|
val snapThreshold = maxOverscroll * 0.05f // 🔥 5% - мгновенный snap!
|
||||||
|
|
||||||
// Track dragging state
|
// Track dragging state
|
||||||
var isDragging by remember { mutableStateOf(false) }
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
@@ -357,45 +357,55 @@ fun ProfileScreen(
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// SNAP ANIMATION - как Telegram's expandAnimator
|
// SNAP ANIMATION - как Telegram's expandAnimator
|
||||||
// При отпускании пальца: snap к 0 или к max в зависимости от порога
|
// При отпускании пальца: snap к 0 или к max в зависимости от порога
|
||||||
|
// 🔥 FIX: Если isPulledDown=true - ВСЕГДА snap к max (даже во время drag!)
|
||||||
|
// isPulledDown имеет ВЫСШИЙ ПРИОРИТЕТ - игнорирует isDragging
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
val targetOverscroll = when {
|
val targetOverscroll = when {
|
||||||
isDragging -> overscrollOffset // Во время drag - напрямую следуем за пальцем
|
isPulledDown -> maxOverscroll // 🔥 ВЫСШИЙ ПРИОРИТЕТ: snap сработал - держим раскрытым!
|
||||||
isPulledDown -> maxOverscroll // После snap - держим раскрытым
|
isDragging -> overscrollOffset // Во время drag (до порога) - следуем за пальцем
|
||||||
overscrollOffset > snapThreshold -> maxOverscroll // Перешли порог - snap к max
|
overscrollOffset > snapThreshold -> maxOverscroll // Перешли порог - snap к max
|
||||||
else -> 0f // Не дотянули - snap обратно
|
else -> 0f // Не дотянули - snap обратно
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telegram: duration = (1 - value) * 250ms при expand, value * 250ms при collapse
|
// 🔥 FIX: Когда isPulledDown=true - анимация должна быть МГНОВЕННОЙ
|
||||||
// Но минимизируем для мгновенного отклика
|
// чтобы аватарка сразу заполнилась после порога
|
||||||
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
||||||
val snapDuration = if (targetOverscroll == maxOverscroll) {
|
|
||||||
((1f - currentProgress) * 150).toInt().coerceIn(50, 150) // Рост - быстро!
|
|
||||||
} else {
|
|
||||||
(currentProgress * 150).toInt().coerceIn(50, 150) // Коллапс - тоже быстро!
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 🔥 Плавная spring анимация для snap
|
||||||
val animatedOverscroll by animateFloatAsState(
|
val animatedOverscroll by animateFloatAsState(
|
||||||
targetValue = targetOverscroll,
|
targetValue = targetOverscroll,
|
||||||
animationSpec = tween(
|
animationSpec = if (isDragging && !isPulledDown) {
|
||||||
durationMillis = if (isDragging) 0 else snapDuration,
|
// Без анимации во время drag (до snap)
|
||||||
easing = LinearOutSlowInEasing // Быстрый старт, плавное завершение
|
spring(stiffness = Spring.StiffnessHigh)
|
||||||
),
|
} else {
|
||||||
|
// Плавная анимация для snap
|
||||||
|
spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy, // Лёгкий bounce
|
||||||
|
stiffness = Spring.StiffnessMediumLow // Плавное движение
|
||||||
|
)
|
||||||
|
},
|
||||||
label = "overscroll"
|
label = "overscroll"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExpansionProgress для передачи в overlay
|
// ExpansionProgress для передачи в overlay
|
||||||
|
// 🔥 FIX: Когда isPulledDown=true - ВСЕГДА используем animatedOverscroll
|
||||||
|
// чтобы аватарка автоматически расширилась до конца
|
||||||
val expansionProgress = when {
|
val expansionProgress = when {
|
||||||
collapseProgress > 0.1f -> 0f // Не расширяем при collapse
|
collapseProgress > 0.1f -> 0f // Не расширяем при collapse
|
||||||
|
isPulledDown -> (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f) // 🔥 AUTO-EXPAND!
|
||||||
isDragging -> (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
isDragging -> (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
||||||
else -> (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f)
|
else -> (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Haptic при достижении порога (как Telegram)
|
// Haptic при достижении порога (как Telegram)
|
||||||
LaunchedEffect(expansionProgress) {
|
// 🔥 FIX: Также автоматически snap к expanded при достижении порога
|
||||||
if (expansionProgress >= 0.33f && !hasTriggeredExpandHaptic && isDragging) {
|
LaunchedEffect(expansionProgress, isDragging) {
|
||||||
|
if (expansionProgress >= 0.10f && !hasTriggeredExpandHaptic && isDragging) {
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
hasTriggeredExpandHaptic = true
|
hasTriggeredExpandHaptic = true
|
||||||
} else if (expansionProgress < 0.2f) {
|
// 🔥 AUTO-SNAP: После порога автоматически snap к expanded
|
||||||
|
isPulledDown = true
|
||||||
|
} else if (expansionProgress < 0.02f) {
|
||||||
hasTriggeredExpandHaptic = false
|
hasTriggeredExpandHaptic = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -853,19 +863,6 @@ private fun CollapsingProfileHeader(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 12.sp, collapseProgress)
|
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 12.sp, collapseProgress)
|
||||||
|
|
||||||
// Haptic feedback при достижении полного квадрата (ТОЛЬКО С АВАТАРКОЙ)
|
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
|
||||||
var hasTriggeredHaptic by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(expansionProgress, hasAvatar) {
|
|
||||||
if (hasAvatar && expansionProgress >= 0.95f && !hasTriggeredHaptic) {
|
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
hasTriggeredHaptic = true
|
|
||||||
} else if (expansionProgress < 0.5f) {
|
|
||||||
hasTriggeredHaptic = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 📝 TEXT - внизу header зоны, внутри блока
|
// 📝 TEXT - внизу header зоны, внутри блока
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user