From 5861e3a7e5d4c145a9f228ca62389bac007d9c7d Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 25 Apr 2026 17:42:19 +0200 Subject: [PATCH] fix(zoom): keep aspect ratio during the avatar zoom-in animation The grow animation used independent scaleX/scaleY derived from the source rect (square avatar) and the image rect (often landscape), so at the start of the transition the full image was squashed into the avatar's square footprint before lerping back to its real proportions. Switches to a uniform scale based on max(src.width/img.width, src.height/img.height) and centers the image on the tapped rect. The image now keeps its aspect ratio throughout, covering the source rect in at least one dimension at progress=0 and growing uniformly to fullscreen. Also extends the entry guard to require non-zero source rect; without it a not-yet-measured thumbnail would yield startScale=0 and collapse the first frame to an invisible point before the animation expanded it. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ui/components/ZoomableContentDialog.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index eba772a37..7e0255b09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -274,15 +274,22 @@ private fun DialogContent( .graphicsLayer { val src = sourceBounds val img = imageBounds - if (src != null && img != null && img.width > 0f && img.height > 0f) { + if (src != null && img != null && + src.width > 0f && src.height > 0f && + img.width > 0f && img.height > 0f + ) { transformOrigin = TransformOrigin(0f, 0f) - val startScaleX = src.width / img.width - val startScaleY = src.height / img.height + // Uniform scale so non-square images keep their aspect ratio during + // the grow animation. max() so the image covers the source rect in + // at least one dimension; the other overflows centered on the tap. + val startScale = maxOf(src.width / img.width, src.height / img.height) + val srcCenter = src.center + val imgCenter = img.center val p = progress() - scaleX = lerp(startScaleX, 1f, p) - scaleY = lerp(startScaleY, 1f, p) - translationX = lerp(src.left - img.left * startScaleX, 0f, p) - translationY = lerp(src.top - img.top * startScaleY, 0f, p) + scaleX = lerp(startScale, 1f, p) + scaleY = lerp(startScale, 1f, p) + translationX = lerp(srcCenter.x - startScale * imgCenter.x, 0f, p) + translationY = lerp(srcCenter.y - startScale * imgCenter.y, 0f, p) } else { // No source bounds: fall back to a plain fade. alpha = progress()