blob: 505dc58b2cbd8e68dc586ae3074c9d68db6eaa66 [file] [log] [blame]
/*
* 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.widget;
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 androidx.annotation.VisibleForTesting;
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 == 1) {
localPaths.put(getX(x+1) - 1, getY(0));
continue;
}
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;
}
canvas.save();
if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
// Flip the canvas along the y-axis of the center of itself before drawing paths.
canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0);
}
drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
drawFilledPath(canvas, mLocalPaths, mFillPaint);
drawLinePath(canvas, mLocalPaths, mLinePaint);
canvas.restore();
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);
}
@VisibleForTesting
void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
if (localPaths.size() == 0) {
return;
}
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);
}
}