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 @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 @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—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@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