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