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