1page.title=Animating a Scroll Gesture
2parent.title=Using Touch Gestures
3parent.link=index.html
4
5trainingnavtop=true
6next.title=Handling Multi-Touch Gestures
7next.link=multi.html
8
9@jd:body
10
11<div id="tb-wrapper">
12<div id="tb">
13
14<!-- table of contents -->
15<h2>This lesson teaches you to</h2>
16<ol>
17  <li><a href="#term">Understand Scrolling Terminology</a></li>
18  <li><a href="#scroll">Implement Touch-Based Scrolling</a></li>
19</ol>
20
21<!-- other docs (NOT javadocs) -->
22<h2>You should also read</h2>
23
24<ul>
25    <li><a href="http://developer.android.com/guide/topics/ui/ui-events.html">Input Events</a> API Guide
26    </li>
27    <li><a href="{@docRoot}guide/topics/sensors/sensors_overview.html">Sensors Overview</a></li>
28    <li><a href="{@docRoot}training/custom-views/making-interactive.html">Making the View Interactive</a> </li>
29    <li>Design Guide for <a href="{@docRoot}design/patterns/gestures.html">Gestures</a></li>
30    <li>Design Guide for <a href="{@docRoot}design/style/touch-feedback.html">Touch Feedback</a></li>
31</ul>
32
33<h2>Try it out</h2>
34
35<div class="download-box">
36  <a href="{@docRoot}shareables/training/InteractiveChart.zip"
37class="button">Download the sample</a>
38 <p class="filename">InteractiveChart.zip</p>
39</div>
40
41</div>
42</div>
43
44<p>In Android, scrolling is typically achieved by using the
45{@link android.widget.ScrollView}
46class. Any standard layout that might extend beyond the bounds of its container should be
47nested in a {@link android.widget.ScrollView} to provide a scrollable view that's
48managed by the framework. Implementing a custom scroller should only be
49necessary for special scenarios. This lesson describes such a scenario: displaying
50a scrolling effect in response to touch gestures using <em>scrollers</em>.
51
52
53<p>You can use scrollers ({@link android.widget.Scroller} or {@link
54android.widget.OverScroller}) to collect the data you need to produce a
55scrolling animation in response to a touch event. They are similar, but
56{@link android.widget.OverScroller}
57includes methods for indicating to users that they've reached the content edges
58after a pan or fling gesture. The {@code InteractiveChart} sample
59uses the {@link android.widget.EdgeEffect} class
60(actually the {@link android.support.v4.widget.EdgeEffectCompat} class)
61to display a "glow" effect when users reach the content edges.</p>
62
63<p class="note"><strong>Note:</strong> We recommend that you
64use {@link android.widget.OverScroller} rather than {@link
65android.widget.Scroller} for scrolling animations.
66{@link android.widget.OverScroller} provides the best backward
67compatibility with older devices.
68<br />
69Also note that you generally only need to use scrollers
70when implementing scrolling yourself. {@link android.widget.ScrollView} and
71{@link android.widget.HorizontalScrollView} do all of this for you if you nest your
72layout within them.
73</p>
74
75
76<p>A scroller is used  to animate scrolling over time, using platform-standard
77scrolling physics (friction, velocity, etc.). The scroller itself doesn't
78actually draw anything. Scrollers track scroll offsets for you over time, but
79they don't  automatically apply those positions to your view. It's your
80responsibility to get and apply new coordinates at a rate that will make the
81scrolling animation look smooth.</p>
82
83
84
85<h2 id="term">Understand Scrolling Terminology</h2>
86
87<p>"Scrolling" is a word that can take on different meanings in Android, depending on the context.</p>
88
89<p><strong>Scrolling</strong> is the general process of moving the viewport (that is, the 'window'
90of content you're looking at). When scrolling is in both the x and y axes, it's called
91<em>panning</em>. The sample application provided with this class, {@code InteractiveChart}, illustrates
92two different types of scrolling, dragging and flinging:</p>
93<ul>
94    <li><strong>Dragging</strong> is the type of scrolling that occurs when a user drags her
95finger across the touch screen. Simple dragging is often implemented by overriding
96{@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} in
97{@link android.view.GestureDetector.OnGestureListener}. For more discussion of dragging, see
98<a href="scale.html">Dragging and Scaling</a>.</li>
99
100    <li><strong>Flinging</strong> is the type of scrolling that occurs when a user
101drags and lifts her finger quickly. After the user lifts her finger, you generally
102want to keep scrolling (moving the viewport), but decelerate until the viewport stops moving.
103Flinging can be implemented by overriding
104{@link android.view.GestureDetector.OnGestureListener#onFling onFling()}
105in {@link android.view.GestureDetector.OnGestureListener}, and by using
106a scroller object. This is the use
107case that is the topic of this lesson.</li>
108</ul>
109
110<p>It's common to use scroller objects
111in conjunction with a fling gesture, but they
112can be used in pretty much any context where you want the UI to display
113scrolling in response to a touch event. For example, you could override
114{@link android.view.View#onTouchEvent onTouchEvent()} to process touch
115events directly, and produce a scrolling effect or a "snapping to page" animation
116in response to those touch events.</p>
117
118
119<h2 id="#scroll">Implement Touch-Based Scrolling</h2>
120
121<p>This section describes how to use a scroller.
122The snippet shown below comes from the {@code InteractiveChart} sample
123provided with this class.
124It uses a
125{@link android.view.GestureDetector}, and overrides the
126{@link android.view.GestureDetector.SimpleOnGestureListener} method
127{@link android.view.GestureDetector.OnGestureListener#onFling onFling()}.
128It uses {@link android.widget.OverScroller} to track the fling gesture.
129If the user reaches the content edges
130after the fling gesture, the app displays a "glow" effect.
131</p>
132
133<p class="note"><strong>Note:</strong> The {@code InteractiveChart} sample app displays a
134chart that you can zoom, pan, scroll, and so on. In the following snippet,
135{@code mContentRect} represents the rectangle coordinates within the view that the chart
136will be drawn into. At any given time, a subset of the total chart domain and range are drawn
137into this rectangular area.
138{@code mCurrentViewport} represents the portion of the chart that is currently
139visible in the screen. Because pixel offsets are generally treated as integers,
140{@code mContentRect} is of the type {@link android.graphics.Rect}. Because the
141graph domain and range are decimal/float values, {@code mCurrentViewport} is of
142the type {@link android.graphics.RectF}.</p>
143
144<p>The first part of the snippet shows the implementation of
145{@link android.view.GestureDetector.OnGestureListener#onFling onFling()}:</p>
146
147<pre>// The current viewport. This rectangle represents the currently visible
148// chart domain and range. The viewport is the part of the app that the
149// user manipulates via touch gestures.
150private RectF mCurrentViewport =
151        new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
152
153// The current destination rectangle (in pixel coordinates) into which the
154// chart data should be drawn.
155private Rect mContentRect;
156
157private OverScroller mScroller;
158private RectF mScrollerStartViewport;
159...
160private final GestureDetector.SimpleOnGestureListener mGestureListener
161        = new GestureDetector.SimpleOnGestureListener() {
162    &#64;Override
163    public boolean onDown(MotionEvent e) {
164        // Initiates the decay phase of any active edge effects.
165        releaseEdgeEffects();
166        mScrollerStartViewport.set(mCurrentViewport);
167        // Aborts any active scroll animations and invalidates.
168        mScroller.forceFinished(true);
169        ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
170        return true;
171    }
172    ...
173    &#64;Override
174    public boolean onFling(MotionEvent e1, MotionEvent e2,
175            float velocityX, float velocityY) {
176        fling((int) -velocityX, (int) -velocityY);
177        return true;
178    }
179};
180
181private void fling(int velocityX, int velocityY) {
182    // Initiates the decay phase of any active edge effects.
183    releaseEdgeEffects();
184    // Flings use math in pixels (as opposed to math based on the viewport).
185    Point surfaceSize = computeScrollSurfaceSize();
186    mScrollerStartViewport.set(mCurrentViewport);
187    int startX = (int) (surfaceSize.x * (mScrollerStartViewport.left -
188            AXIS_X_MIN) / (
189            AXIS_X_MAX - AXIS_X_MIN));
190    int startY = (int) (surfaceSize.y * (AXIS_Y_MAX -
191            mScrollerStartViewport.bottom) / (
192            AXIS_Y_MAX - AXIS_Y_MIN));
193    // Before flinging, aborts the current animation.
194    mScroller.forceFinished(true);
195    // Begins the animation
196    mScroller.fling(
197            // Current scroll position
198            startX,
199            startY,
200            velocityX,
201            velocityY,
202            /*
203             * Minimum and maximum scroll positions. The minimum scroll
204             * position is generally zero and the maximum scroll position
205             * is generally the content size less the screen size. So if the
206             * content width is 1000 pixels and the screen width is 200
207             * pixels, the maximum scroll offset should be 800 pixels.
208             */
209            0, surfaceSize.x - mContentRect.width(),
210            0, surfaceSize.y - mContentRect.height(),
211            // The edges of the content. This comes into play when using
212            // the EdgeEffect class to draw "glow" overlays.
213            mContentRect.width() / 2,
214            mContentRect.height() / 2);
215    // Invalidates to trigger computeScroll()
216    ViewCompat.postInvalidateOnAnimation(this);
217}</pre>
218
219<p>When {@link android.view.GestureDetector.OnGestureListener#onFling onFling()} calls
220{@link android.support.v4.view.ViewCompat#postInvalidateOnAnimation postInvalidateOnAnimation()},
221it triggers
222{@link android.view.View#computeScroll computeScroll()} to update the values for x and y.
223This is typically be done when a view child is animating a scroll using a scroller object, as in this example. </p>
224
225<p>Most views pass the scroller object's x and y position directly to
226{@link android.view.View#scrollTo scrollTo()}.
227The following implementation of {@link android.view.View#computeScroll computeScroll()}
228takes a different approach&mdash;it calls
229{@link android.widget.OverScroller#computeScrollOffset computeScrollOffset()} to get the current
230location of x and y. When the criteria for displaying an overscroll "glow" edge effect are met
231(the display is zoomed in, x or y is out of bounds, and the app isn't already showing an overscroll),
232the code sets up the overscroll glow effect and calls
233{@link android.support.v4.view.ViewCompat#postInvalidateOnAnimation postInvalidateOnAnimation()}
234to trigger an invalidate on the view:</p>
235
236<pre>// Edge effect / overscroll tracking objects.
237private EdgeEffectCompat mEdgeEffectTop;
238private EdgeEffectCompat mEdgeEffectBottom;
239private EdgeEffectCompat mEdgeEffectLeft;
240private EdgeEffectCompat mEdgeEffectRight;
241
242private boolean mEdgeEffectTopActive;
243private boolean mEdgeEffectBottomActive;
244private boolean mEdgeEffectLeftActive;
245private boolean mEdgeEffectRightActive;
246
247&#64;Override
248public void computeScroll() {
249    super.computeScroll();
250
251    boolean needsInvalidate = false;
252
253    // The scroller isn't finished, meaning a fling or programmatic pan
254    // operation is currently active.
255    if (mScroller.computeScrollOffset()) {
256        Point surfaceSize = computeScrollSurfaceSize();
257        int currX = mScroller.getCurrX();
258        int currY = mScroller.getCurrY();
259
260        boolean canScrollX = (mCurrentViewport.left > AXIS_X_MIN
261                || mCurrentViewport.right < AXIS_X_MAX);
262        boolean canScrollY = (mCurrentViewport.top > AXIS_Y_MIN
263                || mCurrentViewport.bottom < AXIS_Y_MAX);
264
265        /*
266         * If you are zoomed in and currX or currY is
267         * outside of bounds and you're not already
268         * showing overscroll, then render the overscroll
269         * glow edge effect.
270         */
271        if (canScrollX
272                && currX < 0
273                && mEdgeEffectLeft.isFinished()
274                && !mEdgeEffectLeftActive) {
275            mEdgeEffectLeft.onAbsorb((int)
276                    OverScrollerCompat.getCurrVelocity(mScroller));
277            mEdgeEffectLeftActive = true;
278            needsInvalidate = true;
279        } else if (canScrollX
280                && currX > (surfaceSize.x - mContentRect.width())
281                && mEdgeEffectRight.isFinished()
282                && !mEdgeEffectRightActive) {
283            mEdgeEffectRight.onAbsorb((int)
284                    OverScrollerCompat.getCurrVelocity(mScroller));
285            mEdgeEffectRightActive = true;
286            needsInvalidate = true;
287        }
288
289        if (canScrollY
290                && currY < 0
291                && mEdgeEffectTop.isFinished()
292                && !mEdgeEffectTopActive) {
293            mEdgeEffectTop.onAbsorb((int)
294                    OverScrollerCompat.getCurrVelocity(mScroller));
295            mEdgeEffectTopActive = true;
296            needsInvalidate = true;
297        } else if (canScrollY
298                && currY > (surfaceSize.y - mContentRect.height())
299                && mEdgeEffectBottom.isFinished()
300                && !mEdgeEffectBottomActive) {
301            mEdgeEffectBottom.onAbsorb((int)
302                    OverScrollerCompat.getCurrVelocity(mScroller));
303            mEdgeEffectBottomActive = true;
304            needsInvalidate = true;
305        }
306        ...
307    }</pre>
308
309<p>Here is the section of the code that performs the actual zoom:</p>
310
311<pre>// Custom object that is functionally similar to Scroller
312Zoomer mZoomer;
313private PointF mZoomFocalPoint = new PointF();
314...
315
316// If a zoom is in progress (either programmatically or via double
317// touch), performs the zoom.
318if (mZoomer.computeZoom()) {
319    float newWidth = (1f - mZoomer.getCurrZoom()) *
320            mScrollerStartViewport.width();
321    float newHeight = (1f - mZoomer.getCurrZoom()) *
322            mScrollerStartViewport.height();
323    float pointWithinViewportX = (mZoomFocalPoint.x -
324            mScrollerStartViewport.left)
325            / mScrollerStartViewport.width();
326    float pointWithinViewportY = (mZoomFocalPoint.y -
327            mScrollerStartViewport.top)
328            / mScrollerStartViewport.height();
329    mCurrentViewport.set(
330            mZoomFocalPoint.x - newWidth * pointWithinViewportX,
331            mZoomFocalPoint.y - newHeight * pointWithinViewportY,
332            mZoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
333            mZoomFocalPoint.y + newHeight * (1 - pointWithinViewportY));
334    constrainViewport();
335    needsInvalidate = true;
336}
337if (needsInvalidate) {
338    ViewCompat.postInvalidateOnAnimation(this);
339}
340</pre>
341
342<p>This is the {@code computeScrollSurfaceSize()} method that's called in the above snippet. It
343computes the current scrollable surface size, in pixels. For example, if the entire chart area is visible,
344this is simply the current size of {@code mContentRect}. If the chart is zoomed in 200% in both directions,
345the returned size will be twice as large horizontally and vertically.</p>
346
347<pre>private Point computeScrollSurfaceSize() {
348    return new Point(
349            (int) (mContentRect.width() * (AXIS_X_MAX - AXIS_X_MIN)
350                    / mCurrentViewport.width()),
351            (int) (mContentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN)
352                    / mCurrentViewport.height()));
353}</pre>
354
355<p>For another example of scroller usage, see the
356<a href="http://github.com/android/platform_frameworks_support/blob/master/v4/java/android/support/v4/view/ViewPager.java">source code</a> for the
357{@link android.support.v4.view.ViewPager} class. It scrolls in response to flings,
358and uses scrolling to implement the "snapping to page" animation.</p>
359
360