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.content.res.ColorStateList;
     23 import android.graphics.Canvas;
     24 import android.graphics.ColorFilter;
     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.PorterDuff;
     30 import android.graphics.PorterDuffXfermode;
     31 import android.graphics.Rect;
     32 import android.graphics.drawable.DrawableWrapper;
     33 import android.os.Handler;
     34 import android.telephony.SignalStrength;
     35 import android.util.LayoutDirection;
     36 
     37 import com.android.settingslib.R;
     38 import com.android.settingslib.Utils;
     39 
     40 /**
     41  * Drawable displaying a mobile cell signal indicator.
     42  */
     43 public class SignalDrawable extends DrawableWrapper {
     44 
     45     private static final String TAG = "SignalDrawable";
     46 
     47     private static final int NUM_DOTS = 3;
     48 
     49     private static final float VIEWPORT = 24f;
     50     private static final float PAD = 2f / VIEWPORT;
     51     private static final float CUT_OUT = 7.9f / VIEWPORT;
     52 
     53     private static final float DOT_SIZE = 3f / VIEWPORT;
     54     private static final float DOT_PADDING = 1.5f / VIEWPORT;
     55 
     56     // All of these are masks to push all of the drawable state into one int for easy callbacks
     57     // and flow through sysui.
     58     private static final int LEVEL_MASK = 0xff;
     59     private static final int NUM_LEVEL_SHIFT = 8;
     60     private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
     61     private static final int STATE_SHIFT = 16;
     62     private static final int STATE_MASK = 0xff << STATE_SHIFT;
     63     private static final int STATE_CUT = 2;
     64     private static final int STATE_CARRIER_CHANGE = 3;
     65 
     66     private static final long DOT_DELAY = 1000;
     67 
     68     private static float[][] X_PATH = new float[][]{
     69             {21.9f / VIEWPORT, 17.0f / VIEWPORT},
     70             {-1.1f / VIEWPORT, -1.1f / VIEWPORT},
     71             {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
     72             {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
     73             {-1.1f / VIEWPORT, 1.1f / VIEWPORT},
     74             {1.9f / VIEWPORT, 1.9f / VIEWPORT},
     75             {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
     76             {1.1f / VIEWPORT, 1.1f / VIEWPORT},
     77             {1.9f / VIEWPORT, -1.9f / VIEWPORT},
     78             {1.9f / VIEWPORT, 1.9f / VIEWPORT},
     79             {1.1f / VIEWPORT, -1.1f / VIEWPORT},
     80             {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
     81     };
     82 
     83     private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     84     private final Paint mTransparentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     85     private final int mDarkModeFillColor;
     86     private final int mLightModeFillColor;
     87     private final Path mCutoutPath = new Path();
     88     private final Path mForegroundPath = new Path();
     89     private final Path mXPath = new Path();
     90     private final Handler mHandler;
     91     private float mDarkIntensity = -1;
     92     private final int mIntrinsicSize;
     93     private boolean mAnimating;
     94     private int mCurrentDot;
     95 
     96     public SignalDrawable(Context context) {
     97         super(context.getDrawable(com.android.internal.R.drawable.ic_signal_cellular));
     98         mDarkModeFillColor = Utils.getColorStateListDefaultColor(context,
     99                 R.color.dark_mode_icon_color_single_tone);
    100         mLightModeFillColor = Utils.getColorStateListDefaultColor(context,
    101                 R.color.light_mode_icon_color_single_tone);
    102         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
    103         mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
    104         mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    105         mHandler = new Handler();
    106         setDarkIntensity(0);
    107     }
    108 
    109     @Override
    110     public int getIntrinsicWidth() {
    111         return mIntrinsicSize;
    112     }
    113 
    114     @Override
    115     public int getIntrinsicHeight() {
    116         return mIntrinsicSize;
    117     }
    118 
    119     private void updateAnimation() {
    120         boolean shouldAnimate = isInState(STATE_CARRIER_CHANGE) && isVisible();
    121         if (shouldAnimate == mAnimating) return;
    122         mAnimating = shouldAnimate;
    123         if (shouldAnimate) {
    124             mChangeDot.run();
    125         } else {
    126             mHandler.removeCallbacks(mChangeDot);
    127         }
    128     }
    129 
    130     @Override
    131     protected boolean onLevelChange(int packedState) {
    132         super.onLevelChange(unpackLevel(packedState));
    133         updateAnimation();
    134         setTintList(ColorStateList.valueOf(mForegroundPaint.getColor()));
    135         invalidateSelf();
    136         return true;
    137     }
    138 
    139     private int unpackLevel(int packedState) {
    140         int numBins = (packedState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
    141         int levelOffset = numBins == (SignalStrength.NUM_SIGNAL_STRENGTH_BINS + 1) ? 10 : 0;
    142         int level = (packedState & LEVEL_MASK);
    143         return level + levelOffset;
    144     }
    145 
    146     public void setDarkIntensity(float darkIntensity) {
    147         if (darkIntensity == mDarkIntensity) {
    148             return;
    149         }
    150         setTintList(ColorStateList.valueOf(getFillColor(darkIntensity)));
    151     }
    152 
    153     @Override
    154     public void setTintList(ColorStateList tint) {
    155         super.setTintList(tint);
    156         int colorForeground = mForegroundPaint.getColor();
    157         mForegroundPaint.setColor(tint.getDefaultColor());
    158         if (colorForeground != mForegroundPaint.getColor()) invalidateSelf();
    159     }
    160 
    161     private int getFillColor(float darkIntensity) {
    162         return getColorForDarkIntensity(
    163                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
    164     }
    165 
    166     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
    167         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
    168     }
    169 
    170     @Override
    171     protected void onBoundsChange(Rect bounds) {
    172         super.onBoundsChange(bounds);
    173         invalidateSelf();
    174     }
    175 
    176     @Override
    177     public void draw(@NonNull Canvas canvas) {
    178         canvas.saveLayer(null, null);
    179         final float width = getBounds().width();
    180         final float height = getBounds().height();
    181 
    182         boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
    183         if (isRtl) {
    184             canvas.save();
    185             // Mirror the drawable
    186             canvas.translate(width, 0);
    187             canvas.scale(-1.0f, 1.0f);
    188         }
    189         super.draw(canvas);
    190         mCutoutPath.reset();
    191         mCutoutPath.setFillType(FillType.WINDING);
    192 
    193         final float padding = Math.round(PAD * width);
    194 
    195         if (isInState(STATE_CARRIER_CHANGE)) {
    196             float dotSize = (DOT_SIZE * height);
    197             float dotPadding = (DOT_PADDING * height);
    198             float dotSpacing = dotPadding + dotSize;
    199             float x = width - padding - dotSize;
    200             float y = height - padding - dotSize;
    201             mForegroundPath.reset();
    202             drawDotAndPadding(x, y, dotPadding, dotSize, 2);
    203             drawDotAndPadding(x - dotSpacing, y, dotPadding, dotSize, 1);
    204             drawDotAndPadding(x - dotSpacing * 2, y, dotPadding, dotSize, 0);
    205             canvas.drawPath(mCutoutPath, mTransparentPaint);
    206             canvas.drawPath(mForegroundPath, mForegroundPaint);
    207         } else if (isInState(STATE_CUT)) {
    208             float cut = (CUT_OUT * width);
    209             mCutoutPath.moveTo(width - padding, height - padding);
    210             mCutoutPath.rLineTo(-cut, 0);
    211             mCutoutPath.rLineTo(0, -cut);
    212             mCutoutPath.rLineTo(cut, 0);
    213             mCutoutPath.rLineTo(0, cut);
    214             canvas.drawPath(mCutoutPath, mTransparentPaint);
    215             mXPath.reset();
    216             mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height);
    217             for (int i = 1; i < X_PATH.length; i++) {
    218                 mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height);
    219             }
    220             canvas.drawPath(mXPath, mForegroundPaint);
    221         }
    222         if (isRtl) {
    223             canvas.restore();
    224         }
    225         canvas.restore();
    226     }
    227 
    228     private void drawDotAndPadding(float x, float y,
    229             float dotPadding, float dotSize, int i) {
    230         if (i == mCurrentDot) {
    231             // Draw dot
    232             mForegroundPath.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
    233             // Draw dot padding
    234             mCutoutPath.addRect(x - dotPadding, y - dotPadding, x + dotSize + dotPadding,
    235                     y + dotSize + dotPadding, Direction.CW);
    236         }
    237     }
    238 
    239     @Override
    240     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
    241         super.setAlpha(alpha);
    242         mForegroundPaint.setAlpha(alpha);
    243     }
    244 
    245     @Override
    246     public void setColorFilter(@Nullable ColorFilter colorFilter) {
    247         super.setColorFilter(colorFilter);
    248         mForegroundPaint.setColorFilter(colorFilter);
    249     }
    250 
    251     @Override
    252     public boolean setVisible(boolean visible, boolean restart) {
    253         boolean changed = super.setVisible(visible, restart);
    254         updateAnimation();
    255         return changed;
    256     }
    257 
    258     private final Runnable mChangeDot = new Runnable() {
    259         @Override
    260         public void run() {
    261             if (++mCurrentDot == NUM_DOTS) {
    262                 mCurrentDot = 0;
    263             }
    264             invalidateSelf();
    265             mHandler.postDelayed(mChangeDot, DOT_DELAY);
    266         }
    267     };
    268 
    269     /**
    270      * Returns whether this drawable is in the specified state.
    271      *
    272      * @param state must be one of {@link #STATE_CARRIER_CHANGE} or {@link #STATE_CUT}
    273      */
    274     private boolean isInState(int state) {
    275         return getState(getLevel()) == state;
    276     }
    277 
    278     public static int getState(int fullState) {
    279         return (fullState & STATE_MASK) >> STATE_SHIFT;
    280     }
    281 
    282     public static int getState(int level, int numLevels, boolean cutOut) {
    283         return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
    284                 | (numLevels << NUM_LEVEL_SHIFT)
    285                 | level;
    286     }
    287 
    288     /** Returns the state representing empty mobile signal with the given number of levels. */
    289     public static int getEmptyState(int numLevels) {
    290         return getState(0, numLevels, true);
    291     }
    292 
    293     /** Returns the state representing carrier change with the given number of levels. */
    294     public static int getCarrierChangeState(int numLevels) {
    295         return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
    296     }
    297 }
    298