fix(calls): bound + unclip the answer drag — disc rides a clamped track (trackMax/lateralMax), clipChildren=false up the chain, halo follows; no clip-square, no full-screen runaway

This commit is contained in:
heaven 2026-06-24 02:54:58 +03:00
parent fe32bed081
commit 628618aafe
2 changed files with 35 additions and 10 deletions

View file

@ -250,6 +250,13 @@ public class IncomingCallActivity extends Activity {
// made "slide to answer" indistinguishable from a twitch. dp(88) a disc // made "slide to answer" indistinguishable from a twitch. dp(88) a disc
// diameter of climb. needsDevice: tune one-handed reach on a tall phone. // diameter of climb. needsDevice: tune one-handed reach on a tall phone.
final float commitDistance = Math.max(touchSlop * 4f, dp(88f)); final float commitDistance = Math.max(touchSlop * 4f, dp(88f));
// The disc rides a BOUNDED track. trackMax caps how far up it can follow the
// finger (progress p already saturates at the commit point, so chasing the
// finger further just slides the disc off into empty screen); lateralMax
// keeps the slide on its vertical rail. Without these the disc trails the
// finger 1:1 across the whole screen.
final float trackMax = commitDistance;
final float lateralMax = dp(24f);
return new View.OnTouchListener() { return new View.OnTouchListener() {
private float downX; private float downX;
private float downY; private float downY;
@ -279,10 +286,11 @@ public class IncomingCallActivity extends Activity {
if (dragging) { if (dragging) {
float up = -dy; // upward = positive progress float up = -dy; // upward = positive progress
float p = (float) Math.min(1.0, Math.max(0.0, up / commitDistance)); float p = (float) Math.min(1.0, Math.max(0.0, up / commitDistance));
// Track the finger UP its rail: a little lateral follow, // Follow the finger UP its rail, clamped to the track: the
// but the disc never travels downward (down = no commit). // disc never travels downward and never past trackMax, with
disc.setTranslationX(0.15f * dx); // only a small clamped lateral give.
disc.setTranslationY(Math.min(0f, dy)); disc.setTranslationX(Math.max(-lateralMax, Math.min(lateralMax, 0.15f * dx)));
disc.setTranslationY(-Math.min(Math.max(up, 0f), trackMax));
applyProgressVisuals(disc, glow, decline, p); applyProgressVisuals(disc, glow, decline, p);
hapticLadder(disc, p); hapticLadder(disc, p);
} }
@ -327,6 +335,10 @@ public class IncomingCallActivity extends Activity {
float gs = 0.58f + 0.42f * p; // peak 1.0 (the 124dp glow already covers the disc) float gs = 0.58f + 0.42f * p; // peak 1.0 (the 124dp glow already covers the disc)
glow.setScaleX(gs); glow.setScaleX(gs);
glow.setScaleY(gs); glow.setScaleY(gs);
// Keep the halo glued behind the moving disc otherwise it blooms at the
// rest position while the disc slides up and away, decoupling the two.
glow.setTranslationX(disc.getTranslationX());
glow.setTranslationY(disc.getTranslationY());
} }
if (decline != null) { if (decline != null) {
// The screen "takes a side": the decline disc recedes as answer climbs. // The screen "takes a side": the decline disc recedes as answer climbs.
@ -381,6 +393,7 @@ public class IncomingCallActivity extends Activity {
} }
if (glow != null) { if (glow != null) {
glow.animate().alpha(0f).scaleX(0.58f).scaleY(0.58f) glow.animate().alpha(0f).scaleX(0.58f).scaleY(0.58f)
.translationX(0f).translationY(0f)
.setInterpolator(new DecelerateInterpolator()) .setInterpolator(new DecelerateInterpolator())
.setDuration(dur + 60) .setDuration(dur + 60)
.start(); .start();

View file

@ -11,7 +11,9 @@
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:paddingStart="24dp" android:paddingStart="24dp"
android:paddingEnd="24dp"> android:paddingEnd="24dp"
android:clipChildren="false"
android:clipToPadding="false">
<Space <Space
android:layout_width="0dp" android:layout_width="0dp"
@ -19,10 +21,12 @@
android:layout_weight="1.1" /> android:layout_weight="1.1" />
<!-- Avatar disc (caller initial, set in code) behind a breathing halo. Sized <!-- Avatar disc (caller initial, set in code) behind a breathing halo. Sized
124dp to match the over-lock hero avatar so answer-from-lock doesn't pop. --> 124dp to match the over-lock hero avatar so answer-from-lock doesn't pop.
clipChildren=false so the pulsing halo can bloom past the 160dp box. -->
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:clipChildren="false">
<View <View
android:id="@+id/call_avatar_halo" android:id="@+id/call_avatar_halo"
@ -72,12 +76,18 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="2" /> android:layout_weight="2" />
<!-- clipChildren/clipToPadding=false so the answer disc can slide UP out of the
action row (and the column + FrameLayout below) without being clipped at a
container edge — otherwise the dragged disc vanishes behind the 124dp glow
box. The whole ancestor chain to the root must opt out. -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingBottom="56dp" android:paddingBottom="56dp"
android:weightSum="2"> android:weightSum="2"
android:clipChildren="false"
android:clipToPadding="false">
<!-- Decline --> <!-- Decline -->
<LinearLayout <LinearLayout
@ -127,7 +137,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> android:gravity="center"
android:clipChildren="false">
<ImageView <ImageView
android:id="@+id/answer_hint" android:id="@+id/answer_hint"
@ -140,7 +151,8 @@
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:clipChildren="false">
<View <View
android:id="@+id/answer_glow" android:id="@+id/answer_glow"