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 SignalDrawable(Context context)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 getIntrinsicWidth()110 public int getIntrinsicWidth() { 111 return mIntrinsicSize; 112 } 113 114 @Override getIntrinsicHeight()115 public int getIntrinsicHeight() { 116 return mIntrinsicSize; 117 } 118 updateAnimation()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 onLevelChange(int packedState)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 unpackLevel(int packedState)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 setDarkIntensity(float darkIntensity)146 public void setDarkIntensity(float darkIntensity) { 147 if (darkIntensity == mDarkIntensity) { 148 return; 149 } 150 setTintList(ColorStateList.valueOf(getFillColor(darkIntensity))); 151 } 152 153 @Override setTintList(ColorStateList tint)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 getFillColor(float darkIntensity)161 private int getFillColor(float darkIntensity) { 162 return getColorForDarkIntensity( 163 darkIntensity, mLightModeFillColor, mDarkModeFillColor); 164 } 165 getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor)166 private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) { 167 return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor); 168 } 169 170 @Override onBoundsChange(Rect bounds)171 protected void onBoundsChange(Rect bounds) { 172 super.onBoundsChange(bounds); 173 invalidateSelf(); 174 } 175 176 @Override draw(@onNull Canvas canvas)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 drawDotAndPadding(float x, float y, float dotPadding, float dotSize, int i)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 setAlpha(@ntRangefrom = 0, to = 255) int alpha)240 public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { 241 super.setAlpha(alpha); 242 mForegroundPaint.setAlpha(alpha); 243 } 244 245 @Override setColorFilter(@ullable ColorFilter colorFilter)246 public void setColorFilter(@Nullable ColorFilter colorFilter) { 247 super.setColorFilter(colorFilter); 248 mForegroundPaint.setColorFilter(colorFilter); 249 } 250 251 @Override setVisible(boolean visible, boolean restart)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 */ isInState(int state)274 private boolean isInState(int state) { 275 return getState(getLevel()) == state; 276 } 277 getState(int fullState)278 public static int getState(int fullState) { 279 return (fullState & STATE_MASK) >> STATE_SHIFT; 280 } 281 getState(int level, int numLevels, boolean cutOut)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. */ getEmptyState(int numLevels)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. */ getCarrierChangeState(int numLevels)294 public static int getCarrierChangeState(int numLevels) { 295 return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); 296 } 297 } 298