2 * Copyright (C) 2017 The Android Open Source Project
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
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
15 package com.android.settingslib.graph;
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.SignalStrength;
36 import android.util.LayoutDirection;
37 import android.util.PathParser;
39 import com.android.settingslib.R;
40 import com.android.settingslib.Utils;
43 * Drawable displaying a mobile cell signal indicator.
45 public class SignalDrawable extends DrawableWrapper {
47 private static final String TAG = "SignalDrawable";
49 private static final int NUM_DOTS = 3;
51 private static final float VIEWPORT = 24f;
52 private static final float PAD = 2f / VIEWPORT;
54 private static final float DOT_SIZE = 3f / VIEWPORT;
55 private static final float DOT_PADDING = 1.5f / VIEWPORT;
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;
67 private static final long DOT_DELAY = 1000;
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 mXPath = new Path();
76 private final Matrix mXScaleMatrix = new Matrix();
77 private final Path mScaledXPath = 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;
86 public SignalDrawable(Context context) {
87 super(context.getDrawable(com.android.internal.R.drawable.ic_signal_cellular));
88 final String xPathString = context.getString(
89 com.android.internal.R.string.config_signalXPath);
90 mXPath.set(PathParser.createPathFromPathData(xPathString));
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();
107 private void updateScaledXPath() {
108 if (getBounds().isEmpty()) {
109 mXScaleMatrix.setScale(1f, 1f);
111 mXScaleMatrix.setScale(getBounds().width() / VIEWPORT, getBounds().height() / VIEWPORT);
113 mXPath.transform(mXScaleMatrix, mScaledXPath);
117 public int getIntrinsicWidth() {
118 return mIntrinsicSize;
122 public int getIntrinsicHeight() {
123 return mIntrinsicSize;
126 private void updateAnimation() {
127 boolean shouldAnimate = isInState(STATE_CARRIER_CHANGE) && isVisible();
128 if (shouldAnimate == mAnimating) return;
129 mAnimating = shouldAnimate;
133 mHandler.removeCallbacks(mChangeDot);
138 protected boolean onLevelChange(int packedState) {
139 super.onLevelChange(unpackLevel(packedState));
141 setTintList(ColorStateList.valueOf(mForegroundPaint.getColor()));
146 private int unpackLevel(int packedState) {
147 int numBins = (packedState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
148 int levelOffset = numBins == (SignalStrength.NUM_SIGNAL_STRENGTH_BINS + 1) ? 10 : 0;
149 int level = (packedState & LEVEL_MASK);
150 return level + levelOffset;
153 public void setDarkIntensity(float darkIntensity) {
154 if (darkIntensity == mDarkIntensity) {
157 setTintList(ColorStateList.valueOf(getFillColor(darkIntensity)));
161 public void setTintList(ColorStateList tint) {
162 super.setTintList(tint);
163 int colorForeground = mForegroundPaint.getColor();
164 mForegroundPaint.setColor(tint.getDefaultColor());
165 if (colorForeground != mForegroundPaint.getColor()) invalidateSelf();
168 private int getFillColor(float darkIntensity) {
169 return getColorForDarkIntensity(
170 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
173 private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
174 return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
178 protected void onBoundsChange(Rect bounds) {
179 super.onBoundsChange(bounds);
185 public void draw(@NonNull Canvas canvas) {
186 canvas.saveLayer(null, null);
187 final float width = getBounds().width();
188 final float height = getBounds().height();
190 boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
193 // Mirror the drawable
194 canvas.translate(width, 0);
195 canvas.scale(-1.0f, 1.0f);
199 mCutoutPath.setFillType(FillType.WINDING);
201 final float padding = Math.round(PAD * width);
203 if (isInState(STATE_CARRIER_CHANGE)) {
204 float dotSize = (DOT_SIZE * height);
205 float dotPadding = (DOT_PADDING * height);
206 float dotSpacing = dotPadding + dotSize;
207 float x = width - padding - dotSize;
208 float y = height - padding - dotSize;
209 mForegroundPath.reset();
210 drawDotAndPadding(x, y, dotPadding, dotSize, 2);
211 drawDotAndPadding(x - dotSpacing, y, dotPadding, dotSize, 1);
212 drawDotAndPadding(x - dotSpacing * 2, y, dotPadding, dotSize, 0);
213 canvas.drawPath(mCutoutPath, mTransparentPaint);
214 canvas.drawPath(mForegroundPath, mForegroundPaint);
215 } else if (isInState(STATE_CUT)) {
216 float cutX = (mCutoutWidthFraction * width / VIEWPORT);
217 float cutY = (mCutoutHeightFraction * height / VIEWPORT);
218 mCutoutPath.moveTo(width, height);
219 mCutoutPath.rLineTo(-cutX, 0);
220 mCutoutPath.rLineTo(0, -cutY);
221 mCutoutPath.rLineTo(cutX, 0);
222 mCutoutPath.rLineTo(0, cutY);
223 canvas.drawPath(mCutoutPath, mTransparentPaint);
224 canvas.drawPath(mScaledXPath, mForegroundPaint);
232 private void drawDotAndPadding(float x, float y,
233 float dotPadding, float dotSize, int i) {
234 if (i == mCurrentDot) {
236 mForegroundPath.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
238 mCutoutPath.addRect(x - dotPadding, y - dotPadding, x + dotSize + dotPadding,
239 y + dotSize + dotPadding, Direction.CW);
244 public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
245 super.setAlpha(alpha);
246 mForegroundPaint.setAlpha(alpha);
250 public void setColorFilter(@Nullable ColorFilter colorFilter) {
251 super.setColorFilter(colorFilter);
252 mForegroundPaint.setColorFilter(colorFilter);
256 public boolean setVisible(boolean visible, boolean restart) {
257 boolean changed = super.setVisible(visible, restart);
262 private final Runnable mChangeDot = new Runnable() {
265 if (++mCurrentDot == NUM_DOTS) {
269 mHandler.postDelayed(mChangeDot, DOT_DELAY);
274 * Returns whether this drawable is in the specified state.
276 * @param state must be one of {@link #STATE_CARRIER_CHANGE} or {@link #STATE_CUT}
278 private boolean isInState(int state) {
279 return getState(getLevel()) == state;
282 public static int getState(int fullState) {
283 return (fullState & STATE_MASK) >> STATE_SHIFT;
286 public static int getState(int level, int numLevels, boolean cutOut) {
287 return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
288 | (numLevels << NUM_LEVEL_SHIFT)
292 /** Returns the state representing empty mobile signal with the given number of levels. */
293 public static int getEmptyState(int numLevels) {
294 return getState(0, numLevels, true);
297 /** Returns the state representing carrier change with the given number of levels. */
298 public static int getCarrierChangeState(int numLevels) {
299 return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);