feat: enhance versioning and avatar handling with dynamic properties and improved UI interactions

This commit is contained in:
2026-02-10 20:41:32 +05:00
parent bbaa04cda5
commit a0ef378909
12 changed files with 401 additions and 99 deletions

View File

@@ -335,6 +335,7 @@ fun AppleEmojiText(
overflow: android.text.TextUtils.TruncateAt? = null,
linkColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color(0xFF54A9EB), // Telegram-style blue
enableLinks: Boolean = true, // 🔥 Включить кликабельные ссылки
onClick: (() -> Unit)? = null, // 🔥 Обычный tap (selection mode в MessageBubble)
onLongClick: (() -> Unit)? = null // 🔥 Callback для long press (selection в MessageBubble)
) {
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
@@ -370,9 +371,12 @@ fun AppleEmojiText(
setLinkColor(linkColor.toArgb())
enableClickableLinks(true, onLongClick)
} else {
// 🔥 Даже без ссылок поддерживаем long press
onLongClickCallback = onLongClick
// 🔥 ВАЖНО: в selection mode полностью отключаем LinkMovementMethod,
// иначе тапы по тексту могут "съедаться" и не доходить до onClick.
enableClickableLinks(false, onLongClick)
}
// 🔥 Поддержка обычного tap (например, для selection mode)
setOnClickListener(onClick?.let { click -> View.OnClickListener { click.invoke() } })
}
},
update = { view ->
@@ -389,9 +393,12 @@ fun AppleEmojiText(
view.setLinkColor(linkColor.toArgb())
view.enableClickableLinks(true, onLongClick)
} else {
// 🔥 Даже без ссылок поддерживаем long press
view.onLongClickCallback = onLongClick
// 🔥 ВАЖНО: в selection mode полностью отключаем LinkMovementMethod,
// иначе тапы по тексту могут "съедаться" и не доходить до onClick.
view.enableClickableLinks(false, onLongClick)
}
// 🔥 Обновляем tap callback, чтобы не было stale lambda
view.setOnClickListener(onClick?.let { click -> View.OnClickListener { click.invoke() } })
},
modifier = modifier
)

View File

@@ -152,7 +152,10 @@ private fun isCenteredTopCutout(
if (notchInfo == null || notchInfo.bounds.width() <= 0f || notchInfo.bounds.height() <= 0f) {
return false
}
val tolerancePx = screenWidthPx * 0.20f
if (notchInfo.gravity != Gravity.CENTER) {
return false
}
val tolerancePx = screenWidthPx * 0.10f
return abs(notchInfo.bounds.centerX() - screenWidthPx / 2f) <= tolerancePx
}
@@ -211,7 +214,6 @@ private fun computeAvatarState(
avatarSizeMinPx: Float, // Into notch size (24dp or notch width)
hasAvatar: Boolean,
// Notch info
notchCenterX: Float, // X position of front camera/notch
notchCenterY: Float,
notchRadiusPx: Float,
// Telegram thresholds in pixels
@@ -265,21 +267,8 @@ private fun computeAvatarState(
val isDrawing = radius <= dp40
val isNear = radius <= dp32
// ═══════════════════════════════════════════════════════════════
// CENTER X - animate towards notch/camera position when collapsing
// Normal: screen center, Collapsed: notch center (front camera)
// ═══════════════════════════════════════════════════════════════
val startX = screenWidthPx / 2f // Normal position = screen center
val endX = notchCenterX // Target = front camera position
val centerX: Float = when {
// Pull-down expansion - stay at screen center
hasAvatar && expansionProgress > 0f -> screenWidthPx / 2f
// Collapsing - animate X towards notch/camera
collapseProgress > 0f -> lerpFloat(endX, startX, diff)
// Normal state - screen center
else -> screenWidthPx / 2f
}
// Always lock X to screen center to avoid OEM cutout offset issues on some devices.
val centerX: Float = screenWidthPx / 2f
// ═══════════════════════════════════════════════════════════════
// CENTER Y - Telegram: avatarY = lerp(endY, startY, diff)
@@ -450,17 +439,8 @@ fun ProfileMetaballOverlay(
}
}
// Notch center position - ONLY use if notch is centered (like front camera)
// If notch is off-center (corner notch), use screen center instead
val notchCenterX = remember(notchInfo, screenWidthPx, hasCenteredNotch) {
if (hasCenteredNotch && notchInfo != null) {
// Centered notch (like Dynamic Island or punch-hole camera)
notchInfo.bounds.centerX()
} else {
// No notch or off-center notch - always use screen center
screenWidthPx / 2f
}
}
// Always use true screen center for X target to keep droplet perfectly centered.
val notchCenterX = screenWidthPx / 2f
val notchCenterY = remember(notchInfo, hasCenteredNotch, statusBarHeightPx) {
resolveSafeNotchCenterY(
@@ -493,7 +473,6 @@ fun ProfileMetaballOverlay(
avatarSizeExpandedPx = avatarSizeExpandedPx,
avatarSizeMinPx = avatarSizeMinPx,
hasAvatar = hasAvatar,
notchCenterX = notchCenterX,
notchCenterY = notchCenterY,
notchRadiusPx = notchRadiusPx,
dp40 = dp40,
@@ -596,19 +575,13 @@ fun ProfileMetaballOverlay(
// Draw target shape at top (notch or black bar fallback)
if (showConnector) {
blackPaint.alpha = connectorPaintAlpha
if (hasRealNotch && notchInfo != null && notchInfo.isLikelyCircle) {
if (hasRealNotch && notchInfo != null) {
nativeCanvas.drawCircle(
notchCenterX,
notchCenterY,
notchRadiusPx,
blackPaint
)
} else if (hasRealNotch && notchInfo != null && notchInfo.isAccurate && notchInfo.path != null) {
nativeCanvas.drawPath(notchInfo.path, blackPaint)
} else if (hasRealNotch && notchInfo != null) {
val bounds = notchInfo.bounds
val rad = kotlin.math.max(bounds.width(), bounds.height()) / 2f
nativeCanvas.drawRoundRect(bounds, rad, rad, blackPaint)
} else {
// No notch fallback: full-width black bar at top
// Like Telegram's ProfileGooeyView when notchInfo == null
@@ -798,7 +771,6 @@ fun ProfileMetaballOverlayCompat(
avatarSizeExpandedPx = avatarSizeExpandedPx,
avatarSizeMinPx = avatarSizeMinPx,
hasAvatar = hasAvatar,
notchCenterX = notchCenterX,
notchCenterY = notchCenterY,
notchRadiusPx = notchRadiusPx,
dp40 = dp40,
@@ -943,9 +915,8 @@ fun ProfileMetaballOverlayCpu(
with(density) { ProfileMetaballConstants.FALLBACK_CAMERA_SIZE.toPx() }
}
}
val notchCenterX = remember(notchInfo, screenWidthPx) {
if (hasRealNotch && notchInfo != null) notchInfo.bounds.centerX() else screenWidthPx / 2f
}
// Always use true screen center for X target to keep droplet perfectly centered.
val notchCenterX = screenWidthPx / 2f
val notchCenterY = remember(notchInfo, hasRealNotch, statusBarHeightPx, blackBarHeightPx) {
resolveSafeNotchCenterY(
notchInfo = notchInfo,
@@ -973,7 +944,6 @@ fun ProfileMetaballOverlayCpu(
avatarSizeExpandedPx = avatarSizeExpandedPx,
avatarSizeMinPx = avatarSizeMinPx,
hasAvatar = hasAvatar,
notchCenterX = notchCenterX,
notchCenterY = notchCenterY,
notchRadiusPx = notchRadiusPx,
dp40 = dp40, dp34 = dp34, dp32 = dp32, dp18 = dp18, dp22 = dp22,
@@ -1081,19 +1051,13 @@ fun ProfileMetaballOverlayCpu(
// Draw target (notch or black bar)
if (showConnector) {
blackPaint.alpha = connectorPaintAlpha
if (hasRealNotch && notchInfo != null && notchInfo.isLikelyCircle) {
val rad = min(notchInfo.bounds.width(), notchInfo.bounds.height()) / 2f
if (hasRealNotch && notchInfo != null) {
offscreenCanvas.drawCircle(
notchInfo.bounds.centerX(),
notchInfo.bounds.bottom - notchInfo.bounds.width() / 2f,
rad, blackPaint
notchCenterX,
notchCenterY,
notchRadiusPx,
blackPaint
)
} else if (hasRealNotch && notchInfo != null && notchInfo.isAccurate && notchInfo.path != null) {
offscreenCanvas.drawPath(notchInfo.path, blackPaint)
} else if (hasRealNotch && notchInfo != null) {
val bounds = notchInfo.bounds
val rad = kotlin.math.max(bounds.width(), bounds.height()) / 2f
offscreenCanvas.drawRoundRect(bounds, rad, rad, blackPaint)
} else {
// No notch: draw black bar
offscreenCanvas.drawRect(0f, 0f, screenWidthPx, blackBarHeightPx, blackPaint)