com.tomdignan.jiffymonitor.ChartFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.tomdignan.jiffymonitor.ChartFragment.java

Source

/*
Jiffy Resource Monitor
Copyright (C) 2012 Tom Dignan <tom@tomdignan.com>
    
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.tomdignan.jiffymonitor;

import java.io.Serializable;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

/**
 * A chart which allows the y value to be plotted while t progresses
 * circularly after each update. 
 *
 * This class does not handle timing. 't' progresses by a constant increment
 * each time you update the chart. It is up to the user of the chart to handle
 * timing. This makes it simpler and re-usable in more ways.
 * 
 * To graph according to time, simply call setNextValue at a constant
 * rate.
 *  
 * @author Tom Dignan
 */
public class ChartFragment extends Fragment implements Updatable {
    private static final String TAG = "ChartFragment";

    /** Public, empty,  constructor */
    public ChartFragment() {
    }

    /** SurfaceView for drawing chart */
    private CircularChartView chartView;

    /** Integer: height in px or LayoutParams constant */
    public static final String PARAM_HEIGHT = "HEIGHT";

    /** Integer: width in px or LayoutParams constant*/
    public static final String PARAM_WIDTH = "WIDTH";

    /** Integer: width of chart line */
    public static final String PARAM_STROKE_WIDTH = "STROKE_WIDTH";

    /** Integer: foreground color 0xAARRGGBB */
    public static final String PARAM_FG_COLOR = "FG_COLOR";

    /** Integer: foreground text color */
    public static final String PARAM_TEXT_COLOR = "FG_TEXT_COLOR";

    /** Integer: size of foreground text */
    public static final String PARAM_TEXT_SIZE = "TEXT_SIZE";

    /** Integer: background color 0xAARRGGBB */
    public static final String PARAM_BG_COLOR = "BG_COLOR";

    /** Integer: disabled background color 0xAARRGGBB */
    public static final String PARAM_BG_DISABLED_COLOR = "BG_DISABLED_COLOR";

    /** Boolean: turn on anti-aliasing */
    public static final String PARAM_ANTI_ALIAS = "ANTI_ALIAS";

    /** Integer: color of grid */
    public static final String PARAM_GRID_COLOR = "GRID_COLOR";

    /** Integer: Thickness of grid lines */
    public static final String PARAM_GRID_THICKNESS = "GRID_THICKNESS";

    /** Key for chart state */
    private static final String STATE_CHART = "STATE_CHART";

    private int bgEnabledColor;
    private int bgDisabledColor;

    /** {@inheritDoc} */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        // requires a bundle with full arguments.
        Bundle args = getArguments();

        long t1 = System.currentTimeMillis();
        Loggy.d(TAG, "checking args time=%l", System.currentTimeMillis());
        if (args == null || !args.containsKey(PARAM_HEIGHT) || !args.containsKey(PARAM_WIDTH)
                || !args.containsKey(PARAM_STROKE_WIDTH) || !args.containsKey(PARAM_ANTI_ALIAS)
                || !args.containsKey(PARAM_FG_COLOR) || !args.containsKey(PARAM_BG_COLOR)
                || !args.containsKey(PARAM_BG_DISABLED_COLOR) || !args.containsKey(PARAM_TEXT_COLOR)
                || !args.containsKey(PARAM_TEXT_SIZE) || !args.containsKey(PARAM_GRID_COLOR)
                || !args.containsKey(PARAM_GRID_THICKNESS)) {
            throw new IllegalStateException("CircularChartFragment requires bundle for PARAM_'s");
        }
        Loggy.d(TAG, "checking args diff=%l", System.currentTimeMillis() - t1);

        ViewGroup.LayoutParams params = container.getLayoutParams();
        params.height = args.getInt(PARAM_HEIGHT);
        params.width = args.getInt(PARAM_WIDTH);

        this.chartView = new CircularChartView(getActivity(), args);

        this.bgEnabledColor = args.getInt(PARAM_BG_COLOR);
        this.bgDisabledColor = args.getInt(PARAM_BG_DISABLED_COLOR);

        this.chartView.setBackgroundColor(bgEnabledColor);
        this.chartView.setLayoutParams(params);

        return this.chartView;
    }

    @Override
    public void onActivityCreated(Bundle inState) {
        super.onActivityCreated(inState);
        if (inState != null) {
            PersistentState state = (PersistentState) inState.get(STATE_CHART);
            Loggy.i(TAG, "onActivityCreated: restoring chart view state");
            this.chartView.restoreState(state);
        } else {
            Loggy.i(TAG, "onActivityCreated: got null bundle");
        }
    }

    /**
     * Sets the displayed name of this resource monitor 
     * @param name
     */
    public void setName(String name) {
        this.chartView.setName(name);
    }

    /**
     * Push another value into the meter to chart during the next time-interval.
     * 
     * @param float percent [0, 1]
     */
    public void setNextValue(float nextval) {
        this.chartView.setNextValue(nextval);
    }

    /**
     * Resets chart state to remove trend line and wipe buffer
     */
    public void reset() {
        this.chartView.reset();
    }

    public void setDisabled() {
        this.chartView.setSleeping(true);
        this.chartView.setBackgroundColor(bgDisabledColor);
        this.chartView.invalidate();
    }

    public void setEnabled() {
        this.chartView.setSleeping(false);
        this.chartView.setBackgroundColor(bgEnabledColor);
    }

    public void forceRedraw() {
        this.chartView.invalidate();
    }

    /** 
     * {@inheritDoc} 
     * Retain inner view state 
     */
    @Override
    public void onSaveInstanceState(Bundle outState) {
        Loggy.i(TAG, "onSaveInstanceState: saving chart view state");
        outState.putSerializable(STATE_CHART, this.chartView.getPersistentState());
        super.onSaveInstanceState(outState);
    }

    /** How many units to show along the x-axis */
    private static final int UNITS = 60;

    /** How many columns are in a grid */
    private static final int UNITS_PER_COLUMN = 5;

    /** How many rows are in a grid */
    private static final int ROWS = 5; // every 20%

    /** 
     * Encapsulates persistent state for the CircularChartView 
     * Used for recreating the view after a configuration change.
     * 
     * Since views have a handle to the context, we cannot retain them
     * over configuration changes without leaking an activity. Using
     * getApplicationContext() is unsuitable for views, so the best
     * way is to save all of this state and bring it back.
     */
    protected static class PersistentState implements Serializable {
        private static final long serialVersionUID = -4238458940986924319L;

        /** Array of values meant to be used in a circular manner **/
        float[] values = new float[UNITS];

        /** Indicates whether values has been filled */
        boolean isFilled = false;

        /** Position in the values array */
        int pos;
    }

    private class CircularChartView extends FrameLayout {
        private static final String TAG = "CircularChartView";
        /** This object should be persisted outside of the view */
        private PersistentState state;

        private String name;

        boolean isSleeping = false;
        int width = 0, height = 0;
        int timeUnitWidth;

        // Paints
        Paint fgColor = new Paint();
        Paint fgTextColor = new Paint();
        Paint gridColor = new Paint();

        // only alloc paths once
        Path column = new Path();
        Path path = new Path();
        Path row = new Path();

        int lineWidth;

        public CircularChartView(Context context, Bundle args) {
            super(context);

            this.lineWidth = args.getInt(PARAM_STROKE_WIDTH);
            this.fgColor.setStrokeWidth(this.lineWidth);
            this.fgColor.setAntiAlias(args.getBoolean(PARAM_ANTI_ALIAS));
            this.fgColor.setColor(args.getInt(PARAM_FG_COLOR));
            this.fgColor.setStyle(Paint.Style.STROKE);

            this.fgTextColor.setColor(args.getInt(PARAM_TEXT_COLOR));
            this.fgTextColor.setTextSize(args.getInt(PARAM_TEXT_SIZE));
            this.fgTextColor.setAntiAlias(args.getBoolean(PARAM_ANTI_ALIAS));

            this.gridColor.setColor(args.getInt(PARAM_GRID_COLOR));
            this.gridColor.setStrokeWidth(args.getInt(PARAM_GRID_THICKNESS));
            this.gridColor.setStyle(Paint.Style.STROKE);

            this.state = new PersistentState();
        }

        public void setName(String name) {
            this.name = name;
        }

        public void restoreState(PersistentState inState) {
            this.state = inState;
        }

        public void setSleeping(boolean isSleeping) {
            this.isSleeping = isSleeping;
        }

        public PersistentState getPersistentState() {
            return this.state;
        }

        /**
         * Sets the next value to draw. Does so in a circular way.
         * @param nextval
         */
        public void setNextValue(float nextval) {
            this.state.values[this.state.pos++] = nextval;
            if (this.state.pos == UNITS) {
                this.state.isFilled = true;
                this.state.pos = 0;
            }
            invalidate();
        }

        /** Reset meter state */
        public void reset() {
            this.state = new PersistentState();
        }

        @Override
        protected void onDraw(Canvas canvas) {

            float textSize = this.fgTextColor.getTextSize();

            if (!this.isSleeping) {
                float t = 0;
                int fpos = this.state.pos - 2;

                // If the state is filled, and fpos is less than zero, then start
                // at the far end of the buffer. If not filled, this would have
                // been unitialized. That's why the isFilled check is there.
                if (this.state.isFilled && fpos < 0) {
                    fpos = UNITS + fpos;
                } else if (!this.state.isFilled && fpos < 0) {
                    return; // Nothing to draw yet.
                }

                path.rewind();
                path.incReserve(UNITS + 1);
                path.moveTo(t, height * (1 - this.state.values[fpos]));

                column.rewind();
                column.moveTo(0, 0);
                column.lineTo(0, height);

                row.rewind();
                row.moveTo(0, 0);
                row.lineTo(width, 0);

                t += this.timeUnitWidth;

                float adjustedHeight = (height - this.lineWidth * 2);
                for (int i = this.state.pos - 1; i >= 0; i--) {
                    float y = adjustedHeight * (1 - this.state.values[i]);
                    float x = t;
                    t += this.timeUnitWidth;
                    path.lineTo(x, y);
                }

                for (int i = UNITS - 2; this.state.isFilled && i >= this.state.pos; i--) {
                    float y = adjustedHeight * (1 - this.state.values[i]);
                    float x = t;
                    t += this.timeUnitWidth;
                    path.lineTo(x, y);
                }

                // draw the last segment
                int lvpos = this.state.pos - 1;
                if (lvpos < 0) {
                    lvpos += UNITS;
                }

                float y = adjustedHeight * (1 - this.state.values[lvpos]);
                path.lineTo(t, y);

                // Draw grid cols
                int totalColumns = UNITS / UNITS_PER_COLUMN;
                for (int i = 0; i < totalColumns; i++) {
                    canvas.drawPath(column, this.gridColor);
                    column.offset(UNITS_PER_COLUMN * this.timeUnitWidth, 0);
                }

                // Draw grid rows
                float rowHeight = this.height / ((float) ROWS);
                for (int i = 0; i < ROWS; i++) {
                    canvas.drawPath(row, this.gridColor);
                    row.offset(0, rowHeight);
                }

                // Draw trend line
                canvas.drawPath(path, this.fgColor);

                // Draw usage text
                canvas.drawText(Float.toString((float) Math.round(this.state.values[lvpos] * 1000) / 10) + "%", 0,
                        textSize + 2, this.fgTextColor);

                if (this.name != null) {
                    float nameWidth = 0;
                    float[] widths = new float[this.name.length()];
                    this.fgTextColor.getTextWidths(this.name, widths);

                    for (int i = 0; i < widths.length; i++) {
                        nameWidth += widths[i];
                    }

                    // Draw chart name
                    canvas.drawText(this.name, this.width - nameWidth, textSize + 2, this.fgTextColor);

                }
            } else if (this.name != null) {
                String message = this.name + " is sleeping.";
                float messageWidth = 0;

                float[] widths = new float[message.length()];
                this.fgTextColor.getTextWidths(message, widths);
                for (int i = 0; i < widths.length; i++) {
                    messageWidth += widths[i];
                }

                canvas.drawText(message, this.width / 2 - messageWidth / 2, this.height / 2 + textSize / 2,
                        this.fgTextColor);
            }
        }

        @Override
        protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
            Loggy.d(TAG, "onSizeChanged: width=%d height=%d", getWidth(), getHeight());

            this.width = getWidth();
            this.height = getHeight();

            // One unit of space per second.
            this.timeUnitWidth = this.width / UNITS;
        }
    }
}