Home | History | Annotate | Download | only in phone
      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.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 import com.android.systemui.qs.SlashDrawable;
     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     // Rounded corners are achieved by arcing a circle of radius `R` from its tangent points along
     89     // the curve (curve  triangle). On the top and left corners of the triangle, the tangents are
     90     // as follows:
     91     //      1) Along the straight lines (y = 0 and x = width):
     92     //          Ps = circleOffset + R
     93     //      2) Along the diagonal line (y = x):
     94     //          Pd = ((Ps^2) / 2)
     95     //              or (remember: sin(/4)  0.7071)
     96     //          Pd = (circleOffset + R - 0.7071, height - R - 0.7071)
     97     //         Where Pd is the (x,y) coords of the point that intersects the circle at the bottom
     98     //         left of the triangle
     99     private static final float RADIUS_RATIO = 0.75f / 17f;
    100     private static final float DIAG_OFFSET_MULTIPLIER = 0.707107f;
    101     // How far the circle defining the corners is inset from the edges
    102     private final float mAppliedCornerInset;
    103 
    104     private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f);
    105     private static final float CUT_WIDTH_DP = 1f / 12f;
    106 
    107     // Where the top and left points of the triangle would be if not for rounding
    108     private final PointF mVirtualTop  = new PointF();
    109     private final PointF mVirtualLeft = new PointF();
    110 
    111     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    112     private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    113     private final int mDarkModeBackgroundColor;
    114     private final int mDarkModeFillColor;
    115     private final int mLightModeBackgroundColor;
    116     private final int mLightModeFillColor;
    117     private final Path mFullPath = new Path();
    118     private final Path mForegroundPath = new Path();
    119     private final Path mXPath = new Path();
    120     // Cut out when STATE_EMPTY
    121     private final Path mCutPath = new Path();
    122     // Draws the slash when in airplane mode
    123     private final SlashArtist mSlash = new SlashArtist();
    124     private final Handler mHandler;
    125     private float mOldDarkIntensity = -1;
    126     private float mNumLevels = 1;
    127     private int mIntrinsicSize;
    128     private int mLevel;
    129     private int mState;
    130     private boolean mVisible;
    131     private boolean mAnimating;
    132     private int mCurrentDot;
    133 
    134     public SignalDrawable(Context context) {
    135         mDarkModeBackgroundColor =
    136                 Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background);
    137         mDarkModeFillColor =
    138                 Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill);
    139         mLightModeBackgroundColor =
    140                 Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background);
    141         mLightModeFillColor =
    142                 Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill);
    143         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
    144 
    145         mHandler = new Handler();
    146         setDarkIntensity(0);
    147 
    148         mAppliedCornerInset = context.getResources()
    149                 .getDimensionPixelSize(R.dimen.stat_sys_mobile_signal_circle_inset);
    150     }
    151 
    152     public void setIntrinsicSize(int size) {
    153         mIntrinsicSize = size;
    154     }
    155 
    156     @Override
    157     public int getIntrinsicWidth() {
    158         return mIntrinsicSize;
    159     }
    160 
    161     @Override
    162     public int getIntrinsicHeight() {
    163         return mIntrinsicSize;
    164     }
    165 
    166     public void setNumLevels(int levels) {
    167         if (levels == mNumLevels) return;
    168         mNumLevels = levels;
    169         invalidateSelf();
    170     }
    171 
    172     private void setSignalState(int state) {
    173         if (state == mState) return;
    174         mState = state;
    175         updateAnimation();
    176         invalidateSelf();
    177     }
    178 
    179     private void updateAnimation() {
    180         boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible;
    181         if (shouldAnimate == mAnimating) return;
    182         mAnimating = shouldAnimate;
    183         if (shouldAnimate) {
    184             mChangeDot.run();
    185         } else {
    186             mHandler.removeCallbacks(mChangeDot);
    187         }
    188     }
    189 
    190     @Override
    191     protected boolean onLevelChange(int state) {
    192         setNumLevels(getNumLevels(state));
    193         setSignalState(getState(state));
    194         int level = getLevel(state);
    195         if (level != mLevel) {
    196             mLevel = level;
    197             invalidateSelf();
    198         }
    199         return true;
    200     }
    201 
    202     public void setColors(int background, int foreground) {
    203         mPaint.setColor(background);
    204         mForegroundPaint.setColor(foreground);
    205     }
    206 
    207     public void setDarkIntensity(float darkIntensity) {
    208         if (darkIntensity == mOldDarkIntensity) {
    209             return;
    210         }
    211         mPaint.setColor(getBackgroundColor(darkIntensity));
    212         mForegroundPaint.setColor(getFillColor(darkIntensity));
    213         mOldDarkIntensity = darkIntensity;
    214         invalidateSelf();
    215     }
    216 
    217     private int getFillColor(float darkIntensity) {
    218         return getColorForDarkIntensity(
    219                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
    220     }
    221 
    222     private int getBackgroundColor(float darkIntensity) {
    223         return getColorForDarkIntensity(
    224                 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
    225     }
    226 
    227     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
    228         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
    229     }
    230 
    231     @Override
    232     protected void onBoundsChange(Rect bounds) {
    233         super.onBoundsChange(bounds);
    234         invalidateSelf();
    235     }
    236 
    237     @Override
    238     public void draw(@NonNull Canvas canvas) {
    239         final float width = getBounds().width();
    240         final float height = getBounds().height();
    241 
    242         boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
    243         if (isRtl) {
    244             canvas.save();
    245             // Mirror the drawable
    246             canvas.translate(width, 0);
    247             canvas.scale(-1.0f, 1.0f);
    248         }
    249         mFullPath.reset();
    250         mFullPath.setFillType(FillType.WINDING);
    251 
    252         final float padding = Math.round(PAD * width);
    253         final float cornerRadius = RADIUS_RATIO * height;
    254         // Offset from circle where the hypotenuse meets the circle
    255         final float diagOffset = DIAG_OFFSET_MULTIPLIER * cornerRadius;
    256 
    257         // 1 - Bottom right, above corner
    258         mFullPath.moveTo(width - padding, height - padding - cornerRadius);
    259         // 2 - Line to top right, below corner
    260         mFullPath.lineTo(width - padding, padding + cornerRadius + mAppliedCornerInset);
    261         // 3 - Arc to top right, on hypotenuse
    262         mFullPath.arcTo(
    263                 width - padding - (2 * cornerRadius),
    264                 padding + mAppliedCornerInset,
    265                 width - padding,
    266                 padding + mAppliedCornerInset + (2 * cornerRadius),
    267                 0.f, -135.f, false
    268         );
    269         // 4 - Line to bottom left, on hypotenuse
    270         mFullPath.lineTo(padding + mAppliedCornerInset + cornerRadius - diagOffset,
    271                 height - padding - cornerRadius - diagOffset);
    272         // 5 - Arc to bottom left, on leg
    273         mFullPath.arcTo(
    274                 padding + mAppliedCornerInset,
    275                 height - padding - (2 * cornerRadius),
    276                 padding + mAppliedCornerInset + ( 2 * cornerRadius),
    277                 height - padding,
    278                 -135.f, -135.f, false
    279         );
    280         // 6 - Line to bottom rght, before corner
    281         mFullPath.lineTo(width - padding - cornerRadius, height - padding);
    282         // 7 - Arc to beginning (bottom right, above corner)
    283         mFullPath.arcTo(
    284                 width - padding - (2 * cornerRadius),
    285                 height - padding - (2 * cornerRadius),
    286                 width - padding,
    287                 height - padding,
    288                 90.f, -90.f, false
    289         );
    290 
    291         if (mState == STATE_CARRIER_CHANGE) {
    292             float cutWidth = (DOT_CUT_WIDTH * width);
    293             float cutHeight = (DOT_CUT_HEIGHT * width);
    294             float dotSize = (DOT_SIZE * height);
    295             float dotPadding = (DOT_PADDING * height);
    296 
    297             mFullPath.moveTo(width - padding, height - padding);
    298             mFullPath.rLineTo(-cutWidth, 0);
    299             mFullPath.rLineTo(0, -cutHeight);
    300             mFullPath.rLineTo(cutWidth, 0);
    301             mFullPath.rLineTo(0, cutHeight);
    302             float dotSpacing = dotPadding * 2 + dotSize;
    303             float x = width - padding - dotSize;
    304             float y = height - padding - dotSize;
    305             mForegroundPath.reset();
    306             drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2);
    307             drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1);
    308             drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0);
    309         } else if (mState == STATE_CUT) {
    310             float cut = (CUT_OUT * width);
    311             mFullPath.moveTo(width - padding, height - padding);
    312             mFullPath.rLineTo(-cut, 0);
    313             mFullPath.rLineTo(0, -cut);
    314             mFullPath.rLineTo(cut, 0);
    315             mFullPath.rLineTo(0, cut);
    316         }
    317 
    318         if (mState == STATE_EMPTY) {
    319             // Where the corners would be if this were a real triangle
    320             mVirtualTop.set(
    321                     width - padding,
    322                     (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius));
    323             mVirtualLeft.set(
    324                     (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius),
    325                     height - padding);
    326 
    327             final float cutWidth = CUT_WIDTH_DP * height;
    328             final float cutDiagInset = cutWidth * INV_TAN;
    329 
    330             // Cut out a smaller triangle from the center of mFullPath
    331             mCutPath.reset();
    332             mCutPath.setFillType(FillType.WINDING);
    333             mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth);
    334             mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset);
    335             mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth);
    336             mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth);
    337 
    338             // Draw empty state as only background
    339             mForegroundPath.reset();
    340             mFullPath.op(mCutPath, Path.Op.DIFFERENCE);
    341         } else if (mState == STATE_AIRPLANE) {
    342             // Airplane mode is slashed, fully drawn background
    343             mForegroundPath.reset();
    344             mSlash.draw((int) height, (int) width, canvas, mPaint);
    345         } else if (mState != STATE_CARRIER_CHANGE) {
    346             mForegroundPath.reset();
    347             int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding));
    348             mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding,
    349                     Direction.CW);
    350             mForegroundPath.op(mFullPath, Op.INTERSECT);
    351         }
    352 
    353         canvas.drawPath(mFullPath, mPaint);
    354         canvas.drawPath(mForegroundPath, mForegroundPaint);
    355         if (mState == STATE_CUT) {
    356             mXPath.reset();
    357             mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height);
    358             for (int i = 1; i < X_PATH.length; i++) {
    359                 mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height);
    360             }
    361             canvas.drawPath(mXPath, mForegroundPaint);
    362         }
    363         if (isRtl) {
    364             canvas.restore();
    365         }
    366     }
    367 
    368     private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize,
    369             int i) {
    370         Path p = (i == mCurrentDot) ? foregroundPath : fullPath;
    371         p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
    372     }
    373 
    374     // This is a fit line based on previous values of provided in assets, but if
    375     // you look at the a plot of this actual fit, it makes a lot of sense, what it does
    376     // is compress the areas that are very visually easy to see changes (the middle sections)
    377     // and spread out the sections that are hard to see (each end of the icon).
    378     // The current fit is cubic, but pretty easy to change the way the code is written (just add
    379     // terms to the end of FIT).
    380     private float calcFit(float v) {
    381         float ret = 0;
    382         float t = v;
    383         for (int i = 0; i < FIT.length; i++) {
    384             ret += FIT[i] * t;
    385             t *= v;
    386         }
    387         return ret;
    388     }
    389 
    390     @Override
    391     public int getAlpha() {
    392         return mPaint.getAlpha();
    393     }
    394 
    395     @Override
    396     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
    397         mPaint.setAlpha(alpha);
    398         mForegroundPaint.setAlpha(alpha);
    399     }
    400 
    401     @Override
    402     public void setColorFilter(@Nullable ColorFilter colorFilter) {
    403         mPaint.setColorFilter(colorFilter);
    404         mForegroundPaint.setColorFilter(colorFilter);
    405     }
    406 
    407     @Override
    408     public int getOpacity() {
    409         return 255;
    410     }
    411 
    412     @Override
    413     public boolean setVisible(boolean visible, boolean restart) {
    414         mVisible = visible;
    415         updateAnimation();
    416         return super.setVisible(visible, restart);
    417     }
    418 
    419     private final Runnable mChangeDot = new Runnable() {
    420         @Override
    421         public void run() {
    422             if (++mCurrentDot == NUM_DOTS) {
    423                 mCurrentDot = 0;
    424             }
    425             invalidateSelf();
    426             mHandler.postDelayed(mChangeDot, DOT_DELAY);
    427         }
    428     };
    429 
    430     public static int getLevel(int fullState) {
    431         return fullState & LEVEL_MASK;
    432     }
    433 
    434     public static int getState(int fullState) {
    435         return (fullState & STATE_MASK) >> STATE_SHIFT;
    436     }
    437 
    438     public static int getNumLevels(int fullState) {
    439         return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
    440     }
    441 
    442     public static int getState(int level, int numLevels, boolean cutOut) {
    443         return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
    444                 | (numLevels << NUM_LEVEL_SHIFT)
    445                 | level;
    446     }
    447 
    448     public static int getCarrierChangeState(int numLevels) {
    449         return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
    450     }
    451 
    452     public static int getEmptyState(int numLevels) {
    453         return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
    454     }
    455 
    456     public static int getAirplaneModeState(int numLevels) {
    457         return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
    458     }
    459 
    460     private final class SlashArtist {
    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(SlashDrawable.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