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