diff --git a/android/app/src/main/java/chat/vojo/app/IncomingCallActivity.java b/android/app/src/main/java/chat/vojo/app/IncomingCallActivity.java
index 137f2d24..69b87961 100644
--- a/android/app/src/main/java/chat/vojo/app/IncomingCallActivity.java
+++ b/android/app/src/main/java/chat/vojo/app/IncomingCallActivity.java
@@ -250,6 +250,13 @@ public class IncomingCallActivity extends Activity {
// made "slide to answer" indistinguishable from a twitch. dp(88) ≈ a disc
// diameter of climb. needsDevice: tune one-handed reach on a tall phone.
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() {
private float downX;
private float downY;
@@ -279,10 +286,11 @@ public class IncomingCallActivity extends Activity {
if (dragging) {
float up = -dy; // upward = positive progress
float p = (float) Math.min(1.0, Math.max(0.0, up / commitDistance));
- // Track the finger UP its rail: a little lateral follow,
- // but the disc never travels downward (down = no commit).
- disc.setTranslationX(0.15f * dx);
- disc.setTranslationY(Math.min(0f, dy));
+ // Follow the finger UP its rail, clamped to the track: the
+ // disc never travels downward and never past trackMax, with
+ // only a small clamped lateral give.
+ 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);
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)
glow.setScaleX(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) {
// The screen "takes a side": the decline disc recedes as answer climbs.
@@ -381,6 +393,7 @@ public class IncomingCallActivity extends Activity {
}
if (glow != null) {
glow.animate().alpha(0f).scaleX(0.58f).scaleY(0.58f)
+ .translationX(0f).translationY(0f)
.setInterpolator(new DecelerateInterpolator())
.setDuration(dur + 60)
.start();
diff --git a/android/app/src/main/res/layout/activity_incoming_call.xml b/android/app/src/main/res/layout/activity_incoming_call.xml
index ac4083fe..1285198c 100644
--- a/android/app/src/main/res/layout/activity_incoming_call.xml
+++ b/android/app/src/main/res/layout/activity_incoming_call.xml
@@ -11,7 +11,9 @@
android:gravity="center_horizontal"
android:fitsSystemWindows="true"
android:paddingStart="24dp"
- android:paddingEnd="24dp">
+ android:paddingEnd="24dp"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+ 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. -->
+ android:layout_height="wrap_content"
+ android:clipChildren="false">
+
+ android:weightSum="2"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+ android:gravity="center"
+ android:clipChildren="false">
+ android:layout_height="wrap_content"
+ android:clipChildren="false">