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