Add InteractiveChart sample code for update to Gestures training class.

Change-Id: I1e245cd6735d54603174045ca557057763861469
diff --git a/samples/training/InteractiveChart/AndroidManifest.xml b/samples/training/InteractiveChart/AndroidManifest.xml
new file mode 100755
index 0000000..1e29155
--- /dev/null
+++ b/samples/training/InteractiveChart/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.interactivechart"
+    android:versionCode="1"
+    android:versionName="1.0">
+
+    <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17" />
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:description="@string/app_description"
+        android:icon="@drawable/ic_launcher"
+        android:theme="@style/AppTheme">
+
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>
diff --git a/samples/training/InteractiveChart/libs/android-support-v4.jar b/samples/training/InteractiveChart/libs/android-support-v4.jar
new file mode 100644
index 0000000..6080877
--- /dev/null
+++ b/samples/training/InteractiveChart/libs/android-support-v4.jar
Binary files differ
diff --git a/samples/training/InteractiveChart/project.properties b/samples/training/InteractiveChart/project.properties
new file mode 100755
index 0000000..7c903ad
--- /dev/null
+++ b/samples/training/InteractiveChart/project.properties
@@ -0,0 +1,27 @@
+#
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-17
diff --git a/samples/training/InteractiveChart/res/drawable-hdpi/ic_launcher.png b/samples/training/InteractiveChart/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..9e2d6ee
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-mdpi/ic_launcher.png b/samples/training/InteractiveChart/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..2292930
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_down.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_down.png
new file mode 100644
index 0000000..38c7b20
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_down.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_left.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_left.png
new file mode 100644
index 0000000..e97e910
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_left.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_right.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_right.png
new file mode 100644
index 0000000..7dc45c0
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_right.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_up.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_up.png
new file mode 100644
index 0000000..2ed3251
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_up.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_in.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_in.png
new file mode 100644
index 0000000..23b9a1c
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_in.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_out.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_out.png
new file mode 100644
index 0000000..bb95274
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_out.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_launcher.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..b1e3613
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/drawable-xxhdpi/ic_launcher.png b/samples/training/InteractiveChart/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2217834
--- /dev/null
+++ b/samples/training/InteractiveChart/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/InteractiveChart/res/layout/activity_main.xml b/samples/training/InteractiveChart/res/layout/activity_main.xml
new file mode 100755
index 0000000..285d8ae
--- /dev/null
+++ b/samples/training/InteractiveChart/res/layout/activity_main.xml
@@ -0,0 +1,32 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<com.example.android.interactivechart.InteractiveLineGraphView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res/com.example.android.interactivechart"
+    android:id="@+id/chart"
+    android:padding="@dimen/chart_padding"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:labelSeparation="10dp"
+    app:labelTextSize="14sp"
+    app:labelTextColor="#d000"
+    app:axisThickness="2dp"
+    app:axisColor="#d000"
+    app:gridThickness="1dp"
+    app:gridColor="#2000"
+    app:dataColor="#a6c"
+    app:dataThickness="8dp" />
diff --git a/samples/training/InteractiveChart/res/menu/main.xml b/samples/training/InteractiveChart/res/menu/main.xml
new file mode 100644
index 0000000..ad758e5
--- /dev/null
+++ b/samples/training/InteractiveChart/res/menu/main.xml
@@ -0,0 +1,43 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_zoom_in"
+        android:icon="@drawable/ic_action_zoom_in"
+        android:title="@string/action_zoom_in"
+        android:showAsAction="never" />
+    <item android:id="@+id/action_zoom_out"
+        android:icon="@drawable/ic_action_zoom_out"
+        android:title="@string/action_zoom_out"
+        android:showAsAction="never" />
+
+    <item android:id="@+id/action_pan_left"
+        android:icon="@drawable/ic_action_pan_left"
+        android:title="@string/action_pan_left"
+        android:showAsAction="never" />
+    <item android:id="@+id/action_pan_right"
+        android:icon="@drawable/ic_action_pan_right"
+        android:title="@string/action_pan_right"
+        android:showAsAction="never" />
+    <item android:id="@+id/action_pan_up"
+        android:icon="@drawable/ic_action_pan_up"
+        android:title="@string/action_pan_up"
+        android:showAsAction="never" />
+    <item android:id="@+id/action_pan_down"
+        android:icon="@drawable/ic_action_pan_down"
+        android:title="@string/action_pan_down"
+        android:showAsAction="never" />
+</menu>
diff --git a/samples/training/InteractiveChart/res/values-sw600dp/dimens.xml b/samples/training/InteractiveChart/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..a7073d4
--- /dev/null
+++ b/samples/training/InteractiveChart/res/values-sw600dp/dimens.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <dimen name="chart_padding">48dp</dimen>
+</resources>
diff --git a/samples/training/InteractiveChart/res/values-v11/styles.xml b/samples/training/InteractiveChart/res/values-v11/styles.xml
new file mode 100644
index 0000000..8ff79ca
--- /dev/null
+++ b/samples/training/InteractiveChart/res/values-v11/styles.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <style name="AppTheme" parent="@android:style/Theme.Holo.Light" />
+</resources>
diff --git a/samples/training/InteractiveChart/res/values-v14/styles.xml b/samples/training/InteractiveChart/res/values-v14/styles.xml
new file mode 100644
index 0000000..530f994
--- /dev/null
+++ b/samples/training/InteractiveChart/res/values-v14/styles.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <style name="AppTheme" parent="@android:style/Theme.Holo.Light.DarkActionBar" />
+</resources>
diff --git a/samples/training/InteractiveChart/res/values/attrs.xml b/samples/training/InteractiveChart/res/values/attrs.xml
new file mode 100644
index 0000000..89e56cf
--- /dev/null
+++ b/samples/training/InteractiveChart/res/values/attrs.xml
@@ -0,0 +1,29 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <declare-styleable name="InteractiveLineGraphView">
+        <attr name="labelTextSize" format="dimension" />
+        <attr name="labelTextColor" format="color" />
+        <attr name="labelSeparation" format="dimension" />
+        <attr name="axisThickness" format="dimension" />
+        <attr name="axisColor" format="color" />
+        <attr name="dataThickness" format="dimension" />
+        <attr name="dataColor" format="color" />
+        <attr name="gridThickness" format="dimension" />
+        <attr name="gridColor" format="color" />
+    </declare-styleable>
+</resources>
diff --git a/samples/training/InteractiveChart/res/values/dimens.xml b/samples/training/InteractiveChart/res/values/dimens.xml
new file mode 100644
index 0000000..51561fd
--- /dev/null
+++ b/samples/training/InteractiveChart/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <dimen name="chart_padding">16dp</dimen>
+    <dimen name="min_chart_size">100dp</dimen>
+</resources>
diff --git a/samples/training/InteractiveChart/res/values/strings.xml b/samples/training/InteractiveChart/res/values/strings.xml
new file mode 100755
index 0000000..b08da58
--- /dev/null
+++ b/samples/training/InteractiveChart/res/values/strings.xml
@@ -0,0 +1,26 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <string name="app_name">Interactive Chart Demo</string>
+    <string name="app_description">A sample application that allows you to navigate a simple line graph using touch gestures.</string>
+    <string name="action_zoom_in">Demo zoom in</string>
+    <string name="action_zoom_out">Demo zoom out</string>
+    <string name="action_pan_left">Demo pan left</string>
+    <string name="action_pan_right">Demo pan right</string>
+    <string name="action_pan_up">Demo pan up</string>
+    <string name="action_pan_down">Demo pan down</string>
+</resources>
diff --git a/samples/training/InteractiveChart/res/values/styles.xml b/samples/training/InteractiveChart/res/values/styles.xml
new file mode 100644
index 0000000..ccb9f7e
--- /dev/null
+++ b/samples/training/InteractiveChart/res/values/styles.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright 2013 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <style name="AppTheme" parent="@android:style/Theme.Light" />
+</resources>
diff --git a/samples/training/InteractiveChart/src/com/example/android/interactivechart/InteractiveLineGraphView.java b/samples/training/InteractiveChart/src/com/example/android/interactivechart/InteractiveLineGraphView.java
new file mode 100644
index 0000000..9407cbc
--- /dev/null
+++ b/samples/training/InteractiveChart/src/com/example/android/interactivechart/InteractiveLineGraphView.java
@@ -0,0 +1,1185 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.interactivechart;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.os.ParcelableCompat;
+import android.support.v4.os.ParcelableCompatCreatorCallbacks;
+import android.support.v4.view.GestureDetectorCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.EdgeEffectCompat;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.widget.OverScroller;
+
+/**
+ * A view representing a simple yet interactive line chart for the function <code>x^3 - x/4</code>.
+ * <p>
+ * This view isn't all that useful on its own; rather it serves as an example of how to correctly
+ * implement these types of gestures to perform zooming and scrolling with interesting content
+ * types.
+ * <p>
+ * The view is interactive in that it can be zoomed and panned using
+ * typical <a href="http://developer.android.com/design/patterns/gestures.html">gestures</a> such
+ * as double-touch, drag, pinch-open, and pinch-close. This is done using the
+ * {@link ScaleGestureDetector}, {@link GestureDetector}, and {@link OverScroller} classes. Note
+ * that the platform-provided view scrolling behavior (e.g. {@link View#scrollBy(int, int)} is NOT
+ * used.
+ * <p>
+ * The view also demonstrates the correct use of
+ * <a href="http://developer.android.com/design/style/touch-feedback.html">touch feedback</a> to
+ * indicate to users that they've reached the content edges after a pan or fling gesture. This
+ * is done using the {@link EdgeEffectCompat} class.
+ * <p>
+ * Finally, this class demonstrates the basics of creating a custom view, including support for
+ * custom attributes (see the constructors), a simple implementation for
+ * {@link #onMeasure(int, int)}, an implementation for {@link #onSaveInstanceState()} and a fairly
+ * straightforward {@link Canvas}-based rendering implementation in
+ * {@link #onDraw(android.graphics.Canvas)}.
+ * <p>
+ * Note that this view doesn't automatically support directional navigation or other accessibility
+ * methods. Activities using this view should generally provide alternate navigation controls.
+ * Activities using this view should also present an alternate, text-based representation of this
+ * view's content for vision-impaired users.
+ */
+public class InteractiveLineGraphView extends View {
+    private static final String TAG = "InteractiveLineGraphView";
+
+    /**
+     * The number of individual points (samples) in the chart series to draw onscreen.
+     */
+    private static final int DRAW_STEPS = 30;
+
+    /**
+     * Initial fling velocity for pan operations, in screen widths (or heights) per second.
+     *
+     * @see #panLeft()
+     * @see #panRight()
+     * @see #panUp()
+     * @see #panDown()
+     */
+    private static final float PAN_VELOCITY_FACTOR = 2f;
+
+    /**
+     * The scaling factor for a single zoom 'step'.
+     *
+     * @see #zoomIn()
+     * @see #zoomOut()
+     */
+    private static final float ZOOM_AMOUNT = 0.25f;
+
+    // Viewport extremes. See mCurrentViewport for a discussion of the viewport.
+    private static final float AXIS_X_MIN = -1f;
+    private static final float AXIS_X_MAX = 1f;
+    private static final float AXIS_Y_MIN = -1f;
+    private static final float AXIS_Y_MAX = 1f;
+
+    /**
+     * The current viewport. This rectangle represents the currently visible chart domain
+     * and range. The currently visible chart X values are from this rectangle's left to its right.
+     * The currently visible chart Y values are from this rectangle's top to its bottom.
+     * <p>
+     * Note that this rectangle's top is actually the smaller Y value, and its bottom is the larger
+     * Y value. Since the chart is drawn onscreen in such a way that chart Y values increase
+     * towards the top of the screen (decreasing pixel Y positions), this rectangle's "top" is drawn
+     * above this rectangle's "bottom" value.
+     *
+     * @see #mContentRect
+     */
+    private RectF mCurrentViewport = new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
+
+    /**
+     * The current destination rectangle (in pixel coordinates) into which the chart data should
+     * be drawn. Chart labels are drawn outside this area.
+     *
+     * @see #mCurrentViewport
+     */
+    private Rect mContentRect = new Rect();
+
+    // Current attribute values and Paints.
+    private float mLabelTextSize;
+    private int mLabelSeparation;
+    private int mLabelTextColor;
+    private Paint mLabelTextPaint;
+    private int mMaxLabelWidth;
+    private int mLabelHeight;
+    private float mGridThickness;
+    private int mGridColor;
+    private Paint mGridPaint;
+    private float mAxisThickness;
+    private int mAxisColor;
+    private Paint mAxisPaint;
+    private float mDataThickness;
+    private int mDataColor;
+    private Paint mDataPaint;
+
+    // State objects and values related to gesture tracking.
+    private ScaleGestureDetector mScaleGestureDetector;
+    private GestureDetectorCompat mGestureDetector;
+    private OverScroller mScroller;
+    private Zoomer mZoomer;
+    private PointF mZoomFocalPoint = new PointF();
+    private RectF mScrollerStartViewport = new RectF(); // Used only for zooms and flings.
+
+    // Edge effect / overscroll tracking objects.
+    private EdgeEffectCompat mEdgeEffectTop;
+    private EdgeEffectCompat mEdgeEffectBottom;
+    private EdgeEffectCompat mEdgeEffectLeft;
+    private EdgeEffectCompat mEdgeEffectRight;
+
+    private boolean mEdgeEffectTopActive;
+    private boolean mEdgeEffectBottomActive;
+    private boolean mEdgeEffectLeftActive;
+    private boolean mEdgeEffectRightActive;
+
+    // Buffers for storing current X and Y stops. See the computeAxisStops method for more details.
+    private final AxisStops mXStopsBuffer = new AxisStops();
+    private final AxisStops mYStopsBuffer = new AxisStops();
+
+    // Buffers used during drawing. These are defined as fields to avoid allocation during
+    // draw calls.
+    private float[] mAxisXPositionsBuffer = new float[]{};
+    private float[] mAxisYPositionsBuffer = new float[]{};
+    private float[] mAxisXLinesBuffer = new float[]{};
+    private float[] mAxisYLinesBuffer = new float[]{};
+    private float[] mSeriesLinesBuffer = new float[(DRAW_STEPS + 1) * 4];
+    private final char[] mLabelBuffer = new char[100];
+    private Point mSurfaceSizeBuffer = new Point();
+
+    /**
+     * The simple math function Y = fun(X) to draw on the chart.
+     * @param x The X value
+     * @return The Y value
+     */
+    protected static float fun(float x) {
+        return (float) Math.pow(x, 3) - x / 4;
+    }
+
+    public InteractiveLineGraphView(Context context) {
+        this(context, null, 0);
+    }
+
+    public InteractiveLineGraphView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public InteractiveLineGraphView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        TypedArray a = context.getTheme().obtainStyledAttributes(
+                attrs, R.styleable.InteractiveLineGraphView, defStyle, defStyle);
+
+        try {
+            mLabelTextColor = a.getColor(
+                    R.styleable.InteractiveLineGraphView_labelTextColor, mLabelTextColor);
+            mLabelTextSize = a.getDimension(
+                    R.styleable.InteractiveLineGraphView_labelTextSize, mLabelTextSize);
+            mLabelSeparation = a.getDimensionPixelSize(
+                    R.styleable.InteractiveLineGraphView_labelSeparation, mLabelSeparation);
+
+            mGridThickness = a.getDimension(
+                    R.styleable.InteractiveLineGraphView_gridThickness, mGridThickness);
+            mGridColor = a.getColor(
+                    R.styleable.InteractiveLineGraphView_gridColor, mGridColor);
+
+            mAxisThickness = a.getDimension(
+                    R.styleable.InteractiveLineGraphView_axisThickness, mAxisThickness);
+            mAxisColor = a.getColor(
+                    R.styleable.InteractiveLineGraphView_axisColor, mAxisColor);
+
+            mDataThickness = a.getDimension(
+                    R.styleable.InteractiveLineGraphView_dataThickness, mDataThickness);
+            mDataColor = a.getColor(
+                    R.styleable.InteractiveLineGraphView_dataColor, mDataColor);
+        } finally {
+            a.recycle();
+        }
+
+        initPaints();
+
+        // Sets up interactions
+        mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener);
+        mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
+
+        mScroller = new OverScroller(context);
+        mZoomer = new Zoomer(context);
+
+        // Sets up edge effects
+        mEdgeEffectLeft = new EdgeEffectCompat(context);
+        mEdgeEffectTop = new EdgeEffectCompat(context);
+        mEdgeEffectRight = new EdgeEffectCompat(context);
+        mEdgeEffectBottom = new EdgeEffectCompat(context);
+    }
+
+    /**
+     * (Re)initializes {@link Paint} objects based on current attribute values.
+     */
+    private void initPaints() {
+        mLabelTextPaint = new Paint();
+        mLabelTextPaint.setAntiAlias(true);
+        mLabelTextPaint.setTextSize(mLabelTextSize);
+        mLabelTextPaint.setColor(mLabelTextColor);
+        mLabelHeight = (int) Math.abs(mLabelTextPaint.getFontMetrics().top);
+        mMaxLabelWidth = (int) mLabelTextPaint.measureText("0000");
+
+        mGridPaint = new Paint();
+        mGridPaint.setStrokeWidth(mGridThickness);
+        mGridPaint.setColor(mGridColor);
+        mGridPaint.setStyle(Paint.Style.STROKE);
+
+        mAxisPaint = new Paint();
+        mAxisPaint.setStrokeWidth(mAxisThickness);
+        mAxisPaint.setColor(mAxisColor);
+        mAxisPaint.setStyle(Paint.Style.STROKE);
+
+        mDataPaint = new Paint();
+        mDataPaint.setStrokeWidth(mDataThickness);
+        mDataPaint.setColor(mDataColor);
+        mDataPaint.setStyle(Paint.Style.STROKE);
+        mDataPaint.setAntiAlias(true);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        mContentRect.set(
+                getPaddingLeft() + mMaxLabelWidth + mLabelSeparation,
+                getPaddingTop(),
+                getWidth() - getPaddingRight(),
+                getHeight() - getPaddingBottom() - mLabelHeight - mLabelSeparation);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int minChartSize = getResources().getDimensionPixelSize(R.dimen.min_chart_size);
+        setMeasuredDimension(
+                Math.max(getSuggestedMinimumWidth(),
+                        resolveSize(minChartSize + getPaddingLeft() + mMaxLabelWidth
+                                + mLabelSeparation + getPaddingRight(),
+                                widthMeasureSpec)),
+                Math.max(getSuggestedMinimumHeight(),
+                        resolveSize(minChartSize + getPaddingTop() + mLabelHeight
+                                + mLabelSeparation + getPaddingBottom(),
+                                heightMeasureSpec)));
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    //     Methods and objects related to drawing
+    //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Draws axes and text labels
+        drawAxes(canvas);
+
+        // Clips the next few drawing operations to the content area
+        int clipRestoreCount = canvas.save();
+        canvas.clipRect(mContentRect);
+
+        drawDataSeriesUnclipped(canvas);
+        drawEdgeEffectsUnclipped(canvas);
+
+        // Removes clipping rectangle
+        canvas.restoreToCount(clipRestoreCount);
+
+        // Draws chart container
+        canvas.drawRect(mContentRect, mAxisPaint);
+    }
+
+    /**
+     * Draws the chart axes and labels onto the canvas.
+     */
+    private void drawAxes(Canvas canvas) {
+        // Computes axis stops (in terms of numerical value and position on screen)
+        int i;
+
+        computeAxisStops(
+                mCurrentViewport.left,
+                mCurrentViewport.right,
+                mContentRect.width() / mMaxLabelWidth / 2,
+                mXStopsBuffer);
+        computeAxisStops(
+                mCurrentViewport.top,
+                mCurrentViewport.bottom,
+                mContentRect.height() / mLabelHeight / 2,
+                mYStopsBuffer);
+
+        // Avoid unnecessary allocations during drawing. Re-use allocated
+        // arrays and only reallocate if the number of stops grows.
+        if (mAxisXPositionsBuffer.length < mXStopsBuffer.numStops) {
+            mAxisXPositionsBuffer = new float[mXStopsBuffer.numStops];
+        }
+        if (mAxisYPositionsBuffer.length < mYStopsBuffer.numStops) {
+            mAxisYPositionsBuffer = new float[mYStopsBuffer.numStops];
+        }
+        if (mAxisXLinesBuffer.length < mXStopsBuffer.numStops * 4) {
+            mAxisXLinesBuffer = new float[mXStopsBuffer.numStops * 4];
+        }
+        if (mAxisYLinesBuffer.length < mYStopsBuffer.numStops * 4) {
+            mAxisYLinesBuffer = new float[mYStopsBuffer.numStops * 4];
+        }
+
+        // Compute positions
+        for (i = 0; i < mXStopsBuffer.numStops; i++) {
+            mAxisXPositionsBuffer[i] = getDrawX(mXStopsBuffer.stops[i]);
+        }
+        for (i = 0; i < mYStopsBuffer.numStops; i++) {
+            mAxisYPositionsBuffer[i] = getDrawY(mYStopsBuffer.stops[i]);
+        }
+
+        // Draws grid lines using drawLines (faster than individual drawLine calls)
+        for (i = 0; i < mXStopsBuffer.numStops; i++) {
+            mAxisXLinesBuffer[i * 4 + 0] = (float) Math.floor(mAxisXPositionsBuffer[i]);
+            mAxisXLinesBuffer[i * 4 + 1] = mContentRect.top;
+            mAxisXLinesBuffer[i * 4 + 2] = (float) Math.floor(mAxisXPositionsBuffer[i]);
+            mAxisXLinesBuffer[i * 4 + 3] = mContentRect.bottom;
+        }
+        canvas.drawLines(mAxisXLinesBuffer, 0, mXStopsBuffer.numStops * 4, mGridPaint);
+
+        for (i = 0; i < mYStopsBuffer.numStops; i++) {
+            mAxisYLinesBuffer[i * 4 + 0] = mContentRect.left;
+            mAxisYLinesBuffer[i * 4 + 1] = (float) Math.floor(mAxisYPositionsBuffer[i]);
+            mAxisYLinesBuffer[i * 4 + 2] = mContentRect.right;
+            mAxisYLinesBuffer[i * 4 + 3] = (float) Math.floor(mAxisYPositionsBuffer[i]);
+        }
+        canvas.drawLines(mAxisYLinesBuffer, 0, mYStopsBuffer.numStops * 4, mGridPaint);
+
+        // Draws X labels
+        int labelOffset;
+        int labelLength;
+        mLabelTextPaint.setTextAlign(Paint.Align.CENTER);
+        for (i = 0; i < mXStopsBuffer.numStops; i++) {
+            // Do not use String.format in high-performance code such as onDraw code.
+            labelLength = formatFloat(mLabelBuffer, mXStopsBuffer.stops[i], mXStopsBuffer.decimals);
+            labelOffset = mLabelBuffer.length - labelLength;
+            canvas.drawText(
+                    mLabelBuffer, labelOffset, labelLength,
+                    mAxisXPositionsBuffer[i],
+                    mContentRect.bottom + mLabelHeight + mLabelSeparation,
+                    mLabelTextPaint);
+        }
+
+        // Draws Y labels
+        mLabelTextPaint.setTextAlign(Paint.Align.RIGHT);
+        for (i = 0; i < mYStopsBuffer.numStops; i++) {
+            // Do not use String.format in high-performance code such as onDraw code.
+            labelLength = formatFloat(mLabelBuffer, mYStopsBuffer.stops[i], mYStopsBuffer.decimals);
+            labelOffset = mLabelBuffer.length - labelLength;
+            canvas.drawText(
+                    mLabelBuffer, labelOffset, labelLength,
+                    mContentRect.left - mLabelSeparation,
+                    mAxisYPositionsBuffer[i] + mLabelHeight / 2,
+                    mLabelTextPaint);
+        }
+    }
+
+    /**
+     * Rounds the given number to the given number of significant digits. Based on an answer on
+     * <a href="http://stackoverflow.com/questions/202302">Stack Overflow</a>.
+     */
+    private static float roundToOneSignificantFigure(double num) {
+        final float d = (float) Math.ceil((float) Math.log10(num < 0 ? -num : num));
+        final int power = 1 - (int) d;
+        final float magnitude = (float) Math.pow(10, power);
+        final long shifted = Math.round(num * magnitude);
+        return shifted / magnitude;
+    }
+
+    private static final int POW10[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
+
+    /**
+     * Formats a float value to the given number of decimals. Returns the length of the string.
+     * The string begins at out.length - [return value].
+     */
+    private static int formatFloat(final char[] out, float val, int digits) {
+        boolean negative = false;
+        if (val == 0) {
+            out[out.length - 1] = '0';
+            return 1;
+        }
+        if (val < 0) {
+            negative = true;
+            val = -val;
+        }
+        if (digits > POW10.length) {
+            digits = POW10.length - 1;
+        }
+        val *= POW10[digits];
+        long lval = Math.round(val);
+        int index = out.length - 1;
+        int charCount = 0;
+        while (lval != 0 || charCount < (digits + 1)) {
+            int digit = (int) (lval % 10);
+            lval = lval / 10;
+            out[index--] = (char) (digit + '0');
+            charCount++;
+            if (charCount == digits) {
+                out[index--] = '.';
+                charCount++;
+            }
+        }
+        if (negative) {
+            out[index--] = '-';
+            charCount++;
+        }
+        return charCount;
+    }
+
+    /**
+     * Computes the set of axis labels to show given start and stop boundaries and an ideal number
+     * of stops between these boundaries.
+     *
+     * @param start The minimum extreme (e.g. the left edge) for the axis.
+     * @param stop The maximum extreme (e.g. the right edge) for the axis.
+     * @param steps The ideal number of stops to create. This should be based on available screen
+     *              space; the more space there is, the more stops should be shown.
+     * @param outStops The destination {@link AxisStops} object to populate.
+     */
+    private static void computeAxisStops(float start, float stop, int steps, AxisStops outStops) {
+        double range = stop - start;
+        if (steps == 0 || range <= 0) {
+            outStops.stops = new float[]{};
+            outStops.numStops = 0;
+            return;
+        }
+
+        double rawInterval = range / steps;
+        double interval = roundToOneSignificantFigure(rawInterval);
+        double intervalMagnitude = Math.pow(10, (int) Math.log10(interval));
+        int intervalSigDigit = (int) (interval / intervalMagnitude);
+        if (intervalSigDigit > 5) {
+            // Use one order of magnitude higher, to avoid intervals like 0.9 or 90
+            interval = Math.floor(10 * intervalMagnitude);
+        }
+
+        double first = Math.ceil(start / interval) * interval;
+        double last = Math.nextUp(Math.floor(stop / interval) * interval);
+
+        double f;
+        int i;
+        int n = 0;
+        for (f = first; f <= last; f += interval) {
+            ++n;
+        }
+
+        outStops.numStops = n;
+
+        if (outStops.stops.length < n) {
+            // Ensure stops contains at least numStops elements.
+            outStops.stops = new float[n];
+        }
+
+        for (f = first, i = 0; i < n; f += interval, ++i) {
+            outStops.stops[i] = (float) f;
+        }
+
+        if (interval < 1) {
+            outStops.decimals = (int) Math.ceil(-Math.log10(interval));
+        } else {
+            outStops.decimals = 0;
+        }
+    }
+
+    /**
+     * Computes the pixel offset for the given X chart value. This may be outside the view bounds.
+     */
+    private float getDrawX(float x) {
+        return mContentRect.left
+                + mContentRect.width()
+                * (x - mCurrentViewport.left) / mCurrentViewport.width();
+    }
+
+    /**
+     * Computes the pixel offset for the given Y chart value. This may be outside the view bounds.
+     */
+    private float getDrawY(float y) {
+        return mContentRect.bottom
+                - mContentRect.height()
+                * (y - mCurrentViewport.top) / mCurrentViewport.height();
+    }
+
+    /**
+     * Draws the currently visible portion of the data series defined by {@link #fun(float)} to the
+     * canvas. This method does not clip its drawing, so users should call {@link Canvas#clipRect
+     * before calling this method.
+     */
+    private void drawDataSeriesUnclipped(Canvas canvas) {
+        mSeriesLinesBuffer[0] = mContentRect.left;
+        mSeriesLinesBuffer[1] = getDrawY(fun(mCurrentViewport.left));
+        mSeriesLinesBuffer[2] = mSeriesLinesBuffer[0];
+        mSeriesLinesBuffer[3] = mSeriesLinesBuffer[1];
+        float x;
+        for (int i = 1; i <= DRAW_STEPS; i++) {
+            mSeriesLinesBuffer[i * 4 + 0] = mSeriesLinesBuffer[(i - 1) * 4 + 2];
+            mSeriesLinesBuffer[i * 4 + 1] = mSeriesLinesBuffer[(i - 1) * 4 + 3];
+
+            x = (mCurrentViewport.left + (mCurrentViewport.width() / DRAW_STEPS * i));
+            mSeriesLinesBuffer[i * 4 + 2] = getDrawX(x);
+            mSeriesLinesBuffer[i * 4 + 3] = getDrawY(fun(x));
+        }
+        canvas.drawLines(mSeriesLinesBuffer, mDataPaint);
+    }
+
+    /**
+     * Draws the overscroll "glow" at the four edges of the chart region, if necessary. The edges
+     * of the chart region are stored in {@link #mContentRect}.
+     *
+     * @see EdgeEffectCompat
+     */
+    private void drawEdgeEffectsUnclipped(Canvas canvas) {
+        // The methods below rotate and translate the canvas as needed before drawing the glow,
+        // since EdgeEffectCompat always draws a top-glow at 0,0.
+
+        boolean needsInvalidate = false;
+
+        if (!mEdgeEffectTop.isFinished()) {
+            final int restoreCount = canvas.save();
+            canvas.translate(mContentRect.left, mContentRect.top);
+            mEdgeEffectTop.setSize(mContentRect.width(), mContentRect.height());
+            if (mEdgeEffectTop.draw(canvas)) {
+                needsInvalidate = true;
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+
+        if (!mEdgeEffectBottom.isFinished()) {
+            final int restoreCount = canvas.save();
+            canvas.translate(2 * mContentRect.left - mContentRect.right, mContentRect.bottom);
+            canvas.rotate(180, mContentRect.width(), 0);
+            mEdgeEffectBottom.setSize(mContentRect.width(), mContentRect.height());
+            if (mEdgeEffectBottom.draw(canvas)) {
+                needsInvalidate = true;
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+
+        if (!mEdgeEffectLeft.isFinished()) {
+            final int restoreCount = canvas.save();
+            canvas.translate(mContentRect.left, mContentRect.bottom);
+            canvas.rotate(-90, 0, 0);
+            mEdgeEffectLeft.setSize(mContentRect.height(), mContentRect.width());
+            if (mEdgeEffectLeft.draw(canvas)) {
+                needsInvalidate = true;
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+
+        if (!mEdgeEffectRight.isFinished()) {
+            final int restoreCount = canvas.save();
+            canvas.translate(mContentRect.right, mContentRect.top);
+            canvas.rotate(90, 0, 0);
+            mEdgeEffectRight.setSize(mContentRect.height(), mContentRect.width());
+            if (mEdgeEffectRight.draw(canvas)) {
+                needsInvalidate = true;
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+
+        if (needsInvalidate) {
+            ViewCompat.postInvalidateOnAnimation(this);
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    //     Methods and objects related to gesture handling
+    //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Finds the chart point (i.e. within the chart's domain and range) represented by the
+     * given pixel coordinates, if that pixel is within the chart region described by
+     * {@link #mContentRect}. If the point is found, the "dest" argument is set to the point and
+     * this function returns true. Otherwise, this function returns false and "dest" is unchanged.
+     */
+    private boolean hitTest(float x, float y, PointF dest) {
+        if (!mContentRect.contains((int) x, (int) y)) {
+            return false;
+        }
+
+        dest.set(
+                mCurrentViewport.left
+                        + mCurrentViewport.width()
+                        * (x - mContentRect.left) / mContentRect.width(),
+                mCurrentViewport.top
+                        + mCurrentViewport.height()
+                        * (y - mContentRect.bottom) / -mContentRect.height());
+        return true;
+     }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean retVal = mScaleGestureDetector.onTouchEvent(event);
+        retVal = mGestureDetector.onTouchEvent(event) || retVal;
+        return retVal || super.onTouchEvent(event);
+    }
+
+    /**
+     * The scale listener, used for handling multi-finger scale gestures.
+     */
+    private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
+            = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+        /**
+         * This is the active focal point in terms of the viewport. Could be a local
+         * variable but kept here to minimize per-frame allocations.
+         */
+        private PointF viewportFocus = new PointF();
+        private float lastSpanX;
+        private float lastSpanY;
+
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
+            lastSpanX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector);
+            lastSpanY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector);
+            return true;
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
+            float spanX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector);
+            float spanY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector);
+
+            float newWidth = lastSpanX / spanX * mCurrentViewport.width();
+            float newHeight = lastSpanY / spanY * mCurrentViewport.height();
+
+            float focusX = scaleGestureDetector.getFocusX();
+            float focusY = scaleGestureDetector.getFocusY();
+            hitTest(focusX, focusY, viewportFocus);
+
+            mCurrentViewport.set(
+                    viewportFocus.x
+                            - newWidth * (focusX - mContentRect.left)
+                            / mContentRect.width(),
+                    viewportFocus.y
+                            - newHeight * (mContentRect.bottom - focusY)
+                            / mContentRect.height(),
+                    0,
+                    0);
+            mCurrentViewport.right = mCurrentViewport.left + newWidth;
+            mCurrentViewport.bottom = mCurrentViewport.top + newHeight;
+            constrainViewport();
+            ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
+
+            lastSpanX = spanX;
+            lastSpanY = spanY;
+            return true;
+        }
+    };
+
+    /**
+     * Ensures that current viewport is inside the viewport extremes defined by {@link #AXIS_X_MIN},
+     * {@link #AXIS_X_MAX}, {@link #AXIS_Y_MIN} and {@link #AXIS_Y_MAX}.
+     */
+    private void constrainViewport() {
+        mCurrentViewport.left = Math.max(AXIS_X_MIN, mCurrentViewport.left);
+        mCurrentViewport.top = Math.max(AXIS_Y_MIN, mCurrentViewport.top);
+        mCurrentViewport.bottom = Math.max(Math.nextUp(mCurrentViewport.top),
+                Math.min(AXIS_Y_MAX, mCurrentViewport.bottom));
+        mCurrentViewport.right = Math.max(Math.nextUp(mCurrentViewport.left),
+                Math.min(AXIS_X_MAX, mCurrentViewport.right));
+    }
+
+    /**
+     * The gesture listener, used for handling simple gestures such as double touches, scrolls,
+     * and flings.
+     */
+    private final GestureDetector.SimpleOnGestureListener mGestureListener
+            = new GestureDetector.SimpleOnGestureListener() {
+        @Override
+        public boolean onDown(MotionEvent e) {
+            releaseEdgeEffects();
+            mScrollerStartViewport.set(mCurrentViewport);
+            mScroller.forceFinished(true);
+            ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
+            return true;
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            mZoomer.forceFinished(true);
+            if (hitTest(e.getX(), e.getY(), mZoomFocalPoint)) {
+                mZoomer.startZoom(ZOOM_AMOUNT);
+            }
+            ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
+            return true;
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+            // Scrolling uses math based on the viewport (as opposed to math using pixels).
+            /**
+             * Pixel offset is the offset in screen pixels, while viewport offset is the
+             * offset within the current viewport. For additional information on surface sizes
+             * and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
+             * additional information about the viewport, see the comments for
+             * {@link mCurrentViewport}.
+             */
+            float viewportOffsetX = distanceX * mCurrentViewport.width() / mContentRect.width();
+            float viewportOffsetY = -distanceY * mCurrentViewport.height() / mContentRect.height();
+            computeScrollSurfaceSize(mSurfaceSizeBuffer);
+            int scrolledX = (int) (mSurfaceSizeBuffer.x
+                    * (mCurrentViewport.left + viewportOffsetX - AXIS_X_MIN)
+                    / (AXIS_X_MAX - AXIS_X_MIN));
+            int scrolledY = (int) (mSurfaceSizeBuffer.y
+                    * (AXIS_Y_MAX - mCurrentViewport.bottom - viewportOffsetY)
+                    / (AXIS_Y_MAX - AXIS_Y_MIN));
+            boolean canScrollX = mCurrentViewport.left > AXIS_X_MIN
+                    || mCurrentViewport.right < AXIS_X_MAX;
+            boolean canScrollY = mCurrentViewport.top > AXIS_Y_MIN
+                    || mCurrentViewport.bottom < AXIS_Y_MAX;
+            setViewportBottomLeft(
+                    mCurrentViewport.left + viewportOffsetX,
+                    mCurrentViewport.bottom + viewportOffsetY);
+
+            if (canScrollX && scrolledX < 0) {
+                mEdgeEffectLeft.onPull(scrolledX / (float) mContentRect.width());
+                mEdgeEffectLeftActive = true;
+            }
+            if (canScrollY && scrolledY < 0) {
+                mEdgeEffectTop.onPull(scrolledY / (float) mContentRect.height());
+                mEdgeEffectTopActive = true;
+            }
+            if (canScrollX && scrolledX > mSurfaceSizeBuffer.x - mContentRect.width()) {
+                mEdgeEffectRight.onPull((scrolledX - mSurfaceSizeBuffer.x + mContentRect.width())
+                        / (float) mContentRect.width());
+                mEdgeEffectRightActive = true;
+            }
+            if (canScrollY && scrolledY > mSurfaceSizeBuffer.y - mContentRect.height()) {
+                mEdgeEffectBottom.onPull((scrolledY - mSurfaceSizeBuffer.y + mContentRect.height())
+                        / (float) mContentRect.height());
+                mEdgeEffectBottomActive = true;
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+            fling((int) -velocityX, (int) -velocityY);
+            return true;
+        }
+    };
+
+    private void releaseEdgeEffects() {
+        mEdgeEffectLeftActive
+                = mEdgeEffectTopActive
+                = mEdgeEffectRightActive
+                = mEdgeEffectBottomActive
+                = false;
+        mEdgeEffectLeft.onRelease();
+        mEdgeEffectTop.onRelease();
+        mEdgeEffectRight.onRelease();
+        mEdgeEffectBottom.onRelease();
+    }
+
+    private void fling(int velocityX, int velocityY) {
+        releaseEdgeEffects();
+        // Flings use math in pixels (as opposed to math based on the viewport).
+        computeScrollSurfaceSize(mSurfaceSizeBuffer);
+        mScrollerStartViewport.set(mCurrentViewport);
+        int startX = (int) (mSurfaceSizeBuffer.x * (mScrollerStartViewport.left - AXIS_X_MIN) / (
+                AXIS_X_MAX - AXIS_X_MIN));
+        int startY = (int) (mSurfaceSizeBuffer.y * (AXIS_Y_MAX - mScrollerStartViewport.bottom) / (
+                AXIS_Y_MAX - AXIS_Y_MIN));
+        mScroller.forceFinished(true);
+        mScroller.fling(
+                startX,
+                startY,
+                velocityX,
+                velocityY,
+                0, mSurfaceSizeBuffer.x - mContentRect.width(),
+                0, mSurfaceSizeBuffer.y - mContentRect.height(),
+                mContentRect.width() / 2,
+                mContentRect.height() / 2);
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    /**
+     * Computes the current scrollable surface size, in pixels. For example, if the entire chart
+     * area is visible, this is simply the current size of {@link #mContentRect}. If the chart
+     * is zoomed in 200% in both directions, the returned size will be twice as large horizontally
+     * and vertically.
+     */
+    private void computeScrollSurfaceSize(Point out) {
+        out.set(
+                (int) (mContentRect.width() * (AXIS_X_MAX - AXIS_X_MIN)
+                        / mCurrentViewport.width()),
+                (int) (mContentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN)
+                        / mCurrentViewport.height()));
+    }
+
+    @Override
+    public void computeScroll() {
+        super.computeScroll();
+
+        boolean needsInvalidate = false;
+
+        if (mScroller.computeScrollOffset()) {
+            // The scroller isn't finished, meaning a fling or programmatic pan operation is
+            // currently active.
+
+            computeScrollSurfaceSize(mSurfaceSizeBuffer);
+            int currX = mScroller.getCurrX();
+            int currY = mScroller.getCurrY();
+
+            boolean canScrollX = (mCurrentViewport.left > AXIS_X_MIN
+                    || mCurrentViewport.right < AXIS_X_MAX);
+            boolean canScrollY = (mCurrentViewport.top > AXIS_Y_MIN
+                    || mCurrentViewport.bottom < AXIS_Y_MAX);
+
+            if (canScrollX
+                    && currX < 0
+                    && mEdgeEffectLeft.isFinished()
+                    && !mEdgeEffectLeftActive) {
+                mEdgeEffectLeft.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
+                mEdgeEffectLeftActive = true;
+                needsInvalidate = true;
+            } else if (canScrollX
+                    && currX > (mSurfaceSizeBuffer.x - mContentRect.width())
+                    && mEdgeEffectRight.isFinished()
+                    && !mEdgeEffectRightActive) {
+                mEdgeEffectRight.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
+                mEdgeEffectRightActive = true;
+                needsInvalidate = true;
+            }
+
+            if (canScrollY
+                    && currY < 0
+                    && mEdgeEffectTop.isFinished()
+                    && !mEdgeEffectTopActive) {
+                mEdgeEffectTop.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
+                mEdgeEffectTopActive = true;
+                needsInvalidate = true;
+            } else if (canScrollY
+                    && currY > (mSurfaceSizeBuffer.y - mContentRect.height())
+                    && mEdgeEffectBottom.isFinished()
+                    && !mEdgeEffectBottomActive) {
+                mEdgeEffectBottom.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
+                mEdgeEffectBottomActive = true;
+                needsInvalidate = true;
+            }
+
+            float currXRange = AXIS_X_MIN + (AXIS_X_MAX - AXIS_X_MIN)
+                    * currX / mSurfaceSizeBuffer.x;
+            float currYRange = AXIS_Y_MAX - (AXIS_Y_MAX - AXIS_Y_MIN)
+                    * currY / mSurfaceSizeBuffer.y;
+            setViewportBottomLeft(currXRange, currYRange);
+        }
+
+        if (mZoomer.computeZoom()) {
+            // Performs the zoom since a zoom is in progress (either programmatically or via
+            // double-touch).
+            float newWidth = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.width();
+            float newHeight = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.height();
+            float pointWithinViewportX = (mZoomFocalPoint.x - mScrollerStartViewport.left)
+                    / mScrollerStartViewport.width();
+            float pointWithinViewportY = (mZoomFocalPoint.y - mScrollerStartViewport.top)
+                    / mScrollerStartViewport.height();
+            mCurrentViewport.set(
+                    mZoomFocalPoint.x - newWidth * pointWithinViewportX,
+                    mZoomFocalPoint.y - newHeight * pointWithinViewportY,
+                    mZoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
+                    mZoomFocalPoint.y + newHeight * (1 - pointWithinViewportY));
+            constrainViewport();
+            needsInvalidate = true;
+        }
+
+        if (needsInvalidate) {
+            ViewCompat.postInvalidateOnAnimation(this);
+        }
+    }
+
+    /**
+     * Sets the current viewport (defined by {@link #mCurrentViewport}) to the given
+     * X and Y positions. Note that the Y value represents the topmost pixel position, and thus
+     * the bottom of the {@link #mCurrentViewport} rectangle. For more details on why top and
+     * bottom are flipped, see {@link #mCurrentViewport}.
+     */
+    private void setViewportBottomLeft(float x, float y) {
+        /**
+         * Constrains within the scroll range. The scroll range is simply the viewport extremes
+         * (AXIS_X_MAX, etc.) minus the viewport size. For example, if the extrema were 0 and 10,
+         * and the viewport size was 2, the scroll range would be 0 to 8.
+         */
+
+        float curWidth = mCurrentViewport.width();
+        float curHeight = mCurrentViewport.height();
+        x = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth));
+        y = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX));
+
+        mCurrentViewport.set(x, y - curHeight, x + curWidth, y);
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    //     Methods for programmatically changing the viewport
+    //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Returns the current viewport (visible extremes for the chart domain and range.)
+     */
+    public RectF getCurrentViewport() {
+        return new RectF(mCurrentViewport);
+    }
+
+    /**
+     * Sets the chart's current viewport.
+     *
+     * @see #getCurrentViewport()
+     */
+    public void setCurrentViewport(RectF viewport) {
+        mCurrentViewport = viewport;
+        constrainViewport();
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    /**
+     * Smoothly zooms the chart in one step.
+     */
+    public void zoomIn() {
+        mScrollerStartViewport.set(mCurrentViewport);
+        mZoomer.forceFinished(true);
+        mZoomer.startZoom(ZOOM_AMOUNT);
+        mZoomFocalPoint.set(
+                (mCurrentViewport.right + mCurrentViewport.left) / 2,
+                (mCurrentViewport.bottom + mCurrentViewport.top) / 2);
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    /**
+     * Smoothly zooms the chart out one step.
+     */
+    public void zoomOut() {
+        mScrollerStartViewport.set(mCurrentViewport);
+        mZoomer.forceFinished(true);
+        mZoomer.startZoom(-ZOOM_AMOUNT);
+        mZoomFocalPoint.set(
+                (mCurrentViewport.right + mCurrentViewport.left) / 2,
+                (mCurrentViewport.bottom + mCurrentViewport.top) / 2);
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    /**
+     * Smoothly pans the chart left one step.
+     */
+    public void panLeft() {
+        fling((int) (-PAN_VELOCITY_FACTOR * getWidth()), 0);
+    }
+
+    /**
+     * Smoothly pans the chart right one step.
+     */
+    public void panRight() {
+        fling((int) (PAN_VELOCITY_FACTOR * getWidth()), 0);
+    }
+
+    /**
+     * Smoothly pans the chart up one step.
+     */
+    public void panUp() {
+        fling(0, (int) (-PAN_VELOCITY_FACTOR * getHeight()));
+    }
+
+    /**
+     * Smoothly pans the chart down one step.
+     */
+    public void panDown() {
+        fling(0, (int) (PAN_VELOCITY_FACTOR * getHeight()));
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    //     Methods related to custom attributes
+    //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public float getLabelTextSize() {
+        return mLabelTextSize;
+    }
+
+    public void setLabelTextSize(float labelTextSize) {
+        mLabelTextSize = labelTextSize;
+        initPaints();
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    public int getLabelTextColor() {
+        return mLabelTextColor;
+    }
+
+    public void setLabelTextColor(int labelTextColor) {
+        mLabelTextColor = labelTextColor;
+        initPaints();
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    public float getGridThickness() {
+        return mGridThickness;
+    }
+
+    public void setGridThickness(float gridThickness) {
+        mGridThickness = gridThickness;
+        initPaints();
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    public int getGridColor() {
+        return mGridColor;
+    }
+
+    public void setGridColor(int gridColor) {
+        mGridColor = gridColor;
+        initPaints();
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    public float getAxisThickness() {
+        return mAxisThickness;
+    }
+
+    public void setAxisThickness(float axisThickness) {
+        mAxisThickness = axisThickness;
+        initPaints();
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    public int getAxisColor() {
+        return mAxisColor;
+    }
+
+    public void setAxisColor(int axisColor) {
+        mAxisColor = axisColor;
+        initPaints();
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    public float getDataThickness() {
+        return mDataThickness;
+    }
+
+    public void setDataThickness(float dataThickness) {
+        mDataThickness = dataThickness;
+    }
+
+    public int getDataColor() {
+        return mDataColor;
+    }
+
+    public void setDataColor(int dataColor) {
+        mDataColor = dataColor;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    //     Methods and classes related to view state persistence.
+    //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.viewport = mCurrentViewport;
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            super.onRestoreInstanceState(state);
+            return;
+        }
+
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        mCurrentViewport = ss.viewport;
+    }
+
+    /**
+     * Persistent state that is saved by InteractiveLineGraphView.
+     */
+    public static class SavedState extends BaseSavedState {
+        private RectF viewport;
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeFloat(viewport.left);
+            out.writeFloat(viewport.top);
+            out.writeFloat(viewport.right);
+            out.writeFloat(viewport.bottom);
+        }
+
+        @Override
+        public String toString() {
+            return "InteractiveLineGraphView.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " viewport=" + viewport.toString() + "}";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        });
+
+        SavedState(Parcel in) {
+            super(in);
+            viewport = new RectF(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat());
+        }
+    }
+
+    /**
+     * A simple class representing axis label values.
+     *
+     * @see #computeAxisStops
+     */
+    private static class AxisStops {
+        float[] stops = new float[]{};
+        int numStops;
+        int decimals;
+    }
+}
diff --git a/samples/training/InteractiveChart/src/com/example/android/interactivechart/MainActivity.java b/samples/training/InteractiveChart/src/com/example/android/interactivechart/MainActivity.java
new file mode 100755
index 0000000..7c1e56c
--- /dev/null
+++ b/samples/training/InteractiveChart/src/com/example/android/interactivechart/MainActivity.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.interactivechart;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class MainActivity extends Activity {
+    private InteractiveLineGraphView mGraphView;
+
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        mGraphView = (InteractiveLineGraphView) findViewById(R.id.chart);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.main, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.action_zoom_in:
+                mGraphView.zoomIn();
+                return true;
+
+            case R.id.action_zoom_out:
+                mGraphView.zoomOut();
+                return true;
+
+            case R.id.action_pan_left:
+                mGraphView.panLeft();
+                return true;
+
+            case R.id.action_pan_right:
+                mGraphView.panRight();
+                return true;
+
+            case R.id.action_pan_up:
+                mGraphView.panUp();
+                return true;
+
+            case R.id.action_pan_down:
+                mGraphView.panDown();
+                return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/samples/training/InteractiveChart/src/com/example/android/interactivechart/OverScrollerCompat.java b/samples/training/InteractiveChart/src/com/example/android/interactivechart/OverScrollerCompat.java
new file mode 100644
index 0000000..d33f062
--- /dev/null
+++ b/samples/training/InteractiveChart/src/com/example/android/interactivechart/OverScrollerCompat.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.interactivechart;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.widget.OverScroller;
+
+/**
+ * A utility class for using {@link android.widget.OverScroller} in a backward-compatible fashion.
+ */
+public class OverScrollerCompat {
+    /**
+     * Disallow instantiation.
+     */
+    private OverScrollerCompat() {
+    }
+
+    /**
+     * @see android.view.ScaleGestureDetector#getCurrentSpanY()
+     */
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+    public static float getCurrVelocity(OverScroller overScroller) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            return overScroller.getCurrVelocity();
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/samples/training/InteractiveChart/src/com/example/android/interactivechart/ScaleGestureDetectorCompat.java b/samples/training/InteractiveChart/src/com/example/android/interactivechart/ScaleGestureDetectorCompat.java
new file mode 100644
index 0000000..fecd9d7
--- /dev/null
+++ b/samples/training/InteractiveChart/src/com/example/android/interactivechart/ScaleGestureDetectorCompat.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.interactivechart;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.ScaleGestureDetector;
+
+/**
+ * A utility class for using {@link android.view.ScaleGestureDetector} in a backward-compatible
+ * fashion.
+ */
+public class ScaleGestureDetectorCompat {
+    /**
+     * Disallow instantiation.
+     */
+    private ScaleGestureDetectorCompat() {
+    }
+
+    /**
+     * @see android.view.ScaleGestureDetector#getCurrentSpanX()
+     */
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static float getCurrentSpanX(ScaleGestureDetector scaleGestureDetector) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            return scaleGestureDetector.getCurrentSpanX();
+        } else {
+            return scaleGestureDetector.getCurrentSpan();
+        }
+    }
+
+    /**
+     * @see android.view.ScaleGestureDetector#getCurrentSpanY()
+     */
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static float getCurrentSpanY(ScaleGestureDetector scaleGestureDetector) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            return scaleGestureDetector.getCurrentSpanY();
+        } else {
+            return scaleGestureDetector.getCurrentSpan();
+        }
+    }
+}
diff --git a/samples/training/InteractiveChart/src/com/example/android/interactivechart/Zoomer.java b/samples/training/InteractiveChart/src/com/example/android/interactivechart/Zoomer.java
new file mode 100644
index 0000000..1ae67e4
--- /dev/null
+++ b/samples/training/InteractiveChart/src/com/example/android/interactivechart/Zoomer.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.interactivechart;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * A simple class that animates double-touch zoom gestures. Functionally similar to a {@link
+ * android.widget.Scroller}.
+ */
+public class Zoomer {
+    /**
+     * The interpolator, used for making zooms animate 'naturally.'
+     */
+    private Interpolator mInterpolator;
+
+    /**
+     * The total animation duration for a zoom.
+     */
+    private int mAnimationDurationMillis;
+
+    /**
+     * Whether or not the current zoom has finished.
+     */
+    private boolean mFinished = true;
+
+    /**
+     * The current zoom value; computed by {@link #computeZoom()}.
+     */
+    private float mCurrentZoom;
+
+    /**
+     * The time the zoom started, computed using {@link android.os.SystemClock#elapsedRealtime()}.
+     */
+    private long mStartRTC;
+
+    /**
+     * The destination zoom factor.
+     */
+    private float mEndZoom;
+
+    public Zoomer(Context context) {
+        mInterpolator = new DecelerateInterpolator();
+        mAnimationDurationMillis = context.getResources().getInteger(
+                android.R.integer.config_shortAnimTime);
+    }
+
+    /**
+     * Forces the zoom finished state to the given value. Unlike {@link #abortAnimation()}, the
+     * current zoom value isn't set to the ending value.
+     *
+     * @see android.widget.Scroller#forceFinished(boolean)
+     */
+    public void forceFinished(boolean finished) {
+        mFinished = finished;
+    }
+
+    /**
+     * Aborts the animation, setting the current zoom value to the ending value.
+     *
+     * @see android.widget.Scroller#abortAnimation()
+     */
+    public void abortAnimation() {
+        mFinished = true;
+        mCurrentZoom = mEndZoom;
+    }
+
+    /**
+     * Starts a zoom from 1.0 to (1.0 + endZoom). That is, to zoom from 100% to 125%, endZoom should
+     * by 0.25f.
+     *
+     * @see android.widget.Scroller#startScroll(int, int, int, int)
+     */
+    public void startZoom(float endZoom) {
+        mStartRTC = SystemClock.elapsedRealtime();
+        mEndZoom = endZoom;
+
+        mFinished = false;
+        mCurrentZoom = 1f;
+    }
+
+    /**
+     * Computes the current zoom level, returning true if the zoom is still active and false if the
+     * zoom has finished.
+     *
+     * @see android.widget.Scroller#computeScrollOffset()
+     */
+    public boolean computeZoom() {
+        if (mFinished) {
+            return false;
+        }
+
+        long tRTC = SystemClock.elapsedRealtime() - mStartRTC;
+        if (tRTC >= mAnimationDurationMillis) {
+            mFinished = true;
+            mCurrentZoom = mEndZoom;
+            return false;
+        }
+
+        float t = tRTC * 1f / mAnimationDurationMillis;
+        mCurrentZoom = mEndZoom * mInterpolator.getInterpolation(t);
+        return true;
+    }
+
+    /**
+     * Returns the current zoom level.
+     *
+     * @see android.widget.Scroller#getCurrX()
+     */
+    public float getCurrZoom() {
+        return mCurrentZoom;
+    }
+}