diff options
author | Roman Nurik <romannurik@google.com> | 2013-01-24 14:19:29 -0800 |
---|---|---|
committer | Roman Nurik <romannurik@google.com> | 2013-03-11 16:22:53 -0700 |
commit | 85364937ce0f4fdd0d8387d3f46fdb8d41fb41dd (patch) | |
tree | b662fa5b0807d9ef297f63647d60b34a67988010 | |
parent | 0c0bad0598f532c52c93c104f909e9e42b63fe88 (diff) | |
download | android_development-85364937ce0f4fdd0d8387d3f46fdb8d41fb41dd.tar.gz android_development-85364937ce0f4fdd0d8387d3f46fdb8d41fb41dd.tar.bz2 android_development-85364937ce0f4fdd0d8387d3f46fdb8d41fb41dd.zip |
Add InteractiveChart sample code for update to Gestures training class.
Change-Id: I1e245cd6735d54603174045ca557057763861469
27 files changed, 1781 insertions, 0 deletions
diff --git a/samples/training/InteractiveChart/AndroidManifest.xml b/samples/training/InteractiveChart/AndroidManifest.xml new file mode 100755 index 000000000..1e29155ab --- /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 Binary files differnew file mode 100644 index 000000000..6080877d4 --- /dev/null +++ b/samples/training/InteractiveChart/libs/android-support-v4.jar diff --git a/samples/training/InteractiveChart/project.properties b/samples/training/InteractiveChart/project.properties new file mode 100755 index 000000000..7c903ad04 --- /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 Binary files differnew file mode 100644 index 000000000..9e2d6eebb --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-hdpi/ic_launcher.png diff --git a/samples/training/InteractiveChart/res/drawable-mdpi/ic_launcher.png b/samples/training/InteractiveChart/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..229293037 --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 000000000..38c7b20d7 --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_down.png 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 Binary files differnew file mode 100644 index 000000000..e97e910e0 --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_left.png 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 Binary files differnew file mode 100644 index 000000000..7dc45c0d4 --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_right.png 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 Binary files differnew file mode 100644 index 000000000..2ed325108 --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_pan_up.png 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 Binary files differnew file mode 100644 index 000000000..23b9a1c18 --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_in.png 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 Binary files differnew file mode 100644 index 000000000..bb952748c --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_action_zoom_out.png diff --git a/samples/training/InteractiveChart/res/drawable-xhdpi/ic_launcher.png b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..b1e36137e --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xhdpi/ic_launcher.png diff --git a/samples/training/InteractiveChart/res/drawable-xxhdpi/ic_launcher.png b/samples/training/InteractiveChart/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..2217834da --- /dev/null +++ b/samples/training/InteractiveChart/res/drawable-xxhdpi/ic_launcher.png 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 000000000..285d8ae18 --- /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 000000000..ad758e52d --- /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 000000000..a7073d413 --- /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 000000000..8ff79ca90 --- /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 000000000..530f994a3 --- /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 000000000..89e56cf73 --- /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 000000000..51561fd76 --- /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 000000000..b08da584e --- /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 000000000..ccb9f7edd --- /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 000000000..9407cbc9e --- /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 000000000..7c1e56ca8 --- /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 000000000..d33f0629b --- /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 000000000..fecd9d7f5 --- /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 000000000..1ae67e4a8 --- /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; + } +} |