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