| page.title=Animating a Scroll Gesture | |
| parent.title=Using Touch Gestures | |
| parent.link=index.html | |
| trainingnavtop=true | |
| next.title=Handling Multi-Touch Gestures | |
| next.link=multi.html | |
| @jd:body | |
| <div id="tb-wrapper"> | |
| <div id="tb"> | |
| <!-- table of contents --> | |
| <h2>This lesson teaches you to</h2> | |
| <ol> | |
| <li><a href="#term">Understand Scrolling Terminology</a></li> | |
| <li><a href="#scroll">Implement Touch-Based Scrolling</a></li> | |
| </ol> | |
| <!-- other docs (NOT javadocs) --> | |
| <h2>You should also read</h2> | |
| <ul> | |
| <li><a href="http://developer.android.com/guide/topics/ui/ui-events.html">Input Events</a> API Guide | |
| </li> | |
| <li><a href="{@docRoot}guide/topics/sensors/sensors_overview.html">Sensors Overview</a></li> | |
| <li><a href="{@docRoot}training/custom-views/making-interactive.html">Making the View Interactive</a> </li> | |
| <li>Design Guide for <a href="{@docRoot}design/patterns/gestures.html">Gestures</a></li> | |
| <li>Design Guide for <a href="{@docRoot}design/style/touch-feedback.html">Touch Feedback</a></li> | |
| </ul> | |
| <h2>Try it out</h2> | |
| <div class="download-box"> | |
| <a href="{@docRoot}shareables/training/InteractiveChart.zip" | |
| class="button">Download the sample</a> | |
| <p class="filename">InteractiveChart.zip</p> | |
| </div> | |
| </div> | |
| </div> | |
| <p>In Android, scrolling is typically achieved by using the | |
| {@link android.widget.ScrollView} | |
| class. Any standard layout that might extend beyond the bounds of its container should be | |
| nested in a {@link android.widget.ScrollView} to provide a scrollable view that's | |
| managed by the framework. Implementing a custom scroller should only be | |
| necessary for special scenarios. This lesson describes such a scenario: displaying | |
| a scrolling effect in response to touch gestures using <em>scrollers</em>. | |
| <p>You can use scrollers ({@link android.widget.Scroller} or {@link | |
| android.widget.OverScroller}) to collect the data you need to produce a | |
| scrolling animation in response to a touch event. They are similar, but | |
| {@link android.widget.OverScroller} | |
| includes methods for indicating to users that they've reached the content edges | |
| after a pan or fling gesture. The {@code InteractiveChart} sample | |
| uses the {@link android.widget.EdgeEffect} class | |
| (actually the {@link android.support.v4.widget.EdgeEffectCompat} class) | |
| to display a "glow" effect when users reach the content edges.</p> | |
| <p class="note"><strong>Note:</strong> We recommend that you | |
| use {@link android.widget.OverScroller} rather than {@link | |
| android.widget.Scroller} for scrolling animations. | |
| {@link android.widget.OverScroller} provides the best backward | |
| compatibility with older devices. | |
| <br /> | |
| Also note that you generally only need to use scrollers | |
| when implementing scrolling yourself. {@link android.widget.ScrollView} and | |
| {@link android.widget.HorizontalScrollView} do all of this for you if you nest your | |
| layout within them. | |
| </p> | |
| <p>A scroller is used to animate scrolling over time, using platform-standard | |
| scrolling physics (friction, velocity, etc.). The scroller itself doesn't | |
| actually draw anything. Scrollers track scroll offsets for you over time, but | |
| they don't automatically apply those positions to your view. It's your | |
| responsibility to get and apply new coordinates at a rate that will make the | |
| scrolling animation look smooth.</p> | |
| <h2 id="term">Understand Scrolling Terminology</h2> | |
| <p>"Scrolling" is a word that can take on different meanings in Android, depending on the context.</p> | |
| <p><strong>Scrolling</strong> is the general process of moving the viewport (that is, the 'window' | |
| of content you're looking at). When scrolling is in both the x and y axes, it's called | |
| <em>panning</em>. The sample application provided with this class, {@code InteractiveChart}, illustrates | |
| two different types of scrolling, dragging and flinging:</p> | |
| <ul> | |
| <li><strong>Dragging</strong> is the type of scrolling that occurs when a user drags her | |
| finger across the touch screen. Simple dragging is often implemented by overriding | |
| {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} in | |
| {@link android.view.GestureDetector.OnGestureListener}. For more discussion of dragging, see | |
| <a href="dragging.jd">Dragging and Scaling</a>.</li> | |
| <li><strong>Flinging</strong> is the type of scrolling that occurs when a user | |
| drags and lifts her finger quickly. After the user lifts her finger, you generally | |
| want to keep scrolling (moving the viewport), but decelerate until the viewport stops moving. | |
| Flinging can be implemented by overriding | |
| {@link android.view.GestureDetector.OnGestureListener#onFling onFling()} | |
| in {@link android.view.GestureDetector.OnGestureListener}, and by using | |
| a scroller object. This is the use | |
| case that is the topic of this lesson.</li> | |
| </ul> | |
| <p>It's common to use scroller objects | |
| in conjunction with a fling gesture, but they | |
| can be used in pretty much any context where you want the UI to display | |
| scrolling in response to a touch event. For example, you could override | |
| {@link android.view.View#onTouchEvent onTouchEvent()} to process touch | |
| events directly, and produce a scrolling effect or a "snapping to page" animation | |
| in response to those touch events.</p> | |
| <h2 id="#scroll">Implement Touch-Based Scrolling</h2> | |
| <p>This section describes how to use a scroller. | |
| The snippet shown below comes from the {@code InteractiveChart} sample | |
| provided with this class. | |
| It uses a | |
| {@link android.view.GestureDetector}, and overrides the | |
| {@link android.view.GestureDetector.SimpleOnGestureListener} method | |
| {@link android.view.GestureDetector.OnGestureListener#onFling onFling()}. | |
| It uses {@link android.widget.OverScroller} to track the fling gesture. | |
| If the user reaches the content edges | |
| after the fling gesture, the app displays a "glow" effect. | |
| </p> | |
| <p class="note"><strong>Note:</strong> The {@code InteractiveChart} sample app displays a | |
| chart that you can zoom, pan, scroll, and so on. In the following snippet, | |
| {@code mContentRect} represents the rectangle coordinates within the view that the chart | |
| will be drawn into. At any given time, a subset of the total chart domain and range are drawn | |
| into this rectangular area. | |
| {@code mCurrentViewport} represents the portion of the chart that is currently | |
| visible in the screen. Because pixel offsets are generally treated as integers, | |
| {@code mContentRect} is of the type {@link android.graphics.Rect}. Because the | |
| graph domain and range are decimal/float values, {@code mCurrentViewport} is of | |
| the type {@link android.graphics.RectF}.</p> | |
| <p>The first part of the snippet shows the implementation of | |
| {@link android.view.GestureDetector.OnGestureListener#onFling onFling()}:</p> | |
| <pre>// The current viewport. This rectangle represents the currently visible | |
| // chart domain and range. The viewport is the part of the app that the | |
| // user manipulates via touch gestures. | |
| 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. | |
| private Rect mContentRect; | |
| private OverScroller mScroller; | |
| private RectF mScrollerStartViewport; | |
| ... | |
| private final GestureDetector.SimpleOnGestureListener mGestureListener | |
| = new GestureDetector.SimpleOnGestureListener() { | |
| @Override | |
| public boolean onDown(MotionEvent e) { | |
| // Initiates the decay phase of any active edge effects. | |
| releaseEdgeEffects(); | |
| mScrollerStartViewport.set(mCurrentViewport); | |
| // Aborts any active scroll animations and invalidates. | |
| mScroller.forceFinished(true); | |
| ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this); | |
| return true; | |
| } | |
| ... | |
| @Override | |
| public boolean onFling(MotionEvent e1, MotionEvent e2, | |
| float velocityX, float velocityY) { | |
| fling((int) -velocityX, (int) -velocityY); | |
| return true; | |
| } | |
| }; | |
| private void fling(int velocityX, int velocityY) { | |
| // Initiates the decay phase of any active edge effects. | |
| releaseEdgeEffects(); | |
| // Flings use math in pixels (as opposed to math based on the viewport). | |
| Point surfaceSize = computeScrollSurfaceSize(); | |
| mScrollerStartViewport.set(mCurrentViewport); | |
| int startX = (int) (surfaceSize.x * (mScrollerStartViewport.left - | |
| AXIS_X_MIN) / ( | |
| AXIS_X_MAX - AXIS_X_MIN)); | |
| int startY = (int) (surfaceSize.y * (AXIS_Y_MAX - | |
| mScrollerStartViewport.bottom) / ( | |
| AXIS_Y_MAX - AXIS_Y_MIN)); | |
| // Before flinging, aborts the current animation. | |
| mScroller.forceFinished(true); | |
| // Begins the animation | |
| mScroller.fling( | |
| // Current scroll position | |
| startX, | |
| startY, | |
| velocityX, | |
| velocityY, | |
| /* | |
| * Minimum and maximum scroll positions. The minimum scroll | |
| * position is generally zero and the maximum scroll position | |
| * is generally the content size less the screen size. So if the | |
| * content width is 1000 pixels and the screen width is 200 | |
| * pixels, the maximum scroll offset should be 800 pixels. | |
| */ | |
| 0, surfaceSize.x - mContentRect.width(), | |
| 0, surfaceSize.y - mContentRect.height(), | |
| // The edges of the content. This comes into play when using | |
| // the EdgeEffect class to draw "glow" overlays. | |
| mContentRect.width() / 2, | |
| mContentRect.height() / 2); | |
| // Invalidates to trigger computeScroll() | |
| ViewCompat.postInvalidateOnAnimation(this); | |
| }</pre> | |
| <p>When {@link android.view.GestureDetector.OnGestureListener#onFling onFling()} calls | |
| {@link android.support.v4.view.ViewCompat#postInvalidateOnAnimation postInvalidateOnAnimation()}, | |
| it triggers | |
| {@link android.view.View#computeScroll computeScroll()} to update the values for x and y. | |
| This is typically be done when a view child is animating a scroll using a scroller object, as in this example. </p> | |
| <p>Most views pass the scroller object's x and y position directly to | |
| {@link android.view.View#scrollTo scrollTo()}. | |
| The following implementation of {@link android.view.View#computeScroll computeScroll()} | |
| takes a different approach—it calls | |
| {@link android.widget.OverScroller#computeScrollOffset computeScrollOffset()} to get the current | |
| location of x and y. When the criteria for displaying an overscroll "glow" edge effect are met | |
| (the display is zoomed in, x or y is out of bounds, and the app isn't already showing an overscroll), | |
| the code sets up the overscroll glow effect and calls | |
| {@link android.support.v4.view.ViewCompat#postInvalidateOnAnimation postInvalidateOnAnimation()} | |
| to trigger an invalidate on the view:</p> | |
| <pre>// 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; | |
| @Override | |
| public void computeScroll() { | |
| super.computeScroll(); | |
| boolean needsInvalidate = false; | |
| // The scroller isn't finished, meaning a fling or programmatic pan | |
| // operation is currently active. | |
| if (mScroller.computeScrollOffset()) { | |
| Point surfaceSize = computeScrollSurfaceSize(); | |
| 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 you are zoomed in and currX or currY is | |
| * outside of bounds and you're not already | |
| * showing overscroll, then render the overscroll | |
| * glow edge effect. | |
| */ | |
| if (canScrollX | |
| && currX < 0 | |
| && mEdgeEffectLeft.isFinished() | |
| && !mEdgeEffectLeftActive) { | |
| mEdgeEffectLeft.onAbsorb((int) | |
| OverScrollerCompat.getCurrVelocity(mScroller)); | |
| mEdgeEffectLeftActive = true; | |
| needsInvalidate = true; | |
| } else if (canScrollX | |
| && currX > (surfaceSize.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 > (surfaceSize.y - mContentRect.height()) | |
| && mEdgeEffectBottom.isFinished() | |
| && !mEdgeEffectBottomActive) { | |
| mEdgeEffectBottom.onAbsorb((int) | |
| OverScrollerCompat.getCurrVelocity(mScroller)); | |
| mEdgeEffectBottomActive = true; | |
| needsInvalidate = true; | |
| } | |
| ... | |
| }</pre> | |
| <p>Here is the section of the code that performs the actual zoom:</p> | |
| <pre>// Custom object that is functionally similar to Scroller | |
| Zoomer mZoomer; | |
| private PointF mZoomFocalPoint = new PointF(); | |
| ... | |
| // If a zoom is in progress (either programmatically or via double | |
| // touch), performs the zoom. | |
| if (mZoomer.computeZoom()) { | |
| 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); | |
| } | |
| </pre> | |
| <p>This is the {@code computeScrollSurfaceSize()} method that's called in the above snippet. It | |
| computes the current scrollable surface size, in pixels. For example, if the entire chart area is visible, | |
| this is simply the current size of {@code mContentRect}. If the chart is zoomed in 200% in both directions, | |
| the returned size will be twice as large horizontally and vertically.</p> | |
| <pre>private Point computeScrollSurfaceSize() { | |
| return new Point( | |
| (int) (mContentRect.width() * (AXIS_X_MAX - AXIS_X_MIN) | |
| / mCurrentViewport.width()), | |
| (int) (mContentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN) | |
| / mCurrentViewport.height())); | |
| }</pre> | |
| <p>For another example of scroller usage, see the | |
| <a href="http://github.com/android/platform_frameworks_support/blob/master/v4/java/android/support/v4/view/ViewPager.java">source code</a> for the | |
| {@link android.support.v4.view.ViewPager} class. It scrolls in response to flings, | |
| and uses scrolling to implement the "snapping to page" animation.</p> | |