拖拽与缩放

编写:Andrwyw - 原文:http://developer.android.com/training/gestures/scale.html

本节课程讲述,使用onTouchEvent()截获触摸事件后,如何使用触摸手势拖拽、缩放屏幕上的对象。

拖拽一个对象

如果我们的目标版本为3.0或以上,我们可以使用View.OnDragListener监听内置的拖放(drag-and-drop)事件,拖拽与释放中有更多相关描述。

对于触摸手势来说,一个很常见的操作是在屏幕上拖拽一个对象。接下来的代码段让用户可以拖拽屏幕上的图片。需要注意以下几点:

  • 拖拽操作时,即使有额外的手指放置到屏幕上了,app也必须保持对最初的点(手指)的追踪。比如,想象在拖拽图片时,用户放置了第二根手指在屏幕上,并且抬起了第一根手指。如果我们的app只是单独地追踪每个点,它会把第二个点当做默认的点,并且把图片移到该点的位置。
  • 为了防止这种情况发生,我们的app需要区分初始点以及随后任意的触摸点。要做到这一点,它需要追踪处理多触摸手势章节中提到过的 ACTION_POINTER_DOWNACTION_POINTER_UP 事件。每当第二根手指按下或拿起时,ACTION_POINTER_DOWNACTION_POINTER_UP 事件就会传递给onTouchEvent()回调函数。
  • ACTION_POINTER_UP事件发生时,示例程序会移除对该点的索引值的引用,确保操作中的点的ID(the active pointer ID)不会引用已经不在触摸屏上的触摸点。这种情况下,app会选择另一个触摸点来作为操作中(active)的点,并保存它当前的x、y值。由于在ACTION_MOVE事件时,这个保存的位置会被用来计算屏幕上的对象将要移动的距离,所以app会始终根据正确的触摸点来计算移动的距离。

下面的代码段允许用户拖拽屏幕上的对象。它会记录操作中的点(active pointer)的初始位置,计算触摸点移动过的距离,再把对象移动到新的位置。如上所述,它也正确地处理了额外触摸点的可能。

需要注意的是,代码段中使用了getActionMasked()函数。我们应该始终使用这个函数(或者最好用MotionEventCompat.getActionMasked()这个兼容版本)来获得MotionEvent对应的动作(action)。不像旧的getAction()函数,getActionMasked()就是设计用来处理多点触摸的。它会返回执行过的动作的掩码值,不包括该点的索引位。

  1. // The ‘active pointer’ is the one currently moving our object.
  2. private int mActivePointerId = INVALID_POINTER_ID;
  3. @Override
  4. public boolean onTouchEvent(MotionEvent ev) {
  5. // Let the ScaleGestureDetector inspect all events.
  6. mScaleDetector.onTouchEvent(ev);
  7. final int action = MotionEventCompat.getActionMasked(ev);
  8. switch (action) {
  9. case MotionEvent.ACTION_DOWN: {
  10. final int pointerIndex = MotionEventCompat.getActionIndex(ev);
  11. final float x = MotionEventCompat.getX(ev, pointerIndex);
  12. final float y = MotionEventCompat.getY(ev, pointerIndex);
  13. // Remember where we started (for dragging)
  14. mLastTouchX = x;
  15. mLastTouchY = y;
  16. // Save the ID of this pointer (for dragging)
  17. mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
  18. break;
  19. }
  20. case MotionEvent.ACTION_MOVE: {
  21. // Find the index of the active pointer and fetch its position
  22. final int pointerIndex =
  23. MotionEventCompat.findPointerIndex(ev, mActivePointerId);
  24. final float x = MotionEventCompat.getX(ev, pointerIndex);
  25. final float y = MotionEventCompat.getY(ev, pointerIndex);
  26. // Calculate the distance moved
  27. final float dx = x - mLastTouchX;
  28. final float dy = y - mLastTouchY;
  29. mPosX += dx;
  30. mPosY += dy;
  31. invalidate();
  32. // Remember this touch position for the next move event
  33. mLastTouchX = x;
  34. mLastTouchY = y;
  35. break;
  36. }
  37. case MotionEvent.ACTION_UP: {
  38. mActivePointerId = INVALID_POINTER_ID;
  39. break;
  40. }
  41. case MotionEvent.ACTION_CANCEL: {
  42. mActivePointerId = INVALID_POINTER_ID;
  43. break;
  44. }
  45. case MotionEvent.ACTION_POINTER_UP: {
  46. final int pointerIndex = MotionEventCompat.getActionIndex(ev);
  47. final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
  48. if (pointerId == mActivePointerId) {
  49. // This was our active pointer going up. Choose a new
  50. // active pointer and adjust accordingly.
  51. final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
  52. mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex);
  53. mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex);
  54. mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
  55. }
  56. break;
  57. }
  58. }
  59. return true;
  60. }

通过拖拽平移

前一节展示了一个,在屏幕上拖拽对象的例子。另一个常见的场景是平移panning),平移是指用户通过拖拽移动引起x、y轴方向发生滚动(scrolling)。上面的代码段直接截获了MotionEvent动作来实现拖拽。这一部分的代码段,利用了平台对常用手势的内置支持。它重写了GestureDetector.SimpleOnGestureListeneronScroll()函数。

更详细地说,当用户拖拽手指来平移内容时,onScroll()函数就会被调用。onScroll()函数只会在手指按下的情况下被调用,一旦手指离开屏幕了,要么手势终止,要么快速滑动(fling)手势开始(如果手指在离开屏幕前快速移动了一段距离)。关于滚动与快速滑动的更多讨论,可以查看滚动手势动画章节。

这里是onScroll()的相关代码段:

  1. // The current viewport. This rectangle represents the currently visible
  2. // chart domain and range.
  3. private RectF mCurrentViewport =
  4. new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
  5. // The current destination rectangle (in pixel coordinates) into which the
  6. // chart data should be drawn.
  7. private Rect mContentRect;
  8. private final GestureDetector.SimpleOnGestureListener mGestureListener
  9. = new GestureDetector.SimpleOnGestureListener() {
  10. ...
  11. @Override
  12. public boolean onScroll(MotionEvent e1, MotionEvent e2,
  13. float distanceX, float distanceY) {
  14. // Scrolling uses math based on the viewport (as opposed to math using pixels).
  15. // Pixel offset is the offset in screen pixels, while viewport offset is the
  16. // offset within the current viewport.
  17. float viewportOffsetX = distanceX * mCurrentViewport.width()
  18. / mContentRect.width();
  19. float viewportOffsetY = -distanceY * mCurrentViewport.height()
  20. / mContentRect.height();
  21. ...
  22. // Updates the viewport, refreshes the display.
  23. setViewportBottomLeft(
  24. mCurrentViewport.left + viewportOffsetX,
  25. mCurrentViewport.bottom + viewportOffsetY);
  26. ...
  27. return true;
  28. }

onScroll()函数中滑动视窗(viewport)来响应触摸手势的实现:

  1. /**
  2. * Sets the current viewport (defined by mCurrentViewport) to the given
  3. * X and Y positions. Note that the Y value represents the topmost pixel position,
  4. * and thus the bottom of the mCurrentViewport rectangle.
  5. */
  6. private void setViewportBottomLeft(float x, float y) {
  7. /*
  8. * Constrains within the scroll range. The scroll range is simply the viewport
  9. * extremes (AXIS_X_MAX, etc.) minus the viewport size. For example, if the
  10. * extremes were 0 and 10, and the viewport size was 2, the scroll range would
  11. * be 0 to 8.
  12. */
  13. float curWidth = mCurrentViewport.width();
  14. float curHeight = mCurrentViewport.height();
  15. x = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth));
  16. y = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX));
  17. mCurrentViewport.set(x, y - curHeight, x + curWidth, y);
  18. // Invalidates the View to update the display.
  19. ViewCompat.postInvalidateOnAnimation(this);
  20. }

使用触摸手势进行缩放

如同检测常用手势章节中提到的,GestureDetector可以帮助我们检测Android中的常见手势,例如滚动,快速滚动以及长按。对于缩放,Android也提供了ScaleGestureDetector类。当我们想让view能识别额外的手势时,我们可以同时使用GestureDetectorScaleGestureDetector类。

为了报告检测到的手势事件,手势检测需要一个作为构造函数参数的listener对象。ScaleGestureDetector使用ScaleGestureDetector.OnScaleGestureListener。Android提供了ScaleGestureDetector.SimpleOnScaleGestureListener类作为帮助类,如果我们不是关注所有的手势事件,我们可以继承(extend)它。

基本的缩放示例

下面的代码段展示了缩放功能中的基本部分。

  1. private ScaleGestureDetector mScaleDetector;
  2. private float mScaleFactor = 1.f;
  3. public MyCustomView(Context mContext){
  4. ...
  5. // View code goes here
  6. ...
  7. mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
  8. }
  9. @Override
  10. public boolean onTouchEvent(MotionEvent ev) {
  11. // Let the ScaleGestureDetector inspect all events.
  12. mScaleDetector.onTouchEvent(ev);
  13. return true;
  14. }
  15. @Override
  16. public void onDraw(Canvas canvas) {
  17. super.onDraw(canvas);
  18. canvas.save();
  19. canvas.scale(mScaleFactor, mScaleFactor);
  20. ...
  21. // onDraw() code goes here
  22. ...
  23. canvas.restore();
  24. }
  25. private class ScaleListener
  26. extends ScaleGestureDetector.SimpleOnScaleGestureListener {
  27. @Override
  28. public boolean onScale(ScaleGestureDetector detector) {
  29. mScaleFactor *= detector.getScaleFactor();
  30. // Don't let the object get too small or too large.
  31. mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
  32. invalidate();
  33. return true;
  34. }
  35. }

更加复杂的缩放示例

这是本章节提供的InteractiveChart示例中一个更复杂的示范。通过使用ScaleGestureDetector中的”span”(getCurrentSpanX/Y)和”focus”(getFocusX/Y)功能,InteractiveChart示例同时支持滚动(平移)以及多指缩放。

  1. @Override
  2. private RectF mCurrentViewport =
  3. new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
  4. private Rect mContentRect;
  5. private ScaleGestureDetector mScaleGestureDetector;
  6. ...
  7. public boolean onTouchEvent(MotionEvent event) {
  8. boolean retVal = mScaleGestureDetector.onTouchEvent(event);
  9. retVal = mGestureDetector.onTouchEvent(event) || retVal;
  10. return retVal || super.onTouchEvent(event);
  11. }
  12. /**
  13. * The scale listener, used for handling multi-finger scale gestures.
  14. */
  15. private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
  16. = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
  17. /**
  18. * This is the active focal point in terms of the viewport. Could be a local
  19. * variable but kept here to minimize per-frame allocations.
  20. */
  21. private PointF viewportFocus = new PointF();
  22. private float lastSpanX;
  23. private float lastSpanY;
  24. // Detects that new pointers are going down.
  25. @Override
  26. public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
  27. lastSpanX = ScaleGestureDetectorCompat.
  28. getCurrentSpanX(scaleGestureDetector);
  29. lastSpanY = ScaleGestureDetectorCompat.
  30. getCurrentSpanY(scaleGestureDetector);
  31. return true;
  32. }
  33. @Override
  34. public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
  35. float spanX = ScaleGestureDetectorCompat.
  36. getCurrentSpanX(scaleGestureDetector);
  37. float spanY = ScaleGestureDetectorCompat.
  38. getCurrentSpanY(scaleGestureDetector);
  39. float newWidth = lastSpanX / spanX * mCurrentViewport.width();
  40. float newHeight = lastSpanY / spanY * mCurrentViewport.height();
  41. float focusX = scaleGestureDetector.getFocusX();
  42. float focusY = scaleGestureDetector.getFocusY();
  43. // Makes sure that the chart point is within the chart region.
  44. // See the sample for the implementation of hitTest().
  45. hitTest(scaleGestureDetector.getFocusX(),
  46. scaleGestureDetector.getFocusY(),
  47. viewportFocus);
  48. mCurrentViewport.set(
  49. viewportFocus.x
  50. - newWidth * (focusX - mContentRect.left)
  51. / mContentRect.width(),
  52. viewportFocus.y
  53. - newHeight * (mContentRect.bottom - focusY)
  54. / mContentRect.height(),
  55. 0,
  56. 0);
  57. mCurrentViewport.right = mCurrentViewport.left + newWidth;
  58. mCurrentViewport.bottom = mCurrentViewport.top + newHeight;
  59. ...
  60. // Invalidates the View to update the display.
  61. ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
  62. lastSpanX = spanX;
  63. lastSpanY = spanY;
  64. return true;
  65. }
  66. };