feat: implement avatar animation and enhance image sharing functionality

This commit is contained in:
2026-02-10 00:06:41 +05:00
parent 3c37a3b0e5
commit bbaa04cda5
10 changed files with 523 additions and 168 deletions

View File

@@ -145,6 +145,37 @@ private val GPU_ALPHA_THRESHOLD_MATRIX = floatArrayOf(
*/
private const val BLACK_BAR_HEIGHT_DP = 32f
private fun isCenteredTopCutout(
notchInfo: NotchInfoUtils.NotchInfo?,
screenWidthPx: Float
): Boolean {
if (notchInfo == null || notchInfo.bounds.width() <= 0f || notchInfo.bounds.height() <= 0f) {
return false
}
val tolerancePx = screenWidthPx * 0.20f
return abs(notchInfo.bounds.centerX() - screenWidthPx / 2f) <= tolerancePx
}
private fun resolveSafeNotchCenterY(
notchInfo: NotchInfoUtils.NotchInfo?,
hasCenteredCutout: Boolean,
statusBarHeightPx: Float,
fallbackCenterY: Float
): Float {
if (!hasCenteredCutout || notchInfo == null) return fallbackCenterY
val rawCenterY = if (notchInfo.isLikelyCircle) {
notchInfo.bounds.bottom - min(notchInfo.bounds.width(), notchInfo.bounds.height()) / 2f
} else {
notchInfo.bounds.centerY()
}
// Some OEM cutout specs report unstable Y; keep collapse target in status-bar safe zone.
val minSafeY = max(statusBarHeightPx * 0.45f, fallbackCenterY)
val maxSafeY = max(statusBarHeightPx * 0.95f, minSafeY + 1f)
return rawCenterY.coerceIn(minSafeY, maxSafeY)
}
/**
* State for avatar position and size during animation
* Like Telegram's ProfileMetaballView state variables
@@ -399,9 +430,13 @@ fun ProfileMetaballOverlay(
}
}
val hasCenteredNotch = remember(notchInfo, screenWidthPx) {
!MetaballDebug.forceNoNotch && isCenteredTopCutout(notchInfo, screenWidthPx)
}
// Calculate notch radius based on real device notch (like Telegram's ProfileGooeyView)
val notchRadiusPx = remember(notchInfo) {
if (notchInfo != null && notchInfo.gravity == Gravity.CENTER) {
val notchRadiusPx = remember(notchInfo, hasCenteredNotch) {
if (hasCenteredNotch && notchInfo != null) {
if (notchInfo.isLikelyCircle) {
// Circular camera cutout - use actual width/height
min(notchInfo.bounds.width(), notchInfo.bounds.height()) / 2f
@@ -417,8 +452,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) {
if (notchInfo != null && notchInfo.gravity == Gravity.CENTER) {
val notchCenterX = remember(notchInfo, screenWidthPx, hasCenteredNotch) {
if (hasCenteredNotch && notchInfo != null) {
// Centered notch (like Dynamic Island or punch-hole camera)
notchInfo.bounds.centerX()
} else {
@@ -427,15 +462,13 @@ fun ProfileMetaballOverlay(
}
}
val notchCenterY = remember(notchInfo) {
if (notchInfo != null && notchInfo.isLikelyCircle) {
// For circle: center is at bottom - width/2 (like Telegram)
notchInfo.bounds.bottom - notchInfo.bounds.width() / 2f
} else if (notchInfo != null) {
notchInfo.bounds.centerY()
} else {
statusBarHeightPx / 2f
}
val notchCenterY = remember(notchInfo, hasCenteredNotch, statusBarHeightPx) {
resolveSafeNotchCenterY(
notchInfo = notchInfo,
hasCenteredCutout = hasCenteredNotch,
statusBarHeightPx = statusBarHeightPx,
fallbackCenterY = statusBarHeightPx / 2f
)
}
// Scale Telegram thresholds to our bigger base avatar size (120dp vs 96dp).
@@ -492,8 +525,7 @@ fun ProfileMetaballOverlay(
val blackBarHeightPx = with(density) { BLACK_BAR_HEIGHT_DP.dp.toPx() }
// Detect if device has a real centered notch (debug override supported)
val hasRealNotch = !MetaballDebug.forceNoNotch &&
notchInfo != null && notchInfo.gravity == Gravity.CENTER && notchInfo.bounds.width() > 0
val hasRealNotch = hasCenteredNotch
// Calculate "v" parameter - thickness of connector based on distance
val distance = avatarState.centerY - notchCenterY
@@ -895,8 +927,9 @@ fun ProfileMetaballOverlayCpu(
val notchInfo = remember(view) {
NotchInfoUtils.getInfo(context) ?: NotchInfoUtils.getInfoFromCutout(view)
}
val hasRealNotch = !MetaballDebug.forceNoNotch &&
notchInfo != null && notchInfo.gravity == Gravity.CENTER && notchInfo.bounds.width() > 0
val hasRealNotch = remember(notchInfo, screenWidthPx) {
!MetaballDebug.forceNoNotch && isCenteredTopCutout(notchInfo, screenWidthPx)
}
val blackBarHeightPx = with(density) { BLACK_BAR_HEIGHT_DP.dp.toPx() }
val notchRadiusPx = remember(notchInfo) {
@@ -913,14 +946,13 @@ fun ProfileMetaballOverlayCpu(
val notchCenterX = remember(notchInfo, screenWidthPx) {
if (hasRealNotch && notchInfo != null) notchInfo.bounds.centerX() else screenWidthPx / 2f
}
val notchCenterY = remember(notchInfo) {
if (hasRealNotch && notchInfo != null && notchInfo.isLikelyCircle) {
notchInfo.bounds.bottom - notchInfo.bounds.width() / 2f
} else if (hasRealNotch && notchInfo != null) {
notchInfo.bounds.centerY()
} else {
blackBarHeightPx / 2f
}
val notchCenterY = remember(notchInfo, hasRealNotch, statusBarHeightPx, blackBarHeightPx) {
resolveSafeNotchCenterY(
notchInfo = notchInfo,
hasCenteredCutout = hasRealNotch,
statusBarHeightPx = statusBarHeightPx,
fallbackCenterY = blackBarHeightPx / 2f
)
}
val thresholdScale = (avatarSizeExpandedPx / with(density) { 96.dp.toPx() }).coerceIn(1f, 1.35f)