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