1 page.title=Animating a Scroll Gesture 2 parent.title=Using Touch Gestures 3 parent.link=index.html 4 5 trainingnavtop=true 6 next.title=Handling Multi-Touch Gestures 7 next.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" 37 class="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} 46 class. Any standard layout that might extend beyond the bounds of its container should be 47 nested in a {@link android.widget.ScrollView} to provide a scrollable view that's 48 managed by the framework. Implementing a custom scroller should only be 49 necessary for special scenarios. This lesson describes such a scenario: displaying 50 a 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 54 android.widget.OverScroller}) to collect the data you need to produce a 55 scrolling animation in response to a touch event. They are similar, but 56 {@link android.widget.OverScroller} 57 includes methods for indicating to users that they've reached the content edges 58 after a pan or fling gesture. The {@code InteractiveChart} sample 59 uses the {@link android.widget.EdgeEffect} class 60 (actually the {@link android.support.v4.widget.EdgeEffectCompat} class) 61 to display a "glow" effect when users reach the content edges.</p> 62 63 <p class="note"><strong>Note:</strong> We recommend that you 64 use {@link android.widget.OverScroller} rather than {@link 65 android.widget.Scroller} for scrolling animations. 66 {@link android.widget.OverScroller} provides the best backward 67 compatibility with older devices. 68 <br /> 69 Also note that you generally only need to use scrollers 70 when implementing scrolling yourself. {@link android.widget.ScrollView} and 71 {@link android.widget.HorizontalScrollView} do all of this for you if you nest your 72 layout within them. 73 </p> 74 75 76 <p>A scroller is used to animate scrolling over time, using platform-standard 77 scrolling physics (friction, velocity, etc.). The scroller itself doesn't 78 actually draw anything. Scrollers track scroll offsets for you over time, but 79 they don't automatically apply those positions to your view. It's your 80 responsibility to get and apply new coordinates at a rate that will make the 81 scrolling 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' 90 of 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 92 two 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 95 finger 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 101 drags and lifts her finger quickly. After the user lifts her finger, you generally 102 want to keep scrolling (moving the viewport), but decelerate until the viewport stops moving. 103 Flinging can be implemented by overriding 104 {@link android.view.GestureDetector.OnGestureListener#onFling onFling()} 105 in {@link android.view.GestureDetector.OnGestureListener}, and by using 106 a scroller object. This is the use 107 case that is the topic of this lesson.</li> 108 </ul> 109 110 <p>It's common to use scroller objects 111 in conjunction with a fling gesture, but they 112 can be used in pretty much any context where you want the UI to display 113 scrolling in response to a touch event. For example, you could override 114 {@link android.view.View#onTouchEvent onTouchEvent()} to process touch 115 events directly, and produce a scrolling effect or a "snapping to page" animation 116 in 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. 122 The snippet shown below comes from the {@code InteractiveChart} sample 123 provided with this class. 124 It 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()}. 128 It uses {@link android.widget.OverScroller} to track the fling gesture. 129 If the user reaches the content edges 130 after 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 134 chart 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 136 will be drawn into. At any given time, a subset of the total chart domain and range are drawn 137 into this rectangular area. 138 {@code mCurrentViewport} represents the portion of the chart that is currently 139 visible in the screen. Because pixel offsets are generally treated as integers, 140 {@code mContentRect} is of the type {@link android.graphics.Rect}. Because the 141 graph domain and range are decimal/float values, {@code mCurrentViewport} is of 142 the 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. 150 private 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. 155 private Rect mContentRect; 156 157 private OverScroller mScroller; 158 private RectF mScrollerStartViewport; 159 ... 160 private 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 181 private 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()}, 221 it triggers 222 {@link android.view.View#computeScroll computeScroll()} to update the values for x and y. 223 This 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()}. 227 The following implementation of {@link android.view.View#computeScroll computeScroll()} 228 takes a different approach—it calls 229 {@link android.widget.OverScroller#computeScrollOffset computeScrollOffset()} to get the current 230 location 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), 232 the code sets up the overscroll glow effect and calls 233 {@link android.support.v4.view.ViewCompat#postInvalidateOnAnimation postInvalidateOnAnimation()} 234 to trigger an invalidate on the view:</p> 235 236 <pre>// Edge effect / overscroll tracking objects. 237 private EdgeEffectCompat mEdgeEffectTop; 238 private EdgeEffectCompat mEdgeEffectBottom; 239 private EdgeEffectCompat mEdgeEffectLeft; 240 private EdgeEffectCompat mEdgeEffectRight; 241 242 private boolean mEdgeEffectTopActive; 243 private boolean mEdgeEffectBottomActive; 244 private boolean mEdgeEffectLeftActive; 245 private boolean mEdgeEffectRightActive; 246 247 @Override 248 public 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 312 Zoomer mZoomer; 313 private PointF mZoomFocalPoint = new PointF(); 314 ... 315 316 // If a zoom is in progress (either programmatically or via double 317 // touch), performs the zoom. 318 if (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 } 337 if (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 343 computes the current scrollable surface size, in pixels. For example, if the entire chart area is visible, 344 this is simply the current size of {@code mContentRect}. If the chart is zoomed in 200% in both directions, 345 the 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, 358 and uses scrolling to implement the "snapping to page" animation.</p> 359 360