Home | History | Annotate | Download | only in gestures
      1 page.title=Dragging and Scaling
      2 parent.title=Using Touch Gestures
      3 parent.link=index.html
      4 
      5 trainingnavtop=true
      6 next.title=Managing Touch Events in a ViewGroup
      7 next.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"
     38 class="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
     46 objects, using {@link android.view.View#onTouchEvent onTouchEvent()} to intercept
     47 touch 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 
     53 listeners 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
     57 the screen. The following snippet lets the user drag an on-screen image. Note
     58 the 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,
     64 imagine that while dragging the image around, the user places a second finger on
     65 the touch screen and lifts the first finger. If your app is just tracking
     66 individual pointers, it will regard the second pointer as the default and move
     67 the image to that location.</li>
     68 
     69 <li>To prevent this from happening, your app needs to distinguish between the 
     70 original 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 
     76 passed to the {@link android.view.View#onTouchEvent onTouchEvent()} callback 
     77 whenever a secondary pointer goes down or up. </li>
     78 
     79 
     80 <li>In the {@link android.view.MotionEvent#ACTION_POINTER_UP} case, the example
     81 extracts this index and ensures that the active pointer ID is not referring to a
     82 pointer that is no longer touching the screen. If it is, the app selects a
     83 different pointer to be active and saves its current X and Y position. Since
     84 this saved position is used in the {@link android.view.MotionEvent#ACTION_MOVE}
     85 case to calculate the distance to move the onscreen object, the app will always
     86 calculate 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
     91 position of the active pointer, calculates the distance the pointer traveled, and moves the object to the
     92 new position. It correctly manages the possibility of additional pointers, as described
     93 above.</p> 
     94 
     95 <p>Notice that the snippet uses the {@link android.view.MotionEvent#getActionMasked getActionMasked()} method. 
     96 You should always use this method (or better yet, the compatability version 
     97 {@link android.support.v4.view.MotionEventCompat#getActionMasked MotionEventCompat.getActionMasked()}) 
     98 to retrieve the action of a
     99 {@link android.view.MotionEvent}. Unlike the older 
    100 {@link android.view.MotionEvent#getAction getAction()} 
    101 method, {@link android.support.v4.view.MotionEventCompat#getActionMasked getActionMasked()} 
    102 is designed to work with multiple pointers. It returns the masked action 
    103 being performed, without including the pointer index bits.</p>
    104 
    105 <pre>// The active pointer is the one currently moving our object.
    106 private int mActivePointerId = INVALID_POINTER_ID;
    107 
    108 &#64;Override
    109 public 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 
    185 common scenario is <em>panning</em>, which is when a user's dragging motion causes scrolling 
    186 in both the x and y axes. The above snippet directly intercepted the {@link android.view.MotionEvent} 
    187 actions to implement dragging. The snippet in this section takes advantage of the platform's 
    188 built-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()} 
    193 is called when a user is dragging his finger to pan the content. 
    194 {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} is only called when 
    195 a finger is down; as soon as the finger is lifted from the screen, the gesture either ends, 
    196 or a fling gesture is started (if the finger was moving with some speed just before it was lifted). 
    197 For 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. 
    204 private 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.
    209 private Rect mContentRect;
    210 
    211 private final GestureDetector.SimpleOnGestureListener mGestureListener
    212             = new GestureDetector.SimpleOnGestureListener() {
    213 ...
    214 
    215 &#64;Override
    216 public 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()} 
    236 scrolls 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  */
    244 private 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
    268 Android such as scrolling, flinging, and long press. For scaling, Android
    269 provides {@link android.view.ScaleGestureDetector}. {@link
    270 android.view.GestureDetector} and {@link android.view.ScaleGestureDetector} can
    271 be 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 
    274 passed to their constructors. {@link android.view.ScaleGestureDetector} uses 
    275 {@link android.view.ScaleGestureDetector.OnScaleGestureListener}. 
    276 Android provides 
    277 {@link android.view.ScaleGestureDetector.SimpleOnScaleGestureListener} 
    278 as a helper class that you can extend if you dont 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;
    286 private float mScaleFactor = 1.f;
    287 
    288 public MyCustomView(Context mContext){
    289     ...
    290     // View code goes here
    291     ...
    292     mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    293 }
    294 
    295 &#64;Override
    296 public boolean onTouchEvent(MotionEvent ev) {
    297     // Let the ScaleGestureDetector inspect all events.
    298     mScaleDetector.onTouchEvent(ev);
    299     return true;
    300 }
    301 
    302 &#64;Override
    303 public 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 
    314 private class ScaleListener 
    315         extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    316     &#64;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. 
    333 The {@code InteractiveChart} sample supports both scrolling (panning) and scaling with multiple fingers,
    334 using 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>&#64;Override
    339 private RectF mCurrentViewport = 
    340         new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
    341 private Rect mContentRect;
    342 private ScaleGestureDetector mScaleGestureDetector;
    343 ...
    344 public 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  */
    353 private 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     &#64;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     &#64;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