diff --git a/docs/downloads/training/InteractiveChart.zip b/docs/downloads/training/InteractiveChart.zip new file mode 100644 index 0000000000000..95248ada943b0 Binary files /dev/null and b/docs/downloads/training/InteractiveChart.zip differ diff --git a/docs/html/training/gestures/detector.jd b/docs/html/training/gestures/detector.jd index 06d0e983f5ccc..65ddb1baee3a1 100644 --- a/docs/html/training/gestures/detector.jd +++ b/docs/html/training/gestures/detector.jd @@ -25,12 +25,18 @@ next.link=movement.html
InteractiveChart.zip
+InteractiveChart.zip
+InteractiveChart.zip
+InteractiveChart.zip
+InteractiveChart.zip
+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. Here is the original source code -for the examples used in this lesson. +touch events.
The previous section showed an example of dragging an object around the screen. Another +common scenario is panning, 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}.
+ +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 Animating a Scroll Gesture.
+ +Here is the snippet for {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()}: + + +
// 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;
+}
+
+The implementation of {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} +scrolls the viewport in response to the touch gesture:
+ +
+/**
+ * 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);
+}
+
+
As discussed in Detecting Common Gestures, @@ -191,10 +277,10 @@ 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.
-Here is a snippet that gives you the basic idea of how to perform scaling. -Here is the original source code -for the examples.
+ +Here is a snippet that illustrates the basic ingredients involved in scaling.
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
@@ -238,3 +324,88 @@ private class ScaleListener
return true;
}
}
+
+
+
+
+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:
+ +@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;
+ }
+};
diff --git a/docs/html/training/gestures/scroll.jd b/docs/html/training/gestures/scroll.jd
index 8576948b5a23c..bd1537a65a673 100644
--- a/docs/html/training/gestures/scroll.jd
+++ b/docs/html/training/gestures/scroll.jd
@@ -14,6 +14,7 @@ next.link=multi.html
InteractiveChart.zip
+You can use scrollers ({@link android.widget.Scroller} or {@link android.widget.OverScroller}) to collect the data you need to produce a -scrolling animation in response to a touch event.
+scrolling animation in response to a touch event. They are similar, but +{@link android.widget.OverScroller} +includes methods for indicating to users that they've reached the content edges +after a pan or fling gesture. The {@code InteractiveChart} sample +uses the the {@link android.widget.EdgeEffect} class +(actually the {@link android.support.v4.widget.EdgeEffectCompat} class) +to display a "glow" effect when users reach the content edges. + +Note: We recommend that you
+use {@link android.widget.OverScroller} rather than {@link
+android.widget.Scroller} for scrolling animations.
+{@link android.widget.OverScroller} provides the best backward
+compatibility with older devices.
+
+Also note that you generally only need to use scrollers
+when implementing scrolling yourself. {@link android.widget.ScrollView} and
+{@link android.widget.HorizontalScrollView} do all of this for you if you nest your
+layout within them.
+
A scroller is used to animate scrolling over time, using platform-standard scrolling physics (friction, velocity, etc.). The scroller itself doesn't @@ -54,101 +80,280 @@ they don't automatically apply those positions to your view. It's your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.
-Note: You generally only need to use scrollers -when implementing scrolling yourself. {@link android.widget.ScrollView} and -{@link android.widget.HorizontalScrollView} do all this for you do all of this for you if you nest your layout within them.
- -This snippet illustrates the basics of using a scroller. It uses a -{@link android.view.GestureDetector}, and overrides the -{@link android.view.GestureDetector.SimpleOnGestureListener} methods -{@link android.view.GestureDetector.OnGestureListener#onDown onDown()} and -{@link android.view.GestureDetector.OnGestureListener#onFling onFling()}. It also -overrides {@link android.view.GestureDetector.OnGestureListener#onScroll onScroll()} -to return {@code false} since you don't need to animate a scroll.
+"Scrolling" is a word that can take on different meanings in Android, depending on the context.
-It's common to use scrollers in conjunction with a fling gesture, but they +
Scrolling is the general process of moving the viewport (that is, the 'window' +of content you're looking at). When scrolling is in both the x and y axes, it's called +panning. The sample application provided with this class, {@code InteractiveChart}, illustrates +two different types of scrolling, dragging and flinging:
+It's common to use scroller objects +in conjunction with a fling gesture, but they can be used in pretty much any context where you want the UI to display -scrolling in response to a touch event. For example, you could override {@link -android.view.View#onTouchEvent onTouchEvent()} to process touch events directly, -and produce a scrolling effect in response to those touch events.
+scrolling in response to a touch event. For example, you could override +{@link android.view.View#onTouchEvent onTouchEvent()} to process touch +events directly, and produce a scrolling effect or a "snapping to page" animation +in response to those touch events. --private OverScroller mScroller = new OverScroller(context); -private GestureDetector.SimpleOnGestureListener mGestureListener +Implement Touch-Based Scrolling
+ +This section describes how to use a scroller. +The snippet shown below comes from the {@code InteractiveChart} sample +provided with this class. +It uses a +{@link android.view.GestureDetector}, and overrides the +{@link android.view.GestureDetector.SimpleOnGestureListener} method +{@link android.view.GestureDetector.OnGestureListener#onFling onFling()}. +It uses {@link android.widget.OverScroller} to track the fling gesture. +If the user reaches the content edges +after the fling gesture, the app displays a "glow" effect. +
+ +Note: The {@code InteractiveChart} sample app displays a +chart that you can zoom, pan, scroll, and so on. In the following snippet, +{@code mContentRect} represents the rectangle coordinates within the view that the chart +will be drawn into. At any given time, a subset of the total chart domain and range are drawn +into this rectangular area. +{@code mCurrentViewport} represents the portion of the chart that is currently +visible in the screen. Because pixel offsets are generally treated as integers, +{@code mContentRect} is of the type {@link android.graphics.Rect}. Because the +graph domain and range are decimal/float values, {@code mCurrentViewport} is of +the type {@link android.graphics.RectF}.
+ +The first part of the snippet shows the implementation of +{@link android.view.GestureDetector.OnGestureListener#onFling onFling()}:
+ +// The current viewport. This rectangle represents the currently visible +// chart domain and range. The viewport is the part of the app that the +// user manipulates via touch gestures. +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 OverScroller mScroller; +private RectF mScrollerStartViewport; +... +private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { - // Abort any active scroll animations and invalidate. + // Initiates the decay phase of any active edge effects. + releaseEdgeEffects(); + mScrollerStartViewport.set(mCurrentViewport); + // Aborts any active scroll animations and invalidates. mScroller.forceFinished(true); - // There is also a compatibility version: - // ViewCompat.postInvalidateOnAnimation - postInvalidateOnAnimation(); + ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this); return true; } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, - float distanceX, float distanceY) { - // You don't use a scroller in onScroll because you don't need to animate - // a scroll. The scroll occurs instantly in response to touch feedback. - return false; - } - + ... @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - // Before flinging, abort the current animation. - mScroller.forceFinished(true); - // Begin the scroll animation - mScroller.fling( - // Current scroll position - startX, - startY, - // Velocities, negated for natural touch response - (int) -velocityX, - (int) -velocityY, - // Minimum and maximum scroll positions. The minimum scroll - // position is generally zero and the maximum scroll position - // is generally the content size less the screen size. So if the - // content width is 1000 pixels and the screen width is 200 - // pixels, the maximum scroll offset should be 800 pixels. - minX, maxX, - minY, maxY, - // The maximum overscroll bounds. This is useful when using - // the EdgeEffect class to draw overscroll "glow" overlays. - mContentRect.width() / 2, - mContentRect.height() / 2); - // Invalidate to trigger computeScroll() - postInvalidateOnAnimation(); + fling((int) -velocityX, (int) -velocityY); return true; } }; +private void fling(int velocityX, int velocityY) { + // Initiates the decay phase of any active edge effects. + releaseEdgeEffects(); + // Flings use math in pixels (as opposed to math based on the viewport). + Point surfaceSize = computeScrollSurfaceSize(); + mScrollerStartViewport.set(mCurrentViewport); + int startX = (int) (surfaceSize.x * (mScrollerStartViewport.left - + AXIS_X_MIN) / ( + AXIS_X_MAX - AXIS_X_MIN)); + int startY = (int) (surfaceSize.y * (AXIS_Y_MAX - + mScrollerStartViewport.bottom) / ( + AXIS_Y_MAX - AXIS_Y_MIN)); + // Before flinging, aborts the current animation. + mScroller.forceFinished(true); + // Begins the animation + mScroller.fling( + // Current scroll position + startX, + startY, + velocityX, + velocityY, + /* + * Minimum and maximum scroll positions. The minimum scroll + * position is generally zero and the maximum scroll position + * is generally the content size less the screen size. So if the + * content width is 1000 pixels and the screen width is 200 + * pixels, the maximum scroll offset should be 800 pixels. + */ + 0, surfaceSize.x - mContentRect.width(), + 0, surfaceSize.y - mContentRect.height(), + // The edges of the content. This comes into play when using + // the EdgeEffect class to draw "glow" overlays. + mContentRect.width() / 2, + mContentRect.height() / 2); + // Invalidates to trigger computeScroll() + ViewCompat.postInvalidateOnAnimation(this); +}+ +When {@link android.view.GestureDetector.OnGestureListener#onFling onFling()} calls +{@link android.support.v4.view.ViewCompat#postInvalidateOnAnimation postInvalidateOnAnimation()}, +it triggers +{@link android.view.View#computeScroll computeScroll()} to update the values for x and y. +This is typically be done when a view child is animating a scroll using a scroller object, as in this example.
+ +Most views pass the scroller object's x and y position directly to +{@link android.view.View#scrollTo scrollTo()}. +The following implementation of {@link android.view.View#computeScroll computeScroll()} +takes a different approach—it calls +{@link android.widget.OverScroller#computeScrollOffset computeScrollOffset()} to get the current +location of x and y. When the criteria for displaying an overscroll "glow" edge effect are met +(the display is zoomed in, x or y is out of bounds, and the app isn't already showing an overscroll), +the code sets up the overscroll glow effect and calls +{@link android.support.v4.view.ViewCompat#postInvalidateOnAnimation postInvalidateOnAnimation()} +to trigger an invalidate on the view:
+ +// Edge effect / overscroll tracking objects. +private EdgeEffectCompat mEdgeEffectTop; +private EdgeEffectCompat mEdgeEffectBottom; +private EdgeEffectCompat mEdgeEffectLeft; +private EdgeEffectCompat mEdgeEffectRight; + +private boolean mEdgeEffectTopActive; +private boolean mEdgeEffectBottomActive; +private boolean mEdgeEffectLeftActive; +private boolean mEdgeEffectRightActive; + @Override public void computeScroll() { super.computeScroll(); - // Compute the current scroll offsets. If this returns true, then the - // scroll has not yet finished. + boolean needsInvalidate = false; + + // The scroller isn't finished, meaning a fling or programmatic pan + // operation is currently active. if (mScroller.computeScrollOffset()) { + Point surfaceSize = computeScrollSurfaceSize(); int currX = mScroller.getCurrX(); int currY = mScroller.getCurrY(); - // Actually render the scrolled viewport, or actually scroll the - // view using View.scrollTo. + boolean canScrollX = (mCurrentViewport.left > AXIS_X_MIN + || mCurrentViewport.right < AXIS_X_MAX); + boolean canScrollY = (mCurrentViewport.top > AXIS_Y_MIN + || mCurrentViewport.bottom < AXIS_Y_MAX); - // If currX or currY are outside the bounds, render the overscroll - // glow using EdgeEffect. + /* + * If you are zoomed in and currX or currY is + * outside of bounds and you're not already + * showing overscroll, then render the overscroll + * glow edge effect. + */ + if (canScrollX + && currX < 0 + && mEdgeEffectLeft.isFinished() + && !mEdgeEffectLeftActive) { + mEdgeEffectLeft.onAbsorb((int) + OverScrollerCompat.getCurrVelocity(mScroller)); + mEdgeEffectLeftActive = true; + needsInvalidate = true; + } else if (canScrollX + && currX > (surfaceSize.x - mContentRect.width()) + && mEdgeEffectRight.isFinished() + && !mEdgeEffectRightActive) { + mEdgeEffectRight.onAbsorb((int) + OverScrollerCompat.getCurrVelocity(mScroller)); + mEdgeEffectRightActive = true; + needsInvalidate = true; + } - } else { - // The scroll has finished. - } + if (canScrollY + && currY < 0 + && mEdgeEffectTop.isFinished() + && !mEdgeEffectTopActive) { + mEdgeEffectTop.onAbsorb((int) + OverScrollerCompat.getCurrVelocity(mScroller)); + mEdgeEffectTopActive = true; + needsInvalidate = true; + } else if (canScrollY + && currY > (surfaceSize.y - mContentRect.height()) + && mEdgeEffectBottom.isFinished() + && !mEdgeEffectBottomActive) { + mEdgeEffectBottom.onAbsorb((int) + OverScrollerCompat.getCurrVelocity(mScroller)); + mEdgeEffectBottomActive = true; + needsInvalidate = true; + } + ... + }+ +Here is the section of the code that performs the actual zoom:
+ +// Custom object that is functionally similar to Scroller +Zoomer mZoomer; +private PointF mZoomFocalPoint = new PointF(); +... + +// If a zoom is in progress (either programmatically or via double +// touch), performs the zoom. +if (mZoomer.computeZoom()) { + float newWidth = (1f - mZoomer.getCurrZoom()) * + mScrollerStartViewport.width(); + float newHeight = (1f - mZoomer.getCurrZoom()) * + mScrollerStartViewport.height(); + float pointWithinViewportX = (mZoomFocalPoint.x - + mScrollerStartViewport.left) + / mScrollerStartViewport.width(); + float pointWithinViewportY = (mZoomFocalPoint.y - + mScrollerStartViewport.top) + / mScrollerStartViewport.height(); + mCurrentViewport.set( + mZoomFocalPoint.x - newWidth * pointWithinViewportX, + mZoomFocalPoint.y - newHeight * pointWithinViewportY, + mZoomFocalPoint.x + newWidth * (1 - pointWithinViewportX), + mZoomFocalPoint.y + newHeight * (1 - pointWithinViewportY)); + constrainViewport(); + needsInvalidate = true; +} +if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); +} ++ +This is the {@code computeScrollSurfaceSize()} method that's called in the above snippet. It +computes the current scrollable surface size, in pixels. For example, if the entire chart area is visible, +this is simply the current size of {@code mContentRect}. If the chart is zoomed in 200% in both directions, +the returned size will be twice as large horizontally and vertically.
+ +private Point computeScrollSurfaceSize() { + return new Point( + (int) (mContentRect.width() * (AXIS_X_MAX - AXIS_X_MIN) + / mCurrentViewport.width()), + (int) (mContentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN) + / mCurrentViewport.height())); }-For another example of scroller usage, see the source code for the -{@link android.support.v4.view.ViewPager} class.
+For another example of scroller usage, see the +source code for the +{@link android.support.v4.view.ViewPager} class. It scrolls in response to flings, +and uses scrolling to implement the "snapping to page" animation.
+ diff --git a/docs/html/training/gestures/viewgroup.jd b/docs/html/training/gestures/viewgroup.jd index 257a5d816e56f..5b32300b99b06 100644 --- a/docs/html/training/gestures/viewgroup.jd +++ b/docs/html/training/gestures/viewgroup.jd @@ -26,12 +26,19 @@ next.link=
InteractiveChart.zip
+