Home | History | Annotate | Download | only in interactivechart
      1 /*
      2  * Copyright 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.interactivechart;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.graphics.Canvas;
     22 import android.graphics.Paint;
     23 import android.graphics.Point;
     24 import android.graphics.PointF;
     25 import android.graphics.Rect;
     26 import android.graphics.RectF;
     27 import android.os.Parcel;
     28 import android.os.Parcelable;
     29 import android.support.v4.os.ParcelableCompat;
     30 import android.support.v4.os.ParcelableCompatCreatorCallbacks;
     31 import android.support.v4.view.GestureDetectorCompat;
     32 import android.support.v4.view.ViewCompat;
     33 import android.support.v4.widget.EdgeEffectCompat;
     34 import android.util.AttributeSet;
     35 import android.view.GestureDetector;
     36 import android.view.MotionEvent;
     37 import android.view.ScaleGestureDetector;
     38 import android.view.View;
     39 import android.widget.OverScroller;
     40 
     41 /**
     42  * A view representing a simple yet interactive line chart for the function <code>x^3 - x/4</code>.
     43  * <p>
     44  * This view isn't all that useful on its own; rather it serves as an example of how to correctly
     45  * implement these types of gestures to perform zooming and scrolling with interesting content
     46  * types.
     47  * <p>
     48  * The view is interactive in that it can be zoomed and panned using
     49  * typical <a href="http://developer.android.com/design/patterns/gestures.html">gestures</a> such
     50  * as double-touch, drag, pinch-open, and pinch-close. This is done using the
     51  * {@link ScaleGestureDetector}, {@link GestureDetector}, and {@link OverScroller} classes. Note
     52  * that the platform-provided view scrolling behavior (e.g. {@link View#scrollBy(int, int)} is NOT
     53  * used.
     54  * <p>
     55  * The view also demonstrates the correct use of
     56  * <a href="http://developer.android.com/design/style/touch-feedback.html">touch feedback</a> to
     57  * indicate to users that they've reached the content edges after a pan or fling gesture. This
     58  * is done using the {@link EdgeEffectCompat} class.
     59  * <p>
     60  * Finally, this class demonstrates the basics of creating a custom view, including support for
     61  * custom attributes (see the constructors), a simple implementation for
     62  * {@link #onMeasure(int, int)}, an implementation for {@link #onSaveInstanceState()} and a fairly
     63  * straightforward {@link Canvas}-based rendering implementation in
     64  * {@link #onDraw(android.graphics.Canvas)}.
     65  * <p>
     66  * Note that this view doesn't automatically support directional navigation or other accessibility
     67  * methods. Activities using this view should generally provide alternate navigation controls.
     68  * Activities using this view should also present an alternate, text-based representation of this
     69  * view's content for vision-impaired users.
     70  */
     71 public class InteractiveLineGraphView extends View {
     72     private static final String TAG = "InteractiveLineGraphView";
     73 
     74     /**
     75      * The number of individual points (samples) in the chart series to draw onscreen.
     76      */
     77     private static final int DRAW_STEPS = 30;
     78 
     79     /**
     80      * Initial fling velocity for pan operations, in screen widths (or heights) per second.
     81      *
     82      * @see #panLeft()
     83      * @see #panRight()
     84      * @see #panUp()
     85      * @see #panDown()
     86      */
     87     private static final float PAN_VELOCITY_FACTOR = 2f;
     88 
     89     /**
     90      * The scaling factor for a single zoom 'step'.
     91      *
     92      * @see #zoomIn()
     93      * @see #zoomOut()
     94      */
     95     private static final float ZOOM_AMOUNT = 0.25f;
     96 
     97     // Viewport extremes. See mCurrentViewport for a discussion of the viewport.
     98     private static final float AXIS_X_MIN = -1f;
     99     private static final float AXIS_X_MAX = 1f;
    100     private static final float AXIS_Y_MIN = -1f;
    101     private static final float AXIS_Y_MAX = 1f;
    102 
    103     /**
    104      * The current viewport. This rectangle represents the currently visible chart domain
    105      * and range. The currently visible chart X values are from this rectangle's left to its right.
    106      * The currently visible chart Y values are from this rectangle's top to its bottom.
    107      * <p>
    108      * Note that this rectangle's top is actually the smaller Y value, and its bottom is the larger
    109      * Y value. Since the chart is drawn onscreen in such a way that chart Y values increase
    110      * towards the top of the screen (decreasing pixel Y positions), this rectangle's "top" is drawn
    111      * above this rectangle's "bottom" value.
    112      *
    113      * @see #mContentRect
    114      */
    115     private RectF mCurrentViewport = new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
    116 
    117     /**
    118      * The current destination rectangle (in pixel coordinates) into which the chart data should
    119      * be drawn. Chart labels are drawn outside this area.
    120      *
    121      * @see #mCurrentViewport
    122      */
    123     private Rect mContentRect = new Rect();
    124 
    125     // Current attribute values and Paints.
    126     private float mLabelTextSize;
    127     private int mLabelSeparation;
    128     private int mLabelTextColor;
    129     private Paint mLabelTextPaint;
    130     private int mMaxLabelWidth;
    131     private int mLabelHeight;
    132     private float mGridThickness;
    133     private int mGridColor;
    134     private Paint mGridPaint;
    135     private float mAxisThickness;
    136     private int mAxisColor;
    137     private Paint mAxisPaint;
    138     private float mDataThickness;
    139     private int mDataColor;
    140     private Paint mDataPaint;
    141 
    142     // State objects and values related to gesture tracking.
    143     private ScaleGestureDetector mScaleGestureDetector;
    144     private GestureDetectorCompat mGestureDetector;
    145     private OverScroller mScroller;
    146     private Zoomer mZoomer;
    147     private PointF mZoomFocalPoint = new PointF();
    148     private RectF mScrollerStartViewport = new RectF(); // Used only for zooms and flings.
    149 
    150     // Edge effect / overscroll tracking objects.
    151     private EdgeEffectCompat mEdgeEffectTop;
    152     private EdgeEffectCompat mEdgeEffectBottom;
    153     private EdgeEffectCompat mEdgeEffectLeft;
    154     private EdgeEffectCompat mEdgeEffectRight;
    155 
    156     private boolean mEdgeEffectTopActive;
    157     private boolean mEdgeEffectBottomActive;
    158     private boolean mEdgeEffectLeftActive;
    159     private boolean mEdgeEffectRightActive;
    160 
    161     // Buffers for storing current X and Y stops. See the computeAxisStops method for more details.
    162     private final AxisStops mXStopsBuffer = new AxisStops();
    163     private final AxisStops mYStopsBuffer = new AxisStops();
    164 
    165     // Buffers used during drawing. These are defined as fields to avoid allocation during
    166     // draw calls.
    167     private float[] mAxisXPositionsBuffer = new float[]{};
    168     private float[] mAxisYPositionsBuffer = new float[]{};
    169     private float[] mAxisXLinesBuffer = new float[]{};
    170     private float[] mAxisYLinesBuffer = new float[]{};
    171     private float[] mSeriesLinesBuffer = new float[(DRAW_STEPS + 1) * 4];
    172     private final char[] mLabelBuffer = new char[100];
    173     private Point mSurfaceSizeBuffer = new Point();
    174 
    175     /**
    176      * The simple math function Y = fun(X) to draw on the chart.
    177      * @param x The X value
    178      * @return The Y value
    179      */
    180     protected static float fun(float x) {
    181         return (float) Math.pow(x, 3) - x / 4;
    182     }
    183 
    184     public InteractiveLineGraphView(Context context) {
    185         this(context, null, 0);
    186     }
    187 
    188     public InteractiveLineGraphView(Context context, AttributeSet attrs) {
    189         this(context, attrs, 0);
    190     }
    191 
    192     public InteractiveLineGraphView(Context context, AttributeSet attrs, int defStyle) {
    193         super(context, attrs, defStyle);
    194 
    195         TypedArray a = context.getTheme().obtainStyledAttributes(
    196                 attrs, R.styleable.InteractiveLineGraphView, defStyle, defStyle);
    197 
    198         try {
    199             mLabelTextColor = a.getColor(
    200                     R.styleable.InteractiveLineGraphView_labelTextColor, mLabelTextColor);
    201             mLabelTextSize = a.getDimension(
    202                     R.styleable.InteractiveLineGraphView_labelTextSize, mLabelTextSize);
    203             mLabelSeparation = a.getDimensionPixelSize(
    204                     R.styleable.InteractiveLineGraphView_labelSeparation, mLabelSeparation);
    205 
    206             mGridThickness = a.getDimension(
    207                     R.styleable.InteractiveLineGraphView_gridThickness, mGridThickness);
    208             mGridColor = a.getColor(
    209                     R.styleable.InteractiveLineGraphView_gridColor, mGridColor);
    210 
    211             mAxisThickness = a.getDimension(
    212                     R.styleable.InteractiveLineGraphView_axisThickness, mAxisThickness);
    213             mAxisColor = a.getColor(
    214                     R.styleable.InteractiveLineGraphView_axisColor, mAxisColor);
    215 
    216             mDataThickness = a.getDimension(
    217                     R.styleable.InteractiveLineGraphView_dataThickness, mDataThickness);
    218             mDataColor = a.getColor(
    219                     R.styleable.InteractiveLineGraphView_dataColor, mDataColor);
    220         } finally {
    221             a.recycle();
    222         }
    223 
    224         initPaints();
    225 
    226         // Sets up interactions
    227         mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener);
    228         mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
    229 
    230         mScroller = new OverScroller(context);
    231         mZoomer = new Zoomer(context);
    232 
    233         // Sets up edge effects
    234         mEdgeEffectLeft = new EdgeEffectCompat(context);
    235         mEdgeEffectTop = new EdgeEffectCompat(context);
    236         mEdgeEffectRight = new EdgeEffectCompat(context);
    237         mEdgeEffectBottom = new EdgeEffectCompat(context);
    238     }
    239 
    240     /**
    241      * (Re)initializes {@link Paint} objects based on current attribute values.
    242      */
    243     private void initPaints() {
    244         mLabelTextPaint = new Paint();
    245         mLabelTextPaint.setAntiAlias(true);
    246         mLabelTextPaint.setTextSize(mLabelTextSize);
    247         mLabelTextPaint.setColor(mLabelTextColor);
    248         mLabelHeight = (int) Math.abs(mLabelTextPaint.getFontMetrics().top);
    249         mMaxLabelWidth = (int) mLabelTextPaint.measureText("0000");
    250 
    251         mGridPaint = new Paint();
    252         mGridPaint.setStrokeWidth(mGridThickness);
    253         mGridPaint.setColor(mGridColor);
    254         mGridPaint.setStyle(Paint.Style.STROKE);
    255 
    256         mAxisPaint = new Paint();
    257         mAxisPaint.setStrokeWidth(mAxisThickness);
    258         mAxisPaint.setColor(mAxisColor);
    259         mAxisPaint.setStyle(Paint.Style.STROKE);
    260 
    261         mDataPaint = new Paint();
    262         mDataPaint.setStrokeWidth(mDataThickness);
    263         mDataPaint.setColor(mDataColor);
    264         mDataPaint.setStyle(Paint.Style.STROKE);
    265         mDataPaint.setAntiAlias(true);
    266     }
    267 
    268     @Override
    269     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    270         super.onSizeChanged(w, h, oldw, oldh);
    271         mContentRect.set(
    272                 getPaddingLeft() + mMaxLabelWidth + mLabelSeparation,
    273                 getPaddingTop(),
    274                 getWidth() - getPaddingRight(),
    275                 getHeight() - getPaddingBottom() - mLabelHeight - mLabelSeparation);
    276     }
    277 
    278     @Override
    279     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    280         int minChartSize = getResources().getDimensionPixelSize(R.dimen.min_chart_size);
    281         setMeasuredDimension(
    282                 Math.max(getSuggestedMinimumWidth(),
    283                         resolveSize(minChartSize + getPaddingLeft() + mMaxLabelWidth
    284                                 + mLabelSeparation + getPaddingRight(),
    285                                 widthMeasureSpec)),
    286                 Math.max(getSuggestedMinimumHeight(),
    287                         resolveSize(minChartSize + getPaddingTop() + mLabelHeight
    288                                 + mLabelSeparation + getPaddingBottom(),
    289                                 heightMeasureSpec)));
    290     }
    291 
    292     ////////////////////////////////////////////////////////////////////////////////////////////////
    293     //
    294     //     Methods and objects related to drawing
    295     //
    296     ////////////////////////////////////////////////////////////////////////////////////////////////
    297 
    298     @Override
    299     protected void onDraw(Canvas canvas) {
    300         super.onDraw(canvas);
    301 
    302         // Draws axes and text labels
    303         drawAxes(canvas);
    304 
    305         // Clips the next few drawing operations to the content area
    306         int clipRestoreCount = canvas.save();
    307         canvas.clipRect(mContentRect);
    308 
    309         drawDataSeriesUnclipped(canvas);
    310         drawEdgeEffectsUnclipped(canvas);
    311 
    312         // Removes clipping rectangle
    313         canvas.restoreToCount(clipRestoreCount);
    314 
    315         // Draws chart container
    316         canvas.drawRect(mContentRect, mAxisPaint);
    317     }
    318 
    319     /**
    320      * Draws the chart axes and labels onto the canvas.
    321      */
    322     private void drawAxes(Canvas canvas) {
    323         // Computes axis stops (in terms of numerical value and position on screen)
    324         int i;
    325 
    326         computeAxisStops(
    327                 mCurrentViewport.left,
    328                 mCurrentViewport.right,
    329                 mContentRect.width() / mMaxLabelWidth / 2,
    330                 mXStopsBuffer);
    331         computeAxisStops(
    332                 mCurrentViewport.top,
    333                 mCurrentViewport.bottom,
    334                 mContentRect.height() / mLabelHeight / 2,
    335                 mYStopsBuffer);
    336 
    337         // Avoid unnecessary allocations during drawing. Re-use allocated
    338         // arrays and only reallocate if the number of stops grows.
    339         if (mAxisXPositionsBuffer.length < mXStopsBuffer.numStops) {
    340             mAxisXPositionsBuffer = new float[mXStopsBuffer.numStops];
    341         }
    342         if (mAxisYPositionsBuffer.length < mYStopsBuffer.numStops) {
    343             mAxisYPositionsBuffer = new float[mYStopsBuffer.numStops];
    344         }
    345         if (mAxisXLinesBuffer.length < mXStopsBuffer.numStops * 4) {
    346             mAxisXLinesBuffer = new float[mXStopsBuffer.numStops * 4];
    347         }
    348         if (mAxisYLinesBuffer.length < mYStopsBuffer.numStops * 4) {
    349             mAxisYLinesBuffer = new float[mYStopsBuffer.numStops * 4];
    350         }
    351 
    352         // Compute positions
    353         for (i = 0; i < mXStopsBuffer.numStops; i++) {
    354             mAxisXPositionsBuffer[i] = getDrawX(mXStopsBuffer.stops[i]);
    355         }
    356         for (i = 0; i < mYStopsBuffer.numStops; i++) {
    357             mAxisYPositionsBuffer[i] = getDrawY(mYStopsBuffer.stops[i]);
    358         }
    359 
    360         // Draws grid lines using drawLines (faster than individual drawLine calls)
    361         for (i = 0; i < mXStopsBuffer.numStops; i++) {
    362             mAxisXLinesBuffer[i * 4 + 0] = (float) Math.floor(mAxisXPositionsBuffer[i]);
    363             mAxisXLinesBuffer[i * 4 + 1] = mContentRect.top;
    364             mAxisXLinesBuffer[i * 4 + 2] = (float) Math.floor(mAxisXPositionsBuffer[i]);
    365             mAxisXLinesBuffer[i * 4 + 3] = mContentRect.bottom;
    366         }
    367         canvas.drawLines(mAxisXLinesBuffer, 0, mXStopsBuffer.numStops * 4, mGridPaint);
    368 
    369         for (i = 0; i < mYStopsBuffer.numStops; i++) {
    370             mAxisYLinesBuffer[i * 4 + 0] = mContentRect.left;
    371             mAxisYLinesBuffer[i * 4 + 1] = (float) Math.floor(mAxisYPositionsBuffer[i]);
    372             mAxisYLinesBuffer[i * 4 + 2] = mContentRect.right;
    373             mAxisYLinesBuffer[i * 4 + 3] = (float) Math.floor(mAxisYPositionsBuffer[i]);
    374         }
    375         canvas.drawLines(mAxisYLinesBuffer, 0, mYStopsBuffer.numStops * 4, mGridPaint);
    376 
    377         // Draws X labels
    378         int labelOffset;
    379         int labelLength;
    380         mLabelTextPaint.setTextAlign(Paint.Align.CENTER);
    381         for (i = 0; i < mXStopsBuffer.numStops; i++) {
    382             // Do not use String.format in high-performance code such as onDraw code.
    383             labelLength = formatFloat(mLabelBuffer, mXStopsBuffer.stops[i], mXStopsBuffer.decimals);
    384             labelOffset = mLabelBuffer.length - labelLength;
    385             canvas.drawText(
    386                     mLabelBuffer, labelOffset, labelLength,
    387                     mAxisXPositionsBuffer[i],
    388                     mContentRect.bottom + mLabelHeight + mLabelSeparation,
    389                     mLabelTextPaint);
    390         }
    391 
    392         // Draws Y labels
    393         mLabelTextPaint.setTextAlign(Paint.Align.RIGHT);
    394         for (i = 0; i < mYStopsBuffer.numStops; i++) {
    395             // Do not use String.format in high-performance code such as onDraw code.
    396             labelLength = formatFloat(mLabelBuffer, mYStopsBuffer.stops[i], mYStopsBuffer.decimals);
    397             labelOffset = mLabelBuffer.length - labelLength;
    398             canvas.drawText(
    399                     mLabelBuffer, labelOffset, labelLength,
    400                     mContentRect.left - mLabelSeparation,
    401                     mAxisYPositionsBuffer[i] + mLabelHeight / 2,
    402                     mLabelTextPaint);
    403         }
    404     }
    405 
    406     /**
    407      * Rounds the given number to the given number of significant digits. Based on an answer on
    408      * <a href="http://stackoverflow.com/questions/202302">Stack Overflow</a>.
    409      */
    410     private static float roundToOneSignificantFigure(double num) {
    411         final float d = (float) Math.ceil((float) Math.log10(num < 0 ? -num : num));
    412         final int power = 1 - (int) d;
    413         final float magnitude = (float) Math.pow(10, power);
    414         final long shifted = Math.round(num * magnitude);
    415         return shifted / magnitude;
    416     }
    417 
    418     private static final int POW10[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
    419 
    420     /**
    421      * Formats a float value to the given number of decimals. Returns the length of the string.
    422      * The string begins at out.length - [return value].
    423      */
    424     private static int formatFloat(final char[] out, float val, int digits) {
    425         boolean negative = false;
    426         if (val == 0) {
    427             out[out.length - 1] = '0';
    428             return 1;
    429         }
    430         if (val < 0) {
    431             negative = true;
    432             val = -val;
    433         }
    434         if (digits > POW10.length) {
    435             digits = POW10.length - 1;
    436         }
    437         val *= POW10[digits];
    438         long lval = Math.round(val);
    439         int index = out.length - 1;
    440         int charCount = 0;
    441         while (lval != 0 || charCount < (digits + 1)) {
    442             int digit = (int) (lval % 10);
    443             lval = lval / 10;
    444             out[index--] = (char) (digit + '0');
    445             charCount++;
    446             if (charCount == digits) {
    447                 out[index--] = '.';
    448                 charCount++;
    449             }
    450         }
    451         if (negative) {
    452             out[index--] = '-';
    453             charCount++;
    454         }
    455         return charCount;
    456     }
    457 
    458     /**
    459      * Computes the set of axis labels to show given start and stop boundaries and an ideal number
    460      * of stops between these boundaries.
    461      *
    462      * @param start The minimum extreme (e.g. the left edge) for the axis.
    463      * @param stop The maximum extreme (e.g. the right edge) for the axis.
    464      * @param steps The ideal number of stops to create. This should be based on available screen
    465      *              space; the more space there is, the more stops should be shown.
    466      * @param outStops The destination {@link AxisStops} object to populate.
    467      */
    468     private static void computeAxisStops(float start, float stop, int steps, AxisStops outStops) {
    469         double range = stop - start;
    470         if (steps == 0 || range <= 0) {
    471             outStops.stops = new float[]{};
    472             outStops.numStops = 0;
    473             return;
    474         }
    475 
    476         double rawInterval = range / steps;
    477         double interval = roundToOneSignificantFigure(rawInterval);
    478         double intervalMagnitude = Math.pow(10, (int) Math.log10(interval));
    479         int intervalSigDigit = (int) (interval / intervalMagnitude);
    480         if (intervalSigDigit > 5) {
    481             // Use one order of magnitude higher, to avoid intervals like 0.9 or 90
    482             interval = Math.floor(10 * intervalMagnitude);
    483         }
    484 
    485         double first = Math.ceil(start / interval) * interval;
    486         double last = Math.nextUp(Math.floor(stop / interval) * interval);
    487 
    488         double f;
    489         int i;
    490         int n = 0;
    491         for (f = first; f <= last; f += interval) {
    492             ++n;
    493         }
    494 
    495         outStops.numStops = n;
    496 
    497         if (outStops.stops.length < n) {
    498             // Ensure stops contains at least numStops elements.
    499             outStops.stops = new float[n];
    500         }
    501 
    502         for (f = first, i = 0; i < n; f += interval, ++i) {
    503             outStops.stops[i] = (float) f;
    504         }
    505 
    506         if (interval < 1) {
    507             outStops.decimals = (int) Math.ceil(-Math.log10(interval));
    508         } else {
    509             outStops.decimals = 0;
    510         }
    511     }
    512 
    513     /**
    514      * Computes the pixel offset for the given X chart value. This may be outside the view bounds.
    515      */
    516     private float getDrawX(float x) {
    517         return mContentRect.left
    518                 + mContentRect.width()
    519                 * (x - mCurrentViewport.left) / mCurrentViewport.width();
    520     }
    521 
    522     /**
    523      * Computes the pixel offset for the given Y chart value. This may be outside the view bounds.
    524      */
    525     private float getDrawY(float y) {
    526         return mContentRect.bottom
    527                 - mContentRect.height()
    528                 * (y - mCurrentViewport.top) / mCurrentViewport.height();
    529     }
    530 
    531     /**
    532      * Draws the currently visible portion of the data series defined by {@link #fun(float)} to the
    533      * canvas. This method does not clip its drawing, so users should call {@link Canvas#clipRect
    534      * before calling this method.
    535      */
    536     private void drawDataSeriesUnclipped(Canvas canvas) {
    537         mSeriesLinesBuffer[0] = mContentRect.left;
    538         mSeriesLinesBuffer[1] = getDrawY(fun(mCurrentViewport.left));
    539         mSeriesLinesBuffer[2] = mSeriesLinesBuffer[0];
    540         mSeriesLinesBuffer[3] = mSeriesLinesBuffer[1];
    541         float x;
    542         for (int i = 1; i <= DRAW_STEPS; i++) {
    543             mSeriesLinesBuffer[i * 4 + 0] = mSeriesLinesBuffer[(i - 1) * 4 + 2];
    544             mSeriesLinesBuffer[i * 4 + 1] = mSeriesLinesBuffer[(i - 1) * 4 + 3];
    545 
    546             x = (mCurrentViewport.left + (mCurrentViewport.width() / DRAW_STEPS * i));
    547             mSeriesLinesBuffer[i * 4 + 2] = getDrawX(x);
    548             mSeriesLinesBuffer[i * 4 + 3] = getDrawY(fun(x));
    549         }
    550         canvas.drawLines(mSeriesLinesBuffer, mDataPaint);
    551     }
    552 
    553     /**
    554      * Draws the overscroll "glow" at the four edges of the chart region, if necessary. The edges
    555      * of the chart region are stored in {@link #mContentRect}.
    556      *
    557      * @see EdgeEffectCompat
    558      */
    559     private void drawEdgeEffectsUnclipped(Canvas canvas) {
    560         // The methods below rotate and translate the canvas as needed before drawing the glow,
    561         // since EdgeEffectCompat always draws a top-glow at 0,0.
    562 
    563         boolean needsInvalidate = false;
    564 
    565         if (!mEdgeEffectTop.isFinished()) {
    566             final int restoreCount = canvas.save();
    567             canvas.translate(mContentRect.left, mContentRect.top);
    568             mEdgeEffectTop.setSize(mContentRect.width(), mContentRect.height());
    569             if (mEdgeEffectTop.draw(canvas)) {
    570                 needsInvalidate = true;
    571             }
    572             canvas.restoreToCount(restoreCount);
    573         }
    574 
    575         if (!mEdgeEffectBottom.isFinished()) {
    576             final int restoreCount = canvas.save();
    577             canvas.translate(2 * mContentRect.left - mContentRect.right, mContentRect.bottom);
    578             canvas.rotate(180, mContentRect.width(), 0);
    579             mEdgeEffectBottom.setSize(mContentRect.width(), mContentRect.height());
    580             if (mEdgeEffectBottom.draw(canvas)) {
    581                 needsInvalidate = true;
    582             }
    583             canvas.restoreToCount(restoreCount);
    584         }
    585 
    586         if (!mEdgeEffectLeft.isFinished()) {
    587             final int restoreCount = canvas.save();
    588             canvas.translate(mContentRect.left, mContentRect.bottom);
    589             canvas.rotate(-90, 0, 0);
    590             mEdgeEffectLeft.setSize(mContentRect.height(), mContentRect.width());
    591             if (mEdgeEffectLeft.draw(canvas)) {
    592                 needsInvalidate = true;
    593             }
    594             canvas.restoreToCount(restoreCount);
    595         }
    596 
    597         if (!mEdgeEffectRight.isFinished()) {
    598             final int restoreCount = canvas.save();
    599             canvas.translate(mContentRect.right, mContentRect.top);
    600             canvas.rotate(90, 0, 0);
    601             mEdgeEffectRight.setSize(mContentRect.height(), mContentRect.width());
    602             if (mEdgeEffectRight.draw(canvas)) {
    603                 needsInvalidate = true;
    604             }
    605             canvas.restoreToCount(restoreCount);
    606         }
    607 
    608         if (needsInvalidate) {
    609             ViewCompat.postInvalidateOnAnimation(this);
    610         }
    611     }
    612 
    613     ////////////////////////////////////////////////////////////////////////////////////////////////
    614     //
    615     //     Methods and objects related to gesture handling
    616     //
    617     ////////////////////////////////////////////////////////////////////////////////////////////////
    618 
    619     /**
    620      * Finds the chart point (i.e. within the chart's domain and range) represented by the
    621      * given pixel coordinates, if that pixel is within the chart region described by
    622      * {@link #mContentRect}. If the point is found, the "dest" argument is set to the point and
    623      * this function returns true. Otherwise, this function returns false and "dest" is unchanged.
    624      */
    625     private boolean hitTest(float x, float y, PointF dest) {
    626         if (!mContentRect.contains((int) x, (int) y)) {
    627             return false;
    628         }
    629 
    630         dest.set(
    631                 mCurrentViewport.left
    632                         + mCurrentViewport.width()
    633                         * (x - mContentRect.left) / mContentRect.width(),
    634                 mCurrentViewport.top
    635                         + mCurrentViewport.height()
    636                         * (y - mContentRect.bottom) / -mContentRect.height());
    637         return true;
    638      }
    639 
    640     @Override
    641     public boolean onTouchEvent(MotionEvent event) {
    642         boolean retVal = mScaleGestureDetector.onTouchEvent(event);
    643         retVal = mGestureDetector.onTouchEvent(event) || retVal;
    644         return retVal || super.onTouchEvent(event);
    645     }
    646 
    647     /**
    648      * The scale listener, used for handling multi-finger scale gestures.
    649      */
    650     private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
    651             = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
    652         /**
    653          * This is the active focal point in terms of the viewport. Could be a local
    654          * variable but kept here to minimize per-frame allocations.
    655          */
    656         private PointF viewportFocus = new PointF();
    657         private float lastSpanX;
    658         private float lastSpanY;
    659 
    660         @Override
    661         public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
    662             lastSpanX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector);
    663             lastSpanY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector);
    664             return true;
    665         }
    666 
    667         @Override
    668         public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
    669             float spanX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector);
    670             float spanY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector);
    671 
    672             float newWidth = lastSpanX / spanX * mCurrentViewport.width();
    673             float newHeight = lastSpanY / spanY * mCurrentViewport.height();
    674 
    675             float focusX = scaleGestureDetector.getFocusX();
    676             float focusY = scaleGestureDetector.getFocusY();
    677             hitTest(focusX, focusY, viewportFocus);
    678 
    679             mCurrentViewport.set(
    680                     viewportFocus.x
    681                             - newWidth * (focusX - mContentRect.left)
    682                             / mContentRect.width(),
    683                     viewportFocus.y
    684                             - newHeight * (mContentRect.bottom - focusY)
    685                             / mContentRect.height(),
    686                     0,
    687                     0);
    688             mCurrentViewport.right = mCurrentViewport.left + newWidth;
    689             mCurrentViewport.bottom = mCurrentViewport.top + newHeight;
    690             constrainViewport();
    691             ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
    692 
    693             lastSpanX = spanX;
    694             lastSpanY = spanY;
    695             return true;
    696         }
    697     };
    698 
    699     /**
    700      * Ensures that current viewport is inside the viewport extremes defined by {@link #AXIS_X_MIN},
    701      * {@link #AXIS_X_MAX}, {@link #AXIS_Y_MIN} and {@link #AXIS_Y_MAX}.
    702      */
    703     private void constrainViewport() {
    704         mCurrentViewport.left = Math.max(AXIS_X_MIN, mCurrentViewport.left);
    705         mCurrentViewport.top = Math.max(AXIS_Y_MIN, mCurrentViewport.top);
    706         mCurrentViewport.bottom = Math.max(Math.nextUp(mCurrentViewport.top),
    707                 Math.min(AXIS_Y_MAX, mCurrentViewport.bottom));
    708         mCurrentViewport.right = Math.max(Math.nextUp(mCurrentViewport.left),
    709                 Math.min(AXIS_X_MAX, mCurrentViewport.right));
    710     }
    711 
    712     /**
    713      * The gesture listener, used for handling simple gestures such as double touches, scrolls,
    714      * and flings.
    715      */
    716     private final GestureDetector.SimpleOnGestureListener mGestureListener
    717             = new GestureDetector.SimpleOnGestureListener() {
    718         @Override
    719         public boolean onDown(MotionEvent e) {
    720             releaseEdgeEffects();
    721             mScrollerStartViewport.set(mCurrentViewport);
    722             mScroller.forceFinished(true);
    723             ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
    724             return true;
    725         }
    726 
    727         @Override
    728         public boolean onDoubleTap(MotionEvent e) {
    729             mZoomer.forceFinished(true);
    730             if (hitTest(e.getX(), e.getY(), mZoomFocalPoint)) {
    731                 mZoomer.startZoom(ZOOM_AMOUNT);
    732             }
    733             ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
    734             return true;
    735         }
    736 
    737         @Override
    738         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    739             // Scrolling uses math based on the viewport (as opposed to math using pixels).
    740             /**
    741              * Pixel offset is the offset in screen pixels, while viewport offset is the
    742              * offset within the current viewport. For additional information on surface sizes
    743              * and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
    744              * additional information about the viewport, see the comments for
    745              * {@link mCurrentViewport}.
    746              */
    747             float viewportOffsetX = distanceX * mCurrentViewport.width() / mContentRect.width();
    748             float viewportOffsetY = -distanceY * mCurrentViewport.height() / mContentRect.height();
    749             computeScrollSurfaceSize(mSurfaceSizeBuffer);
    750             int scrolledX = (int) (mSurfaceSizeBuffer.x
    751                     * (mCurrentViewport.left + viewportOffsetX - AXIS_X_MIN)
    752                     / (AXIS_X_MAX - AXIS_X_MIN));
    753             int scrolledY = (int) (mSurfaceSizeBuffer.y
    754                     * (AXIS_Y_MAX - mCurrentViewport.bottom - viewportOffsetY)
    755                     / (AXIS_Y_MAX - AXIS_Y_MIN));
    756             boolean canScrollX = mCurrentViewport.left > AXIS_X_MIN
    757                     || mCurrentViewport.right < AXIS_X_MAX;
    758             boolean canScrollY = mCurrentViewport.top > AXIS_Y_MIN
    759                     || mCurrentViewport.bottom < AXIS_Y_MAX;
    760             setViewportBottomLeft(
    761                     mCurrentViewport.left + viewportOffsetX,
    762                     mCurrentViewport.bottom + viewportOffsetY);
    763 
    764             if (canScrollX && scrolledX < 0) {
    765                 mEdgeEffectLeft.onPull(scrolledX / (float) mContentRect.width());
    766                 mEdgeEffectLeftActive = true;
    767             }
    768             if (canScrollY && scrolledY < 0) {
    769                 mEdgeEffectTop.onPull(scrolledY / (float) mContentRect.height());
    770                 mEdgeEffectTopActive = true;
    771             }
    772             if (canScrollX && scrolledX > mSurfaceSizeBuffer.x - mContentRect.width()) {
    773                 mEdgeEffectRight.onPull((scrolledX - mSurfaceSizeBuffer.x + mContentRect.width())
    774                         / (float) mContentRect.width());
    775                 mEdgeEffectRightActive = true;
    776             }
    777             if (canScrollY && scrolledY > mSurfaceSizeBuffer.y - mContentRect.height()) {
    778                 mEdgeEffectBottom.onPull((scrolledY - mSurfaceSizeBuffer.y + mContentRect.height())
    779                         / (float) mContentRect.height());
    780                 mEdgeEffectBottomActive = true;
    781             }
    782             return true;
    783         }
    784 
    785         @Override
    786         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    787             fling((int) -velocityX, (int) -velocityY);
    788             return true;
    789         }
    790     };
    791 
    792     private void releaseEdgeEffects() {
    793         mEdgeEffectLeftActive
    794                 = mEdgeEffectTopActive
    795                 = mEdgeEffectRightActive
    796                 = mEdgeEffectBottomActive
    797                 = false;
    798         mEdgeEffectLeft.onRelease();
    799         mEdgeEffectTop.onRelease();
    800         mEdgeEffectRight.onRelease();
    801         mEdgeEffectBottom.onRelease();
    802     }
    803 
    804     private void fling(int velocityX, int velocityY) {
    805         releaseEdgeEffects();
    806         // Flings use math in pixels (as opposed to math based on the viewport).
    807         computeScrollSurfaceSize(mSurfaceSizeBuffer);
    808         mScrollerStartViewport.set(mCurrentViewport);
    809         int startX = (int) (mSurfaceSizeBuffer.x * (mScrollerStartViewport.left - AXIS_X_MIN) / (
    810                 AXIS_X_MAX - AXIS_X_MIN));
    811         int startY = (int) (mSurfaceSizeBuffer.y * (AXIS_Y_MAX - mScrollerStartViewport.bottom) / (
    812                 AXIS_Y_MAX - AXIS_Y_MIN));
    813         mScroller.forceFinished(true);
    814         mScroller.fling(
    815                 startX,
    816                 startY,
    817                 velocityX,
    818                 velocityY,
    819                 0, mSurfaceSizeBuffer.x - mContentRect.width(),
    820                 0, mSurfaceSizeBuffer.y - mContentRect.height(),
    821                 mContentRect.width() / 2,
    822                 mContentRect.height() / 2);
    823         ViewCompat.postInvalidateOnAnimation(this);
    824     }
    825 
    826     /**
    827      * Computes the current scrollable surface size, in pixels. For example, if the entire chart
    828      * area is visible, this is simply the current size of {@link #mContentRect}. If the chart
    829      * is zoomed in 200% in both directions, the returned size will be twice as large horizontally
    830      * and vertically.
    831      */
    832     private void computeScrollSurfaceSize(Point out) {
    833         out.set(
    834                 (int) (mContentRect.width() * (AXIS_X_MAX - AXIS_X_MIN)
    835                         / mCurrentViewport.width()),
    836                 (int) (mContentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN)
    837                         / mCurrentViewport.height()));
    838     }
    839 
    840     @Override
    841     public void computeScroll() {
    842         super.computeScroll();
    843 
    844         boolean needsInvalidate = false;
    845 
    846         if (mScroller.computeScrollOffset()) {
    847             // The scroller isn't finished, meaning a fling or programmatic pan operation is
    848             // currently active.
    849 
    850             computeScrollSurfaceSize(mSurfaceSizeBuffer);
    851             int currX = mScroller.getCurrX();
    852             int currY = mScroller.getCurrY();
    853 
    854             boolean canScrollX = (mCurrentViewport.left > AXIS_X_MIN
    855                     || mCurrentViewport.right < AXIS_X_MAX);
    856             boolean canScrollY = (mCurrentViewport.top > AXIS_Y_MIN
    857                     || mCurrentViewport.bottom < AXIS_Y_MAX);
    858 
    859             if (canScrollX
    860                     && currX < 0
    861                     && mEdgeEffectLeft.isFinished()
    862                     && !mEdgeEffectLeftActive) {
    863                 mEdgeEffectLeft.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
    864                 mEdgeEffectLeftActive = true;
    865                 needsInvalidate = true;
    866             } else if (canScrollX
    867                     && currX > (mSurfaceSizeBuffer.x - mContentRect.width())
    868                     && mEdgeEffectRight.isFinished()
    869                     && !mEdgeEffectRightActive) {
    870                 mEdgeEffectRight.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
    871                 mEdgeEffectRightActive = true;
    872                 needsInvalidate = true;
    873             }
    874 
    875             if (canScrollY
    876                     && currY < 0
    877                     && mEdgeEffectTop.isFinished()
    878                     && !mEdgeEffectTopActive) {
    879                 mEdgeEffectTop.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
    880                 mEdgeEffectTopActive = true;
    881                 needsInvalidate = true;
    882             } else if (canScrollY
    883                     && currY > (mSurfaceSizeBuffer.y - mContentRect.height())
    884                     && mEdgeEffectBottom.isFinished()
    885                     && !mEdgeEffectBottomActive) {
    886                 mEdgeEffectBottom.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
    887                 mEdgeEffectBottomActive = true;
    888                 needsInvalidate = true;
    889             }
    890 
    891             float currXRange = AXIS_X_MIN + (AXIS_X_MAX - AXIS_X_MIN)
    892                     * currX / mSurfaceSizeBuffer.x;
    893             float currYRange = AXIS_Y_MAX - (AXIS_Y_MAX - AXIS_Y_MIN)
    894                     * currY / mSurfaceSizeBuffer.y;
    895             setViewportBottomLeft(currXRange, currYRange);
    896         }
    897 
    898         if (mZoomer.computeZoom()) {
    899             // Performs the zoom since a zoom is in progress (either programmatically or via
    900             // double-touch).
    901             float newWidth = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.width();
    902             float newHeight = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.height();
    903             float pointWithinViewportX = (mZoomFocalPoint.x - mScrollerStartViewport.left)
    904                     / mScrollerStartViewport.width();
    905             float pointWithinViewportY = (mZoomFocalPoint.y - mScrollerStartViewport.top)
    906                     / mScrollerStartViewport.height();
    907             mCurrentViewport.set(
    908                     mZoomFocalPoint.x - newWidth * pointWithinViewportX,
    909                     mZoomFocalPoint.y - newHeight * pointWithinViewportY,
    910                     mZoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
    911                     mZoomFocalPoint.y + newHeight * (1 - pointWithinViewportY));
    912             constrainViewport();
    913             needsInvalidate = true;
    914         }
    915 
    916         if (needsInvalidate) {
    917             ViewCompat.postInvalidateOnAnimation(this);
    918         }
    919     }
    920 
    921     /**
    922      * Sets the current viewport (defined by {@link #mCurrentViewport}) to the given
    923      * X and Y positions. Note that the Y value represents the topmost pixel position, and thus
    924      * the bottom of the {@link #mCurrentViewport} rectangle. For more details on why top and
    925      * bottom are flipped, see {@link #mCurrentViewport}.
    926      */
    927     private void setViewportBottomLeft(float x, float y) {
    928         /**
    929          * Constrains within the scroll range. The scroll range is simply the viewport extremes
    930          * (AXIS_X_MAX, etc.) minus the viewport size. For example, if the extrema were 0 and 10,
    931          * and the viewport size was 2, the scroll range would be 0 to 8.
    932          */
    933 
    934         float curWidth = mCurrentViewport.width();
    935         float curHeight = mCurrentViewport.height();
    936         x = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth));
    937         y = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX));
    938 
    939         mCurrentViewport.set(x, y - curHeight, x + curWidth, y);
    940         ViewCompat.postInvalidateOnAnimation(this);
    941     }
    942 
    943     ////////////////////////////////////////////////////////////////////////////////////////////////
    944     //
    945     //     Methods for programmatically changing the viewport
    946     //
    947     ////////////////////////////////////////////////////////////////////////////////////////////////
    948 
    949     /**
    950      * Returns the current viewport (visible extremes for the chart domain and range.)
    951      */
    952     public RectF getCurrentViewport() {
    953         return new RectF(mCurrentViewport);
    954     }
    955 
    956     /**
    957      * Sets the chart's current viewport.
    958      *
    959      * @see #getCurrentViewport()
    960      */
    961     public void setCurrentViewport(RectF viewport) {
    962         mCurrentViewport = viewport;
    963         constrainViewport();
    964         ViewCompat.postInvalidateOnAnimation(this);
    965     }
    966 
    967     /**
    968      * Smoothly zooms the chart in one step.
    969      */
    970     public void zoomIn() {
    971         mScrollerStartViewport.set(mCurrentViewport);
    972         mZoomer.forceFinished(true);
    973         mZoomer.startZoom(ZOOM_AMOUNT);
    974         mZoomFocalPoint.set(
    975                 (mCurrentViewport.right + mCurrentViewport.left) / 2,
    976                 (mCurrentViewport.bottom + mCurrentViewport.top) / 2);
    977         ViewCompat.postInvalidateOnAnimation(this);
    978     }
    979 
    980     /**
    981      * Smoothly zooms the chart out one step.
    982      */
    983     public void zoomOut() {
    984         mScrollerStartViewport.set(mCurrentViewport);
    985         mZoomer.forceFinished(true);
    986         mZoomer.startZoom(-ZOOM_AMOUNT);
    987         mZoomFocalPoint.set(
    988                 (mCurrentViewport.right + mCurrentViewport.left) / 2,
    989                 (mCurrentViewport.bottom + mCurrentViewport.top) / 2);
    990         ViewCompat.postInvalidateOnAnimation(this);
    991     }
    992 
    993     /**
    994      * Smoothly pans the chart left one step.
    995      */
    996     public void panLeft() {
    997         fling((int) (-PAN_VELOCITY_FACTOR * getWidth()), 0);
    998     }
    999 
   1000     /**
   1001      * Smoothly pans the chart right one step.
   1002      */
   1003     public void panRight() {
   1004         fling((int) (PAN_VELOCITY_FACTOR * getWidth()), 0);
   1005     }
   1006 
   1007     /**
   1008      * Smoothly pans the chart up one step.
   1009      */
   1010     public void panUp() {
   1011         fling(0, (int) (-PAN_VELOCITY_FACTOR * getHeight()));
   1012     }
   1013 
   1014     /**
   1015      * Smoothly pans the chart down one step.
   1016      */
   1017     public void panDown() {
   1018         fling(0, (int) (PAN_VELOCITY_FACTOR * getHeight()));
   1019     }
   1020 
   1021     ////////////////////////////////////////////////////////////////////////////////////////////////
   1022     //
   1023     //     Methods related to custom attributes
   1024     //
   1025     ////////////////////////////////////////////////////////////////////////////////////////////////
   1026 
   1027     public float getLabelTextSize() {
   1028         return mLabelTextSize;
   1029     }
   1030 
   1031     public void setLabelTextSize(float labelTextSize) {
   1032         mLabelTextSize = labelTextSize;
   1033         initPaints();
   1034         ViewCompat.postInvalidateOnAnimation(this);
   1035     }
   1036 
   1037     public int getLabelTextColor() {
   1038         return mLabelTextColor;
   1039     }
   1040 
   1041     public void setLabelTextColor(int labelTextColor) {
   1042         mLabelTextColor = labelTextColor;
   1043         initPaints();
   1044         ViewCompat.postInvalidateOnAnimation(this);
   1045     }
   1046 
   1047     public float getGridThickness() {
   1048         return mGridThickness;
   1049     }
   1050 
   1051     public void setGridThickness(float gridThickness) {
   1052         mGridThickness = gridThickness;
   1053         initPaints();
   1054         ViewCompat.postInvalidateOnAnimation(this);
   1055     }
   1056 
   1057     public int getGridColor() {
   1058         return mGridColor;
   1059     }
   1060 
   1061     public void setGridColor(int gridColor) {
   1062         mGridColor = gridColor;
   1063         initPaints();
   1064         ViewCompat.postInvalidateOnAnimation(this);
   1065     }
   1066 
   1067     public float getAxisThickness() {
   1068         return mAxisThickness;
   1069     }
   1070 
   1071     public void setAxisThickness(float axisThickness) {
   1072         mAxisThickness = axisThickness;
   1073         initPaints();
   1074         ViewCompat.postInvalidateOnAnimation(this);
   1075     }
   1076 
   1077     public int getAxisColor() {
   1078         return mAxisColor;
   1079     }
   1080 
   1081     public void setAxisColor(int axisColor) {
   1082         mAxisColor = axisColor;
   1083         initPaints();
   1084         ViewCompat.postInvalidateOnAnimation(this);
   1085     }
   1086 
   1087     public float getDataThickness() {
   1088         return mDataThickness;
   1089     }
   1090 
   1091     public void setDataThickness(float dataThickness) {
   1092         mDataThickness = dataThickness;
   1093     }
   1094 
   1095     public int getDataColor() {
   1096         return mDataColor;
   1097     }
   1098 
   1099     public void setDataColor(int dataColor) {
   1100         mDataColor = dataColor;
   1101     }
   1102 
   1103     ////////////////////////////////////////////////////////////////////////////////////////////////
   1104     //
   1105     //     Methods and classes related to view state persistence.
   1106     //
   1107     ////////////////////////////////////////////////////////////////////////////////////////////////
   1108 
   1109     @Override
   1110     public Parcelable onSaveInstanceState() {
   1111         Parcelable superState = super.onSaveInstanceState();
   1112         SavedState ss = new SavedState(superState);
   1113         ss.viewport = mCurrentViewport;
   1114         return ss;
   1115     }
   1116 
   1117     @Override
   1118     public void onRestoreInstanceState(Parcelable state) {
   1119         if (!(state instanceof SavedState)) {
   1120             super.onRestoreInstanceState(state);
   1121             return;
   1122         }
   1123 
   1124         SavedState ss = (SavedState) state;
   1125         super.onRestoreInstanceState(ss.getSuperState());
   1126 
   1127         mCurrentViewport = ss.viewport;
   1128     }
   1129 
   1130     /**
   1131      * Persistent state that is saved by InteractiveLineGraphView.
   1132      */
   1133     public static class SavedState extends BaseSavedState {
   1134         private RectF viewport;
   1135 
   1136         public SavedState(Parcelable superState) {
   1137             super(superState);
   1138         }
   1139 
   1140         @Override
   1141         public void writeToParcel(Parcel out, int flags) {
   1142             super.writeToParcel(out, flags);
   1143             out.writeFloat(viewport.left);
   1144             out.writeFloat(viewport.top);
   1145             out.writeFloat(viewport.right);
   1146             out.writeFloat(viewport.bottom);
   1147         }
   1148 
   1149         @Override
   1150         public String toString() {
   1151             return "InteractiveLineGraphView.SavedState{"
   1152                     + Integer.toHexString(System.identityHashCode(this))
   1153                     + " viewport=" + viewport.toString() + "}";
   1154         }
   1155 
   1156         public static final Parcelable.Creator<SavedState> CREATOR
   1157                 = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
   1158             @Override
   1159             public SavedState createFromParcel(Parcel in, ClassLoader loader) {
   1160                 return new SavedState(in);
   1161             }
   1162 
   1163             @Override
   1164             public SavedState[] newArray(int size) {
   1165                 return new SavedState[size];
   1166             }
   1167         });
   1168 
   1169         SavedState(Parcel in) {
   1170             super(in);
   1171             viewport = new RectF(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat());
   1172         }
   1173     }
   1174 
   1175     /**
   1176      * A simple class representing axis label values.
   1177      *
   1178      * @see #computeAxisStops
   1179      */
   1180     private static class AxisStops {
   1181         float[] stops = new float[]{};
   1182         int numStops;
   1183         int decimals;
   1184     }
   1185 }
   1186