1 /*
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 
15 package com.android.settingslib.graph;
16 
17 import android.annotation.Nullable;
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.graphics.Canvas;
21 import android.graphics.CornerPathEffect;
22 import android.graphics.DashPathEffect;
23 import android.graphics.LinearGradient;
24 import android.graphics.Paint;
25 import android.graphics.Paint.Cap;
26 import android.graphics.Paint.Join;
27 import android.graphics.Paint.Style;
28 import android.graphics.Path;
29 import android.graphics.Shader.TileMode;
30 import android.graphics.drawable.Drawable;
31 import android.util.AttributeSet;
32 import android.util.SparseIntArray;
33 import android.util.TypedValue;
34 import android.view.View;
35 import com.android.settingslib.R;
36 
37 public class UsageGraph extends View {
38 
39     private static final int PATH_DELIM = -1;
40 
41     private final Paint mLinePaint;
42     private final Paint mFillPaint;
43     private final Paint mDottedPaint;
44 
45     private final Drawable mDivider;
46     private final Drawable mTintedDivider;
47     private final int mDividerSize;
48 
49     private final Path mPath = new Path();
50 
51     // Paths in coordinates they are passed in.
52     private final SparseIntArray mPaths = new SparseIntArray();
53     // Paths in local coordinates for drawing.
54     private final SparseIntArray mLocalPaths = new SparseIntArray();
55     private final int mCornerRadius;
56 
57     private int mAccentColor;
58     private boolean mShowProjection;
59     private boolean mProjectUp;
60 
61     private float mMaxX = 100;
62     private float mMaxY = 100;
63 
64     private float mMiddleDividerLoc = .5f;
65     private int mMiddleDividerTint = -1;
66     private int mTopDividerTint = -1;
67 
UsageGraph(Context context, @Nullable AttributeSet attrs)68     public UsageGraph(Context context, @Nullable AttributeSet attrs) {
69         super(context, attrs);
70         final Resources resources = context.getResources();
71 
72         mLinePaint = new Paint();
73         mLinePaint.setStyle(Style.STROKE);
74         mLinePaint.setStrokeCap(Cap.ROUND);
75         mLinePaint.setStrokeJoin(Join.ROUND);
76         mLinePaint.setAntiAlias(true);
77         mCornerRadius = resources.getDimensionPixelSize(R.dimen.usage_graph_line_corner_radius);
78         mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
79         mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width));
80 
81         mFillPaint = new Paint(mLinePaint);
82         mFillPaint.setStyle(Style.FILL);
83 
84         mDottedPaint = new Paint(mLinePaint);
85         mDottedPaint.setStyle(Style.STROKE);
86         float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
87         float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
88         mDottedPaint.setStrokeWidth(dots * 3);
89         mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
90         mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
91 
92         TypedValue v = new TypedValue();
93         context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
94         mDivider = context.getDrawable(v.resourceId);
95         mTintedDivider = context.getDrawable(v.resourceId);
96         mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size);
97     }
98 
clearPaths()99     void clearPaths() {
100         mPaths.clear();
101     }
102 
setMax(int maxX, int maxY)103     void setMax(int maxX, int maxY) {
104         mMaxX = maxX;
105         mMaxY = maxY;
106     }
107 
setDividerLoc(int height)108     void setDividerLoc(int height) {
109         mMiddleDividerLoc = 1 - height / mMaxY;
110     }
111 
setDividerColors(int middleColor, int topColor)112     void setDividerColors(int middleColor, int topColor) {
113         mMiddleDividerTint = middleColor;
114         mTopDividerTint = topColor;
115     }
116 
addPath(SparseIntArray points)117     public void addPath(SparseIntArray points) {
118         for (int i = 0; i < points.size(); i++) {
119             mPaths.put(points.keyAt(i), points.valueAt(i));
120         }
121         mPaths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
122         calculateLocalPaths();
123         postInvalidate();
124     }
125 
setAccentColor(int color)126     void setAccentColor(int color) {
127         mAccentColor = color;
128         mLinePaint.setColor(mAccentColor);
129         updateGradient();
130         postInvalidate();
131     }
132 
setShowProjection(boolean showProjection, boolean projectUp)133     void setShowProjection(boolean showProjection, boolean projectUp) {
134         mShowProjection = showProjection;
135         mProjectUp = projectUp;
136         postInvalidate();
137     }
138 
139     @Override
onSizeChanged(int w, int h, int oldw, int oldh)140     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
141         super.onSizeChanged(w, h, oldw, oldh);
142         updateGradient();
143         calculateLocalPaths();
144     }
145 
calculateLocalPaths()146     private void calculateLocalPaths() {
147         if (getWidth() == 0) return;
148         mLocalPaths.clear();
149         int pendingXLoc = 0;
150         int pendingYLoc = PATH_DELIM;
151         for (int i = 0; i < mPaths.size(); i++) {
152             int x = mPaths.keyAt(i);
153             int y = mPaths.valueAt(i);
154             if (y == PATH_DELIM) {
155                 if (i == mPaths.size() - 1 && pendingYLoc != PATH_DELIM) {
156                     // Connect to the end of the graph.
157                     mLocalPaths.put(pendingXLoc, pendingYLoc);
158                 }
159                 // Clear out any pending points.
160                 pendingYLoc = PATH_DELIM;
161                 mLocalPaths.put(pendingXLoc + 1, PATH_DELIM);
162             } else {
163                 final int lx = getX(x);
164                 final int ly = getY(y);
165                 pendingXLoc = lx;
166                 if (mLocalPaths.size() > 0) {
167                     int lastX = mLocalPaths.keyAt(mLocalPaths.size() - 1);
168                     int lastY = mLocalPaths.valueAt(mLocalPaths.size() - 1);
169                     if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
170                         pendingYLoc = ly;
171                         continue;
172                     }
173                 }
174                 mLocalPaths.put(lx, ly);
175             }
176         }
177     }
178 
hasDiff(int x1, int x2)179     private boolean hasDiff(int x1, int x2) {
180         return Math.abs(x2 - x1) >= mCornerRadius;
181     }
182 
getX(float x)183     private int getX(float x) {
184         return (int) (x / mMaxX * getWidth());
185     }
186 
getY(float y)187     private int getY(float y) {
188         return (int) (getHeight() * (1 - (y / mMaxY)));
189     }
190 
updateGradient()191     private void updateGradient() {
192         mFillPaint.setShader(new LinearGradient(0, 0, 0, getHeight(),
193                 getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
194     }
195 
getColor(int color, float alphaScale)196     private int getColor(int color, float alphaScale) {
197         return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
198     }
199 
200     @Override
onDraw(Canvas canvas)201     protected void onDraw(Canvas canvas) {
202         // Draw lines across the top, middle, and bottom.
203         if (mMiddleDividerLoc != 0) {
204             drawDivider(0, canvas, mTopDividerTint);
205         }
206         drawDivider((int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc), canvas,
207                 mMiddleDividerTint);
208         drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
209 
210         if (mLocalPaths.size() == 0) {
211             return;
212         }
213         if (mShowProjection) {
214             drawProjection(canvas);
215         }
216         drawFilledPath(canvas);
217         drawLinePath(canvas);
218     }
219 
drawProjection(Canvas canvas)220     private void drawProjection(Canvas canvas) {
221         mPath.reset();
222         int x = mLocalPaths.keyAt(mLocalPaths.size() - 2);
223         int y = mLocalPaths.valueAt(mLocalPaths.size() - 2);
224         mPath.moveTo(x, y);
225         mPath.lineTo(canvas.getWidth(), mProjectUp ? 0 : canvas.getHeight());
226         canvas.drawPath(mPath, mDottedPaint);
227     }
228 
drawLinePath(Canvas canvas)229     private void drawLinePath(Canvas canvas) {
230         mPath.reset();
231         mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
232         for (int i = 1; i < mLocalPaths.size(); i++) {
233             int x = mLocalPaths.keyAt(i);
234             int y = mLocalPaths.valueAt(i);
235             if (y == PATH_DELIM) {
236                 if (++i < mLocalPaths.size()) {
237                     mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
238                 }
239             } else {
240                 mPath.lineTo(x, y);
241             }
242         }
243         canvas.drawPath(mPath, mLinePaint);
244     }
245 
drawFilledPath(Canvas canvas)246     private void drawFilledPath(Canvas canvas) {
247         mPath.reset();
248         float lastStartX = mLocalPaths.keyAt(0);
249         mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
250         for (int i = 1; i < mLocalPaths.size(); i++) {
251             int x = mLocalPaths.keyAt(i);
252             int y = mLocalPaths.valueAt(i);
253             if (y == PATH_DELIM) {
254                 mPath.lineTo(mLocalPaths.keyAt(i - 1), getHeight());
255                 mPath.lineTo(lastStartX, getHeight());
256                 mPath.close();
257                 if (++i < mLocalPaths.size()) {
258                     lastStartX = mLocalPaths.keyAt(i);
259                     mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
260                 }
261             } else {
262                 mPath.lineTo(x, y);
263             }
264         }
265         canvas.drawPath(mPath, mFillPaint);
266     }
267 
drawDivider(int y, Canvas canvas, int tintColor)268     private void drawDivider(int y, Canvas canvas, int tintColor) {
269         Drawable d = mDivider;
270         if (tintColor != -1) {
271             mTintedDivider.setTint(tintColor);
272             d = mTintedDivider;
273         }
274         d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
275         d.draw(canvas);
276     }
277 }
278