blob: f2e4eb82d12285e6c4263c947586dc2c4d8594a6 [file] [log] [blame]
&& repo sync -j8518edbf2012-11-30 16:28:27 -08001page.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>
kmccormicke23f97b2013-03-12 14:46:07 -070018 <li><a href="#pan">Drag to Pan</a></li>
&& repo sync -j8518edbf2012-11-30 16:28:27 -080019 <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>
&& repo sync -j8518edbf2012-11-30 16:28:27 -080029 <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
kmccormicke23f97b2013-03-12 14:46:07 -070034<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>
&& repo sync -j8518edbf2012-11-30 16:28:27 -080041
42</div>
43</div>
kmccormicke23f97b2013-03-12 14:46:07 -070044
&& repo sync -j8518edbf2012-11-30 16:28:27 -080045<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
kmccormicke23f97b2013-03-12 14:46:07 -070047touch events.
&& repo sync -j8518edbf2012-11-30 16:28:27 -080048</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
kmccormicke23f97b2013-03-12 14:46:07 -0700137 // Calculate the distance moved
138 final float dx = x - mLastTouchX;
139 final float dy = y - mLastTouchY;
&& repo sync -j8518edbf2012-11-30 16:28:27 -0800140
kmccormicke23f97b2013-03-12 14:46:07 -0700141 mPosX += dx;
142 mPosY += dy;
&& repo sync -j8518edbf2012-11-30 16:28:27 -0800143
kmccormicke23f97b2013-03-12 14:46:07 -0700144 invalidate();
145
&& repo sync -j8518edbf2012-11-30 16:28:27 -0800146 // 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
kmccormicke23f97b2013-03-12 14:46:07 -0700182<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
&& repo sync -j8518edbf2012-11-30 16:28:27 -0800264<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 dont care about all of the reported events.</p>
279
kmccormicke23f97b2013-03-12 14:46:07 -0700280
281<h3>Basic scaling example</h3>
282
283<p>Here is a snippet that illustrates the basic ingredients involved in scaling.</p>
&& repo sync -j8518edbf2012-11-30 16:28:27 -0800284
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>
kmccormicke23f97b2013-03-12 14:46:07 -0700327
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>