&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 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> |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 17 | <li><a href="#term">Understand Scrolling Terminology</a></li> |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 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> |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 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 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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> |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 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 |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 |
kmccormick | 76dfc02 | 2013-04-03 12:41:12 -0700 | [diff] [blame] | 59 | uses the {@link android.widget.EdgeEffect} class |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 | |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 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 | |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 83 | |
| 84 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 85 | <h2 id="term">Understand Scrolling Terminology</h2> |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 86 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 87 | <p>"Scrolling" is a word that can take on different meanings in Android, depending on the context.</p> |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 88 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 |
Scott Main | d7abd97 | 2013-11-04 18:33:43 -0800 | [diff] [blame] | 98 | <a href="scale.html">Dragging and Scaling</a>.</li> |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 112 | can be used in pretty much any context where you want the UI to display |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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> |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 117 | |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 118 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 161 | = new GestureDetector.SimpleOnGestureListener() { |
| 162 | @Override |
| 163 | public boolean onDown(MotionEvent e) { |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 164 | // Initiates the decay phase of any active edge effects. |
| 165 | releaseEdgeEffects(); |
| 166 | mScrollerStartViewport.set(mCurrentViewport); |
| 167 | // Aborts any active scroll animations and invalidates. |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 168 | mScroller.forceFinished(true); |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 169 | ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this); |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 170 | return true; |
| 171 | } |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 172 | ... |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 173 | @Override |
| 174 | public boolean onFling(MotionEvent e1, MotionEvent e2, |
| 175 | float velocityX, float velocityY) { |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 176 | fling((int) -velocityX, (int) -velocityY); |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 177 | return true; |
| 178 | } |
| 179 | }; |
| 180 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 | |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 247 | @Override |
| 248 | public void computeScroll() { |
| 249 | super.computeScroll(); |
| 250 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 251 | boolean needsInvalidate = false; |
| 252 | |
| 253 | // The scroller isn't finished, meaning a fling or programmatic pan |
| 254 | // operation is currently active. |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 255 | if (mScroller.computeScrollOffset()) { |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 256 | Point surfaceSize = computeScrollSurfaceSize(); |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 257 | int currX = mScroller.getCurrX(); |
| 258 | int currY = mScroller.getCurrY(); |
| 259 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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); |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 264 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 | } |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 288 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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())); |
&& repo sync -j8 | 518edbf | 2012-11-30 16:28:27 -0800 | [diff] [blame] | 353 | }</pre> |
| 354 | |
kmccormick | e23f97b | 2013-03-12 14:46:07 -0700 | [diff] [blame] | 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 | |