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.settingslib.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;
35import com.android.settingslib.R;
36
37public 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
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
99    void clearPaths() {
100        mPaths.clear();
101    }
102
103    void setMax(int maxX, int maxY) {
104        mMaxX = maxX;
105        mMaxY = maxY;
106    }
107
108    void setDividerLoc(int height) {
109        mMiddleDividerLoc = 1 - height / mMaxY;
110    }
111
112    void setDividerColors(int middleColor, int topColor) {
113        mMiddleDividerTint = middleColor;
114        mTopDividerTint = topColor;
115    }
116
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
126    void setAccentColor(int color) {
127        mAccentColor = color;
128        mLinePaint.setColor(mAccentColor);
129        updateGradient();
130        postInvalidate();
131    }
132
133    void setShowProjection(boolean showProjection, boolean projectUp) {
134        mShowProjection = showProjection;
135        mProjectUp = projectUp;
136        postInvalidate();
137    }
138
139    @Override
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
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
179    private boolean hasDiff(int x1, int x2) {
180        return Math.abs(x2 - x1) >= mCornerRadius;
181    }
182
183    private int getX(float x) {
184        return (int) (x / mMaxX * getWidth());
185    }
186
187    private int getY(float y) {
188        return (int) (getHeight() * (1 - (y / mMaxY)));
189    }
190
191    private void updateGradient() {
192        mFillPaint.setShader(new LinearGradient(0, 0, 0, getHeight(),
193                getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
194    }
195
196    private int getColor(int color, float alphaScale) {
197        return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
198    }
199
200    @Override
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
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
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
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
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