platform-packages-apps-Settings / src / com / android / settings / graph / UsageGraph.java
UsageGraph.java
Raw
/*
 * 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);
    }
}