412 lines
16 KiB
Plaintext
412 lines
16 KiB
Plaintext
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>
|