com.ruesga.rview.widget.ActivityStatsChart.java Source code

Java tutorial

Introduction

Here is the source code for com.ruesga.rview.widget.ActivityStatsChart.java

Source

/*
 * Copyright (C) 2016 Jorge Ruesga
 *
 * 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.ruesga.rview.widget;

import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.AsyncTask;
import android.support.v4.view.ViewCompat;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.AccelerateInterpolator;

import com.ruesga.rview.R;
import com.ruesga.rview.model.Stats;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;

public class ActivityStatsChart extends View {

    private class AggregateStatsTask extends AsyncTask<Void, Void, Void> {
        private final List<Stats> mStats;

        AggregateStatsTask(List<Stats> stats) {
            mStats = stats;
        }

        @Override
        protected Void doInBackground(Void... params) {
            updateView(aggregateStats(mStats));
            return null;
        }

        @Override
        protected void onPostExecute(Void v) {
            // Refresh the view
            animateChart();
        }

        @SuppressLint("UseSparseArrays")
        private Map<Long, Integer> aggregateStats(List<Stats> stats) {
            Map<Long, Integer> aggregatedStats = new TreeMap<>();
            Calendar e = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
            truncateCalendar(e);
            Calendar s = (Calendar) e.clone();
            s.add(Calendar.DAY_OF_YEAR, (MAX_DAYS - 1) * -1);

            while (s.compareTo(e) <= 0) {
                aggregatedStats.put(s.getTimeInMillis(), 0);
                s.add(Calendar.DAY_OF_YEAR, 1);
            }

            Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
            for (Stats stat : stats) {
                c.setTimeInMillis(stat.mDate.getTime());
                truncateCalendar(c);

                // If the event is not in our map just ignore it
                long timestamp = c.getTimeInMillis();
                if (aggregatedStats.containsKey(timestamp)) {
                    aggregatedStats.put(timestamp, aggregatedStats.get(timestamp) + 1);
                }
            }

            return aggregatedStats;
        }

        private void truncateCalendar(Calendar c) {
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
        }

        private void updateView(Map<Long, Integer> aggregatedStats) {
            float[] data = new float[MAX_DAYS];
            float max = 0f;
            float min = -1f;
            int i = 0;
            for (Long key : aggregatedStats.keySet()) {
                data[i] = aggregatedStats.get(key);
                i++;
            }
            for (float v : data) {
                max = Math.max(max, v);
                if (min == -1) {
                    min = v;
                } else {
                    min = Math.min(min, v);
                }
            }

            // Swap the data
            synchronized (mLock) {
                mData = data;
                mMinVal = Math.abs(min);
                mMaxVal = max;
                computeDrawObjects();
            }
        }
    }

    private static final int MAX_Y_TICKS_LABELS = 5;
    private static final int MAX_DAYS = 30;

    private float[] mData;
    private final String[] mYTicksLabels = new String[MAX_Y_TICKS_LABELS];
    private float mMinVal, mMaxVal;
    private boolean mDataAvailable;

    private final RectF mViewArea = new RectF();
    private final PointF mPoint = new PointF();

    private final Path mLinePath = new Path();
    private final Path mAreaPath = new Path();

    private final Object mLock = new Object();

    private final Paint mLinePaint;
    private final Paint mAreaPaint;
    private final Paint mGridLinesPaint;
    private final TextPaint mTicksPaint;

    private final DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.getDefault());
    private final DecimalFormat mYTicksFormatter = new DecimalFormat("#0", symbols);
    private final DecimalFormat mYTicksDecFormatter = new DecimalFormat("#0.00", symbols);

    private ValueAnimator mAnimator;
    private float mAnimationDelta = 0f;

    private AggregateStatsTask mTask;

    public ActivityStatsChart(Context context) {
        this(context, null);
    }

    public ActivityStatsChart(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ActivityStatsChart(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        final Resources r = getResources();
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        int color = Color.DKGRAY;
        int textColor = Color.WHITE;

        Resources.Theme theme = context.getTheme();
        TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.ActivityStatsChart, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
            case R.styleable.ActivityStatsChart_charLineColor:
                color = a.getColor(attr, color);
                break;

            case R.styleable.ActivityStatsChart_charLineTextColor:
                textColor = a.getColor(attr, textColor);
                break;
            }
        }
        a.recycle();

        mLinePaint = new Paint();
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setColor(color);
        mLinePaint.setStrokeWidth(
                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.5f, r.getDisplayMetrics()));

        mAreaPaint = new Paint(mLinePaint);
        mAreaPaint.setStyle(Paint.Style.FILL);
        mAreaPaint.setAlpha(180);

        mGridLinesPaint = new Paint();
        mGridLinesPaint.setStyle(Paint.Style.STROKE);
        mGridLinesPaint.setColor(textColor);
        mGridLinesPaint.setStrokeWidth(
                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.5f, r.getDisplayMetrics()));
        mGridLinesPaint.setAlpha(90);
        mGridLinesPaint.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 0));

        mTicksPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);
        mTicksPaint.setColor(textColor);
        mTicksPaint.setTextAlign(Paint.Align.RIGHT);
        mTicksPaint.setAlpha(180);
        mTicksPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 8f, r.getDisplayMetrics()));

        // Ensure we have a background. Otherwise it will not draw anything
        if (getBackground() == null) {
            setBackgroundColor(Color.TRANSPARENT);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        if (mTask != null) {
            mTask.cancel(true);
        }

        if (mAnimator != null && mAnimator.isRunning()) {
            mAnimator.cancel();
        }
    }

    public void update(List<Stats> stats) {
        if (mTask != null) {
            mTask.cancel(true);
        }

        mTask = new AggregateStatsTask(stats);
        mTask.execute();
    }

    private void computeDrawObjects() {
        final float steps = (mMaxVal - mMinVal) * 0.1f;
        final float min = Math.max(0, mMinVal - steps);
        final float max = mMaxVal + steps;

        // Choose the right formatter based on the max/min diff
        final DecimalFormat formatter = (max - min) < (MAX_Y_TICKS_LABELS * 2) ? mYTicksDecFormatter
                : mYTicksFormatter;

        mLinePath.reset();
        mAreaPath.reset();
        if (mData != null && mData.length > 0) {
            for (int i = 0; i < mData.length; i++) {
                fillPointFromValue(i, mData[i], min, max);
                if (i == 0) {
                    mLinePath.moveTo(mPoint.x, mPoint.y);
                    mAreaPath.moveTo(mPoint.x, mPoint.y);
                } else {
                    mLinePath.lineTo(mPoint.x, mPoint.y);
                    mAreaPath.lineTo(mPoint.x, mPoint.y);
                }
            }
            mAreaPath.lineTo(mViewArea.right, mViewArea.bottom);
            mAreaPath.lineTo(mViewArea.left, mViewArea.bottom);
            mAreaPath.close();

            float gap = mViewArea.height() / (MAX_Y_TICKS_LABELS * 1f);
            for (int i = 0; i < MAX_Y_TICKS_LABELS; i++) {
                float v = Math.max(max - ((max - min) * (gap * (i + 1))) / mViewArea.height(), 0);
                mYTicksLabels[i] = formatter.format(Math.abs(v));
            }

            mDataAvailable = true;
        } else {
            float y = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f,
                    getResources().getDisplayMetrics());
            mLinePath.moveTo(mViewArea.left, mViewArea.bottom - y);
            mLinePath.lineTo(mViewArea.right, mViewArea.bottom - y);
            mAreaPath.addRect(mViewArea.left, mViewArea.bottom - y, mViewArea.right, mViewArea.bottom,
                    Path.Direction.CCW);

            mDataAvailable = false;
        }
    }

    private void fillPointFromValue(int pos, float value, float min, float max) {
        mPoint.x = mViewArea.left + ((mViewArea.width() / (mData.length - 1)) * pos);
        mPoint.y = mViewArea.bottom - (((value - min) * mViewArea.height()) / (max - min));
        mPoint.y = Math.max(mViewArea.bottom - (mViewArea.height() * mAnimationDelta), mPoint.y);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // Draw the grid lines
        float gap = mViewArea.height() / (MAX_Y_TICKS_LABELS * 1f);
        float y = mViewArea.top + gap;
        for (int i = 0; i < MAX_Y_TICKS_LABELS; i++) {
            canvas.drawLine(mViewArea.left, y, mViewArea.right, y, mGridLinesPaint);
            y += gap;
        }

        // Draw the points data
        canvas.drawPath(mAreaPath, mAreaPaint);
        canvas.drawPath(mLinePath, mLinePaint);

        if (mDataAvailable) {
            // Draw ticks labels
            float margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f,
                    getResources().getDisplayMetrics());
            y = mViewArea.top + gap;
            for (int i = 0; i < MAX_Y_TICKS_LABELS; i++) {
                canvas.drawText(mYTicksLabels[i], mViewArea.right - margin, y - margin, mTicksPaint);
                y += gap;
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        // View area
        mViewArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
                getHeight() - getPaddingBottom());

        // Create the drawing objects
        synchronized (mLock) {
            computeDrawObjects();
        }
    }

    private void animateChart() {
        // Animate the chart
        if (mAnimator != null && mAnimator.isRunning()) {
            mAnimator.cancel();
        }
        mAnimationDelta = 0f;
        mAnimator = ValueAnimator.ofFloat(0f, 1f);
        mAnimator.setInterpolator(new AccelerateInterpolator());
        mAnimator.setDuration(350L);
        mAnimator.addUpdateListener(animation -> {
            mAnimationDelta = animation.getAnimatedFraction();
            computeDrawObjects();
            ViewCompat.postInvalidateOnAnimation(this);
        });
        mAnimator.start();
    }
}