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