Files
app_Settings/src/com/android/settings/graph/UsageGraph.java
Alex Kulesza 4a121ecfae Fixing bug in UsageGraph rendering.
The calculateLocalPaths() method of UsageGraph converts a set of paths
in (milliseconds, percent) coordinates into the actual pixel values that
will be used for drawing. For the most part this is a one to one
process, but not always: input points that are too closely spaced to
draw accurately are skipped. The last point in the path, however, is
never skipped, in order to ensure that the graph ends at the correct
location.

The previous implementation of this method had a bug: the y-coordinates
of points that were skipped would be stored indefinitely (in the local
variable pendingYLoc) and then added back at the very end of the path
(under the condition i == mPaths.size() - 1 && pendingYLoc !=
PATH_DELIM). Under the right conditions, this led to the strange uptick
at the end of the graph seen in the associated bug.

This CL fixes the problem and attempts to make the logic slightly
clearer. It also adds tests, one of which (_similarPointMiddle) fails
for the previous code.

In more detail, previously pendingXLoc was used to hold the last x
coordinate seen, while pendingYLoc was used to hold the last *skipped* y
coordinate, or PATH_DELIM otherwise. The difference between these was
somewhat subtle and hard to understand from a quick read of the code,
and there was a bug: pendingYLoc never got reset to PATH_DELIM even if
later points were added. In this CL I have removed the pendingLoc
variables in favor of a single lx/ly pair, which always holds the local
coordinates of the most recent point, and I added an explicit boolean
skippedLastPoint to track whether the point (lx, ly) has already been
added or was skipped.

Bug: 64065296
Test: make RunSettingsRoboTests
Change-Id: I45ccffea1280d851bfae5143c2e84d188e133731
2017-07-31 11:12:17 -04:00

310 lines
11 KiB
Java

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.settings.graph;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.Drawable;
import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.view.View;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settingslib.R;
public class UsageGraph extends View {
private static final int PATH_DELIM = -1;
public static final String LOG_TAG = "UsageGraph";
private final Paint mLinePaint;
private final Paint mFillPaint;
private final Paint mDottedPaint;
private final Drawable mDivider;
private final Drawable mTintedDivider;
private final int mDividerSize;
private final Path mPath = new Path();
// Paths in coordinates they are passed in.
private final SparseIntArray mPaths = new SparseIntArray();
// Paths in local coordinates for drawing.
private final SparseIntArray mLocalPaths = new SparseIntArray();
// Paths for projection in coordinates they are passed in.
private final SparseIntArray mProjectedPaths = new SparseIntArray();
// Paths for projection in local coordinates for drawing.
private final SparseIntArray mLocalProjectedPaths = new SparseIntArray();
private final int mCornerRadius;
private int mAccentColor;
private float mMaxX = 100;
private float mMaxY = 100;
private float mMiddleDividerLoc = .5f;
private int mMiddleDividerTint = -1;
private int mTopDividerTint = -1;
public UsageGraph(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final Resources resources = context.getResources();
mLinePaint = new Paint();
mLinePaint.setStyle(Style.STROKE);
mLinePaint.setStrokeCap(Cap.ROUND);
mLinePaint.setStrokeJoin(Join.ROUND);
mLinePaint.setAntiAlias(true);
mCornerRadius = resources.getDimensionPixelSize(R.dimen.usage_graph_line_corner_radius);
mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width));
mFillPaint = new Paint(mLinePaint);
mFillPaint.setStyle(Style.FILL);
mDottedPaint = new Paint(mLinePaint);
mDottedPaint.setStyle(Style.STROKE);
float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
mDottedPaint.setStrokeWidth(dots * 3);
mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
TypedValue v = new TypedValue();
context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
mDivider = context.getDrawable(v.resourceId);
mTintedDivider = context.getDrawable(v.resourceId);
mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size);
}
void clearPaths() {
mPaths.clear();
mLocalPaths.clear();
mProjectedPaths.clear();
mLocalProjectedPaths.clear();
}
void setMax(int maxX, int maxY) {
final long startTime = System.currentTimeMillis();
mMaxX = maxX;
mMaxY = maxY;
calculateLocalPaths();
postInvalidate();
BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
}
void setDividerLoc(int height) {
mMiddleDividerLoc = 1 - height / mMaxY;
}
void setDividerColors(int middleColor, int topColor) {
mMiddleDividerTint = middleColor;
mTopDividerTint = topColor;
}
public void addPath(SparseIntArray points) {
addPathAndUpdate(points, mPaths, mLocalPaths);
}
public void addProjectedPath(SparseIntArray points) {
addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
}
private void addPathAndUpdate(
SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
final long startTime = System.currentTimeMillis();
for (int i = 0, size = points.size(); i < size; i++) {
paths.put(points.keyAt(i), points.valueAt(i));
}
// Add a delimiting value immediately after the last point.
paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
calculateLocalPaths(paths, localPaths);
postInvalidate();
BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
}
void setAccentColor(int color) {
mAccentColor = color;
mLinePaint.setColor(mAccentColor);
updateGradient();
postInvalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
final long startTime = System.currentTimeMillis();
super.onSizeChanged(w, h, oldw, oldh);
updateGradient();
calculateLocalPaths();
BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
}
private void calculateLocalPaths() {
calculateLocalPaths(mPaths, mLocalPaths);
calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
}
@VisibleForTesting
void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
final long startTime = System.currentTimeMillis();
if (getWidth() == 0) {
return;
}
localPaths.clear();
// Store the local coordinates of the most recent point.
int lx = 0;
int ly = PATH_DELIM;
boolean skippedLastPoint = false;
for (int i = 0; i < paths.size(); i++) {
int x = paths.keyAt(i);
int y = paths.valueAt(i);
if (y == PATH_DELIM) {
if (i == paths.size() - 1 && skippedLastPoint) {
// Add back skipped point to complete the path.
localPaths.put(lx, ly);
}
skippedLastPoint = false;
localPaths.put(lx + 1, PATH_DELIM);
} else {
lx = getX(x);
ly = getY(y);
// Skip this point if it is not far enough from the last one added.
if (localPaths.size() > 0) {
int lastX = localPaths.keyAt(localPaths.size() - 1);
int lastY = localPaths.valueAt(localPaths.size() - 1);
if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
skippedLastPoint = true;
continue;
}
}
skippedLastPoint = false;
localPaths.put(lx, ly);
}
}
BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
}
private boolean hasDiff(int x1, int x2) {
return Math.abs(x2 - x1) >= mCornerRadius;
}
private int getX(float x) {
return (int) (x / mMaxX * getWidth());
}
private int getY(float y) {
return (int) (getHeight() * (1 - (y / mMaxY)));
}
private void updateGradient() {
mFillPaint.setShader(
new LinearGradient(
0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
}
private int getColor(int color, float alphaScale) {
return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
}
@Override
protected void onDraw(Canvas canvas) {
final long startTime = System.currentTimeMillis();
// Draw lines across the top, middle, and bottom.
if (mMiddleDividerLoc != 0) {
drawDivider(0, canvas, mTopDividerTint);
}
drawDivider(
(int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
canvas,
mMiddleDividerTint);
drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
return;
}
drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
drawFilledPath(canvas, mLocalPaths, mFillPaint);
drawLinePath(canvas, mLocalPaths, mLinePaint);
BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
}
private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
if (localPaths.size() == 0) {
return;
}
mPath.reset();
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
for (int i = 1; i < localPaths.size(); i++) {
int x = localPaths.keyAt(i);
int y = localPaths.valueAt(i);
if (y == PATH_DELIM) {
if (++i < localPaths.size()) {
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, paint);
}
private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
mPath.reset();
float lastStartX = localPaths.keyAt(0);
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
for (int i = 1; i < localPaths.size(); i++) {
int x = localPaths.keyAt(i);
int y = localPaths.valueAt(i);
if (y == PATH_DELIM) {
mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
mPath.lineTo(lastStartX, getHeight());
mPath.close();
if (++i < localPaths.size()) {
lastStartX = localPaths.keyAt(i);
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, paint);
}
private void drawDivider(int y, Canvas canvas, int tintColor) {
Drawable d = mDivider;
if (tintColor != -1) {
mTintedDivider.setTint(tintColor);
d = mTintedDivider;
}
d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
d.draw(canvas);
}
}