• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.systemui.statusbar.phone;
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.Paint.Style;
27 import android.graphics.Path;
28 import android.graphics.Path.Direction;
29 import android.graphics.Path.FillType;
30 import android.graphics.Path.Op;
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 import android.util.Log;
37 
38 import com.android.settingslib.R;
39 import com.android.settingslib.Utils;
40 
41 public class SignalDrawable extends Drawable {
42 
43     private static final String TAG = "SignalDrawable";
44 
45     private static final int NUM_DOTS = 3;
46 
47     private static final float VIEWPORT = 24f;
48     private static final float PAD = 2f / VIEWPORT;
49     private static final float CUT_OUT = 7.9f / VIEWPORT;
50 
51     private static final float DOT_SIZE = 3f / VIEWPORT;
52     private static final float DOT_PADDING = 1f / VIEWPORT;
53     private static final float DOT_CUT_WIDTH = (DOT_SIZE * 3) + (DOT_PADDING * 5);
54     private static final float DOT_CUT_HEIGHT = (DOT_SIZE * 1) + (DOT_PADDING * 1);
55 
56     private static final float[] FIT = {2.26f, -3.02f, 1.76f};
57 
58     // All of these are masks to push all of the drawable state into one int for easy callbacks
59     // and flow through sysui.
60     private static final int LEVEL_MASK = 0xff;
61     private static final int NUM_LEVEL_SHIFT = 8;
62     private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
63     private static final int STATE_SHIFT = 16;
64     private static final int STATE_MASK = 0xff << STATE_SHIFT;
65     private static final int STATE_NONE = 0;
66     private static final int STATE_EMPTY = 1;
67     private static final int STATE_CUT = 2;
68     private static final int STATE_CARRIER_CHANGE = 3;
69     private static final int STATE_AIRPLANE = 4;
70 
71     private static final long DOT_DELAY = 1000;
72 
73     private static float[][] X_PATH = new float[][]{
74             {21.9f / VIEWPORT, 17.0f / VIEWPORT},
75             {-1.1f / VIEWPORT, -1.1f / VIEWPORT},
76             {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
77             {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
78             {-1.1f / VIEWPORT, 1.1f / VIEWPORT},
79             {1.9f / VIEWPORT, 1.9f / VIEWPORT},
80             {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
81             {1.1f / VIEWPORT, 1.1f / VIEWPORT},
82             {1.9f / VIEWPORT, -1.9f / VIEWPORT},
83             {1.9f / VIEWPORT, 1.9f / VIEWPORT},
84             {1.1f / VIEWPORT, -1.1f / VIEWPORT},
85             {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
86     };
87 
88     private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f);
89     private static final float CUT_WIDTH_DP = 1f / 12f;
90 
91     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
92     private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
93     private final int mDarkModeBackgroundColor;
94     private final int mDarkModeFillColor;
95     private final int mLightModeBackgroundColor;
96     private final int mLightModeFillColor;
97     private final Path mFullPath = new Path();
98     private final Path mForegroundPath = new Path();
99     private final Path mXPath = new Path();
100     // Cut out when STATE_EMPTY
101     private final Path mCutPath = new Path();
102     // Draws the slash when in airplane mode
103     private final SlashArtist mSlash = new SlashArtist();
104     private final Handler mHandler;
105     private float mOldDarkIntensity = -1;
106     private float mNumLevels = 1;
107     private int mIntrinsicSize;
108     private int mLevel;
109     private int mState;
110     private boolean mVisible;
111     private boolean mAnimating;
112     private int mCurrentDot;
113 
SignalDrawable(Context context)114     public SignalDrawable(Context context) {
115         mDarkModeBackgroundColor =
116                 Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background);
117         mDarkModeFillColor =
118                 Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill);
119         mLightModeBackgroundColor =
120                 Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background);
121         mLightModeFillColor =
122                 Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill);
123         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
124 
125         mHandler = new Handler();
126         setDarkIntensity(0);
127     }
128 
setIntrinsicSize(int size)129     public void setIntrinsicSize(int size) {
130         mIntrinsicSize = size;
131     }
132 
133     @Override
getIntrinsicWidth()134     public int getIntrinsicWidth() {
135         return mIntrinsicSize;
136     }
137 
138     @Override
getIntrinsicHeight()139     public int getIntrinsicHeight() {
140         return mIntrinsicSize;
141     }
142 
setNumLevels(int levels)143     public void setNumLevels(int levels) {
144         if (levels == mNumLevels) return;
145         mNumLevels = levels;
146         invalidateSelf();
147     }
148 
setSignalState(int state)149     private void setSignalState(int state) {
150         if (state == mState) return;
151         mState = state;
152         updateAnimation();
153         invalidateSelf();
154     }
155 
updateAnimation()156     private void updateAnimation() {
157         boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible;
158         if (shouldAnimate == mAnimating) return;
159         mAnimating = shouldAnimate;
160         if (shouldAnimate) {
161             mChangeDot.run();
162         } else {
163             mHandler.removeCallbacks(mChangeDot);
164         }
165     }
166 
167     @Override
onLevelChange(int state)168     protected boolean onLevelChange(int state) {
169         setNumLevels(getNumLevels(state));
170         setSignalState(getState(state));
171         int level = getLevel(state);
172         if (level != mLevel) {
173             mLevel = level;
174             invalidateSelf();
175         }
176         return true;
177     }
178 
setDarkIntensity(float darkIntensity)179     public void setDarkIntensity(float darkIntensity) {
180         if (darkIntensity == mOldDarkIntensity) {
181             return;
182         }
183         mPaint.setColor(getBackgroundColor(darkIntensity));
184         mForegroundPaint.setColor(getFillColor(darkIntensity));
185         mOldDarkIntensity = darkIntensity;
186         invalidateSelf();
187     }
188 
getFillColor(float darkIntensity)189     private int getFillColor(float darkIntensity) {
190         return getColorForDarkIntensity(
191                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
192     }
193 
getBackgroundColor(float darkIntensity)194     private int getBackgroundColor(float darkIntensity) {
195         return getColorForDarkIntensity(
196                 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
197     }
198 
getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor)199     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
200         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
201     }
202 
203     @Override
onBoundsChange(Rect bounds)204     protected void onBoundsChange(Rect bounds) {
205         super.onBoundsChange(bounds);
206         invalidateSelf();
207     }
208 
209     @Override
draw(@onNull Canvas canvas)210     public void draw(@NonNull Canvas canvas) {
211         boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
212         if (isRtl) {
213             canvas.save();
214             // Mirror the drawable
215             canvas.translate(canvas.getWidth(), 0);
216             canvas.scale(-1.0f, 1.0f);
217         }
218         mFullPath.reset();
219         mFullPath.setFillType(FillType.WINDING);
220         float width = getBounds().width();
221         float height = getBounds().height();
222         float padding = Math.round(PAD * width);
223         mFullPath.moveTo(width - padding, height - padding);
224         mFullPath.lineTo(width - padding, padding);
225         mFullPath.lineTo(padding, height - padding);
226         mFullPath.lineTo(width - padding, height - padding);
227 
228         if (mState == STATE_CARRIER_CHANGE) {
229             float cutWidth = (DOT_CUT_WIDTH * width);
230             float cutHeight = (DOT_CUT_HEIGHT * width);
231             float dotSize = (DOT_SIZE * height);
232             float dotPadding = (DOT_PADDING * height);
233 
234             mFullPath.moveTo(width - padding, height - padding);
235             mFullPath.rLineTo(-cutWidth, 0);
236             mFullPath.rLineTo(0, -cutHeight);
237             mFullPath.rLineTo(cutWidth, 0);
238             mFullPath.rLineTo(0, cutHeight);
239             float dotSpacing = dotPadding * 2 + dotSize;
240             float x = width - padding - dotSize;
241             float y = height - padding - dotSize;
242             mForegroundPath.reset();
243             drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2);
244             drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1);
245             drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0);
246         } else if (mState == STATE_CUT) {
247             float cut = (CUT_OUT * width);
248             mFullPath.moveTo(width - padding, height - padding);
249             mFullPath.rLineTo(-cut, 0);
250             mFullPath.rLineTo(0, -cut);
251             mFullPath.rLineTo(cut, 0);
252             mFullPath.rLineTo(0, cut);
253         }
254 
255         if (mState == STATE_EMPTY) {
256             final float cutWidth = CUT_WIDTH_DP * height;
257             final float cutDiagInset = cutWidth * INV_TAN;
258 
259             // Cut out a smaller triangle from the center of mFullPath
260             mCutPath.reset();
261             mCutPath.setFillType(FillType.WINDING);
262             mCutPath.moveTo(width - padding - cutWidth,
263                     height - padding - cutWidth);
264             mCutPath.lineTo(width - padding - cutWidth, padding + cutDiagInset);
265             mCutPath.lineTo(padding + cutDiagInset, height - padding - cutWidth);
266             mCutPath.lineTo(width - padding - cutWidth,
267                     height - padding - cutWidth);
268 
269             // Draw empty state as only background
270             mForegroundPath.reset();
271             mFullPath.op(mCutPath, Path.Op.DIFFERENCE);
272         } else if (mState == STATE_AIRPLANE) {
273             // Airplane mode is slashed, full-signal
274             mForegroundPath.set(mFullPath);
275             mFullPath.reset();
276             mSlash.draw((int) height, (int) width, canvas, mForegroundPaint);
277         } else if (mState != STATE_CARRIER_CHANGE) {
278             mForegroundPath.reset();
279             int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding));
280             mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding,
281                     Direction.CW);
282             mForegroundPath.op(mFullPath, Op.INTERSECT);
283         }
284 
285         canvas.drawPath(mFullPath, mPaint);
286         canvas.drawPath(mForegroundPath, mForegroundPaint);
287         if (mState == STATE_CUT) {
288             mXPath.reset();
289             mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height);
290             for (int i = 1; i < X_PATH.length; i++) {
291                 mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height);
292             }
293             canvas.drawPath(mXPath, mForegroundPaint);
294         }
295         if (isRtl) {
296             canvas.restore();
297         }
298     }
299 
drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize, int i)300     private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize,
301             int i) {
302         Path p = (i == mCurrentDot) ? foregroundPath : fullPath;
303         p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
304     }
305 
306     // This is a fit line based on previous values of provided in assets, but if
307     // you look at the a plot of this actual fit, it makes a lot of sense, what it does
308     // is compress the areas that are very visually easy to see changes (the middle sections)
309     // and spread out the sections that are hard to see (each end of the icon).
310     // The current fit is cubic, but pretty easy to change the way the code is written (just add
311     // terms to the end of FIT).
calcFit(float v)312     private float calcFit(float v) {
313         float ret = 0;
314         float t = v;
315         for (int i = 0; i < FIT.length; i++) {
316             ret += FIT[i] * t;
317             t *= v;
318         }
319         return ret;
320     }
321 
322     @Override
getAlpha()323     public int getAlpha() {
324         return mPaint.getAlpha();
325     }
326 
327     @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)328     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
329         mPaint.setAlpha(alpha);
330         mForegroundPaint.setAlpha(alpha);
331     }
332 
333     @Override
setColorFilter(@ullable ColorFilter colorFilter)334     public void setColorFilter(@Nullable ColorFilter colorFilter) {
335         mPaint.setColorFilter(colorFilter);
336         mForegroundPaint.setColorFilter(colorFilter);
337     }
338 
339     @Override
getOpacity()340     public int getOpacity() {
341         return 255;
342     }
343 
344     @Override
setVisible(boolean visible, boolean restart)345     public boolean setVisible(boolean visible, boolean restart) {
346         mVisible = visible;
347         updateAnimation();
348         return super.setVisible(visible, restart);
349     }
350 
351     private final Runnable mChangeDot = new Runnable() {
352         @Override
353         public void run() {
354             if (++mCurrentDot == NUM_DOTS) {
355                 mCurrentDot = 0;
356             }
357             invalidateSelf();
358             mHandler.postDelayed(mChangeDot, DOT_DELAY);
359         }
360     };
361 
getLevel(int fullState)362     public static int getLevel(int fullState) {
363         return fullState & LEVEL_MASK;
364     }
365 
getState(int fullState)366     public static int getState(int fullState) {
367         return (fullState & STATE_MASK) >> STATE_SHIFT;
368     }
369 
getNumLevels(int fullState)370     public static int getNumLevels(int fullState) {
371         return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
372     }
373 
getState(int level, int numLevels, boolean cutOut)374     public static int getState(int level, int numLevels, boolean cutOut) {
375         return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
376                 | (numLevels << NUM_LEVEL_SHIFT)
377                 | level;
378     }
379 
getCarrierChangeState(int numLevels)380     public static int getCarrierChangeState(int numLevels) {
381         return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
382     }
383 
getEmptyState(int numLevels)384     public static int getEmptyState(int numLevels) {
385         return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
386     }
387 
getAirplaneModeState(int numLevels)388     public static int getAirplaneModeState(int numLevels) {
389         return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
390     }
391 
392     private final class SlashArtist {
393         // These values are derived in un-rotated (vertical) orientation
394         private static final float SLASH_WIDTH = 1.8384776f;
395         private static final float SLASH_HEIGHT = 22f;
396         private static final float CENTER_X = 10.65f;
397         private static final float CENTER_Y = 15.869239f;
398         private static final float SCALE = 24f;
399 
400         // Bottom is derived during animation
401         private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
402         private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
403         private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
404         private static final float BOTTOM = (CENTER_Y + (SLASH_HEIGHT / 2)) / SCALE;
405         // Draw the slash washington-monument style; rotate to no-u-turn style
406         private static final float ROTATION = -45f;
407 
408         private final Path mPath = new Path();
409         private final RectF mSlashRect = new RectF();
410 
draw(int height, int width, @NonNull Canvas canvas, Paint paint)411         void draw(int height, int width, @NonNull Canvas canvas, Paint paint) {
412             Matrix m = new Matrix();
413             updateRect(
414                     scale(LEFT, width),
415                     scale(TOP, height),
416                     scale(RIGHT, width),
417                     scale(BOTTOM, height));
418 
419             mPath.reset();
420             // Draw the slash vertically
421             mPath.addRect(mSlashRect, Direction.CW);
422             m.setRotate(ROTATION, width / 2, height / 2);
423             mPath.transform(m);
424             canvas.drawPath(mPath, paint);
425 
426             // Rotate back to vertical, and draw the cut-out rect next to this one
427             m.setRotate(-ROTATION, width / 2, height / 2);
428             mPath.transform(m);
429             m.setTranslate(mSlashRect.width(), 0);
430             mPath.transform(m);
431             mPath.addRect(mSlashRect, Direction.CW);
432             m.setRotate(ROTATION, width / 2, height / 2);
433             mPath.transform(m);
434             canvas.clipOutPath(mPath);
435         }
436 
updateRect(float left, float top, float right, float bottom)437         void updateRect(float left, float top, float right, float bottom) {
438             mSlashRect.left = left;
439             mSlashRect.top = top;
440             mSlashRect.right = right;
441             mSlashRect.bottom = bottom;
442         }
443 
scale(float frac, int width)444         private float scale(float frac, int width) {
445             return frac * width;
446         }
447     }
448 }
449