| /* | |
| * 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.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.Nullable; | |
| import androidx.annotation.VisibleForTesting; | |
| import com.android.settings.R; | |
| import com.android.settings.fuelgauge.BatteryUtils; | |
| 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( | |
| com.android.settingslib.R.dimen.usage_graph_line_corner_radius); | |
| mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius)); | |
| mLinePaint.setStrokeWidth(resources.getDimensionPixelSize( | |
| com.android.settingslib.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( | |
| com.android.settingslib.R.dimen.usage_graph_dot_size); | |
| float interval = resources.getDimensionPixelSize( | |
| com.android.settingslib.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( | |
| com.android.settingslib.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); | |
| } | |
| } |