1 /* 2 * Copyright (C) 2019 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.car.developeroptions.widget; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Canvas; 23 import android.graphics.CornerPathEffect; 24 import android.graphics.DashPathEffect; 25 import android.graphics.LinearGradient; 26 import android.graphics.Paint; 27 import android.graphics.Paint.Cap; 28 import android.graphics.Paint.Join; 29 import android.graphics.Paint.Style; 30 import android.graphics.Path; 31 import android.graphics.Shader.TileMode; 32 import android.graphics.drawable.Drawable; 33 import android.util.AttributeSet; 34 import android.util.SparseIntArray; 35 import android.util.TypedValue; 36 import android.view.View; 37 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.car.developeroptions.fuelgauge.BatteryUtils; 41 import com.android.settingslib.R; 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(R.dimen.usage_graph_line_corner_radius); 88 mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius)); 89 mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width)); 90 91 mFillPaint = new Paint(mLinePaint); 92 mFillPaint.setStyle(Style.FILL); 93 94 mDottedPaint = new Paint(mLinePaint); 95 mDottedPaint.setStyle(Style.STROKE); 96 float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size); 97 float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval); 98 mDottedPaint.setStrokeWidth(dots * 3); 99 mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0)); 100 mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots)); 101 102 TypedValue v = new TypedValue(); 103 context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true); 104 mDivider = context.getDrawable(v.resourceId); 105 mTintedDivider = context.getDrawable(v.resourceId); 106 mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size); 107 } 108 clearPaths()109 void clearPaths() { 110 mPaths.clear(); 111 mLocalPaths.clear(); 112 mProjectedPaths.clear(); 113 mLocalProjectedPaths.clear(); 114 } 115 setMax(int maxX, int maxY)116 void setMax(int maxX, int maxY) { 117 final long startTime = System.currentTimeMillis(); 118 mMaxX = maxX; 119 mMaxY = maxY; 120 calculateLocalPaths(); 121 postInvalidate(); 122 BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime); 123 } 124 setDividerLoc(int height)125 void setDividerLoc(int height) { 126 mMiddleDividerLoc = 1 - height / mMaxY; 127 } 128 setDividerColors(int middleColor, int topColor)129 void setDividerColors(int middleColor, int topColor) { 130 mMiddleDividerTint = middleColor; 131 mTopDividerTint = topColor; 132 } 133 addPath(SparseIntArray points)134 public void addPath(SparseIntArray points) { 135 addPathAndUpdate(points, mPaths, mLocalPaths); 136 } 137 addProjectedPath(SparseIntArray points)138 public void addProjectedPath(SparseIntArray points) { 139 addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths); 140 } 141 addPathAndUpdate( SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths)142 private void addPathAndUpdate( 143 SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) { 144 final long startTime = System.currentTimeMillis(); 145 for (int i = 0, size = points.size(); i < size; i++) { 146 paths.put(points.keyAt(i), points.valueAt(i)); 147 } 148 // Add a delimiting value immediately after the last point. 149 paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM); 150 calculateLocalPaths(paths, localPaths); 151 postInvalidate(); 152 BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime); 153 } 154 setAccentColor(int color)155 void setAccentColor(int color) { 156 mAccentColor = color; 157 mLinePaint.setColor(mAccentColor); 158 updateGradient(); 159 postInvalidate(); 160 } 161 162 @Override onSizeChanged(int w, int h, int oldw, int oldh)163 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 164 final long startTime = System.currentTimeMillis(); 165 super.onSizeChanged(w, h, oldw, oldh); 166 updateGradient(); 167 calculateLocalPaths(); 168 BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime); 169 } 170 calculateLocalPaths()171 private void calculateLocalPaths() { 172 calculateLocalPaths(mPaths, mLocalPaths); 173 calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths); 174 } 175 176 @VisibleForTesting calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths)177 void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) { 178 final long startTime = System.currentTimeMillis(); 179 if (getWidth() == 0) { 180 return; 181 } 182 localPaths.clear(); 183 // Store the local coordinates of the most recent point. 184 int lx = 0; 185 int ly = PATH_DELIM; 186 boolean skippedLastPoint = false; 187 for (int i = 0; i < paths.size(); i++) { 188 int x = paths.keyAt(i); 189 int y = paths.valueAt(i); 190 if (y == PATH_DELIM) { 191 if (i == 1) { 192 localPaths.put(getX(x+1) - 1, getY(0)); 193 continue; 194 } 195 if (i == paths.size() - 1 && skippedLastPoint) { 196 // Add back skipped point to complete the path. 197 localPaths.put(lx, ly); 198 } 199 skippedLastPoint = false; 200 localPaths.put(lx + 1, PATH_DELIM); 201 } else { 202 lx = getX(x); 203 ly = getY(y); 204 // Skip this point if it is not far enough from the last one added. 205 if (localPaths.size() > 0) { 206 int lastX = localPaths.keyAt(localPaths.size() - 1); 207 int lastY = localPaths.valueAt(localPaths.size() - 1); 208 if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) { 209 skippedLastPoint = true; 210 continue; 211 } 212 } 213 skippedLastPoint = false; 214 localPaths.put(lx, ly); 215 } 216 } 217 BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime); 218 } 219 hasDiff(int x1, int x2)220 private boolean hasDiff(int x1, int x2) { 221 return Math.abs(x2 - x1) >= mCornerRadius; 222 } 223 getX(float x)224 private int getX(float x) { 225 return (int) (x / mMaxX * getWidth()); 226 } 227 getY(float y)228 private int getY(float y) { 229 return (int) (getHeight() * (1 - (y / mMaxY))); 230 } 231 updateGradient()232 private void updateGradient() { 233 mFillPaint.setShader( 234 new LinearGradient( 235 0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP)); 236 } 237 getColor(int color, float alphaScale)238 private int getColor(int color, float alphaScale) { 239 return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff)); 240 } 241 242 @Override onDraw(Canvas canvas)243 protected void onDraw(Canvas canvas) { 244 final long startTime = System.currentTimeMillis(); 245 // Draw lines across the top, middle, and bottom. 246 if (mMiddleDividerLoc != 0) { 247 drawDivider(0, canvas, mTopDividerTint); 248 } 249 drawDivider( 250 (int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc), 251 canvas, 252 mMiddleDividerTint); 253 drawDivider(canvas.getHeight() - mDividerSize, canvas, -1); 254 255 if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) { 256 return; 257 } 258 259 canvas.save(); 260 if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) { 261 // Flip the canvas along the y-axis of the center of itself before drawing paths. 262 canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0); 263 } 264 drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint); 265 drawFilledPath(canvas, mLocalPaths, mFillPaint); 266 drawLinePath(canvas, mLocalPaths, mLinePaint); 267 canvas.restore(); 268 BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime); 269 } 270 drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint)271 private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) { 272 if (localPaths.size() == 0) { 273 return; 274 } 275 mPath.reset(); 276 mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0)); 277 for (int i = 1; i < localPaths.size(); i++) { 278 int x = localPaths.keyAt(i); 279 int y = localPaths.valueAt(i); 280 if (y == PATH_DELIM) { 281 if (++i < localPaths.size()) { 282 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i)); 283 } 284 } else { 285 mPath.lineTo(x, y); 286 } 287 } 288 canvas.drawPath(mPath, paint); 289 } 290 drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint)291 private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) { 292 mPath.reset(); 293 float lastStartX = localPaths.keyAt(0); 294 mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0)); 295 for (int i = 1; i < localPaths.size(); i++) { 296 int x = localPaths.keyAt(i); 297 int y = localPaths.valueAt(i); 298 if (y == PATH_DELIM) { 299 mPath.lineTo(localPaths.keyAt(i - 1), getHeight()); 300 mPath.lineTo(lastStartX, getHeight()); 301 mPath.close(); 302 if (++i < localPaths.size()) { 303 lastStartX = localPaths.keyAt(i); 304 mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i)); 305 } 306 } else { 307 mPath.lineTo(x, y); 308 } 309 } 310 canvas.drawPath(mPath, paint); 311 } 312 drawDivider(int y, Canvas canvas, int tintColor)313 private void drawDivider(int y, Canvas canvas, int tintColor) { 314 Drawable d = mDivider; 315 if (tintColor != -1) { 316 mTintedDivider.setTint(tintColor); 317 d = mTintedDivider; 318 } 319 d.setBounds(0, y, canvas.getWidth(), y + mDividerSize); 320 d.draw(canvas); 321 } 322 } 323