1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package com.android.settingslib.graph;
16
17import android.animation.ArgbEvaluator;
18import android.annotation.IntRange;
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.ColorFilter;
24import android.graphics.Matrix;
25import android.graphics.Paint;
26import android.graphics.Path;
27import android.graphics.Path.Direction;
28import android.graphics.Path.FillType;
29import android.graphics.Path.Op;
30import android.graphics.PointF;
31import android.graphics.Rect;
32import android.graphics.RectF;
33import android.graphics.drawable.Drawable;
34import android.os.Handler;
35import android.util.LayoutDirection;
36
37import com.android.settingslib.R;
38import com.android.settingslib.Utils;
39
40public class SignalDrawable extends Drawable {
41
42    private static final String TAG = "SignalDrawable";
43
44    private static final int NUM_DOTS = 3;
45
46    private static final float VIEWPORT = 24f;
47    private static final float PAD = 2f / VIEWPORT;
48    private static final float CUT_OUT = 7.9f / VIEWPORT;
49
50    private static final float DOT_SIZE = 3f / VIEWPORT;
51    private static final float DOT_PADDING = 1f / VIEWPORT;
52    private static final float DOT_CUT_WIDTH = (DOT_SIZE * 3) + (DOT_PADDING * 5);
53    private static final float DOT_CUT_HEIGHT = (DOT_SIZE * 1) + (DOT_PADDING * 1);
54
55    private static final float[] FIT = {2.26f, -3.02f, 1.76f};
56
57    // All of these are masks to push all of the drawable state into one int for easy callbacks
58    // and flow through sysui.
59    private static final int LEVEL_MASK = 0xff;
60    private static final int NUM_LEVEL_SHIFT = 8;
61    private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
62    private static final int STATE_SHIFT = 16;
63    private static final int STATE_MASK = 0xff << STATE_SHIFT;
64    private static final int STATE_NONE = 0;
65    private static final int STATE_EMPTY = 1;
66    private static final int STATE_CUT = 2;
67    private static final int STATE_CARRIER_CHANGE = 3;
68    private static final int STATE_AIRPLANE = 4;
69
70    private static final long DOT_DELAY = 1000;
71
72    private static float[][] X_PATH = new float[][]{
73            {21.9f / VIEWPORT, 17.0f / VIEWPORT},
74            {-1.1f / VIEWPORT, -1.1f / VIEWPORT},
75            {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
76            {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
77            {-1.1f / VIEWPORT, 1.1f / VIEWPORT},
78            {1.9f / VIEWPORT, 1.9f / VIEWPORT},
79            {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
80            {1.1f / VIEWPORT, 1.1f / VIEWPORT},
81            {1.9f / VIEWPORT, -1.9f / VIEWPORT},
82            {1.9f / VIEWPORT, 1.9f / VIEWPORT},
83            {1.1f / VIEWPORT, -1.1f / VIEWPORT},
84            {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
85    };
86
87    // Rounded corners are achieved by arcing a circle of radius `R` from its tangent points along
88    // the curve (curve ≡ triangle). On the top and left corners of the triangle, the tangents are
89    // as follows:
90    //      1) Along the straight lines (y = 0 and x = width):
91    //          Ps = circleOffset + R
92    //      2) Along the diagonal line (y = x):
93    //          Pd = √((Ps^2) / 2)
94    //              or (remember: sin(π/4) ≈ 0.7071)
95    //          Pd = (circleOffset + R - 0.7071, height - R - 0.7071)
96    //         Where Pd is the (x,y) coords of the point that intersects the circle at the bottom
97    //         left of the triangle
98    private static final float RADIUS_RATIO = 0.75f / 17f;
99    private static final float DIAG_OFFSET_MULTIPLIER = 0.707107f;
100    // How far the circle defining the corners is inset from the edges
101    private final float mAppliedCornerInset;
102
103    private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f);
104    private static final float CUT_WIDTH_DP = 1f / 12f;
105
106    // Where the top and left points of the triangle would be if not for rounding
107    private final PointF mVirtualTop  = new PointF();
108    private final PointF mVirtualLeft = new PointF();
109
110    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
111    private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
112    private final int mDarkModeBackgroundColor;
113    private final int mDarkModeFillColor;
114    private final int mLightModeBackgroundColor;
115    private final int mLightModeFillColor;
116    private final Path mFullPath = new Path();
117    private final Path mForegroundPath = new Path();
118    private final Path mXPath = new Path();
119    // Cut out when STATE_EMPTY
120    private final Path mCutPath = new Path();
121    // Draws the slash when in airplane mode
122    private final SlashArtist mSlash = new SlashArtist();
123    private final Handler mHandler;
124    private float mOldDarkIntensity = -1;
125    private float mNumLevels = 1;
126    private int mIntrinsicSize;
127    private int mLevel;
128    private int mState;
129    private boolean mVisible;
130    private boolean mAnimating;
131    private int mCurrentDot;
132
133    public SignalDrawable(Context context) {
134        mDarkModeBackgroundColor =
135                Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background);
136        mDarkModeFillColor =
137                Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill);
138        mLightModeBackgroundColor =
139                Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background);
140        mLightModeFillColor =
141                Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill);
142        mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
143
144        mHandler = new Handler();
145        setDarkIntensity(0);
146
147        mAppliedCornerInset = context.getResources()
148                .getDimensionPixelSize(R.dimen.stat_sys_mobile_signal_circle_inset);
149    }
150
151    public void setIntrinsicSize(int size) {
152        mIntrinsicSize = size;
153    }
154
155    @Override
156    public int getIntrinsicWidth() {
157        return mIntrinsicSize;
158    }
159
160    @Override
161    public int getIntrinsicHeight() {
162        return mIntrinsicSize;
163    }
164
165    public void setNumLevels(int levels) {
166        if (levels == mNumLevels) return;
167        mNumLevels = levels;
168        invalidateSelf();
169    }
170
171    private void setSignalState(int state) {
172        if (state == mState) return;
173        mState = state;
174        updateAnimation();
175        invalidateSelf();
176    }
177
178    private void updateAnimation() {
179        boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible;
180        if (shouldAnimate == mAnimating) return;
181        mAnimating = shouldAnimate;
182        if (shouldAnimate) {
183            mChangeDot.run();
184        } else {
185            mHandler.removeCallbacks(mChangeDot);
186        }
187    }
188
189    @Override
190    protected boolean onLevelChange(int state) {
191        setNumLevels(getNumLevels(state));
192        setSignalState(getState(state));
193        int level = getLevel(state);
194        if (level != mLevel) {
195            mLevel = level;
196            invalidateSelf();
197        }
198        return true;
199    }
200
201    public void setColors(int background, int foreground) {
202        mPaint.setColor(background);
203        mForegroundPaint.setColor(foreground);
204    }
205
206    public void setDarkIntensity(float darkIntensity) {
207        if (darkIntensity == mOldDarkIntensity) {
208            return;
209        }
210        mPaint.setColor(getBackgroundColor(darkIntensity));
211        mForegroundPaint.setColor(getFillColor(darkIntensity));
212        mOldDarkIntensity = darkIntensity;
213        invalidateSelf();
214    }
215
216    private int getFillColor(float darkIntensity) {
217        return getColorForDarkIntensity(
218                darkIntensity, mLightModeFillColor, mDarkModeFillColor);
219    }
220
221    private int getBackgroundColor(float darkIntensity) {
222        return getColorForDarkIntensity(
223                darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
224    }
225
226    private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
227        return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
228    }
229
230    @Override
231    protected void onBoundsChange(Rect bounds) {
232        super.onBoundsChange(bounds);
233        invalidateSelf();
234    }
235
236    @Override
237    public void draw(@NonNull Canvas canvas) {
238        final float width = getBounds().width();
239        final float height = getBounds().height();
240
241        boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
242        if (isRtl) {
243            canvas.save();
244            // Mirror the drawable
245            canvas.translate(width, 0);
246            canvas.scale(-1.0f, 1.0f);
247        }
248        mFullPath.reset();
249        mFullPath.setFillType(FillType.WINDING);
250
251        final float padding = Math.round(PAD * width);
252        final float cornerRadius = RADIUS_RATIO * height;
253        // Offset from circle where the hypotenuse meets the circle
254        final float diagOffset = DIAG_OFFSET_MULTIPLIER * cornerRadius;
255
256        // 1 - Bottom right, above corner
257        mFullPath.moveTo(width - padding, height - padding - cornerRadius);
258        // 2 - Line to top right, below corner
259        mFullPath.lineTo(width - padding, padding + cornerRadius + mAppliedCornerInset);
260        // 3 - Arc to top right, on hypotenuse
261        mFullPath.arcTo(
262                width - padding - (2 * cornerRadius),
263                padding + mAppliedCornerInset,
264                width - padding,
265                padding + mAppliedCornerInset + (2 * cornerRadius),
266                0.f, -135.f, false
267        );
268        // 4 - Line to bottom left, on hypotenuse
269        mFullPath.lineTo(padding + mAppliedCornerInset + cornerRadius - diagOffset,
270                height - padding - cornerRadius - diagOffset);
271        // 5 - Arc to bottom left, on leg
272        mFullPath.arcTo(
273                padding + mAppliedCornerInset,
274                height - padding - (2 * cornerRadius),
275                padding + mAppliedCornerInset + ( 2 * cornerRadius),
276                height - padding,
277                -135.f, -135.f, false
278        );
279        // 6 - Line to bottom rght, before corner
280        mFullPath.lineTo(width - padding - cornerRadius, height - padding);
281        // 7 - Arc to beginning (bottom right, above corner)
282        mFullPath.arcTo(
283                width - padding - (2 * cornerRadius),
284                height - padding - (2 * cornerRadius),
285                width - padding,
286                height - padding,
287                90.f, -90.f, false
288        );
289
290        if (mState == STATE_CARRIER_CHANGE) {
291            float cutWidth = (DOT_CUT_WIDTH * width);
292            float cutHeight = (DOT_CUT_HEIGHT * width);
293            float dotSize = (DOT_SIZE * height);
294            float dotPadding = (DOT_PADDING * height);
295
296            mFullPath.moveTo(width - padding, height - padding);
297            mFullPath.rLineTo(-cutWidth, 0);
298            mFullPath.rLineTo(0, -cutHeight);
299            mFullPath.rLineTo(cutWidth, 0);
300            mFullPath.rLineTo(0, cutHeight);
301            float dotSpacing = dotPadding * 2 + dotSize;
302            float x = width - padding - dotSize;
303            float y = height - padding - dotSize;
304            mForegroundPath.reset();
305            drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2);
306            drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1);
307            drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0);
308        } else if (mState == STATE_CUT) {
309            float cut = (CUT_OUT * width);
310            mFullPath.moveTo(width - padding, height - padding);
311            mFullPath.rLineTo(-cut, 0);
312            mFullPath.rLineTo(0, -cut);
313            mFullPath.rLineTo(cut, 0);
314            mFullPath.rLineTo(0, cut);
315        }
316
317        if (mState == STATE_EMPTY) {
318            // Where the corners would be if this were a real triangle
319            mVirtualTop.set(
320                    width - padding,
321                    (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius));
322            mVirtualLeft.set(
323                    (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius),
324                    height - padding);
325
326            final float cutWidth = CUT_WIDTH_DP * height;
327            final float cutDiagInset = cutWidth * INV_TAN;
328
329            // Cut out a smaller triangle from the center of mFullPath
330            mCutPath.reset();
331            mCutPath.setFillType(FillType.WINDING);
332            mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth);
333            mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset);
334            mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth);
335            mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth);
336
337            // Draw empty state as only background
338            mForegroundPath.reset();
339            mFullPath.op(mCutPath, Path.Op.DIFFERENCE);
340        } else if (mState == STATE_AIRPLANE) {
341            // Airplane mode is slashed, fully drawn background
342            mForegroundPath.reset();
343            mSlash.draw((int) height, (int) width, canvas, mPaint);
344        } else if (mState != STATE_CARRIER_CHANGE) {
345            mForegroundPath.reset();
346            int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding));
347            mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding,
348                    Direction.CW);
349            mForegroundPath.op(mFullPath, Op.INTERSECT);
350        }
351
352        canvas.drawPath(mFullPath, mPaint);
353        canvas.drawPath(mForegroundPath, mForegroundPaint);
354        if (mState == STATE_CUT) {
355            mXPath.reset();
356            mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height);
357            for (int i = 1; i < X_PATH.length; i++) {
358                mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height);
359            }
360            canvas.drawPath(mXPath, mForegroundPaint);
361        }
362        if (isRtl) {
363            canvas.restore();
364        }
365    }
366
367    private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize,
368            int i) {
369        Path p = (i == mCurrentDot) ? foregroundPath : fullPath;
370        p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
371    }
372
373    // This is a fit line based on previous values of provided in assets, but if
374    // you look at the a plot of this actual fit, it makes a lot of sense, what it does
375    // is compress the areas that are very visually easy to see changes (the middle sections)
376    // and spread out the sections that are hard to see (each end of the icon).
377    // The current fit is cubic, but pretty easy to change the way the code is written (just add
378    // terms to the end of FIT).
379    private float calcFit(float v) {
380        float ret = 0;
381        float t = v;
382        for (int i = 0; i < FIT.length; i++) {
383            ret += FIT[i] * t;
384            t *= v;
385        }
386        return ret;
387    }
388
389    @Override
390    public int getAlpha() {
391        return mPaint.getAlpha();
392    }
393
394    @Override
395    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
396        mPaint.setAlpha(alpha);
397        mForegroundPaint.setAlpha(alpha);
398    }
399
400    @Override
401    public void setColorFilter(@Nullable ColorFilter colorFilter) {
402        mPaint.setColorFilter(colorFilter);
403        mForegroundPaint.setColorFilter(colorFilter);
404    }
405
406    @Override
407    public int getOpacity() {
408        return 255;
409    }
410
411    @Override
412    public boolean setVisible(boolean visible, boolean restart) {
413        mVisible = visible;
414        updateAnimation();
415        return super.setVisible(visible, restart);
416    }
417
418    private final Runnable mChangeDot = new Runnable() {
419        @Override
420        public void run() {
421            if (++mCurrentDot == NUM_DOTS) {
422                mCurrentDot = 0;
423            }
424            invalidateSelf();
425            mHandler.postDelayed(mChangeDot, DOT_DELAY);
426        }
427    };
428
429    public static int getLevel(int fullState) {
430        return fullState & LEVEL_MASK;
431    }
432
433    public static int getState(int fullState) {
434        return (fullState & STATE_MASK) >> STATE_SHIFT;
435    }
436
437    public static int getNumLevels(int fullState) {
438        return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
439    }
440
441    public static int getState(int level, int numLevels, boolean cutOut) {
442        return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
443                | (numLevels << NUM_LEVEL_SHIFT)
444                | level;
445    }
446
447    public static int getCarrierChangeState(int numLevels) {
448        return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
449    }
450
451    public static int getEmptyState(int numLevels) {
452        return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
453    }
454
455    public static int getAirplaneModeState(int numLevels) {
456        return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
457    }
458
459    private final class SlashArtist {
460        private static final float CORNER_RADIUS = 1f;
461        // These values are derived in un-rotated (vertical) orientation
462        private static final float SLASH_WIDTH = 1.8384776f;
463        private static final float SLASH_HEIGHT = 22f;
464        private static final float CENTER_X = 10.65f;
465        private static final float CENTER_Y = 15.869239f;
466        private static final float SCALE = 24f;
467
468        // Bottom is derived during animation
469        private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
470        private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
471        private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
472        private static final float BOTTOM = (CENTER_Y + (SLASH_HEIGHT / 2)) / SCALE;
473        // Draw the slash washington-monument style; rotate to no-u-turn style
474        private static final float ROTATION = -45f;
475
476        private final Path mPath = new Path();
477        private final RectF mSlashRect = new RectF();
478
479        void draw(int height, int width, @NonNull Canvas canvas, Paint paint) {
480            Matrix m = new Matrix();
481            final float radius = scale(CORNER_RADIUS, width);
482            updateRect(
483                    scale(LEFT, width),
484                    scale(TOP, height),
485                    scale(RIGHT, width),
486                    scale(BOTTOM, height));
487
488            mPath.reset();
489            // Draw the slash vertically
490            mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
491            m.setRotate(ROTATION, width / 2, height / 2);
492            mPath.transform(m);
493            canvas.drawPath(mPath, paint);
494
495            // Rotate back to vertical, and draw the cut-out rect next to this one
496            m.setRotate(-ROTATION, width / 2, height / 2);
497            mPath.transform(m);
498            m.setTranslate(mSlashRect.width(), 0);
499            mPath.transform(m);
500            mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
501            m.setRotate(ROTATION, width / 2, height / 2);
502            mPath.transform(m);
503            canvas.clipOutPath(mPath);
504        }
505
506        void updateRect(float left, float top, float right, float bottom) {
507            mSlashRect.left = left;
508            mSlashRect.top = top;
509            mSlashRect.right = right;
510            mSlashRect.bottom = bottom;
511        }
512
513        private float scale(float frac, int width) {
514            return frac * width;
515        }
516    }
517}
518