1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.widget; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.CornerPathEffect; 23 import android.graphics.DashPathEffect; 24 import android.graphics.LinearGradient; 25 import android.graphics.Paint; 26 import android.graphics.Paint.Cap; 27 import android.graphics.Paint.Join; 28 import android.graphics.Paint.Style; 29 import android.graphics.Path; 30 import android.graphics.Shader.TileMode; 31 import android.graphics.drawable.Drawable; 32 import android.util.AttributeSet; 33 import android.util.SparseIntArray; 34 import android.util.TypedValue; 35 import android.view.View; 36 37 import androidx.annotation.Nullable; 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.settings.R; 41 import com.android.settings.fuelgauge.BatteryUtils; 42 43 public class UsageGraph extends View { 44 45 private static final int PATH_DELIM = -1; 46 public static final String LOG_TAG = "UsageGraph"; 47 48 private final Paint mLinePaint; 49 private final Paint mFillPaint; 50 private final Paint mDottedPaint; 51 52 private final Drawable mDivider; 53 private final Drawable mTintedDivider; 54 private final int mDividerSize; 55 56 private final Path mPath = new Path(); 57 58 // Paths in coordinates they are passed in. 59 private final SparseIntArray mPaths = new SparseIntArray(); 60 // Paths in local coordinates for drawing. 61 private final SparseIntArray mLocalPaths = new SparseIntArray(); 62 63 // Paths for projection in coordinates they are passed in. 64 private final SparseIntArray mProjectedPaths = new SparseIntArray(); 65 // Paths for projection in local coordinates for drawing. 66 private final SparseIntArray mLocalProjectedPaths = new SparseIntArray(); 67 68 private final int mCornerRadius; 69 private int mAccentColor; 70 71 private float mMaxX = 100; 72 private float mMaxY = 100; 73 74 private float mMiddleDividerLoc = .5f; 75 private int mMiddleDividerTint = -1; 76 private int mTopDividerTint = -1; 77 UsageGraph(Context context, @Nullable AttributeSet attrs)78 public UsageGraph(Context context, @Nullable AttributeSet attrs) { 79 super(context, attrs); 80 final Resources resources = context.getResources(); 81 82 mLinePaint = new Paint(); 83 mLinePaint.setStyle(Style.STROKE); 84 mLinePaint.setStrokeCap(Cap.ROUND); 85 mLinePaint.setStrokeJoin(Join.ROUND); 86 mLinePaint.setAntiAlias(true); 87 mCornerRadius = resources.getDimensionPixelSize( 88 com.android.settingslib.R.dimen.usage_graph_line_corner_radius); 89 mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius)); 90 mLinePaint.setStrokeWidth(resources.getDimensionPixelSize( 91 com.android.settingslib.R.dimen.usage_graph_line_width)); 92 93 mFillPaint = new Paint(mLinePaint); 94 mFillPaint.setStyle(Style.FILL); 95 96 mDottedPaint = new Paint(mLinePaint); 97 mDottedPaint.setStyle(Style.STROKE); 98 float dots = resources.getDimensionPixelSize( 99 com.android.settingslib.R.dimen.usage_graph_dot_size); 100 float interval = resources.getDimensionPixelSize( 101 com.android.settingslib.R.dimen.usage_graph_dot_interval); 102 mDottedPaint.setStrokeWidth(dots * 3); 103 mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0)); 104 mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots)); 105 106 TypedValue v = new TypedValue(); 107 context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true); 108 mDivider = context.getDrawable(v.resourceId); 109 mTintedDivider = context.getDrawable(v.resourceId); 110 mDividerSize = resources.getDimensionPixelSize( 111 com.android.settingslib.R.dimen.usage_graph_divider_size); 112 } 113 clearPaths()114 void clearPaths() { 115 mPaths.clear(); 116 mLocalPaths.clear(); 117 mProjectedPaths.clear(); 118 mLocalProjectedPaths.clear(); 119 } 120 setMax(int maxX, int maxY)121 void setMax(int maxX, int maxY) { 122 final long startTime = System.currentTimeMillis(); 123 mMaxX = maxX; 124 mMaxY = maxY; 125 calculateLocalPaths(); 126 postInvalidate(); 127 BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime); 128 } 129 setDividerLoc(int height)130 void setDividerLoc(int height) { 131 mMiddleDividerLoc = 1 - height / mMaxY; 132 } 133 setDividerColors(int middleColor, int topColor)134 void setDividerColors(int middleColor, int topColor) { 135 mMiddleDividerTint = middleColor; 136 mTopDividerTint = topColor; 137 } 138 addPath(SparseIntArray points)139 public void addPath(SparseIntArray points) { 140 addPathAndUpdate(points, mPaths, mLocalPaths); 141 } 142 addProjectedPath(SparseIntArray points)143 public void addProjectedPath(SparseIntArray points) { 144 addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths); 145 } 146 addPathAndUpdate( SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths)147 private void addPathAndUpdate( 148 SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) { 149 final long startTime = System.currentTimeMillis(); 150 for (int i = 0, size = points.size(); i < size; i++) { 151 paths.put(points.keyAt(i), points.valueAt(i)); 152 } 153 // Add a delimiting value immediately after the last point. 154 paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM); 155 calculateLocalPaths(paths, localPaths); 156 postInvalidate(); 157 BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime); 158 } 159 setAccentColor(int color)160 void setAccentColor(int color) { 161 mAccentColor = color; 162 mLinePaint.setColor(mAccentColor); 163 updateGradient(); 164 postInvalidate(); 165 } 166 167 @Override onSizeChanged(int w, int h, int oldw, int oldh)168 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 169 final long startTime = System.currentTimeMillis(); 170 super.onSizeChanged(w, h, oldw, oldh); 171 updateGradient(); 172 calculateLocalPaths(); 173 BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime); 174 } 175 calculateLocalPaths()176 private void calculateLocalPaths() { 177 calculateLocalPaths(mPaths, mLocalPaths); 178 calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths); 179 } 180 181 @VisibleForTesting calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths)182 void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) { 183 final long startTime = System.currentTimeMillis(); 184 if (getWidth() == 0) { 185 return; 186 } 187 localPaths.clear(); 188 // Store the local coordinates of the most recent point. 189 int lx = 0; 190 int ly = PATH_DELIM; 191 boolean skippedLastPoint = false; 192 for (int i = 0; i < paths.size(); i++) { 193 int x = paths.keyAt(i); 194 int y = paths.valueAt(i); 195 if (y == PATH_DELIM) { 196 if (i == 1) { 197 localPaths.put(getX(x+1) - 1, getY(0)); 198 continue; 199 } 200 if (i == paths.size() - 1 && skippedLastPoint) { 201 // Add back skipped point to complete the path. 202 localPaths.put(lx, ly); 203 } 204 skippedLastPoint = false; 205 localPaths.put(lx + 1, PATH_DELIM); 206 } else { 207 lx = getX(x); 208 ly = getY(y); 209 // Skip this point if it is not far enough from the last one added. 210 if (localPaths.size() > 0) { 211 int lastX = localPaths.keyAt(localPaths.size() - 1); 212 int lastY = localPaths.valueAt(localPaths.size() - 1); 213 if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) { 214 skippedLastPoint = true; 215 continue; 216 } 217 } 218 skippedLastPoint = false; 219 localPaths.put(lx, ly); 220 } 221 } 222 BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime); 223 } 224 hasDiff(int x1, int x2)225 private boolean hasDiff(int x1, int x2) { 226 return Math.abs(x2 - x1) >= mCornerRadius; 227 } 228 getX(float x)229 private int getX(float x) { 230 return (int) (x / mMaxX * getWidth()); 231 } 232 getY(float y)233 private int getY(float y) { 234 return (int) (getHeight() * (1 - (y / mMaxY))); 235 } 236 updateGradient()237 private void updateGradient() { 238 mFillPaint.setShader( 239 new LinearGradient( 240 0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP)); 241 } 242 getColor(int color, float alphaScale)243 private int getColor(int color, float alphaScale) { 244 return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff)); 245 } 246 247 @Override onDraw(Canvas canvas)248 protected void onDraw(Canvas canvas) { 249 final long startTime = System.currentTimeMillis(); 250 // Draw lines across the top, middle, and bottom. 251 if (mMiddleDividerLoc != 0) { 252 drawDivider(0, canvas, mTopDividerTint); 253 } 254 drawDivider( 255 (int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc), 256 canvas, 257 mMiddleDividerTint); 258 drawDivider(canvas.getHeight() - mDividerSize, canvas, -1); 259 260 if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) { 261 return; 262 } 263 264 canvas.save(); 265 if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) { 266 // Flip the canvas along the y-axis of the center of itself before drawing paths. 267 canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0); 268 } 269 drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint); 270 drawFilledPath(canvas, mLocalPaths, mFillPaint); 271 drawLinePath(canvas, mLocalPaths, mLinePaint); 272 canvas.restore(); 273 BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime); 274 } 275 drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint)276 private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) { 277 if (localPaths.size() == 0) { 278 return; 279 } 280 mPath.reset(); 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 if (++i < localPaths.size()) { 287 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i)); 288 } 289 } else { 290 mPath.lineTo(x, y); 291 } 292 } 293 canvas.drawPath(mPath, paint); 294 } 295 296 @VisibleForTesting drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint)297 void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) { 298 if (localPaths.size() == 0) { 299 return; 300 } 301 mPath.reset(); 302 float lastStartX = localPaths.keyAt(0); 303 mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0)); 304 for (int i = 1; i < localPaths.size(); i++) { 305 int x = localPaths.keyAt(i); 306 int y = localPaths.valueAt(i); 307 if (y == PATH_DELIM) { 308 mPath.lineTo(localPaths.keyAt(i - 1), getHeight()); 309 mPath.lineTo(lastStartX, getHeight()); 310 mPath.close(); 311 if (++i < localPaths.size()) { 312 lastStartX = localPaths.keyAt(i); 313 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i)); 314 } 315 } else { 316 mPath.lineTo(x, y); 317 } 318 } 319 canvas.drawPath(mPath, paint); 320 } 321 drawDivider(int y, Canvas canvas, int tintColor)322 private void drawDivider(int y, Canvas canvas, int tintColor) { 323 Drawable d = mDivider; 324 if (tintColor != -1) { 325 mTintedDivider.setTint(tintColor); 326 d = mTintedDivider; 327 } 328 d.setBounds(0, y, canvas.getWidth(), y + mDividerSize); 329 d.draw(canvas); 330 } 331 } 332