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