blob: 51e0270f7e2b84f4aa2f126d8d2deff3ebd94bca [file] [log] [blame]
Alex Kuleszac6610982017-06-02 18:16:28 -04001/*
2 * Copyright (C) 2016 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
15package com.android.settings.graph;
16
17import android.annotation.Nullable;
18import android.content.Context;
19import android.content.res.Resources;
20import android.graphics.Canvas;
21import android.graphics.CornerPathEffect;
22import android.graphics.DashPathEffect;
23import android.graphics.LinearGradient;
24import android.graphics.Paint;
25import android.graphics.Paint.Cap;
26import android.graphics.Paint.Join;
27import android.graphics.Paint.Style;
28import android.graphics.Path;
29import android.graphics.Shader.TileMode;
30import android.graphics.drawable.Drawable;
31import android.util.AttributeSet;
32import android.util.SparseIntArray;
33import android.util.TypedValue;
34import android.view.View;
Alex Kuleszac57ceaa2017-06-06 19:24:48 -040035
Fan Zhangde117042018-09-04 13:52:15 -070036import androidx.annotation.VisibleForTesting;
37
Salvador Martinez79616272017-07-19 17:14:21 -070038import com.android.settings.fuelgauge.BatteryUtils;
Alex Kuleszac6610982017-06-02 18:16:28 -040039import com.android.settingslib.R;
40
41public class UsageGraph extends View {
42
43 private static final int PATH_DELIM = -1;
Salvador Martinez79616272017-07-19 17:14:21 -070044 public static final String LOG_TAG = "UsageGraph";
Alex Kuleszac6610982017-06-02 18:16:28 -040045
46 private final Paint mLinePaint;
47 private final Paint mFillPaint;
48 private final Paint mDottedPaint;
49
50 private final Drawable mDivider;
51 private final Drawable mTintedDivider;
52 private final int mDividerSize;
53
54 private final Path mPath = new Path();
55
56 // Paths in coordinates they are passed in.
57 private final SparseIntArray mPaths = new SparseIntArray();
58 // Paths in local coordinates for drawing.
59 private final SparseIntArray mLocalPaths = new SparseIntArray();
Alex Kuleszac6610982017-06-02 18:16:28 -040060
Alex Kuleszac57ceaa2017-06-06 19:24:48 -040061 // Paths for projection in coordinates they are passed in.
62 private final SparseIntArray mProjectedPaths = new SparseIntArray();
63 // Paths for projection in local coordinates for drawing.
64 private final SparseIntArray mLocalProjectedPaths = new SparseIntArray();
65
66 private final int mCornerRadius;
Alex Kuleszac6610982017-06-02 18:16:28 -040067 private int mAccentColor;
Alex Kuleszac6610982017-06-02 18:16:28 -040068
69 private float mMaxX = 100;
70 private float mMaxY = 100;
71
72 private float mMiddleDividerLoc = .5f;
73 private int mMiddleDividerTint = -1;
74 private int mTopDividerTint = -1;
75
76 public UsageGraph(Context context, @Nullable AttributeSet attrs) {
77 super(context, attrs);
78 final Resources resources = context.getResources();
79
80 mLinePaint = new Paint();
81 mLinePaint.setStyle(Style.STROKE);
82 mLinePaint.setStrokeCap(Cap.ROUND);
83 mLinePaint.setStrokeJoin(Join.ROUND);
84 mLinePaint.setAntiAlias(true);
85 mCornerRadius = resources.getDimensionPixelSize(R.dimen.usage_graph_line_corner_radius);
86 mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
87 mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width));
88
89 mFillPaint = new Paint(mLinePaint);
90 mFillPaint.setStyle(Style.FILL);
91
92 mDottedPaint = new Paint(mLinePaint);
93 mDottedPaint.setStyle(Style.STROKE);
94 float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
95 float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
96 mDottedPaint.setStrokeWidth(dots * 3);
Alex Kulesza4a121ec2017-07-30 18:11:24 -040097 mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
Alex Kuleszac6610982017-06-02 18:16:28 -040098 mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
99
100 TypedValue v = new TypedValue();
101 context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
102 mDivider = context.getDrawable(v.resourceId);
103 mTintedDivider = context.getDrawable(v.resourceId);
104 mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size);
105 }
106
107 void clearPaths() {
108 mPaths.clear();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400109 mLocalPaths.clear();
110 mProjectedPaths.clear();
111 mLocalProjectedPaths.clear();
Alex Kuleszac6610982017-06-02 18:16:28 -0400112 }
113
114 void setMax(int maxX, int maxY) {
Salvador Martinez79616272017-07-19 17:14:21 -0700115 final long startTime = System.currentTimeMillis();
Alex Kuleszac6610982017-06-02 18:16:28 -0400116 mMaxX = maxX;
117 mMaxY = maxY;
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400118 calculateLocalPaths();
119 postInvalidate();
Salvador Martinez79616272017-07-19 17:14:21 -0700120 BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
Alex Kuleszac6610982017-06-02 18:16:28 -0400121 }
122
123 void setDividerLoc(int height) {
124 mMiddleDividerLoc = 1 - height / mMaxY;
125 }
126
127 void setDividerColors(int middleColor, int topColor) {
128 mMiddleDividerTint = middleColor;
129 mTopDividerTint = topColor;
130 }
131
132 public void addPath(SparseIntArray points) {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400133 addPathAndUpdate(points, mPaths, mLocalPaths);
134 }
135
136 public void addProjectedPath(SparseIntArray points) {
137 addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
138 }
139
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400140 private void addPathAndUpdate(
141 SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
Salvador Martinez79616272017-07-19 17:14:21 -0700142 final long startTime = System.currentTimeMillis();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400143 for (int i = 0, size = points.size(); i < size; i++) {
144 paths.put(points.keyAt(i), points.valueAt(i));
Alex Kuleszac6610982017-06-02 18:16:28 -0400145 }
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400146 // Add a delimiting value immediately after the last point.
147 paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
148 calculateLocalPaths(paths, localPaths);
Alex Kuleszac6610982017-06-02 18:16:28 -0400149 postInvalidate();
Salvador Martinez79616272017-07-19 17:14:21 -0700150 BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
Alex Kuleszac6610982017-06-02 18:16:28 -0400151 }
152
153 void setAccentColor(int color) {
154 mAccentColor = color;
155 mLinePaint.setColor(mAccentColor);
156 updateGradient();
157 postInvalidate();
158 }
159
Alex Kuleszac6610982017-06-02 18:16:28 -0400160 @Override
161 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Salvador Martinez79616272017-07-19 17:14:21 -0700162 final long startTime = System.currentTimeMillis();
Alex Kuleszac6610982017-06-02 18:16:28 -0400163 super.onSizeChanged(w, h, oldw, oldh);
164 updateGradient();
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400165 calculateLocalPaths();
Salvador Martinez79616272017-07-19 17:14:21 -0700166 BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400167 }
168
169 private void calculateLocalPaths() {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400170 calculateLocalPaths(mPaths, mLocalPaths);
171 calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
Alex Kuleszac6610982017-06-02 18:16:28 -0400172 }
173
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400174 @VisibleForTesting
175 void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
Salvador Martinez79616272017-07-19 17:14:21 -0700176 final long startTime = System.currentTimeMillis();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400177 if (getWidth() == 0) {
178 return;
179 }
180 localPaths.clear();
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400181 // Store the local coordinates of the most recent point.
182 int lx = 0;
183 int ly = PATH_DELIM;
184 boolean skippedLastPoint = false;
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400185 for (int i = 0; i < paths.size(); i++) {
186 int x = paths.keyAt(i);
187 int y = paths.valueAt(i);
Alex Kuleszac6610982017-06-02 18:16:28 -0400188 if (y == PATH_DELIM) {
Doris Ling02b4cf22018-03-28 17:14:23 -0700189 if (i == 1) {
190 localPaths.put(getX(x+1) - 1, getY(0));
191 continue;
192 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400193 if (i == paths.size() - 1 && skippedLastPoint) {
194 // Add back skipped point to complete the path.
195 localPaths.put(lx, ly);
Alex Kuleszac6610982017-06-02 18:16:28 -0400196 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400197 skippedLastPoint = false;
198 localPaths.put(lx + 1, PATH_DELIM);
Alex Kuleszac6610982017-06-02 18:16:28 -0400199 } else {
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400200 lx = getX(x);
201 ly = getY(y);
202 // Skip this point if it is not far enough from the last one added.
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400203 if (localPaths.size() > 0) {
204 int lastX = localPaths.keyAt(localPaths.size() - 1);
205 int lastY = localPaths.valueAt(localPaths.size() - 1);
Alex Kuleszac6610982017-06-02 18:16:28 -0400206 if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400207 skippedLastPoint = true;
Alex Kuleszac6610982017-06-02 18:16:28 -0400208 continue;
209 }
210 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400211 skippedLastPoint = false;
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400212 localPaths.put(lx, ly);
Alex Kuleszac6610982017-06-02 18:16:28 -0400213 }
214 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400215 BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
Alex Kuleszac6610982017-06-02 18:16:28 -0400216 }
217
218 private boolean hasDiff(int x1, int x2) {
219 return Math.abs(x2 - x1) >= mCornerRadius;
220 }
221
222 private int getX(float x) {
223 return (int) (x / mMaxX * getWidth());
224 }
225
226 private int getY(float y) {
227 return (int) (getHeight() * (1 - (y / mMaxY)));
228 }
229
230 private void updateGradient() {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400231 mFillPaint.setShader(
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400232 new LinearGradient(
233 0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
Alex Kuleszac6610982017-06-02 18:16:28 -0400234 }
235
236 private int getColor(int color, float alphaScale) {
237 return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
238 }
239
240 @Override
241 protected void onDraw(Canvas canvas) {
Salvador Martinez79616272017-07-19 17:14:21 -0700242 final long startTime = System.currentTimeMillis();
Alex Kuleszac6610982017-06-02 18:16:28 -0400243 // Draw lines across the top, middle, and bottom.
244 if (mMiddleDividerLoc != 0) {
245 drawDivider(0, canvas, mTopDividerTint);
246 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400247 drawDivider(
248 (int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
249 canvas,
Alex Kuleszac6610982017-06-02 18:16:28 -0400250 mMiddleDividerTint);
251 drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
252
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400253 if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
Alex Kuleszac6610982017-06-02 18:16:28 -0400254 return;
255 }
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400256
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400257 drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
258 drawFilledPath(canvas, mLocalPaths, mFillPaint);
259 drawLinePath(canvas, mLocalPaths, mLinePaint);
Salvador Martinez79616272017-07-19 17:14:21 -0700260 BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400261 }
262
263 private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
264 if (localPaths.size() == 0) {
265 return;
Alex Kuleszac6610982017-06-02 18:16:28 -0400266 }
Alex Kuleszac6610982017-06-02 18:16:28 -0400267 mPath.reset();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400268 mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
269 for (int i = 1; i < localPaths.size(); i++) {
270 int x = localPaths.keyAt(i);
271 int y = localPaths.valueAt(i);
Alex Kuleszac6610982017-06-02 18:16:28 -0400272 if (y == PATH_DELIM) {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400273 if (++i < localPaths.size()) {
274 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
Alex Kuleszac6610982017-06-02 18:16:28 -0400275 }
276 } else {
277 mPath.lineTo(x, y);
278 }
279 }
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400280 canvas.drawPath(mPath, paint);
Alex Kuleszac6610982017-06-02 18:16:28 -0400281 }
282
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400283 private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
Alex Kuleszac6610982017-06-02 18:16:28 -0400284 mPath.reset();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400285 float lastStartX = localPaths.keyAt(0);
286 mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
287 for (int i = 1; i < localPaths.size(); i++) {
288 int x = localPaths.keyAt(i);
289 int y = localPaths.valueAt(i);
Alex Kuleszac6610982017-06-02 18:16:28 -0400290 if (y == PATH_DELIM) {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400291 mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
Alex Kuleszac6610982017-06-02 18:16:28 -0400292 mPath.lineTo(lastStartX, getHeight());
293 mPath.close();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400294 if (++i < localPaths.size()) {
295 lastStartX = localPaths.keyAt(i);
296 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
Alex Kuleszac6610982017-06-02 18:16:28 -0400297 }
298 } else {
299 mPath.lineTo(x, y);
300 }
301 }
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400302 canvas.drawPath(mPath, paint);
Alex Kuleszac6610982017-06-02 18:16:28 -0400303 }
304
305 private void drawDivider(int y, Canvas canvas, int tintColor) {
306 Drawable d = mDivider;
307 if (tintColor != -1) {
308 mTintedDivider.setTint(tintColor);
309 d = mTintedDivider;
310 }
311 d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
312 d.draw(canvas);
313 }
314}