1 /*
2  * Copyright (C) 2016 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.settings.widget;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.CornerPathEffect;
23 import android.graphics.DashPathEffect;
24 import android.graphics.LinearGradient;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Cap;
27 import android.graphics.Paint.Join;
28 import android.graphics.Paint.Style;
29 import android.graphics.Path;
30 import android.graphics.Shader.TileMode;
31 import android.graphics.drawable.Drawable;
32 import android.util.AttributeSet;
33 import android.util.SparseIntArray;
34 import android.util.TypedValue;
35 import android.view.View;
36 
37 import androidx.annotation.Nullable;
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.settings.R;
41 import com.android.settings.fuelgauge.BatteryUtils;
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(
88                 com.android.settingslib.R.dimen.usage_graph_line_corner_radius);
89         mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
90         mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(
91                 com.android.settingslib.R.dimen.usage_graph_line_width));
92 
93         mFillPaint = new Paint(mLinePaint);
94         mFillPaint.setStyle(Style.FILL);
95 
96         mDottedPaint = new Paint(mLinePaint);
97         mDottedPaint.setStyle(Style.STROKE);
98         float dots = resources.getDimensionPixelSize(
99                 com.android.settingslib.R.dimen.usage_graph_dot_size);
100         float interval = resources.getDimensionPixelSize(
101                 com.android.settingslib.R.dimen.usage_graph_dot_interval);
102         mDottedPaint.setStrokeWidth(dots * 3);
103         mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
104         mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
105 
106         TypedValue v = new TypedValue();
107         context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
108         mDivider = context.getDrawable(v.resourceId);
109         mTintedDivider = context.getDrawable(v.resourceId);
110         mDividerSize = resources.getDimensionPixelSize(
111                 com.android.settingslib.R.dimen.usage_graph_divider_size);
112     }
113 
clearPaths()114     void clearPaths() {
115         mPaths.clear();
116         mLocalPaths.clear();
117         mProjectedPaths.clear();
118         mLocalProjectedPaths.clear();
119     }
120 
setMax(int maxX, int maxY)121     void setMax(int maxX, int maxY) {
122         final long startTime = System.currentTimeMillis();
123         mMaxX = maxX;
124         mMaxY = maxY;
125         calculateLocalPaths();
126         postInvalidate();
127         BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
128     }
129 
setDividerLoc(int height)130     void setDividerLoc(int height) {
131         mMiddleDividerLoc = 1 - height / mMaxY;
132     }
133 
setDividerColors(int middleColor, int topColor)134     void setDividerColors(int middleColor, int topColor) {
135         mMiddleDividerTint = middleColor;
136         mTopDividerTint = topColor;
137     }
138 
addPath(SparseIntArray points)139     public void addPath(SparseIntArray points) {
140         addPathAndUpdate(points, mPaths, mLocalPaths);
141     }
142 
addProjectedPath(SparseIntArray points)143     public void addProjectedPath(SparseIntArray points) {
144         addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
145     }
146 
addPathAndUpdate( SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths)147     private void addPathAndUpdate(
148             SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
149         final long startTime = System.currentTimeMillis();
150         for (int i = 0, size = points.size(); i < size; i++) {
151             paths.put(points.keyAt(i), points.valueAt(i));
152         }
153         // Add a delimiting value immediately after the last point.
154         paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
155         calculateLocalPaths(paths, localPaths);
156         postInvalidate();
157         BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
158     }
159 
setAccentColor(int color)160     void setAccentColor(int color) {
161         mAccentColor = color;
162         mLinePaint.setColor(mAccentColor);
163         updateGradient();
164         postInvalidate();
165     }
166 
167     @Override
onSizeChanged(int w, int h, int oldw, int oldh)168     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
169         final long startTime = System.currentTimeMillis();
170         super.onSizeChanged(w, h, oldw, oldh);
171         updateGradient();
172         calculateLocalPaths();
173         BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
174     }
175 
calculateLocalPaths()176     private void calculateLocalPaths() {
177         calculateLocalPaths(mPaths, mLocalPaths);
178         calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
179     }
180 
181     @VisibleForTesting
calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths)182     void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
183         final long startTime = System.currentTimeMillis();
184         if (getWidth() == 0) {
185             return;
186         }
187         localPaths.clear();
188         // Store the local coordinates of the most recent point.
189         int lx = 0;
190         int ly = PATH_DELIM;
191         boolean skippedLastPoint = false;
192         for (int i = 0; i < paths.size(); i++) {
193             int x = paths.keyAt(i);
194             int y = paths.valueAt(i);
195             if (y == PATH_DELIM) {
196                 if (i == 1) {
197                     localPaths.put(getX(x+1) - 1, getY(0));
198                     continue;
199                 }
200                 if (i == paths.size() - 1 && skippedLastPoint) {
201                     // Add back skipped point to complete the path.
202                     localPaths.put(lx, ly);
203                 }
204                 skippedLastPoint = false;
205                 localPaths.put(lx + 1, PATH_DELIM);
206             } else {
207                 lx = getX(x);
208                 ly = getY(y);
209                 // Skip this point if it is not far enough from the last one added.
210                 if (localPaths.size() > 0) {
211                     int lastX = localPaths.keyAt(localPaths.size() - 1);
212                     int lastY = localPaths.valueAt(localPaths.size() - 1);
213                     if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
214                         skippedLastPoint = true;
215                         continue;
216                     }
217                 }
218                 skippedLastPoint = false;
219                 localPaths.put(lx, ly);
220             }
221         }
222         BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
223     }
224 
hasDiff(int x1, int x2)225     private boolean hasDiff(int x1, int x2) {
226         return Math.abs(x2 - x1) >= mCornerRadius;
227     }
228 
getX(float x)229     private int getX(float x) {
230         return (int) (x / mMaxX * getWidth());
231     }
232 
getY(float y)233     private int getY(float y) {
234         return (int) (getHeight() * (1 - (y / mMaxY)));
235     }
236 
updateGradient()237     private void updateGradient() {
238         mFillPaint.setShader(
239                 new LinearGradient(
240                         0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
241     }
242 
getColor(int color, float alphaScale)243     private int getColor(int color, float alphaScale) {
244         return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
245     }
246 
247     @Override
onDraw(Canvas canvas)248     protected void onDraw(Canvas canvas) {
249         final long startTime = System.currentTimeMillis();
250         // Draw lines across the top, middle, and bottom.
251         if (mMiddleDividerLoc != 0) {
252             drawDivider(0, canvas, mTopDividerTint);
253         }
254         drawDivider(
255                 (int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
256                 canvas,
257                 mMiddleDividerTint);
258         drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
259 
260         if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
261             return;
262         }
263 
264         canvas.save();
265         if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
266             // Flip the canvas along the y-axis of the center of itself before drawing paths.
267             canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0);
268         }
269         drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
270         drawFilledPath(canvas, mLocalPaths, mFillPaint);
271         drawLinePath(canvas, mLocalPaths, mLinePaint);
272         canvas.restore();
273         BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
274     }
275 
drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint)276     private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
277         if (localPaths.size() == 0) {
278             return;
279         }
280         mPath.reset();
281         mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
282         for (int i = 1; i < localPaths.size(); i++) {
283             int x = localPaths.keyAt(i);
284             int y = localPaths.valueAt(i);
285             if (y == PATH_DELIM) {
286                 if (++i < localPaths.size()) {
287                     mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
288                 }
289             } else {
290                 mPath.lineTo(x, y);
291             }
292         }
293         canvas.drawPath(mPath, paint);
294     }
295 
296     @VisibleForTesting
drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint)297     void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
298         if (localPaths.size() == 0) {
299             return;
300         }
301         mPath.reset();
302         float lastStartX = localPaths.keyAt(0);
303         mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
304         for (int i = 1; i < localPaths.size(); i++) {
305             int x = localPaths.keyAt(i);
306             int y = localPaths.valueAt(i);
307             if (y == PATH_DELIM) {
308                 mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
309                 mPath.lineTo(lastStartX, getHeight());
310                 mPath.close();
311                 if (++i < localPaths.size()) {
312                     lastStartX = localPaths.keyAt(i);
313                     mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
314                 }
315             } else {
316                 mPath.lineTo(x, y);
317             }
318         }
319         canvas.drawPath(mPath, paint);
320     }
321 
drawDivider(int y, Canvas canvas, int tintColor)322     private void drawDivider(int y, Canvas canvas, int tintColor) {
323         Drawable d = mDivider;
324         if (tintColor != -1) {
325             mTintedDivider.setTint(tintColor);
326             d = mTintedDivider;
327         }
328         d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
329         d.draw(canvas);
330     }
331 }
332