blob: 1fff0fbb3f619d431af5d24b7d97ea0adbcd9d43 [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.settingslib.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();
private final int mCornerRadius;
private int mAccentColor;
private boolean mShowProjection;
private boolean mProjectUp;
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();
}
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) {
for (int i = 0; i < points.size(); i++) {
mPaths.put(points.keyAt(i), points.valueAt(i));
}
mPaths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
calculateLocalPaths();
postInvalidate();
}
void setAccentColor(int color) {
mAccentColor = color;
mLinePaint.setColor(mAccentColor);
updateGradient();
postInvalidate();
}
void setShowProjection(boolean showProjection, boolean projectUp) {
mShowProjection = showProjection;
mProjectUp = projectUp;
postInvalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateGradient();
calculateLocalPaths();
}
private void calculateLocalPaths() {
if (getWidth() == 0) return;
mLocalPaths.clear();
int pendingXLoc = 0;
int pendingYLoc = PATH_DELIM;
for (int i = 0; i < mPaths.size(); i++) {
int x = mPaths.keyAt(i);
int y = mPaths.valueAt(i);
if (y == PATH_DELIM) {
if (i == mPaths.size() - 1 && pendingYLoc != PATH_DELIM) {
// Connect to the end of the graph.
mLocalPaths.put(pendingXLoc, pendingYLoc);
}
// Clear out any pending points.
pendingYLoc = PATH_DELIM;
mLocalPaths.put(pendingXLoc + 1, PATH_DELIM);
} else {
final int lx = getX(x);
final int ly = getY(y);
pendingXLoc = lx;
if (mLocalPaths.size() > 0) {
int lastX = mLocalPaths.keyAt(mLocalPaths.size() - 1);
int lastY = mLocalPaths.valueAt(mLocalPaths.size() - 1);
if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
pendingYLoc = ly;
continue;
}
}
mLocalPaths.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) {
return;
}
if (mShowProjection) {
drawProjection(canvas);
}
drawFilledPath(canvas);
drawLinePath(canvas);
}
private void drawProjection(Canvas canvas) {
mPath.reset();
int x = mLocalPaths.keyAt(mLocalPaths.size() - 2);
int y = mLocalPaths.valueAt(mLocalPaths.size() - 2);
mPath.moveTo(x, y);
mPath.lineTo(canvas.getWidth(), mProjectUp ? 0 : canvas.getHeight());
canvas.drawPath(mPath, mDottedPaint);
}
private void drawLinePath(Canvas canvas) {
mPath.reset();
mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
for (int i = 1; i < mLocalPaths.size(); i++) {
int x = mLocalPaths.keyAt(i);
int y = mLocalPaths.valueAt(i);
if (y == PATH_DELIM) {
if (++i < mLocalPaths.size()) {
mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, mLinePaint);
}
private void drawFilledPath(Canvas canvas) {
mPath.reset();
float lastStartX = mLocalPaths.keyAt(0);
mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
for (int i = 1; i < mLocalPaths.size(); i++) {
int x = mLocalPaths.keyAt(i);
int y = mLocalPaths.valueAt(i);
if (y == PATH_DELIM) {
mPath.lineTo(mLocalPaths.keyAt(i - 1), getHeight());
mPath.lineTo(lastStartX, getHeight());
mPath.close();
if (++i < mLocalPaths.size()) {
lastStartX = mLocalPaths.keyAt(i);
mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, mFillPaint);
}
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);
}
}