feat(calls): polish native incoming ring — axis-locked slide-to-answer (up only), color-morph + 3-rung haptics, breathing avatar halo, decline tap-only, reduced-motion
This commit is contained in:
parent
501ed9ac96
commit
aa83b05a31
8 changed files with 245 additions and 120 deletions
|
|
@ -1,10 +1,12 @@
|
|||
package chat.vojo.app;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
|
@ -17,8 +19,9 @@ import android.view.MotionEvent;
|
|||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
|
@ -71,6 +74,18 @@ public class IncomingCallActivity extends Activity {
|
|||
private ValueAnimator hintAnimator;
|
||||
private volatile boolean dragHintSuppressed;
|
||||
|
||||
// Slow "breathing" pulse behind the caller avatar (the native mirror of the
|
||||
// web css.CallerPulse) so the lockscreen ring's avatar isn't dead-static.
|
||||
private ValueAnimator haloAnimator;
|
||||
|
||||
// Drag-to-answer: the answer disc ripens Success-green → go-green as the slide
|
||||
// nears commit (bound to the colors.xml call_answer / call_answer_confirm
|
||||
// tokens), and a 3-rung ascending haptic ladder fires along the way.
|
||||
private static final int ANSWER_TINT_BASE = 0xFF7DD3A8;
|
||||
private static final int ANSWER_TINT_CONFIRM = 0xFF34D399;
|
||||
private final ArgbEvaluator argbEval = new ArgbEvaluator();
|
||||
private int lastHapticRung = -1;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
// Render the ring screen in the in-app language (matches CallStyle / strip /
|
||||
|
|
@ -108,16 +123,18 @@ public class IncomingCallActivity extends Activity {
|
|||
View declineBtn = findViewById(R.id.call_decline);
|
||||
answerBtn.setOnClickListener(v -> onAnswer());
|
||||
declineBtn.setOnClickListener(v -> onDecline());
|
||||
// Drag-to-activate: a plain tap fires the action immediately; the moment a
|
||||
// drag begins, the button follows the finger, a radial glow charges up
|
||||
// with the drag progress, and the action commits once dragged past its own
|
||||
// area (releasing short of that springs back, cancelling). Both buttons get
|
||||
// their own listener instance (independent per-gesture state — safe under
|
||||
// multitouch). performClick() routes the commit through the click listener
|
||||
// above, so the keyboard/TalkBack click path stays live.
|
||||
answerBtn.setOnTouchListener(dragToCommit(findViewById(R.id.answer_glow)));
|
||||
declineBtn.setOnTouchListener(dragToCommit(findViewById(R.id.decline_glow)));
|
||||
// Slide-up-to-answer: a plain tap fires immediately; the moment a drag
|
||||
// begins, the disc tracks the finger UP its rail (the direction the chevron
|
||||
// hint points), ripens toward go-green, drives a radial glow + an ascending
|
||||
// haptic ladder, and commits once dragged past the threshold (or flicked
|
||||
// up). Releasing short springs back, cancelling. DECLINE is tap-only — only
|
||||
// Answer carries the slide, so a stray drag can never reject a call.
|
||||
// performClick() routes the commit through the click listener above, so the
|
||||
// keyboard/TalkBack path stays live.
|
||||
answerBtn.setOnTouchListener(
|
||||
dragToCommit(answerBtn, findViewById(R.id.answer_glow), declineBtn));
|
||||
startDragHint(findViewById(R.id.answer_hint));
|
||||
startAvatarHalo(findViewById(R.id.call_avatar_halo));
|
||||
|
||||
acquireRingWakeLock();
|
||||
current = this;
|
||||
|
|
@ -151,7 +168,12 @@ public class IncomingCallActivity extends Activity {
|
|||
return false;
|
||||
}
|
||||
String callerName = intent.getStringExtra("caller_name");
|
||||
if (callerName == null || callerName.trim().isEmpty()) callerName = "Vojo";
|
||||
if (callerName == null || callerName.trim().isEmpty()) {
|
||||
// No caller name in the ring payload — name the state, not the app
|
||||
// ("Vojo" read as if the app itself were calling). Matches the web
|
||||
// strip's Call.unknown_caller fallback.
|
||||
callerName = getString(R.string.call_unknown_caller);
|
||||
}
|
||||
|
||||
((TextView) findViewById(R.id.call_name)).setText(callerName);
|
||||
((TextView) findViewById(R.id.call_subtitle)).setText(R.string.call_incoming);
|
||||
|
|
@ -211,30 +233,27 @@ public class IncomingCallActivity extends Activity {
|
|||
return km != null && km.isKeyguardLocked();
|
||||
}
|
||||
|
||||
// ── Drag-to-activate gesture (slide a ring button to commit) ──
|
||||
// ── Slide-up-to-answer gesture (drag the answer disc up to commit) ──
|
||||
|
||||
// A fresh stateful OnTouchListener per call so two buttons never share
|
||||
// per-gesture state. Tap (no drag past touch-slop) → performClick() on UP.
|
||||
// Drag → the disc tracks the finger 1:1 while a radial [glow] behind it
|
||||
// charges up (alpha + scale) CONTINUOUSLY with the drag progress (0→1 over
|
||||
// the commit distance), and the disc scales up with it. Crossing the commit
|
||||
// threshold fires a one-shot haptic; releasing past it performs the click,
|
||||
// releasing short of it springs everything back and does nothing. Values are
|
||||
// set directly (not via animate()) during the drag so the glow stays glued to
|
||||
// the finger with zero latency; animate() is used only for the press lift and
|
||||
// the spring-back.
|
||||
private View.OnTouchListener dragToCommit(final View glow) {
|
||||
// Tap (no drag past touch-slop) → performClick() on UP. Drag → the disc tracks
|
||||
// the finger UP its rail (the axis the chevron hint points; sideways/downward
|
||||
// drags never commit, so the affordance can't lie), and CONTINUOUSLY along the
|
||||
// slide the disc scales + tilts, ripens Success-green → go-green, drives the
|
||||
// radial [glow], recedes the [decline] disc, and steps a 3-rung haptic ladder.
|
||||
// Dragging (or flicking) past the commit distance performs the click; releasing
|
||||
// short springs everything back. Progress visuals are set directly (zero
|
||||
// latency) during the drag; animate() is used only for the press-lift and the
|
||||
// spring-back. DECLINE is tap-only — only the answer disc gets this listener.
|
||||
private View.OnTouchListener dragToCommit(final View disc, final View glow, final View decline) {
|
||||
final int touchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
|
||||
// Deliberate upward travel — NOT the old ~40dp (half a disc radius), which
|
||||
// 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));
|
||||
return new View.OnTouchListener() {
|
||||
private float downX;
|
||||
private float downY;
|
||||
private boolean dragging;
|
||||
private boolean pastThreshold;
|
||||
|
||||
private float commitThreshold(View v) {
|
||||
// Past roughly the button's own radius+ → "dragged out of its area".
|
||||
return Math.max(touchSlop * 4f, v.getWidth() * 0.55f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent e) {
|
||||
|
|
@ -243,54 +262,46 @@ public class IncomingCallActivity extends Activity {
|
|||
downX = e.getRawX();
|
||||
downY = e.getRawY();
|
||||
dragging = false;
|
||||
pastThreshold = false;
|
||||
v.animate().scaleX(1.06f).scaleY(1.06f).setDuration(120).start();
|
||||
lastHapticRung = -1;
|
||||
disc.animate().scaleX(1.06f).scaleY(1.06f).setDuration(120).start();
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
float dx = e.getRawX() - downX;
|
||||
float dy = e.getRawY() - downY;
|
||||
double dist = Math.hypot(dx, dy);
|
||||
if (!dragging && dist > touchSlop) {
|
||||
if (!dragging && Math.hypot(dx, dy) > touchSlop) {
|
||||
dragging = true;
|
||||
dragHintSuppressed = true;
|
||||
// Cancel the in-flight press-lift so it stops fighting the
|
||||
// direct scale-sets below (the old code let both run for
|
||||
// ~120ms → a scale jitter at the start of every drag).
|
||||
disc.animate().cancel();
|
||||
}
|
||||
if (dragging) {
|
||||
float threshold = commitThreshold(v);
|
||||
float p = (float) Math.min(1.0, dist / threshold);
|
||||
v.setTranslationX(dx);
|
||||
v.setTranslationY(dy);
|
||||
v.setScaleX(1.06f + 0.16f * p);
|
||||
v.setScaleY(1.06f + 0.16f * p);
|
||||
if (glow != null) {
|
||||
glow.setAlpha(0.18f + 0.82f * p);
|
||||
glow.setScaleX(0.58f + 0.52f * p);
|
||||
glow.setScaleY(0.58f + 0.52f * p);
|
||||
}
|
||||
boolean past = p >= 1f;
|
||||
if (past != pastThreshold) {
|
||||
pastThreshold = past;
|
||||
if (past) {
|
||||
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
}
|
||||
}
|
||||
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));
|
||||
applyProgressVisuals(disc, glow, decline, p);
|
||||
hapticLadder(disc, p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_UP: {
|
||||
float dx = e.getRawX() - downX;
|
||||
float dy = e.getRawY() - downY;
|
||||
boolean commit = !dragging || Math.hypot(dx, dy) >= commitThreshold(v);
|
||||
float up = -(e.getRawY() - downY);
|
||||
boolean commit = !dragging || up >= commitDistance;
|
||||
if (commit) {
|
||||
// Tap or a committed drag → fire. The activity finishes
|
||||
// Tap or a committed slide → fire. The activity finishes
|
||||
// immediately, so no need to settle the visuals.
|
||||
v.performClick();
|
||||
disc.performClick();
|
||||
} else {
|
||||
springBackDragButton(v, glow);
|
||||
springBackDragButton(disc, glow, decline, up / commitDistance);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
springBackDragButton(v, glow);
|
||||
springBackDragButton(disc, glow, decline, 0f);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -299,35 +310,105 @@ public class IncomingCallActivity extends Activity {
|
|||
};
|
||||
}
|
||||
|
||||
// Spring the disc back to rest (overshoot) and fade the glow out. Re-enables
|
||||
// the idle drag hint.
|
||||
private void springBackDragButton(View v, View glow) {
|
||||
v.animate()
|
||||
// Maps drag progress p (0→1) onto every live visual, so the disc, glow, tint,
|
||||
// tilt and the receding decline disc move as one body. Driven directly from the
|
||||
// finger during a drag and from the spring-back on release.
|
||||
private void applyProgressVisuals(View disc, View glow, View decline, float p) {
|
||||
disc.setScaleX(1.06f + 0.16f * p);
|
||||
disc.setScaleY(1.06f + 0.16f * p);
|
||||
disc.setRotation(-6f * p); // subtle "lifting the handset" tilt
|
||||
// Ripen toward go-green only over the last 40% so a short release leaves the
|
||||
// disc at its base tint (no colour snap on spring-back).
|
||||
float cp = Math.min(1f, Math.max(0f, (p - 0.6f) / 0.4f));
|
||||
disc.setBackgroundTintList(ColorStateList.valueOf(
|
||||
(Integer) argbEval.evaluate(cp, ANSWER_TINT_BASE, ANSWER_TINT_CONFIRM)));
|
||||
if (glow != null) {
|
||||
glow.setAlpha(0.18f + 0.82f * p);
|
||||
float gs = 0.58f + 0.42f * p; // peak 1.0 (the 124dp glow already covers the disc)
|
||||
glow.setScaleX(gs);
|
||||
glow.setScaleY(gs);
|
||||
}
|
||||
if (decline != null) {
|
||||
// The screen "takes a side": the decline disc recedes as answer climbs.
|
||||
decline.setAlpha(1f - 0.45f * p);
|
||||
decline.setScaleX(1f - 0.12f * p);
|
||||
decline.setScaleY(1f - 0.12f * p);
|
||||
}
|
||||
}
|
||||
|
||||
// Three ascending rungs along the slide (engage / near / commit), each once per
|
||||
// drag. performHapticFeedback honours the system haptic toggle (no Vibrator
|
||||
// permission / no manual settings check needed). needsDevice: the felt crescendo
|
||||
// wants a richer VibrationEffect.Composition ladder (see drag spec) on real HW.
|
||||
private void hapticLadder(View v, float p) {
|
||||
int rung = p >= 1f ? 3 : p >= 0.85f ? 2 : p >= 0.45f ? 1 : 0;
|
||||
if (rung <= lastHapticRung) return;
|
||||
lastHapticRung = rung;
|
||||
if (rung == 0) return;
|
||||
v.performHapticFeedback(rung >= 3
|
||||
? HapticFeedbackConstants.LONG_PRESS : HapticFeedbackConstants.CLOCK_TICK);
|
||||
}
|
||||
|
||||
// Spring the disc back to rest and fade the glow out (slightly trailing, so the
|
||||
// halo is the last energy to leave). A gentle, distance-aware decelerate settle
|
||||
// replaces the old OvershootInterpolator(2.2f) "boing". Under reduced motion the
|
||||
// framework zeroes these durations, so they snap to rest. Re-enables the idle
|
||||
// hint only once the disc has actually landed (withEndAction).
|
||||
private void springBackDragButton(View disc, View glow, View decline, float pRelease) {
|
||||
float p = Math.min(1f, Math.max(0f, pRelease));
|
||||
long dur = (long) (200 + 140 * p);
|
||||
disc.animate()
|
||||
.translationX(0)
|
||||
.translationY(0)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setInterpolator(new OvershootInterpolator(2.2f))
|
||||
.setDuration(340)
|
||||
.rotation(0f)
|
||||
.setInterpolator(new DecelerateInterpolator(1.6f))
|
||||
.setDuration(dur)
|
||||
.withEndAction(() -> dragHintSuppressed = false)
|
||||
.start();
|
||||
if (glow != null) {
|
||||
glow.animate().alpha(0f).scaleX(0.58f).scaleY(0.58f).setDuration(220).start();
|
||||
// Animate the tint back so a top-zone release doesn't snap green→green.
|
||||
float cp = Math.min(1f, Math.max(0f, (p - 0.6f) / 0.4f));
|
||||
if (cp > 0f) {
|
||||
Integer fromColor = (Integer) argbEval.evaluate(cp, ANSWER_TINT_BASE, ANSWER_TINT_CONFIRM);
|
||||
ValueAnimator tint = ValueAnimator.ofObject(argbEval, fromColor, ANSWER_TINT_BASE);
|
||||
tint.setDuration(dur);
|
||||
tint.addUpdateListener(a -> disc.setBackgroundTintList(
|
||||
ColorStateList.valueOf((Integer) a.getAnimatedValue())));
|
||||
tint.start();
|
||||
} else {
|
||||
disc.setBackgroundTintList(ColorStateList.valueOf(ANSWER_TINT_BASE));
|
||||
}
|
||||
if (glow != null) {
|
||||
glow.animate().alpha(0f).scaleX(0.58f).scaleY(0.58f)
|
||||
.setInterpolator(new DecelerateInterpolator())
|
||||
.setDuration(dur + 60)
|
||||
.start();
|
||||
}
|
||||
if (decline != null) {
|
||||
decline.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(dur).start();
|
||||
}
|
||||
dragHintSuppressed = false;
|
||||
}
|
||||
|
||||
// Looping "slide up to answer" affordance: the chevrons rise and fade in a
|
||||
// 1.5s cycle. Suppressed (held invisible) while a drag is active so the hint
|
||||
// never fights the live gesture.
|
||||
// 1.5s eased cycle. Suppressed (held invisible) while a drag is active so the
|
||||
// hint never fights the live gesture. Reduced motion → no loop (held static),
|
||||
// since an infinite looping affordance is exactly what motion-sensitive users
|
||||
// disable.
|
||||
private void startDragHint(final View hint) {
|
||||
if (hint == null) return;
|
||||
if (!ValueAnimator.areAnimatorsEnabled()) {
|
||||
hint.setTranslationY(0f);
|
||||
hint.setAlpha(0.85f);
|
||||
return;
|
||||
}
|
||||
hint.setAlpha(0f);
|
||||
final float from = dp(10f);
|
||||
final float to = dp(-8f);
|
||||
ValueAnimator a = ValueAnimator.ofFloat(0f, 1f);
|
||||
a.setDuration(1500);
|
||||
a.setRepeatCount(ValueAnimator.INFINITE);
|
||||
a.setInterpolator(new LinearInterpolator());
|
||||
a.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
a.addUpdateListener(an -> {
|
||||
if (dragHintSuppressed) {
|
||||
hint.setAlpha(0f);
|
||||
|
|
@ -349,6 +430,29 @@ public class IncomingCallActivity extends Activity {
|
|||
hintAnimator = a;
|
||||
}
|
||||
|
||||
// Slow "breathing" halo behind the caller avatar — the native mirror of the
|
||||
// web css.CallerPulse so the most-stared-at call surface isn't a dead static
|
||||
// disc. The halo scales out while fading (a ring expanding + dissolving), green
|
||||
// (#7dd3a8) = "answerable call ringing". Reduced motion → no pulse (held clear).
|
||||
private void startAvatarHalo(final View halo) {
|
||||
if (halo == null) return;
|
||||
halo.setAlpha(0f);
|
||||
if (!ValueAnimator.areAnimatorsEnabled()) return;
|
||||
ValueAnimator a = ValueAnimator.ofFloat(0f, 1f);
|
||||
a.setDuration(1800);
|
||||
a.setRepeatCount(ValueAnimator.INFINITE);
|
||||
a.setInterpolator(new LinearInterpolator());
|
||||
a.addUpdateListener(an -> {
|
||||
float f = (float) an.getAnimatedValue();
|
||||
float s = 0.9f + 0.25f * f;
|
||||
halo.setScaleX(s);
|
||||
halo.setScaleY(s);
|
||||
halo.setAlpha(0.5f * (1f - f));
|
||||
});
|
||||
a.start();
|
||||
haloAnimator = a;
|
||||
}
|
||||
|
||||
private float dp(float value) {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, value, getResources().getDisplayMetrics());
|
||||
|
|
@ -393,6 +497,10 @@ public class IncomingCallActivity extends Activity {
|
|||
hintAnimator.cancel();
|
||||
hintAnimator = null;
|
||||
}
|
||||
if (haloAnimator != null) {
|
||||
haloAnimator.cancel();
|
||||
haloAnimator = null;
|
||||
}
|
||||
if (current == this) current = null;
|
||||
releaseRingWakeLock();
|
||||
super.onDestroy();
|
||||
|
|
|
|||
14
android/app/src/main/res/drawable/bg_call_avatar_halo.xml
Normal file
14
android/app/src/main/res/drawable/bg_call_avatar_halo.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!-- Soft radial halo behind the caller avatar on the full-screen ring. Animated
|
||||
in code (scale out + fade) into a slow "breathing" pulse — the native mirror
|
||||
of the web css.CallerPulse, so the lockscreen ring no longer has a dead,
|
||||
static avatar. Success/green tint (#7dd3a8) = "an answerable call is ringing",
|
||||
matching the answer disc, glow and chevrons. Suppressed under reduced motion. -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<gradient
|
||||
android:type="radial"
|
||||
android:gradientRadius="80dp"
|
||||
android:startColor="#807DD3A8"
|
||||
android:centerColor="#337DD3A8"
|
||||
android:endColor="#007DD3A8" />
|
||||
</shape>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Soft radial halo behind the Decline disc (Critical/red tint to match
|
||||
call_decline). Same code-driven alpha + scale as the answer glow. -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<gradient
|
||||
android:type="radial"
|
||||
android:gradientRadius="62dp"
|
||||
android:startColor="#A6C08E7B"
|
||||
android:centerColor="#4DC08E7B"
|
||||
android:endColor="#00C08E7B" />
|
||||
</shape>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<!-- Material "call" handset. White fill; each button tints it (answer/decline)
|
||||
via android:tint. The decline button rotates this 135° in the layout. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="30dp"
|
||||
android:width="38dp"
|
||||
android:height="38dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -18,23 +18,39 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_weight="1.1" />
|
||||
|
||||
<!-- Fallback avatar disc with the caller's initial (set in code). -->
|
||||
<TextView
|
||||
android:id="@+id/call_avatar"
|
||||
android:layout_width="112dp"
|
||||
android:layout_height="112dp"
|
||||
android:background="@drawable/bg_call_avatar"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/call_avatar_on"
|
||||
android:textSize="46sp"
|
||||
android:textStyle="bold"
|
||||
android:includeFontPadding="false" />
|
||||
<!-- 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. -->
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:id="@+id/call_avatar_halo"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0"
|
||||
android:importantForAccessibility="no"
|
||||
android:background="@drawable/bg_call_avatar_halo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_avatar"
|
||||
android:layout_width="124dp"
|
||||
android:layout_height="124dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_call_avatar"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/call_avatar_on"
|
||||
android:textSize="51sp"
|
||||
android:textStyle="bold"
|
||||
android:includeFontPadding="false" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="@color/call_text"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
|
|
@ -80,31 +96,20 @@
|
|||
android:layout_marginBottom="4dp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:id="@+id/decline_glow"
|
||||
android:layout_width="124dp"
|
||||
android:layout_height="124dp"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/bg_call_glow_decline" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/call_decline"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_call_circle"
|
||||
android:backgroundTint="@color/call_decline"
|
||||
android:src="@drawable/ic_call_handset"
|
||||
android:tint="@color/call_decline_on"
|
||||
android:scaleType="center"
|
||||
android:rotation="135"
|
||||
android:contentDescription="@string/call_decline" />
|
||||
</FrameLayout>
|
||||
<!-- Decline is tap-only (no drag / no glow) — only Answer carries the
|
||||
slide affordance, so a stray drag can never reject a call. -->
|
||||
<ImageButton
|
||||
android:id="@+id/call_decline"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:background="@drawable/bg_call_circle"
|
||||
android:backgroundTint="@color/call_decline"
|
||||
android:elevation="6dp"
|
||||
android:src="@drawable/ic_call_handset"
|
||||
android:tint="@color/call_decline_on"
|
||||
android:scaleType="center"
|
||||
android:rotation="135"
|
||||
android:contentDescription="@string/call_decline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -130,6 +135,7 @@
|
|||
android:layout_height="40dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:src="@drawable/ic_call_drag_chevrons"
|
||||
android:importantForAccessibility="no"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<FrameLayout
|
||||
|
|
@ -142,6 +148,7 @@
|
|||
android:layout_height="124dp"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0"
|
||||
android:importantForAccessibility="no"
|
||||
android:background="@drawable/bg_call_glow_answer" />
|
||||
|
||||
<ImageButton
|
||||
|
|
@ -151,6 +158,7 @@
|
|||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_call_circle"
|
||||
android:backgroundTint="@color/call_answer"
|
||||
android:elevation="8dp"
|
||||
android:src="@drawable/ic_call_handset"
|
||||
android:tint="@color/call_answer_on"
|
||||
android:scaleType="center"
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
<string name="call_answer">Ответить</string>
|
||||
<string name="call_decline">Отклонить</string>
|
||||
<string name="call_incoming">Входящий звонок</string>
|
||||
<string name="call_unknown_caller">Неизвестный абонент</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
<color name="call_avatar_on">#0c0c0e</color>
|
||||
<color name="call_answer">#7dd3a8</color>
|
||||
<color name="call_answer_on">#0e2e1f</color>
|
||||
<!-- Drag-to-answer "ripen" target: the answer disc morphs Success #7dd3a8 →
|
||||
this saturated go-green as the slide nears the commit threshold, so the
|
||||
disc visibly says "release now". Stays within the green family. -->
|
||||
<color name="call_answer_confirm">#34d399</color>
|
||||
<color name="call_decline">#c08e7b</color>
|
||||
<color name="call_decline_on">#3a1f17</color>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@
|
|||
<string name="call_answer">Answer</string>
|
||||
<string name="call_decline">Decline</string>
|
||||
<string name="call_incoming">Incoming call</string>
|
||||
<string name="call_unknown_caller">Unknown caller</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue