Previously, projections were hard-coded in UsageGraph as lines from the last known point to the corner of the graph. This change replaces that with support for arbitrary projection curves. Logic for hiding/showing the projection is now gone; if the client does not want a projection, it simply does not supply one. There are two active clients of this code: the data usage graph and the battery usage graph. The data graph does not use projections and is essentially unchanged. The battery graph now implements its linear extrapolation directly in BatteryInfo. Bug: 38400320 Test: make SettingsUnitTests SettingsGoogleUnitTests Test: manual (screenshots in comments) Change-Id: I754e66f6b18ecb8b936143399f8e9e3368fc1ce4
285 lines
10 KiB
Java
285 lines
10 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.util.AttributeSet;
|
|
import android.util.SparseIntArray;
|
|
import android.util.TypedValue;
|
|
import android.view.View;
|
|
|
|
import com.android.settingslib.R;
|
|
|
|
public class UsageGraph extends View {
|
|
|
|
private static final int PATH_DELIM = -1;
|
|
|
|
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) {
|
|
mMaxX = maxX;
|
|
mMaxY = maxY;
|
|
}
|
|
|
|
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) {
|
|
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();
|
|
}
|
|
|
|
void setAccentColor(int color) {
|
|
mAccentColor = color;
|
|
mLinePaint.setColor(mAccentColor);
|
|
updateGradient();
|
|
postInvalidate();
|
|
}
|
|
|
|
@Override
|
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
super.onSizeChanged(w, h, oldw, oldh);
|
|
updateGradient();
|
|
calculateLocalPaths(mPaths, mLocalPaths);
|
|
calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
|
|
}
|
|
|
|
private void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
|
|
if (getWidth() == 0) {
|
|
return;
|
|
}
|
|
localPaths.clear();
|
|
int pendingXLoc = 0;
|
|
int pendingYLoc = PATH_DELIM;
|
|
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 && pendingYLoc != PATH_DELIM) {
|
|
// Connect to the end of the graph.
|
|
localPaths.put(pendingXLoc, pendingYLoc);
|
|
}
|
|
// Clear out any pending points.
|
|
pendingYLoc = PATH_DELIM;
|
|
localPaths.put(pendingXLoc + 1, PATH_DELIM);
|
|
} else {
|
|
final int lx = getX(x);
|
|
final int ly = getY(y);
|
|
pendingXLoc = lx;
|
|
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)) {
|
|
pendingYLoc = ly;
|
|
continue;
|
|
}
|
|
}
|
|
localPaths.put(lx, ly);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// 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 && mProjectedPaths.size() == 0) {
|
|
return;
|
|
}
|
|
drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
|
|
drawFilledPath(canvas, mLocalPaths, mFillPaint);
|
|
drawLinePath(canvas, mLocalPaths, mLinePaint);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|