Home | History | Annotate | Download | only in graph
      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 
    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