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