UsageGraph.java revision c57ceaaa8cfc033bb397eab7af0b4359befbef52
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    }
114
115    void setDividerLoc(int height) {
116        mMiddleDividerLoc = 1 - height / mMaxY;
117    }
118
119    void setDividerColors(int middleColor, int topColor) {
120        mMiddleDividerTint = middleColor;
121        mTopDividerTint = topColor;
122    }
123
124    public void addPath(SparseIntArray points) {
125        addPathAndUpdate(points, mPaths, mLocalPaths);
126    }
127
128    public void addProjectedPath(SparseIntArray points) {
129        addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
130    }
131
132    private void addPathAndUpdate(SparseIntArray points, SparseIntArray paths,
133            SparseIntArray localPaths) {
134        for (int i = 0, size = points.size(); i < size; i++) {
135            paths.put(points.keyAt(i), points.valueAt(i));
136        }
137        // Add a delimiting value immediately after the last point.
138        paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
139        calculateLocalPaths(paths, localPaths);
140        postInvalidate();
141    }
142
143    void setAccentColor(int color) {
144        mAccentColor = color;
145        mLinePaint.setColor(mAccentColor);
146        updateGradient();
147        postInvalidate();
148    }
149
150    @Override
151    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
152        super.onSizeChanged(w, h, oldw, oldh);
153        updateGradient();
154        calculateLocalPaths(mPaths, mLocalPaths);
155        calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
156    }
157
158    private void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
159        if (getWidth() == 0) {
160            return;
161        }
162        localPaths.clear();
163        int pendingXLoc = 0;
164        int pendingYLoc = PATH_DELIM;
165        for (int i = 0; i < paths.size(); i++) {
166            int x = paths.keyAt(i);
167            int y = paths.valueAt(i);
168            if (y == PATH_DELIM) {
169                if (i == paths.size() - 1 && pendingYLoc != PATH_DELIM) {
170                    // Connect to the end of the graph.
171                    localPaths.put(pendingXLoc, pendingYLoc);
172                }
173                // Clear out any pending points.
174                pendingYLoc = PATH_DELIM;
175                localPaths.put(pendingXLoc + 1, PATH_DELIM);
176            } else {
177                final int lx = getX(x);
178                final int ly = getY(y);
179                pendingXLoc = lx;
180                if (localPaths.size() > 0) {
181                    int lastX = localPaths.keyAt(localPaths.size() - 1);
182                    int lastY = localPaths.valueAt(localPaths.size() - 1);
183                    if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
184                        pendingYLoc = ly;
185                        continue;
186                    }
187                }
188                localPaths.put(lx, ly);
189            }
190        }
191    }
192
193    private boolean hasDiff(int x1, int x2) {
194        return Math.abs(x2 - x1) >= mCornerRadius;
195    }
196
197    private int getX(float x) {
198        return (int) (x / mMaxX * getWidth());
199    }
200
201    private int getY(float y) {
202        return (int) (getHeight() * (1 - (y / mMaxY)));
203    }
204
205    private void updateGradient() {
206        mFillPaint.setShader(
207                new LinearGradient(0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0,
208                        TileMode.CLAMP));
209    }
210
211    private int getColor(int color, float alphaScale) {
212        return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
213    }
214
215    @Override
216    protected void onDraw(Canvas canvas) {
217        // Draw lines across the top, middle, and bottom.
218        if (mMiddleDividerLoc != 0) {
219            drawDivider(0, canvas, mTopDividerTint);
220        }
221        drawDivider((int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc), canvas,
222                mMiddleDividerTint);
223        drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
224
225        if (mLocalPaths.size() == 0 && mProjectedPaths.size() == 0) {
226            return;
227        }
228        drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
229        drawFilledPath(canvas, mLocalPaths, mFillPaint);
230        drawLinePath(canvas, mLocalPaths, mLinePaint);
231    }
232
233    private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
234        if (localPaths.size() == 0) {
235            return;
236        }
237        mPath.reset();
238        mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
239        for (int i = 1; i < localPaths.size(); i++) {
240            int x = localPaths.keyAt(i);
241            int y = localPaths.valueAt(i);
242            if (y == PATH_DELIM) {
243                if (++i < localPaths.size()) {
244                    mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
245                }
246            } else {
247                mPath.lineTo(x, y);
248            }
249        }
250        canvas.drawPath(mPath, paint);
251    }
252
253    private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
254        mPath.reset();
255        float lastStartX = localPaths.keyAt(0);
256        mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
257        for (int i = 1; i < localPaths.size(); i++) {
258            int x = localPaths.keyAt(i);
259            int y = localPaths.valueAt(i);
260            if (y == PATH_DELIM) {
261                mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
262                mPath.lineTo(lastStartX, getHeight());
263                mPath.close();
264                if (++i < localPaths.size()) {
265                    lastStartX = localPaths.keyAt(i);
266                    mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
267                }
268            } else {
269                mPath.lineTo(x, y);
270            }
271        }
272        canvas.drawPath(mPath, paint);
273    }
274
275    private void drawDivider(int y, Canvas canvas, int tintColor) {
276        Drawable d = mDivider;
277        if (tintColor != -1) {
278            mTintedDivider.setTint(tintColor);
279            d = mTintedDivider;
280        }
281        d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
282        d.draw(canvas);
283    }
284}
285