/* * 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 == 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; } 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); } }