blob: 3b41fa99d6c492581f9d3b54a00b9eecb324974c [file] [log] [blame]
Jason Monk48edc0c2017-04-10 15:01:27 -04001/*
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
Evan Laird06e9fd82018-02-10 09:36:55 -080015package com.android.settingslib.graph;
Jason Monk48edc0c2017-04-10 15:01:27 -040016
17import android.animation.ArgbEvaluator;
18import android.annotation.IntRange;
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
Amin Shaikh0ad7e512019-04-29 07:36:34 -040022import android.content.res.ColorStateList;
Jason Monk48edc0c2017-04-10 15:01:27 -040023import android.graphics.Canvas;
24import android.graphics.ColorFilter;
Amin Shaikh408d2d32019-06-04 10:28:02 -040025import android.graphics.Matrix;
Jason Monk48edc0c2017-04-10 15:01:27 -040026import android.graphics.Paint;
Jason Monk48edc0c2017-04-10 15:01:27 -040027import android.graphics.Path;
28import android.graphics.Path.Direction;
29import android.graphics.Path.FillType;
Amin Shaikh0ad7e512019-04-29 07:36:34 -040030import android.graphics.PorterDuff;
31import android.graphics.PorterDuffXfermode;
Jason Monk48edc0c2017-04-10 15:01:27 -040032import android.graphics.Rect;
Amin Shaikh0ad7e512019-04-29 07:36:34 -040033import android.graphics.drawable.DrawableWrapper;
Jason Monk48edc0c2017-04-10 15:01:27 -040034import android.os.Handler;
Peter Wang98598bf2019-11-19 16:08:46 -080035import android.telephony.CellSignalStrength;
Jason Monk54b610f2017-05-25 16:11:52 -040036import android.util.LayoutDirection;
Amin Shaikh408d2d32019-06-04 10:28:02 -040037import android.util.PathParser;
Jason Monk48edc0c2017-04-10 15:01:27 -040038
39import com.android.settingslib.R;
40import com.android.settingslib.Utils;
41
Amin Shaikh0ad7e512019-04-29 07:36:34 -040042/**
43 * Drawable displaying a mobile cell signal indicator.
44 */
45public class SignalDrawable extends DrawableWrapper {
Jason Monk48edc0c2017-04-10 15:01:27 -040046
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;
Jason Monk48edc0c2017-04-10 15:01:27 -040053
54 private static final float DOT_SIZE = 3f / VIEWPORT;
Amin Shaikh0ad7e512019-04-29 07:36:34 -040055 private static final float DOT_PADDING = 1.5f / VIEWPORT;
Jason Monk48edc0c2017-04-10 15:01:27 -040056
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;
Jason Monk7e6c83c2017-04-26 14:35:24 -040062 private static final int STATE_SHIFT = 16;
63 private static final int STATE_MASK = 0xff << STATE_SHIFT;
Jason Monk7e6c83c2017-04-26 14:35:24 -040064 private static final int STATE_CUT = 2;
65 private static final int STATE_CARRIER_CHANGE = 3;
Jason Monk48edc0c2017-04-10 15:01:27 -040066
67 private static final long DOT_DELAY = 1000;
68
Jason Monk48edc0c2017-04-10 15:01:27 -040069 private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Amin Shaikh0ad7e512019-04-29 07:36:34 -040070 private final Paint mTransparentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Jason Monk48edc0c2017-04-10 15:01:27 -040071 private final int mDarkModeFillColor;
Jason Monk48edc0c2017-04-10 15:01:27 -040072 private final int mLightModeFillColor;
Amin Shaikh0ad7e512019-04-29 07:36:34 -040073 private final Path mCutoutPath = new Path();
Jason Monk48edc0c2017-04-10 15:01:27 -040074 private final Path mForegroundPath = new Path();
75 private final Path mXPath = new Path();
Amin Shaikh408d2d32019-06-04 10:28:02 -040076 private final Matrix mXScaleMatrix = new Matrix();
77 private final Path mScaledXPath = new Path();
Jason Monk48edc0c2017-04-10 15:01:27 -040078 private final Handler mHandler;
Amin Shaikh408d2d32019-06-04 10:28:02 -040079 private final float mCutoutWidthFraction;
80 private final float mCutoutHeightFraction;
Amin Shaikh0ad7e512019-04-29 07:36:34 -040081 private float mDarkIntensity = -1;
82 private final int mIntrinsicSize;
Jason Monk48edc0c2017-04-10 15:01:27 -040083 private boolean mAnimating;
84 private int mCurrentDot;
85
86 public SignalDrawable(Context context) {
Amin Shaikh0ad7e512019-04-29 07:36:34 -040087 super(context.getDrawable(com.android.internal.R.drawable.ic_signal_cellular));
Amin Shaikh408d2d32019-06-04 10:28:02 -040088 final String xPathString = context.getString(
89 com.android.internal.R.string.config_signalXPath);
90 mXPath.set(PathParser.createPathFromPathData(xPathString));
91 updateScaledXPath();
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);
Amin Shaikh0ad7e512019-04-29 07:36:34 -040096 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);
Jason Monk48edc0c2017-04-10 15:01:27 -0400100 mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400101 mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
102 mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
Jason Monk48edc0c2017-04-10 15:01:27 -0400103 mHandler = new Handler();
104 setDarkIntensity(0);
Jason Monk01df36f2017-06-07 13:02:47 -0400105 }
106
Amin Shaikh408d2d32019-06-04 10:28:02 -0400107 private void updateScaledXPath() {
108 if (getBounds().isEmpty()) {
109 mXScaleMatrix.setScale(1f, 1f);
110 } else {
111 mXScaleMatrix.setScale(getBounds().width() / VIEWPORT, getBounds().height() / VIEWPORT);
112 }
113 mXPath.transform(mXScaleMatrix, mScaledXPath);
114 }
115
Jason Monk48edc0c2017-04-10 15:01:27 -0400116 @Override
117 public int getIntrinsicWidth() {
118 return mIntrinsicSize;
119 }
120
121 @Override
122 public int getIntrinsicHeight() {
123 return mIntrinsicSize;
124 }
125
Jason Monk48edc0c2017-04-10 15:01:27 -0400126 private void updateAnimation() {
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400127 boolean shouldAnimate = isInState(STATE_CARRIER_CHANGE) && isVisible();
Jason Monk48edc0c2017-04-10 15:01:27 -0400128 if (shouldAnimate == mAnimating) return;
129 mAnimating = shouldAnimate;
130 if (shouldAnimate) {
131 mChangeDot.run();
132 } else {
133 mHandler.removeCallbacks(mChangeDot);
134 }
135 }
136
137 @Override
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400138 protected boolean onLevelChange(int packedState) {
139 super.onLevelChange(unpackLevel(packedState));
140 updateAnimation();
141 setTintList(ColorStateList.valueOf(mForegroundPaint.getColor()));
Amin Shaikhb7e99e12019-06-12 10:31:46 -0400142 invalidateSelf();
Jason Monk48edc0c2017-04-10 15:01:27 -0400143 return true;
144 }
145
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400146 private int unpackLevel(int packedState) {
147 int numBins = (packedState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
Peter Wang98598bf2019-11-19 16:08:46 -0800148 int levelOffset = numBins == (CellSignalStrength.getNumSignalStrengthLevels() + 1) ? 10 : 0;
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400149 int level = (packedState & LEVEL_MASK);
150 return level + levelOffset;
Evan Laird5e43f2d2017-07-13 11:09:39 -0400151 }
152
Jason Monk48edc0c2017-04-10 15:01:27 -0400153 public void setDarkIntensity(float darkIntensity) {
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400154 if (darkIntensity == mDarkIntensity) {
Jason Monk48edc0c2017-04-10 15:01:27 -0400155 return;
156 }
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400157 setTintList(ColorStateList.valueOf(getFillColor(darkIntensity)));
158 }
159
160 @Override
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();
Jason Monk48edc0c2017-04-10 15:01:27 -0400166 }
167
168 private int getFillColor(float darkIntensity) {
169 return getColorForDarkIntensity(
170 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
171 }
172
Jason Monk48edc0c2017-04-10 15:01:27 -0400173 private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
174 return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
175 }
176
177 @Override
178 protected void onBoundsChange(Rect bounds) {
179 super.onBoundsChange(bounds);
Amin Shaikh408d2d32019-06-04 10:28:02 -0400180 updateScaledXPath();
Jason Monk48edc0c2017-04-10 15:01:27 -0400181 invalidateSelf();
182 }
183
184 @Override
185 public void draw(@NonNull Canvas canvas) {
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400186 canvas.saveLayer(null, null);
Evan Lairdae5b7002017-07-06 11:41:21 -0400187 final float width = getBounds().width();
188 final float height = getBounds().height();
189
Jason Monk54b610f2017-05-25 16:11:52 -0400190 boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
191 if (isRtl) {
192 canvas.save();
193 // Mirror the drawable
Evan Lairdae5b7002017-07-06 11:41:21 -0400194 canvas.translate(width, 0);
Jason Monk54b610f2017-05-25 16:11:52 -0400195 canvas.scale(-1.0f, 1.0f);
196 }
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400197 super.draw(canvas);
198 mCutoutPath.reset();
199 mCutoutPath.setFillType(FillType.WINDING);
Evan Lairdea77e6e2017-06-21 18:24:36 -0400200
Evan Lairdea77e6e2017-06-21 18:24:36 -0400201 final float padding = Math.round(PAD * width);
Evan Lairdada84322017-06-01 14:47:27 -0400202
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400203 if (isInState(STATE_CARRIER_CHANGE)) {
Jason Monk48edc0c2017-04-10 15:01:27 -0400204 float dotSize = (DOT_SIZE * height);
205 float dotPadding = (DOT_PADDING * height);
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400206 float dotSpacing = dotPadding + dotSize;
Jason Monk48edc0c2017-04-10 15:01:27 -0400207 float x = width - padding - dotSize;
208 float y = height - padding - dotSize;
209 mForegroundPath.reset();
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400210 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)) {
Amin Shaikh408d2d32019-06-04 10:28:02 -0400216 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);
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400223 canvas.drawPath(mCutoutPath, mTransparentPaint);
Amin Shaikh408d2d32019-06-04 10:28:02 -0400224 canvas.drawPath(mScaledXPath, mForegroundPaint);
Jason Monk48edc0c2017-04-10 15:01:27 -0400225 }
Jason Monk54b610f2017-05-25 16:11:52 -0400226 if (isRtl) {
227 canvas.restore();
228 }
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400229 canvas.restore();
Jason Monk48edc0c2017-04-10 15:01:27 -0400230 }
231
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400232 private void drawDotAndPadding(float x, float y,
233 float dotPadding, float dotSize, int i) {
234 if (i == mCurrentDot) {
235 // Draw dot
236 mForegroundPath.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
237 // Draw dot padding
238 mCutoutPath.addRect(x - dotPadding, y - dotPadding, x + dotSize + dotPadding,
239 y + dotSize + dotPadding, Direction.CW);
Jason Monk48edc0c2017-04-10 15:01:27 -0400240 }
Jason Monk48edc0c2017-04-10 15:01:27 -0400241 }
242
243 @Override
244 public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400245 super.setAlpha(alpha);
Jason Monk48edc0c2017-04-10 15:01:27 -0400246 mForegroundPaint.setAlpha(alpha);
247 }
248
249 @Override
250 public void setColorFilter(@Nullable ColorFilter colorFilter) {
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400251 super.setColorFilter(colorFilter);
Jason Monk48edc0c2017-04-10 15:01:27 -0400252 mForegroundPaint.setColorFilter(colorFilter);
253 }
254
255 @Override
Jason Monk48edc0c2017-04-10 15:01:27 -0400256 public boolean setVisible(boolean visible, boolean restart) {
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400257 boolean changed = super.setVisible(visible, restart);
Jason Monk48edc0c2017-04-10 15:01:27 -0400258 updateAnimation();
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400259 return changed;
Jason Monk48edc0c2017-04-10 15:01:27 -0400260 }
261
262 private final Runnable mChangeDot = new Runnable() {
263 @Override
264 public void run() {
265 if (++mCurrentDot == NUM_DOTS) {
266 mCurrentDot = 0;
267 }
268 invalidateSelf();
269 mHandler.postDelayed(mChangeDot, DOT_DELAY);
270 }
271 };
272
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400273 /**
274 * Returns whether this drawable is in the specified state.
275 *
276 * @param state must be one of {@link #STATE_CARRIER_CHANGE} or {@link #STATE_CUT}
277 */
278 private boolean isInState(int state) {
279 return getState(getLevel()) == state;
Jason Monk48edc0c2017-04-10 15:01:27 -0400280 }
281
282 public static int getState(int fullState) {
283 return (fullState & STATE_MASK) >> STATE_SHIFT;
284 }
285
Jason Monk48edc0c2017-04-10 15:01:27 -0400286 public static int getState(int level, int numLevels, boolean cutOut) {
287 return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
288 | (numLevels << NUM_LEVEL_SHIFT)
289 | level;
290 }
291
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400292 /** Returns the state representing empty mobile signal with the given number of levels. */
293 public static int getEmptyState(int numLevels) {
Amin Shaikh4f913082019-05-06 21:06:36 +0000294 return getState(0, numLevels, true);
Amin Shaikh0ad7e512019-04-29 07:36:34 -0400295 }
296
297 /** Returns the state representing carrier change with the given number of levels. */
Jason Monk48edc0c2017-04-10 15:01:27 -0400298 public static int getCarrierChangeState(int numLevels) {
299 return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
300 }
Jason Monk48edc0c2017-04-10 15:01:27 -0400301}