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