1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.developeroptions.widget;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Canvas;
23 import android.graphics.CornerPathEffect;
24 import android.graphics.DashPathEffect;
25 import android.graphics.LinearGradient;
26 import android.graphics.Paint;
27 import android.graphics.Paint.Cap;
28 import android.graphics.Paint.Join;
29 import android.graphics.Paint.Style;
30 import android.graphics.Path;
31 import android.graphics.Shader.TileMode;
32 import android.graphics.drawable.Drawable;
33 import android.util.AttributeSet;
34 import android.util.SparseIntArray;
35 import android.util.TypedValue;
36 import android.view.View;
37 
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.car.developeroptions.fuelgauge.BatteryUtils;
41 import com.android.settingslib.R;
42 
43 public class UsageGraph extends View {
44 
45     private static final int PATH_DELIM = -1;
46     public static final String LOG_TAG = "UsageGraph";
47 
48     private final Paint mLinePaint;
49     private final Paint mFillPaint;
50     private final Paint mDottedPaint;
51 
52     private final Drawable mDivider;
53     private final Drawable mTintedDivider;
54     private final int mDividerSize;
55 
56     private final Path mPath = new Path();
57 
58     // Paths in coordinates they are passed in.
59     private final SparseIntArray mPaths = new SparseIntArray();
60     // Paths in local coordinates for drawing.
61     private final SparseIntArray mLocalPaths = new SparseIntArray();
62 
63     // Paths for projection in coordinates they are passed in.
64     private final SparseIntArray mProjectedPaths = new SparseIntArray();
65     // Paths for projection in local coordinates for drawing.
66     private final SparseIntArray mLocalProjectedPaths = new SparseIntArray();
67 
68     private final int mCornerRadius;
69     private int mAccentColor;
70 
71     private float mMaxX = 100;
72     private float mMaxY = 100;
73 
74     private float mMiddleDividerLoc = .5f;
75     private int mMiddleDividerTint = -1;
76     private int mTopDividerTint = -1;
77 
UsageGraph(Context context, @Nullable AttributeSet attrs)78     public UsageGraph(Context context, @Nullable AttributeSet attrs) {
79         super(context, attrs);
80         final Resources resources = context.getResources();
81 
82         mLinePaint = new Paint();
83         mLinePaint.setStyle(Style.STROKE);
84         mLinePaint.setStrokeCap(Cap.ROUND);
85         mLinePaint.setStrokeJoin(Join.ROUND);
86         mLinePaint.setAntiAlias(true);
87         mCornerRadius = resources.getDimensionPixelSize(R.dimen.usage_graph_line_corner_radius);
88         mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
89         mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width));
90 
91         mFillPaint = new Paint(mLinePaint);
92         mFillPaint.setStyle(Style.FILL);
93 
94         mDottedPaint = new Paint(mLinePaint);
95         mDottedPaint.setStyle(Style.STROKE);
96         float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
97         float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
98         mDottedPaint.setStrokeWidth(dots * 3);
99         mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
100         mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
101 
102         TypedValue v = new TypedValue();
103         context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
104         mDivider = context.getDrawable(v.resourceId);
105         mTintedDivider = context.getDrawable(v.resourceId);
106         mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size);
107     }
108 
clearPaths()109     void clearPaths() {
110         mPaths.clear();
111         mLocalPaths.clear();
112         mProjectedPaths.clear();
113         mLocalProjectedPaths.clear();
114     }
115 
setMax(int maxX, int maxY)116     void setMax(int maxX, int maxY) {
117         final long startTime = System.currentTimeMillis();
118         mMaxX = maxX;
119         mMaxY = maxY;
120         calculateLocalPaths();
121         postInvalidate();
122         BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
123     }
124 
setDividerLoc(int height)125     void setDividerLoc(int height) {
126         mMiddleDividerLoc = 1 - height / mMaxY;
127     }
128 
setDividerColors(int middleColor, int topColor)129     void setDividerColors(int middleColor, int topColor) {
130         mMiddleDividerTint = middleColor;
131         mTopDividerTint = topColor;
132     }
133 
addPath(SparseIntArray points)134     public void addPath(SparseIntArray points) {
135         addPathAndUpdate(points, mPaths, mLocalPaths);
136     }
137 
addProjectedPath(SparseIntArray points)138     public void addProjectedPath(SparseIntArray points) {
139         addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
140     }
141 
addPathAndUpdate( SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths)142     private void addPathAndUpdate(
143             SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
144         final long startTime = System.currentTimeMillis();
145         for (int i = 0, size = points.size(); i < size; i++) {
146             paths.put(points.keyAt(i), points.valueAt(i));
147         }
148         // Add a delimiting value immediately after the last point.
149         paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
150         calculateLocalPaths(paths, localPaths);
151         postInvalidate();
152         BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
153     }
154 
setAccentColor(int color)155     void setAccentColor(int color) {
156         mAccentColor = color;
157         mLinePaint.setColor(mAccentColor);
158         updateGradient();
159         postInvalidate();
160     }
161 
162     @Override
onSizeChanged(int w, int h, int oldw, int oldh)163     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
164         final long startTime = System.currentTimeMillis();
165         super.onSizeChanged(w, h, oldw, oldh);
166         updateGradient();
167         calculateLocalPaths();
168         BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
169     }
170 
calculateLocalPaths()171     private void calculateLocalPaths() {
172         calculateLocalPaths(mPaths, mLocalPaths);
173         calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
174     }
175 
176     @VisibleForTesting
calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths)177     void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
178         final long startTime = System.currentTimeMillis();
179         if (getWidth() == 0) {
180             return;
181         }
182         localPaths.clear();
183         // Store the local coordinates of the most recent point.
184         int lx = 0;
185         int ly = PATH_DELIM;
186         boolean skippedLastPoint = false;
187         for (int i = 0; i < paths.size(); i++) {
188             int x = paths.keyAt(i);
189             int y = paths.valueAt(i);
190             if (y == PATH_DELIM) {
191                 if (i == 1) {
192                     localPaths.put(getX(x+1) - 1, getY(0));
193                     continue;
194                 }
195                 if (i == paths.size() - 1 && skippedLastPoint) {
196                     // Add back skipped point to complete the path.
197                     localPaths.put(lx, ly);
198                 }
199                 skippedLastPoint = false;
200                 localPaths.put(lx + 1, PATH_DELIM);
201             } else {
202                 lx = getX(x);
203                 ly = getY(y);
204                 // Skip this point if it is not far enough from the last one added.
205                 if (localPaths.size() > 0) {
206                     int lastX = localPaths.keyAt(localPaths.size() - 1);
207                     int lastY = localPaths.valueAt(localPaths.size() - 1);
208                     if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
209                         skippedLastPoint = true;
210                         continue;
211                     }
212                 }
213                 skippedLastPoint = false;
214                 localPaths.put(lx, ly);
215             }
216         }
217         BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
218     }
219 
hasDiff(int x1, int x2)220     private boolean hasDiff(int x1, int x2) {
221         return Math.abs(x2 - x1) >= mCornerRadius;
222     }
223 
getX(float x)224     private int getX(float x) {
225         return (int) (x / mMaxX * getWidth());
226     }
227 
getY(float y)228     private int getY(float y) {
229         return (int) (getHeight() * (1 - (y / mMaxY)));
230     }
231 
updateGradient()232     private void updateGradient() {
233         mFillPaint.setShader(
234                 new LinearGradient(
235                         0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
236     }
237 
getColor(int color, float alphaScale)238     private int getColor(int color, float alphaScale) {
239         return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
240     }
241 
242     @Override
onDraw(Canvas canvas)243     protected void onDraw(Canvas canvas) {
244         final long startTime = System.currentTimeMillis();
245         // Draw lines across the top, middle, and bottom.
246         if (mMiddleDividerLoc != 0) {
247             drawDivider(0, canvas, mTopDividerTint);
248         }
249         drawDivider(
250                 (int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
251                 canvas,
252                 mMiddleDividerTint);
253         drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
254 
255         if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
256             return;
257         }
258 
259         canvas.save();
260         if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
261             // Flip the canvas along the y-axis of the center of itself before drawing paths.
262             canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0);
263         }
264         drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
265         drawFilledPath(canvas, mLocalPaths, mFillPaint);
266         drawLinePath(canvas, mLocalPaths, mLinePaint);
267         canvas.restore();
268         BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
269     }
270 
drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint)271     private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
272         if (localPaths.size() == 0) {
273             return;
274         }
275         mPath.reset();
276         mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
277         for (int i = 1; i < localPaths.size(); i++) {
278             int x = localPaths.keyAt(i);
279             int y = localPaths.valueAt(i);
280             if (y == PATH_DELIM) {
281                 if (++i < localPaths.size()) {
282                     mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
283                 }
284             } else {
285                 mPath.lineTo(x, y);
286             }
287         }
288         canvas.drawPath(mPath, paint);
289     }
290 
drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint)291     private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
292         mPath.reset();
293         float lastStartX = localPaths.keyAt(0);
294         mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
295         for (int i = 1; i < localPaths.size(); i++) {
296             int x = localPaths.keyAt(i);
297             int y = localPaths.valueAt(i);
298             if (y == PATH_DELIM) {
299                 mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
300                 mPath.lineTo(lastStartX, getHeight());
301                 mPath.close();
302                 if (++i < localPaths.size()) {
303                     lastStartX = localPaths.keyAt(i);
304                     mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
305                 }
306             } else {
307                 mPath.lineTo(x, y);
308             }
309         }
310         canvas.drawPath(mPath, paint);
311     }
312 
drawDivider(int y, Canvas canvas, int tintColor)313     private void drawDivider(int y, Canvas canvas, int tintColor) {
314         Drawable d = mDivider;
315         if (tintColor != -1) {
316             mTintedDivider.setTint(tintColor);
317             d = mTintedDivider;
318         }
319         d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
320         d.draw(canvas);
321     }
322 }
323