com.blestep.sportsbracelet.view.TimelineChartView.java Source code

Java tutorial

Introduction

Here is the source code for com.blestep.sportsbracelet.view.TimelineChartView.java

Source

/*
 * Copyright (C) 2015 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.blestep.sportsbracelet.view;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.OverScroller;

import com.blestep.sportsbracelet.R;

public class TimelineChartView extends View {

    private static final String TAG = "TimelineChartView";

    /**
     * A class that represents a item information.
     */
    public static class Item {
        private Item() {
        }

        public String label;
        public double stepCount;
        public double stepDuration;
        public double stepDistance;
        public double stepCalorie;
    }

    /**
     * An interface definition to notify item selection event.
     */
    public interface OnSelectedItemChangedListener {
        /**
         * Called when a item was selected.
         *
         * @param selectedItem information about the selected item
         */
        void onSelectedItemChanged(Item selectedItem);

        /**
         * Called when there is no selection.
         */
        // void onNothingSelected();
    }

    private Cursor mCursor;
    private SparseArray<Object[]> mData = new SparseArray<>();
    private double mMaxValue;
    private double mTargetValue;
    private final Item mItem = new Item();

    private final RectF mViewArea = new RectF();
    private final RectF mGraphArea = new RectF();
    private final RectF mFooterArea = new RectF();
    private float mFooterBarHeight;

    private float mBarItemWidth;
    private float mBarItemSpace;
    private float mBarWidth;
    private float mTopSpaceHeight;
    private Shader mViewAreaBgShader;

    private Paint mViewAreaBgPaint;
    private Paint mGraphAreaBgPaint;
    private Paint mFooterAreaBgPaint;
    private Paint mGraphBottomLinePaint;

    private boolean mIsShowTargetDashedLine;
    private Paint mGraphTargetDashedLinePaint;

    private Paint mBarItemBgPaint;
    private Paint mHighlightBarItemBgPaint;
    private TextPaint mLabelFgPaint;
    private TextPaint mHighlightLabelFgPaint;

    private int mCurrentPosition = -1;
    private long mLastPosition = -1;
    private float mCurrentOffset = 0.f;
    private float mLastOffset = -1.f;
    private float mMaxOffset = 0.f;
    private float mInitialTouchOffset = 0.f;
    private float mInitialTouchX = 0.f;
    private float mInitialTouchY = 0.f;

    private int mMaxBarItemsInScreen = 0;
    private final int[] mItemsOnScreen = new int[2];
    /**
     * footer
     */
    private float mTickLabelMinHeight;

    private VelocityTracker mVelocityTracker;
    private OverScroller mScroller;
    /**
     * ?
     */
    private float mTouchSlop;
    /**
     * 
     */
    private float mMaxFlingVelocity;

    private static final int STATE_IDLE = 0;
    private static final int STATE_INITIALIZE = 1;
    private static final int STATE_MOVING = 2;
    private static final int STATE_FLINGING = 3;
    private static final int STATE_SCROLLING = 4;
    private int mState = STATE_IDLE;

    private static final int MSG_ON_SELECTION_ITEM_CHANGED = 1;
    private static final int MSG_COMPUTE_DATA = 4;
    private static final int MSG_UPDATE_COMPUTED_DATA = 5;

    private Handler mUiHandler;
    private Handler mBackgroundHandler;
    private HandlerThread mBackgroundHandlerThread;

    private final Handler.Callback mMessenger = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
            // Ui thread
            case MSG_ON_SELECTION_ITEM_CHANGED:
                notifyOnSelectionItemChanged();
                return true;
            case MSG_UPDATE_COMPUTED_DATA:

                // Redraw the data and notify the changes
                notifyOnSelectionItemChanged();

                // Update the graph view
                ViewCompat.postInvalidateOnAnimation(TimelineChartView.this);
                return true;

            // Non-Ui thread
            case MSG_COMPUTE_DATA:
                performComputeData();
                return true;
            }
            return false;
        }
    };

    private OnSelectedItemChangedListener mOnSelectedItemChangedCallback;

    private final Object mLock = new Object();

    public TimelineChartView(Context ctx) {
        this(ctx, null, 0);
    }

    public TimelineChartView(Context ctx, AttributeSet attrs) {
        this(ctx, attrs, 0);
    }

    public TimelineChartView(Context ctx, AttributeSet attrs, int defStyleAttr) {
        super(ctx, attrs, defStyleAttr);
        init(ctx, attrs, defStyleAttr);
    }

    private void init(Context ctx, AttributeSet attrs, int defStyleAttr) {
        mUiHandler = new Handler(Looper.getMainLooper(), mMessenger);

        final Resources res = getResources();

        final ViewConfiguration vc = ViewConfiguration.get(ctx);
        mTouchSlop = vc.getScaledTouchSlop() / 2;
        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
        mScroller = new OverScroller(ctx);

        int footerLabelColor = ContextCompat.getColor(getContext(), R.color.tlcStepsFooterLabelColor);
        int barItemBg = ContextCompat.getColor(getContext(), R.color.tlcStepsBarItemBg);
        int highlightBarItemBg = ContextCompat.getColor(getContext(), R.color.tlcStepsHighlightBarItemBg);

        mBarItemBgPaint = new Paint();
        mBarItemBgPaint.setColor(barItemBg);
        mHighlightBarItemBgPaint = new Paint();
        mHighlightBarItemBgPaint.setColor(highlightBarItemBg);

        mFooterBarHeight = res.getDimension(R.dimen.tlcDefFooterBarHeight);

        mViewAreaBgPaint = new Paint();

        mGraphAreaBgPaint = new Paint();
        mGraphAreaBgPaint.setColor(Color.TRANSPARENT);

        mFooterAreaBgPaint = new Paint();
        mFooterAreaBgPaint.setColor(Color.TRANSPARENT);

        mGraphBottomLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mGraphBottomLinePaint.setColor(Color.WHITE);
        mGraphBottomLinePaint.setStrokeWidth(1);
        // 
        mGraphTargetDashedLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mGraphTargetDashedLinePaint.setStyle(Paint.Style.STROKE);
        mGraphTargetDashedLinePaint.setColor(Color.WHITE);
        mGraphTargetDashedLinePaint.setStrokeWidth(1);
        PathEffect pathEffect = new DashPathEffect(new float[] { 9, 3 }, 1);
        mGraphTargetDashedLinePaint.setPathEffect(pathEffect);

        // labelPaint??
        mLabelFgPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);
        mLabelFgPaint.setColor(footerLabelColor);
        //        DisplayMetrics dp = getResources().getDisplayMetrics();
        //        float labelSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, res.getDimension(R.dimen.tlcDefFooterLabelSize), dp);
        mLabelFgPaint.setTextSize(res.getDimension(R.dimen.tlcDefFooterLabelSize));

        mHighlightLabelFgPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);
        mHighlightLabelFgPaint.setColor(Color.WHITE);
        mHighlightLabelFgPaint.setTextSize(res.getDimension(R.dimen.tlcDefFooterLabelSize));

        mBarItemWidth = res.getDimension(R.dimen.tlcDefBarItemWidth);
        mBarItemSpace = res.getDimension(R.dimen.tlcDefBarItemSpace);
        mTopSpaceHeight = res.getDimension(R.dimen.tlcDefTopSpace);

        // SurfaceView requires a background
        if (getBackground() == null) {
            setBackgroundColor(ContextCompat.getColor(getContext(), android.R.color.transparent));
        }

        // Initialize stuff
        setupBackgroundHandler();
        setupTickLabels();

        // Initialize the drawing refs (this will be update when we have
        // the real size of the canvas)
        computeBoundAreas();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        setupBackgroundHandler();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        // Destroy background thread
        mBackgroundHandlerThread.quit();
        mBackgroundHandler = null;
        mBackgroundHandlerThread = null;

        // Destroy cursor
        releaseCursor();

        // Destroy internal tracking variables
        clear();
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }

    /**
     * Returns the height in pixels of the footer area.
     */
    //    public float getFooterBarHeight() {
    //        return mFooterBarHeight;
    //    }

    /**
     * Sets the height in pixels of the footer area.
     */
    //    public void setFooterHeight(float height) {
    //        if (mFooterBarHeight != height) {
    //            mFooterBarHeight = height;
    //            computeBoundAreas();
    //            setupTickLabels();
    //            requestLayout();
    //            ViewCompat.postInvalidateOnAnimation(this);
    //        }
    //    }

    /**
     * Returns the space in pixels between bar items.
     */
    public float getBarItemSpace() {
        return mBarItemSpace;
    }

    /**
     * Sets the space in pixels between bar items.
     */
    public void setBarItemSpace(float barItemSpace) {
        if (mBarItemSpace != barItemSpace) {
            mBarItemSpace = barItemSpace;
            computeMaxBarItemsInScreen();
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /**
     * Returns the width in pixels of a bar item.
     */
    public float getBarItemWidth() {
        return mBarItemWidth;
    }

    /**
     * Sets the width in pixels of a bar item.
     */
    public void setBarItemWidth(float barItemWidth) {
        if (mBarItemWidth != barItemWidth) {
            mBarItemWidth = barItemWidth;
            computeBoundAreas();
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /**
     * ?
     *
     * @param mIsShowTargetDashedLine
     */
    public void setIsShowTargetDashedLine(boolean mIsShowTargetDashedLine) {
        this.mIsShowTargetDashedLine = mIsShowTargetDashedLine;
    }

    /**
     * 
     *
     * @param mTargetValue
     */
    public void setTargetValue(double mTargetValue) {
        this.mTargetValue = mTargetValue;
    }

    public void addOnSelectedItemChangedListener(OnSelectedItemChangedListener cb) {
        mOnSelectedItemChangedCallback = cb;
    }

    /**
     * Registers the cursor and start observing changes on it.
     * <p/>
     * The cursor <i>MUST</i> follow the next constrains:
     * <ul>
     * <li>The first field must contains a timestamp, which represent
     * a time in the graph timeline. This value will be the key to access to
     * the graph information.</li>
     * <li>One or more float/double numeric in the rest of the fields of
     * the cursor. Every one of this fields will represent a serie in the
     * graph.</li>
     * </ul>
     *
     * @param c the cursor to observe.
     */
    public void observeData(Cursor c) {
        releaseCursor();

        // Ensure we have a valid handler (if for some reason view wasn't attached yet)
        setupBackgroundHandler();

        // Save the cursor reference and listen for changes
        mCursor = c;
        reloadCursorData();
    }

    @Override
    public boolean canScrollHorizontally(int direction) {
        final float x = mScroller.getCurrX();
        return (direction < 0 && x < mMaxOffset) || (direction > 0 && x > 0);
    }

    @Override
    public boolean canScrollVertically(int direction) {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onTouchEvent(final MotionEvent event) {

        final int action = event.getActionMasked();
        final int index = event.getActionIndex();
        final int pointerId = event.getPointerId(index);

        switch (action) {
        case MotionEvent.ACTION_DOWN:
            // Initialize velocity tracker
            if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
            } else {
                mVelocityTracker.clear();
            }
            mVelocityTracker.addMovement(event);
            mScroller.forceFinished(true);
            mState = STATE_INITIALIZE;

            mInitialTouchOffset = mCurrentOffset;
            mInitialTouchX = event.getX();
            mInitialTouchY = event.getY();
            return true;

        case MotionEvent.ACTION_MOVE:
            mVelocityTracker.addMovement(event);
            float diffX = event.getX() - mInitialTouchX;
            float diffY = event.getY() - mInitialTouchY;
            if (Math.abs(diffX) > mTouchSlop || mState >= STATE_MOVING) {
                mCurrentOffset = mInitialTouchOffset + diffX;
                if (mCurrentOffset < 0) {
                    onOverScroll();
                    mCurrentOffset = 0;
                } else if (mCurrentOffset > mMaxOffset) {
                    onOverScroll();
                    mCurrentOffset = mMaxOffset;
                }
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                mState = STATE_MOVING;
                ViewCompat.postInvalidateOnAnimation(this);
            } else if (Math.abs(diffY) > mTouchSlop && mState < STATE_MOVING) {
                return false;
            }
            return true;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            if (mState >= STATE_MOVING) {
                final int velocity = (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, pointerId);
                mScroller.forceFinished(true);
                mState = STATE_FLINGING;
                mScroller.fling((int) mCurrentOffset, 0, velocity, 0, 0, (int) mMaxOffset, 0, 0);
                ViewCompat.postInvalidateOnAnimation(this);
            } else {
                // Reset scrolling state
                mState = STATE_IDLE;
            }
            return true;
        }
        return false;
    }

    private void onOverScroll() {
        final boolean needOverScroll;
        synchronized (mLock) {
            needOverScroll = mData.size() >= Math.floor(mMaxBarItemsInScreen / 2);
        }
        final int overScrollMode = ViewCompat.getOverScrollMode(this);
        if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS
                || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && needOverScroll)) {
            boolean needsInvalidate = false;
            if (mCurrentOffset > mMaxOffset) {
                needsInvalidate = true;
            }
            if (mCurrentOffset < 0) {
                needsInvalidate = true;
            }

            if (needsInvalidate) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void computeScroll() {
        super.computeScroll();

        // Determine whether we still scrolling and needs a viewport refresh
        final boolean scrolling = mScroller.computeScrollOffset();
        if (scrolling) {
            float x = mScroller.getCurrX();
            if (x > mMaxOffset || x < 0) {
                return;
            }
            mCurrentOffset = x;
            ViewCompat.postInvalidateOnAnimation(this);
        } else if (mState > STATE_MOVING) {
            boolean needsInvalidate = false;
            final boolean needOverScroll;
            synchronized (mLock) {
                needOverScroll = mData.size() >= Math.floor(mMaxBarItemsInScreen / 2);
            }
            final int overScrollMode = ViewCompat.getOverScrollMode(this);
            if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS
                    || (needOverScroll && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS)) {
                float x = mScroller.getCurrX();
                if (x >= mMaxOffset) {
                    needsInvalidate = true;
                }
                if (x < 0) {
                    needsInvalidate = true;
                }
            }
            if (!needsInvalidate) {
                // Reset state
                mState = STATE_IDLE;
                mLastPosition = -1;
            } else {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }

        int position = computeNearestPositionFromOffset(mCurrentOffset);

        // If we are not centered in a item, perform an scroll
        if (mState == STATE_IDLE) {
            smoothScrollTo(position);
        }

        if (mCurrentPosition != position) {
            // Don't perform selection operations while we are just scrolling
            if (mState != STATE_SCROLLING) {
                mCurrentPosition = position;

                // Notify any valid item, but only notify invalid items if
                // we are not panning/scrolling
                if (mCurrentPosition >= 0 || !scrolling) {
                    Message.obtain(mUiHandler, MSG_ON_SELECTION_ITEM_CHANGED).sendToTarget();
                }
            }
        }
    }

    /**
     * Performs a smooth transition of the current viewport of this view to
     * the timestamp passed as argument. If timestamp doesn't exists no
     * operation will be performed.
     */
    public void smoothScrollTo(int position) {

        final float offset = computeOffsetForPosition(position);
        if (offset >= 0 && offset != mCurrentOffset) {
            int dx = (int) (mCurrentOffset - offset) * -1;
            mScroller.forceFinished(true);
            mState = STATE_SCROLLING;
            mLastPosition = mCurrentPosition;
            mScroller.startScroll((int) mCurrentOffset, 0, dx, 0);
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private float computeOffsetForPosition(int position) {
        final SparseArray<Object[]> data;
        synchronized (mLock) {
            data = mData;
        }
        final int index = data.indexOfKey(position);
        if (index >= 0) {
            final int size = data.size();
            return (mBarWidth * (size - index - 1));
        }
        return -1;
    }

    private int computeNearestPositionFromOffset(float offset) {
        final SparseArray<Object[]> data;
        synchronized (mLock) {
            data = mData;
        }
        int size = data.size() - 1;
        if (size < 0) {
            return -1;
        }

        // So we are in an bar area, so we have a valid index
        final int index = size - ((int) Math.ceil((offset - (mBarItemWidth / 2) - mBarItemSpace) / mBarWidth));
        return data.keyAt(index);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onDraw(Canvas c) {
        // 
        c.clipRect(mViewArea);
        mViewAreaBgShader = new LinearGradient(0, 0, 0, mViewArea.height(),
                ContextCompat.getColor(getContext(), R.color.tlcStepsGraphBgColorStart),
                ContextCompat.getColor(getContext(), R.color.tlcStepsGraphBgColorEnd), Shader.TileMode.CLAMP);
        mViewAreaBgPaint.setShader(mViewAreaBgShader);
        // 
        c.drawRect(mViewArea, mViewAreaBgPaint);
        c.drawRect(mGraphArea, mGraphAreaBgPaint);
        c.drawRect(mFooterArea, mFooterAreaBgPaint);
        // 
        c.drawLine(0, mGraphArea.height(), mGraphArea.width(), mGraphArea.height(), mGraphBottomLinePaint);

        final SparseArray<Object[]> data;
        final double maxValue;
        synchronized (mLock) {
            data = mData;
            if (mIsShowTargetDashedLine && mTargetValue > mMaxValue) {
                maxValue = mTargetValue;
            } else {
                maxValue = mMaxValue;
            }
        }
        boolean hasData = data.size() > 0;
        if (hasData) {

            // ???
            computeItemsOnScreen(data.size());
            // 
            drawTargetLine(c);
            // 
            drawBarItems(c, data, maxValue);
            // 
            drawTickLabels(c, data);

        }
    }

    private void drawTargetLine(Canvas c) {
        if (mIsShowTargetDashedLine && mTargetValue > 0) {
            if (mTargetValue > mMaxValue) {
                Path path = new Path();
                path.moveTo(0, mTopSpaceHeight);
                path.lineTo(mGraphArea.width(), mTopSpaceHeight);
                c.drawPath(path, mGraphTargetDashedLinePaint);
            } else {
                float height = mGraphArea.height();
                float y = (float) (height - (height * ((mTargetValue * 100) / mMaxValue)) / 100) + mTopSpaceHeight;
                Path path = new Path();
                path.moveTo(0, y);
                path.lineTo(mGraphArea.width(), y);
                c.drawPath(path, mGraphTargetDashedLinePaint);
            }
        }
    }

    private void drawBarItems(Canvas c, SparseArray<Object[]> data, double maxValue) {

        final float halfItemBarWidth = mBarItemWidth / 2;
        final float height = mGraphArea.height();
        final Paint seriesBgPaint;
        final Paint highlightSeriesBgPaint;
        synchronized (mLock) {
            seriesBgPaint = mBarItemBgPaint;
            highlightSeriesBgPaint = mHighlightBarItemBgPaint;
        }

        // Apply zoom animation
        final float graphCenterX = mGraphArea.left + (mGraphArea.width() / 2);

        final int size = data.size() - 1;
        for (int i = mItemsOnScreen[1]; i >= mItemsOnScreen[0] && i <= data.size(); i--) {
            final float barCenterX = graphCenterX + mCurrentOffset - (mBarWidth * (size - i));
            // ?
            double value = (double) data.valueAt(i)[1];
            float barTop = (float) (height - ((height * ((value * 100) / maxValue)) / 100)) + mTopSpaceHeight;
            float barBottom = height;
            float barLeft = barCenterX - halfItemBarWidth;
            float barRight = barCenterX + halfItemBarWidth;
            final Paint paint;
            // ?
            paint = barLeft < graphCenterX && barRight > graphCenterX
                    && (mLastPosition == mCurrentPosition || (mState != STATE_SCROLLING)) ? highlightSeriesBgPaint
                            : seriesBgPaint;
            // 
            c.drawRect(barLeft, mGraphArea.top + barTop, barRight, mGraphArea.top + barBottom, paint);
        }
    }

    private void drawTickLabels(Canvas c, SparseArray<Object[]> data) {
        final int size = data.size() - 1;
        final float graphCenterX = mGraphArea.left + (mGraphArea.width() / 2);
        final float halfItemBarWidth = mBarItemWidth / 2;
        for (int i = mItemsOnScreen[1]; i >= mItemsOnScreen[0] && i <= data.size(); i--) {
            final float barCenterX = graphCenterX + mCurrentOffset - (mBarWidth * (size - i));
            float barLeft = barCenterX - halfItemBarWidth;
            float barRight = barCenterX + halfItemBarWidth;
            // Update the dynamic layout
            String label = (String) data.valueAt(i)[0];
            // 
            // Calculate the x position and draw the layout
            final float x = graphCenterX + mCurrentOffset - (mBarWidth * (size - i))
                    - (mLabelFgPaint.measureText(label) / 2);
            final int restoreCount = c.save();
            c.translate(x, mFooterArea.top + (mFooterArea.height() / 2 - mTickLabelMinHeight / 2));
            final Paint paint;
            // ?
            paint = barLeft < graphCenterX && barRight > graphCenterX
                    && (mLastPosition == mCurrentPosition || (mState != STATE_SCROLLING)) ? mHighlightLabelFgPaint
                            : mLabelFgPaint;
            c.drawText(label, 0, 0, paint);
            c.restoreToCount(restoreCount);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mViewArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
                getHeight() - getPaddingBottom());
        computeBoundAreas();
    }

    private void computeItemsOnScreen(int Datasize) {
        if (mLastOffset == mCurrentOffset) {
            return;
        }
        int size = Datasize - 1;
        float offset = mCurrentOffset + (mBarItemWidth / 2);
        int last = size - (int) Math.floor(offset / mBarWidth) + (int) Math.ceil(mMaxBarItemsInScreen / 2);
        int rest = 0;
        if (last > size) {
            rest = last - size;
            last = size;
        }
        int first = last - (mMaxBarItemsInScreen - 1) + rest;
        if (first < 0) {
            first = 0;
        }

        // Save the item positions
        mItemsOnScreen[0] = first;
        mItemsOnScreen[1] = last;
        mLastOffset = mCurrentOffset;
    }

    private void computeBoundAreas() {

        mGraphArea.set(mViewArea);
        mGraphArea.bottom = Math.max(mViewArea.bottom - mFooterBarHeight, 0);
        mFooterArea.set(mViewArea);
        mFooterArea.top = mGraphArea.bottom;
        mFooterArea.bottom = mGraphArea.bottom + mFooterBarHeight;

        // Compute max bar items here too
        computeMaxBarItemsInScreen();
    }

    private void computeMaxBarItemsInScreen() {
        mBarWidth = mBarItemWidth + mBarItemSpace;
        mMaxBarItemsInScreen = (int) Math.ceil(mGraphArea.width() / mBarWidth) + 2;
    }

    private synchronized void setupBackgroundHandler() {
        if (mBackgroundHandler == null) {
            // Create a background thread
            mBackgroundHandlerThread = new HandlerThread(TAG + "BackgroundThread");
            mBackgroundHandlerThread.start();
            mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper(), mMessenger);
        }
    }

    @SuppressWarnings("unchecked")
    private void setupTickLabels() {
        synchronized (mLock) {
            mTickLabelMinHeight = mLabelFgPaint.descent() + mLabelFgPaint.ascent();
        }
    }

    private void reloadCursorData() {
        Message.obtain(mBackgroundHandler, MSG_COMPUTE_DATA).sendToTarget();
    }

    private void performComputeData() {
        clear();
        // Process the data
        processData();

        // Swap temporary refs
        mScroller.forceFinished(true);
        mState = STATE_IDLE;

        // Update the view and notify
        Message.obtain(mUiHandler, MSG_UPDATE_COMPUTED_DATA).sendToTarget();

        // Update the graph view
        ViewCompat.postInvalidateOnAnimation(TimelineChartView.this);
    }

    private void processData() {
        if (mCursor != null && !mCursor.isClosed() && mCursor.moveToFirst()) {
            // Load the cursor to memory
            double max = 0d;
            final SparseArray<Object[]> data = new SparseArray<>(mCursor.getCount());

            do {
                int position = mCursor.getInt(0);
                String label = mCursor.getString(1);
                double stepCount = mCursor.getDouble(2);
                double stepDuration = mCursor.getDouble(3);
                double stepDistance = mCursor.getDouble(4);
                double stepCalorie = mCursor.getDouble(5);
                if (stepCount > max) {
                    max = stepCount;
                }
                Object[] values = new Object[] { label, stepCount, stepDuration, stepDistance, stepCalorie };
                data.put(position, values);
            } while (mCursor.moveToNext());

            // Calculate the max available offset
            int size = data.size() - 1;
            float maxOffset = mBarWidth * size;

            //swap data
            synchronized (mLock) {
                mData = data;
                mMaxValue = max;
                mMaxOffset = maxOffset;
                mLastOffset = -1.f;
                mCurrentPosition = size;
                mCurrentOffset = computeOffsetForPosition(mCurrentPosition);
                setupTickLabels();
            }
        }
    }

    private void clear() {
        synchronized (mLock) {
            mData.clear();
            mMaxValue = 0d;
            mCurrentPosition = -1;
            mCurrentOffset = 0;
        }
    }

    private void notifyOnSelectionItemChanged() {
        if (mOnSelectedItemChangedCallback == null) {
            return;
        }
        final Item item = obtainItem(mCurrentPosition);
        if (item == null) {
            // mOnSelectedItemChangedCallback.onNothingSelected();
        } else {
            mOnSelectedItemChangedCallback.onSelectedItemChanged(item);
        }
    }

    private Item obtainItem(int position) {
        final Object[] data;
        if (position == -1)
            return null;
        synchronized (mLock) {
            data = mData.get(position);
        }
        if (data == null) {
            return null;
        }

        // Compute item. Restore original sort before notify
        mItem.label = (String) data[0];
        mItem.stepCount = (double) data[1];
        mItem.stepDuration = (double) data[2];
        mItem.stepDistance = (double) data[3];
        mItem.stepCalorie = (double) data[4];
        return mItem;
    }

    private void releaseCursor() {
        if (mCursor != null) {
            if (!mCursor.isClosed()) {
                mCursor.close();
            }
            mCursor = null;
            if (mItem != null) {
                mItem.label = "";
                mItem.stepCount = 0;
                mItem.stepDuration = 0;
                mItem.stepDistance = 0;
                mItem.stepCalorie = 0;
            }
        }
    }
}