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