1page.title=Dragging and Scaling 2parent.title=Using Touch Gestures 3parent.link=index.html 4 5trainingnavtop=true 6next.title=Managing Touch Events in a ViewGroup 7next.link=viewgroup.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="#drag">Drag an Object</a></li> 18 <li><a href="#pan">Drag to Pan</a></li> 19 <li><a href="#scale">Use Touch to Perform Scaling</a></li> 20</ol> 21 22<!-- other docs (NOT javadocs) --> 23<h2>You should also read</h2> 24 25<ul> 26 <li><a href="http://developer.android.com/guide/topics/ui/ui-events.html">Input Events</a> API Guide 27 </li> 28 <li><a href="{@docRoot}guide/topics/sensors/sensors_overview.html">Sensors Overview</a></li> 29 <li><a href="{@docRoot}training/custom-views/making-interactive.html">Making the View Interactive</a> </li> 30 <li>Design Guide for <a href="{@docRoot}design/patterns/gestures.html">Gestures</a></li> 31 <li>Design Guide for <a href="{@docRoot}design/style/touch-feedback.html">Touch Feedback</a></li> 32</ul> 33 34<h2>Try it out</h2> 35 36<div class="download-box"> 37 <a href="{@docRoot}shareables/training/InteractiveChart.zip" 38class="button">Download the sample</a> 39 <p class="filename">InteractiveChart.zip</p> 40</div> 41 42</div> 43</div> 44 45<p>This lesson describes how to use touch gestures to drag and scale on-screen 46objects, using {@link android.view.View#onTouchEvent onTouchEvent()} to intercept 47touch events. 48</p> 49 50<h2 id="drag">Drag an Object</h2> 51 52<p class="note">If you are targeting Android 3.0 or higher, you can use the built-in drag-and-drop event 53listeners with {@link android.view.View.OnDragListener}, as described in 54<a href="{@docRoot}guide/topics/ui/drag-drop.html">Drag and Drop</a>. 55 56<p>A common operation for a touch gesture is to use it to drag an object across 57the screen. The following snippet lets the user drag an on-screen image. Note 58the following:</p> 59 60<ul> 61 62<li>In a drag (or scroll) operation, the app has to keep track of the original pointer 63(finger), even if additional fingers get placed on the screen. For example, 64imagine that while dragging the image around, the user places a second finger on 65the touch screen and lifts the first finger. If your app is just tracking 66individual pointers, it will regard the second pointer as the default and move 67the image to that location.</li> 68 69<li>To prevent this from happening, your app needs to distinguish between the 70original pointer and any follow-on pointers. To do this, it tracks the 71{@link android.view.MotionEvent#ACTION_POINTER_DOWN} and 72{@link android.view.MotionEvent#ACTION_POINTER_UP} events described in 73<a href="multi.html">Handling Multi-Touch Gestures</a>. 74{@link android.view.MotionEvent#ACTION_POINTER_DOWN} and 75{@link android.view.MotionEvent#ACTION_POINTER_UP} are 76passed to the {@link android.view.View#onTouchEvent onTouchEvent()} callback 77whenever a secondary pointer goes down or up. </li> 78 79 80<li>In the {@link android.view.MotionEvent#ACTION_POINTER_UP} case, the example 81extracts this index and ensures that the active pointer ID is not referring to a 82pointer that is no longer touching the screen. If it is, the app selects a 83different pointer to be active and saves its current X and Y position. Since 84this saved position is used in the {@link android.view.MotionEvent#ACTION_MOVE} 85case to calculate the distance to move the onscreen object, the app will always 86calculate the distance to move using data from the correct pointer.</li> 87 88</ul> 89 90<p>The following snippet enables a user to drag an object around on the screen. It records the initial 91position of the active pointer, calculates the distance the pointer traveled, and moves the object to the 92new position. It correctly manages the possibility of additional pointers, as described 93above.</p> 94 95<p>Notice that the snippet uses the {@link android.view.MotionEvent#getActionMasked getActionMasked()} method. 96You should always use this method (or better yet, the compatability version 97{@link android.support.v4.view.MotionEventCompat#getActionMasked MotionEventCompat.getActionMasked()}) 98to retrieve the action of a 99{@link android.view.MotionEvent}. Unlike the older 100{@link android.view.MotionEvent#getAction getAction()} 101method, {@link android.support.v4.view.MotionEventCompat#getActionMasked getActionMasked()} 102is designed to work with multiple pointers. It returns the masked action 103being performed, without including the pointer index bits.</p> 104 105<pre>// The ‘active pointer’ is the one currently moving our object. 106private int mActivePointerId = INVALID_POINTER_ID; 107 108@Override 109public boolean onTouchEvent(MotionEvent ev) { 110 // Let the ScaleGestureDetector inspect all events. 111 mScaleDetector.onTouchEvent(ev); 112 113 final int action = MotionEventCompat.getActionMasked(ev); 114 115 switch (action) { 116 case MotionEvent.ACTION_DOWN: { 117 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 118 final float x = MotionEventCompat.getX(ev, pointerIndex); 119 final float y = MotionEventCompat.getY(ev, pointerIndex); 120 121 // Remember where we started (for dragging) 122 mLastTouchX = x; 123 mLastTouchY = y; 124 // Save the ID of this pointer (for dragging) 125 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 126 break; 127 } 128 129 case MotionEvent.ACTION_MOVE: { 130 // Find the index of the active pointer and fetch its position 131 final int pointerIndex = 132 MotionEventCompat.findPointerIndex(ev, mActivePointerId); 133 134 final float x = MotionEventCompat.getX(ev, pointerIndex); 135 final float y = MotionEventCompat.getY(ev, pointerIndex); 136 137 // Calculate the distance moved 138 final float dx = x - mLastTouchX; 139 final float dy = y - mLastTouchY; 140 141 mPosX += dx; 142 mPosY += dy; 143 144 invalidate(); 145 146 // Remember this touch position for the next move event 147 mLastTouchX = x; 148 mLastTouchY = y; 149 150 break; 151 } 152 153 case MotionEvent.ACTION_UP: { 154 mActivePointerId = INVALID_POINTER_ID; 155 break; 156 } 157 158 case MotionEvent.ACTION_CANCEL: { 159 mActivePointerId = INVALID_POINTER_ID; 160 break; 161 } 162 163 case MotionEvent.ACTION_POINTER_UP: { 164 165 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 166 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 167 168 if (pointerId == mActivePointerId) { 169 // This was our active pointer going up. Choose a new 170 // active pointer and adjust accordingly. 171 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 172 mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex); 173 mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex); 174 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 175 } 176 break; 177 } 178 } 179 return true; 180}</pre> 181 182<h2 id="pan">Drag to Pan</h2> 183 184<p>The previous section showed an example of dragging an object around the screen. Another 185common scenario is <em>panning</em>, which is when a user's dragging motion causes scrolling 186in both the x and y axes. The above snippet directly intercepted the {@link android.view.MotionEvent} 187actions to implement dragging. The snippet in this section takes advantage of the platform's 188built-in support for common gestures. It overrides 189{@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} in 190{@link android.view.GestureDetector.SimpleOnGestureListener}.</p> 191 192<p>To provide a little more context, {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} 193is called when a user is dragging his finger to pan the content. 194{@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} is only called when 195a finger is down; as soon as the finger is lifted from the screen, the gesture either ends, 196or a fling gesture is started (if the finger was moving with some speed just before it was lifted). 197For more discussion of scrolling vs. flinging, see <a href="scroll.html">Animating a Scroll Gesture</a>.</p> 198 199<p>Here is the snippet for {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()}: 200 201 202<pre>// The current viewport. This rectangle represents the currently visible 203// chart domain and range. 204private RectF mCurrentViewport = 205 new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX); 206 207// The current destination rectangle (in pixel coordinates) into which the 208// chart data should be drawn. 209private Rect mContentRect; 210 211private final GestureDetector.SimpleOnGestureListener mGestureListener 212 = new GestureDetector.SimpleOnGestureListener() { 213... 214 215@Override 216public boolean onScroll(MotionEvent e1, MotionEvent e2, 217 float distanceX, float distanceY) { 218 // Scrolling uses math based on the viewport (as opposed to math using pixels). 219 220 // Pixel offset is the offset in screen pixels, while viewport offset is the 221 // offset within the current viewport. 222 float viewportOffsetX = distanceX * mCurrentViewport.width() 223 / mContentRect.width(); 224 float viewportOffsetY = -distanceY * mCurrentViewport.height() 225 / mContentRect.height(); 226 ... 227 // Updates the viewport, refreshes the display. 228 setViewportBottomLeft( 229 mCurrentViewport.left + viewportOffsetX, 230 mCurrentViewport.bottom + viewportOffsetY); 231 ... 232 return true; 233}</pre> 234 235<p>The implementation of {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} 236scrolls the viewport in response to the touch gesture:</p> 237 238<pre> 239/** 240 * Sets the current viewport (defined by mCurrentViewport) to the given 241 * X and Y positions. Note that the Y value represents the topmost pixel position, 242 * and thus the bottom of the mCurrentViewport rectangle. 243 */ 244private void setViewportBottomLeft(float x, float y) { 245 /* 246 * Constrains within the scroll range. The scroll range is simply the viewport 247 * extremes (AXIS_X_MAX, etc.) minus the viewport size. For example, if the 248 * extremes were 0 and 10, and the viewport size was 2, the scroll range would 249 * be 0 to 8. 250 */ 251 252 float curWidth = mCurrentViewport.width(); 253 float curHeight = mCurrentViewport.height(); 254 x = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth)); 255 y = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX)); 256 257 mCurrentViewport.set(x, y - curHeight, x + curWidth, y); 258 259 // Invalidates the View to update the display. 260 ViewCompat.postInvalidateOnAnimation(this); 261} 262</pre> 263 264<h2 id="scale">Use Touch to Perform Scaling</h2> 265 266<p>As discussed in <a href="detector.html">Detecting Common Gestures</a>, 267{@link android.view.GestureDetector} helps you detect common gestures used by 268Android such as scrolling, flinging, and long press. For scaling, Android 269provides {@link android.view.ScaleGestureDetector}. {@link 270android.view.GestureDetector} and {@link android.view.ScaleGestureDetector} can 271be used together when you want a view to recognize additional gestures.</p> 272 273<p>To report detected gesture events, gesture detectors use listener objects 274passed to their constructors. {@link android.view.ScaleGestureDetector} uses 275{@link android.view.ScaleGestureDetector.OnScaleGestureListener}. 276Android provides 277{@link android.view.ScaleGestureDetector.SimpleOnScaleGestureListener} 278as a helper class that you can extend if you don’t care about all of the reported events.</p> 279 280 281<h3>Basic scaling example</h3> 282 283<p>Here is a snippet that illustrates the basic ingredients involved in scaling.</p> 284 285<pre>private ScaleGestureDetector mScaleDetector; 286private float mScaleFactor = 1.f; 287 288public MyCustomView(Context mContext){ 289 ... 290 // View code goes here 291 ... 292 mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); 293} 294 295@Override 296public boolean onTouchEvent(MotionEvent ev) { 297 // Let the ScaleGestureDetector inspect all events. 298 mScaleDetector.onTouchEvent(ev); 299 return true; 300} 301 302@Override 303public void onDraw(Canvas canvas) { 304 super.onDraw(canvas); 305 306 canvas.save(); 307 canvas.scale(mScaleFactor, mScaleFactor); 308 ... 309 // onDraw() code goes here 310 ... 311 canvas.restore(); 312} 313 314private class ScaleListener 315 extends ScaleGestureDetector.SimpleOnScaleGestureListener { 316 @Override 317 public boolean onScale(ScaleGestureDetector detector) { 318 mScaleFactor *= detector.getScaleFactor(); 319 320 // Don't let the object get too small or too large. 321 mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)); 322 323 invalidate(); 324 return true; 325 } 326}</pre> 327 328 329 330 331<h3>More complex scaling example</h3> 332<p>Here is a more complex example from the {@code InteractiveChart} sample provided with this class. 333The {@code InteractiveChart} sample supports both scrolling (panning) and scaling with multiple fingers, 334using the {@link android.view.ScaleGestureDetector} "span" 335({@link android.view.ScaleGestureDetector#getCurrentSpanX getCurrentSpanX/Y}) and 336"focus" ({@link android.view.ScaleGestureDetector#getFocusX getFocusX/Y}) features:</p> 337 338<pre>@Override 339private RectF mCurrentViewport = 340 new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX); 341private Rect mContentRect; 342private ScaleGestureDetector mScaleGestureDetector; 343... 344public boolean onTouchEvent(MotionEvent event) { 345 boolean retVal = mScaleGestureDetector.onTouchEvent(event); 346 retVal = mGestureDetector.onTouchEvent(event) || retVal; 347 return retVal || super.onTouchEvent(event); 348} 349 350/** 351 * The scale listener, used for handling multi-finger scale gestures. 352 */ 353private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener 354 = new ScaleGestureDetector.SimpleOnScaleGestureListener() { 355 /** 356 * This is the active focal point in terms of the viewport. Could be a local 357 * variable but kept here to minimize per-frame allocations. 358 */ 359 private PointF viewportFocus = new PointF(); 360 private float lastSpanX; 361 private float lastSpanY; 362 363 // Detects that new pointers are going down. 364 @Override 365 public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { 366 lastSpanX = ScaleGestureDetectorCompat. 367 getCurrentSpanX(scaleGestureDetector); 368 lastSpanY = ScaleGestureDetectorCompat. 369 getCurrentSpanY(scaleGestureDetector); 370 return true; 371 } 372 373 @Override 374 public boolean onScale(ScaleGestureDetector scaleGestureDetector) { 375 376 float spanX = ScaleGestureDetectorCompat. 377 getCurrentSpanX(scaleGestureDetector); 378 float spanY = ScaleGestureDetectorCompat. 379 getCurrentSpanY(scaleGestureDetector); 380 381 float newWidth = lastSpanX / spanX * mCurrentViewport.width(); 382 float newHeight = lastSpanY / spanY * mCurrentViewport.height(); 383 384 float focusX = scaleGestureDetector.getFocusX(); 385 float focusY = scaleGestureDetector.getFocusY(); 386 // Makes sure that the chart point is within the chart region. 387 // See the sample for the implementation of hitTest(). 388 hitTest(scaleGestureDetector.getFocusX(), 389 scaleGestureDetector.getFocusY(), 390 viewportFocus); 391 392 mCurrentViewport.set( 393 viewportFocus.x 394 - newWidth * (focusX - mContentRect.left) 395 / mContentRect.width(), 396 viewportFocus.y 397 - newHeight * (mContentRect.bottom - focusY) 398 / mContentRect.height(), 399 0, 400 0); 401 mCurrentViewport.right = mCurrentViewport.left + newWidth; 402 mCurrentViewport.bottom = mCurrentViewport.top + newHeight; 403 ... 404 // Invalidates the View to update the display. 405 ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this); 406 407 lastSpanX = spanX; 408 lastSpanY = spanY; 409 return true; 410 } 411};</pre> 412