• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&#64;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&#64;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&#64;Override
296public boolean onTouchEvent(MotionEvent ev) {
297    // Let the ScaleGestureDetector inspect all events.
298    mScaleDetector.onTouchEvent(ev);
299    return true;
300}
301
302&#64;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    &#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.
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>&#64;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    &#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