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 
15 package com.android.settingslib.graph;
16 
17 import android.animation.ArgbEvaluator;
18 import android.annotation.IntRange;
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.graphics.Canvas;
23 import android.graphics.ColorFilter;
24 import android.graphics.Matrix;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.graphics.Path.Direction;
28 import android.graphics.Path.FillType;
29 import android.graphics.Path.Op;
30 import android.graphics.PointF;
31 import android.graphics.Rect;
32 import android.graphics.RectF;
33 import android.graphics.drawable.Drawable;
34 import android.os.Handler;
35 import android.util.LayoutDirection;
36 
37 import com.android.settingslib.R;
38 import com.android.settingslib.Utils;
39 
40 public 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 
SignalDrawable(Context context)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 
setIntrinsicSize(int size)151     public void setIntrinsicSize(int size) {
152         mIntrinsicSize = size;
153     }
154 
155     @Override
getIntrinsicWidth()156     public int getIntrinsicWidth() {
157         return mIntrinsicSize;
158     }
159 
160     @Override
getIntrinsicHeight()161     public int getIntrinsicHeight() {
162         return mIntrinsicSize;
163     }
164 
setNumLevels(int levels)165     public void setNumLevels(int levels) {
166         if (levels == mNumLevels) return;
167         mNumLevels = levels;
168         invalidateSelf();
169     }
170 
setSignalState(int state)171     private void setSignalState(int state) {
172         if (state == mState) return;
173         mState = state;
174         updateAnimation();
175         invalidateSelf();
176     }
177 
updateAnimation()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
onLevelChange(int state)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 
setColors(int background, int foreground)201     public void setColors(int background, int foreground) {
202         mPaint.setColor(background);
203         mForegroundPaint.setColor(foreground);
204     }
205 
setDarkIntensity(float darkIntensity)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 
getFillColor(float darkIntensity)216     private int getFillColor(float darkIntensity) {
217         return getColorForDarkIntensity(
218                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
219     }
220 
getBackgroundColor(float darkIntensity)221     private int getBackgroundColor(float darkIntensity) {
222         return getColorForDarkIntensity(
223                 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
224     }
225 
getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor)226     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
227         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
228     }
229 
230     @Override
onBoundsChange(Rect bounds)231     protected void onBoundsChange(Rect bounds) {
232         super.onBoundsChange(bounds);
233         invalidateSelf();
234     }
235 
236     @Override
draw(@onNull Canvas canvas)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 
drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize, int i)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).
calcFit(float v)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
getAlpha()390     public int getAlpha() {
391         return mPaint.getAlpha();
392     }
393 
394     @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)395     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
396         mPaint.setAlpha(alpha);
397         mForegroundPaint.setAlpha(alpha);
398     }
399 
400     @Override
setColorFilter(@ullable ColorFilter colorFilter)401     public void setColorFilter(@Nullable ColorFilter colorFilter) {
402         mPaint.setColorFilter(colorFilter);
403         mForegroundPaint.setColorFilter(colorFilter);
404     }
405 
406     @Override
getOpacity()407     public int getOpacity() {
408         return 255;
409     }
410 
411     @Override
setVisible(boolean visible, boolean restart)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 
getLevel(int fullState)429     public static int getLevel(int fullState) {
430         return fullState & LEVEL_MASK;
431     }
432 
getState(int fullState)433     public static int getState(int fullState) {
434         return (fullState & STATE_MASK) >> STATE_SHIFT;
435     }
436 
getNumLevels(int fullState)437     public static int getNumLevels(int fullState) {
438         return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
439     }
440 
getState(int level, int numLevels, boolean cutOut)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 
getCarrierChangeState(int numLevels)447     public static int getCarrierChangeState(int numLevels) {
448         return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
449     }
450 
getEmptyState(int numLevels)451     public static int getEmptyState(int numLevels) {
452         return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
453     }
454 
getAirplaneModeState(int numLevels)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 
draw(int height, int width, @NonNull Canvas canvas, Paint paint)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 
updateRect(float left, float top, float right, float bottom)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 
scale(float frac, int width)513         private float scale(float frac, int width) {
514             return frac * width;
515         }
516     }
517 }
518