blob: b9d517d5e8d11b2c0620954e807b687bd6349f65 [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;
Aurimas Liutikas03fcde32018-04-17 11:22:43 -070031import androidx.annotation.VisibleForTesting;
Alex Kuleszac6610982017-06-02 18:16:28 -040032import android.util.AttributeSet;
33import android.util.SparseIntArray;
34import android.util.TypedValue;
35import android.view.View;
Alex Kuleszac57ceaa2017-06-06 19:24:48 -040036
Salvador Martinez79616272017-07-19 17:14:21 -070037import com.android.settings.fuelgauge.BatteryUtils;
Alex Kuleszac6610982017-06-02 18:16:28 -040038import com.android.settingslib.R;
39
40public class UsageGraph extends View {
41
42 private static final int PATH_DELIM = -1;
Salvador Martinez79616272017-07-19 17:14:21 -070043 public static final String LOG_TAG = "UsageGraph";
Alex Kuleszac6610982017-06-02 18:16:28 -040044
45 private final Paint mLinePaint;
46 private final Paint mFillPaint;
47 private final Paint mDottedPaint;
48
49 private final Drawable mDivider;
50 private final Drawable mTintedDivider;
51 private final int mDividerSize;
52
53 private final Path mPath = new Path();
54
55 // Paths in coordinates they are passed in.
56 private final SparseIntArray mPaths = new SparseIntArray();
57 // Paths in local coordinates for drawing.
58 private final SparseIntArray mLocalPaths = new SparseIntArray();
Alex Kuleszac6610982017-06-02 18:16:28 -040059
Alex Kuleszac57ceaa2017-06-06 19:24:48 -040060 // Paths for projection in coordinates they are passed in.
61 private final SparseIntArray mProjectedPaths = new SparseIntArray();
62 // Paths for projection in local coordinates for drawing.
63 private final SparseIntArray mLocalProjectedPaths = new SparseIntArray();
64
65 private final int mCornerRadius;
Alex Kuleszac6610982017-06-02 18:16:28 -040066 private int mAccentColor;
Alex Kuleszac6610982017-06-02 18:16:28 -040067
68 private float mMaxX = 100;
69 private float mMaxY = 100;
70
71 private float mMiddleDividerLoc = .5f;
72 private int mMiddleDividerTint = -1;
73 private int mTopDividerTint = -1;
74
75 public UsageGraph(Context context, @Nullable AttributeSet attrs) {
76 super(context, attrs);
77 final Resources resources = context.getResources();
78
79 mLinePaint = new Paint();
80 mLinePaint.setStyle(Style.STROKE);
81 mLinePaint.setStrokeCap(Cap.ROUND);
82 mLinePaint.setStrokeJoin(Join.ROUND);
83 mLinePaint.setAntiAlias(true);
84 mCornerRadius = resources.getDimensionPixelSize(R.dimen.usage_graph_line_corner_radius);
85 mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
86 mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width));
87
88 mFillPaint = new Paint(mLinePaint);
89 mFillPaint.setStyle(Style.FILL);
90
91 mDottedPaint = new Paint(mLinePaint);
92 mDottedPaint.setStyle(Style.STROKE);
93 float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
94 float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
95 mDottedPaint.setStrokeWidth(dots * 3);
Alex Kulesza4a121ec2017-07-30 18:11:24 -040096 mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
Alex Kuleszac6610982017-06-02 18:16:28 -040097 mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
98
99 TypedValue v = new TypedValue();
100 context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
101 mDivider = context.getDrawable(v.resourceId);
102 mTintedDivider = context.getDrawable(v.resourceId);
103 mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size);
104 }
105
106 void clearPaths() {
107 mPaths.clear();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400108 mLocalPaths.clear();
109 mProjectedPaths.clear();
110 mLocalProjectedPaths.clear();
Alex Kuleszac6610982017-06-02 18:16:28 -0400111 }
112
113 void setMax(int maxX, int maxY) {
Salvador Martinez79616272017-07-19 17:14:21 -0700114 final long startTime = System.currentTimeMillis();
Alex Kuleszac6610982017-06-02 18:16:28 -0400115 mMaxX = maxX;
116 mMaxY = maxY;
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400117 calculateLocalPaths();
118 postInvalidate();
Salvador Martinez79616272017-07-19 17:14:21 -0700119 BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
Alex Kuleszac6610982017-06-02 18:16:28 -0400120 }
121
122 void setDividerLoc(int height) {
123 mMiddleDividerLoc = 1 - height / mMaxY;
124 }
125
126 void setDividerColors(int middleColor, int topColor) {
127 mMiddleDividerTint = middleColor;
128 mTopDividerTint = topColor;
129 }
130
131 public void addPath(SparseIntArray points) {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400132 addPathAndUpdate(points, mPaths, mLocalPaths);
133 }
134
135 public void addProjectedPath(SparseIntArray points) {
136 addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
137 }
138
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400139 private void addPathAndUpdate(
140 SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
Salvador Martinez79616272017-07-19 17:14:21 -0700141 final long startTime = System.currentTimeMillis();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400142 for (int i = 0, size = points.size(); i < size; i++) {
143 paths.put(points.keyAt(i), points.valueAt(i));
Alex Kuleszac6610982017-06-02 18:16:28 -0400144 }
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400145 // Add a delimiting value immediately after the last point.
146 paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
147 calculateLocalPaths(paths, localPaths);
Alex Kuleszac6610982017-06-02 18:16:28 -0400148 postInvalidate();
Salvador Martinez79616272017-07-19 17:14:21 -0700149 BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
Alex Kuleszac6610982017-06-02 18:16:28 -0400150 }
151
152 void setAccentColor(int color) {
153 mAccentColor = color;
154 mLinePaint.setColor(mAccentColor);
155 updateGradient();
156 postInvalidate();
157 }
158
Alex Kuleszac6610982017-06-02 18:16:28 -0400159 @Override
160 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Salvador Martinez79616272017-07-19 17:14:21 -0700161 final long startTime = System.currentTimeMillis();
Alex Kuleszac6610982017-06-02 18:16:28 -0400162 super.onSizeChanged(w, h, oldw, oldh);
163 updateGradient();
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400164 calculateLocalPaths();
Salvador Martinez79616272017-07-19 17:14:21 -0700165 BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400166 }
167
168 private void calculateLocalPaths() {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400169 calculateLocalPaths(mPaths, mLocalPaths);
170 calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
Alex Kuleszac6610982017-06-02 18:16:28 -0400171 }
172
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400173 @VisibleForTesting
174 void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
Salvador Martinez79616272017-07-19 17:14:21 -0700175 final long startTime = System.currentTimeMillis();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400176 if (getWidth() == 0) {
177 return;
178 }
179 localPaths.clear();
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400180 // Store the local coordinates of the most recent point.
181 int lx = 0;
182 int ly = PATH_DELIM;
183 boolean skippedLastPoint = false;
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400184 for (int i = 0; i < paths.size(); i++) {
185 int x = paths.keyAt(i);
186 int y = paths.valueAt(i);
Alex Kuleszac6610982017-06-02 18:16:28 -0400187 if (y == PATH_DELIM) {
Doris Ling02b4cf22018-03-28 17:14:23 -0700188 if (i == 1) {
189 localPaths.put(getX(x+1) - 1, getY(0));
190 continue;
191 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400192 if (i == paths.size() - 1 && skippedLastPoint) {
193 // Add back skipped point to complete the path.
194 localPaths.put(lx, ly);
Alex Kuleszac6610982017-06-02 18:16:28 -0400195 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400196 skippedLastPoint = false;
197 localPaths.put(lx + 1, PATH_DELIM);
Alex Kuleszac6610982017-06-02 18:16:28 -0400198 } else {
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400199 lx = getX(x);
200 ly = getY(y);
201 // Skip this point if it is not far enough from the last one added.
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400202 if (localPaths.size() > 0) {
203 int lastX = localPaths.keyAt(localPaths.size() - 1);
204 int lastY = localPaths.valueAt(localPaths.size() - 1);
Alex Kuleszac6610982017-06-02 18:16:28 -0400205 if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400206 skippedLastPoint = true;
Alex Kuleszac6610982017-06-02 18:16:28 -0400207 continue;
208 }
209 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400210 skippedLastPoint = false;
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400211 localPaths.put(lx, ly);
Alex Kuleszac6610982017-06-02 18:16:28 -0400212 }
213 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400214 BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
Alex Kuleszac6610982017-06-02 18:16:28 -0400215 }
216
217 private boolean hasDiff(int x1, int x2) {
218 return Math.abs(x2 - x1) >= mCornerRadius;
219 }
220
221 private int getX(float x) {
222 return (int) (x / mMaxX * getWidth());
223 }
224
225 private int getY(float y) {
226 return (int) (getHeight() * (1 - (y / mMaxY)));
227 }
228
229 private void updateGradient() {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400230 mFillPaint.setShader(
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400231 new LinearGradient(
232 0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
Alex Kuleszac6610982017-06-02 18:16:28 -0400233 }
234
235 private int getColor(int color, float alphaScale) {
236 return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
237 }
238
239 @Override
240 protected void onDraw(Canvas canvas) {
Salvador Martinez79616272017-07-19 17:14:21 -0700241 final long startTime = System.currentTimeMillis();
Alex Kuleszac6610982017-06-02 18:16:28 -0400242 // Draw lines across the top, middle, and bottom.
243 if (mMiddleDividerLoc != 0) {
244 drawDivider(0, canvas, mTopDividerTint);
245 }
Alex Kulesza4a121ec2017-07-30 18:11:24 -0400246 drawDivider(
247 (int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
248 canvas,
Alex Kuleszac6610982017-06-02 18:16:28 -0400249 mMiddleDividerTint);
250 drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
251
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400252 if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
Alex Kuleszac6610982017-06-02 18:16:28 -0400253 return;
254 }
Alex Kulesza82dbcd92017-06-29 12:26:59 -0400255
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400256 drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
257 drawFilledPath(canvas, mLocalPaths, mFillPaint);
258 drawLinePath(canvas, mLocalPaths, mLinePaint);
Salvador Martinez79616272017-07-19 17:14:21 -0700259 BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400260 }
261
262 private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
263 if (localPaths.size() == 0) {
264 return;
Alex Kuleszac6610982017-06-02 18:16:28 -0400265 }
Alex Kuleszac6610982017-06-02 18:16:28 -0400266 mPath.reset();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400267 mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
268 for (int i = 1; i < localPaths.size(); i++) {
269 int x = localPaths.keyAt(i);
270 int y = localPaths.valueAt(i);
Alex Kuleszac6610982017-06-02 18:16:28 -0400271 if (y == PATH_DELIM) {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400272 if (++i < localPaths.size()) {
273 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
Alex Kuleszac6610982017-06-02 18:16:28 -0400274 }
275 } else {
276 mPath.lineTo(x, y);
277 }
278 }
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400279 canvas.drawPath(mPath, paint);
Alex Kuleszac6610982017-06-02 18:16:28 -0400280 }
281
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400282 private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
Alex Kuleszac6610982017-06-02 18:16:28 -0400283 mPath.reset();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400284 float lastStartX = localPaths.keyAt(0);
285 mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
286 for (int i = 1; i < localPaths.size(); i++) {
287 int x = localPaths.keyAt(i);
288 int y = localPaths.valueAt(i);
Alex Kuleszac6610982017-06-02 18:16:28 -0400289 if (y == PATH_DELIM) {
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400290 mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
Alex Kuleszac6610982017-06-02 18:16:28 -0400291 mPath.lineTo(lastStartX, getHeight());
292 mPath.close();
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400293 if (++i < localPaths.size()) {
294 lastStartX = localPaths.keyAt(i);
295 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
Alex Kuleszac6610982017-06-02 18:16:28 -0400296 }
297 } else {
298 mPath.lineTo(x, y);
299 }
300 }
Alex Kuleszac57ceaa2017-06-06 19:24:48 -0400301 canvas.drawPath(mPath, paint);
Alex Kuleszac6610982017-06-02 18:16:28 -0400302 }
303
304 private void drawDivider(int y, Canvas canvas, int tintColor) {
305 Drawable d = mDivider;
306 if (tintColor != -1) {
307 mTintedDivider.setTint(tintColor);
308 d = mTintedDivider;
309 }
310 d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
311 d.draw(canvas);
312 }
313}