在 ACTION_POINTER_UP 事件,例程中提取了原始指针的 index 确保活动指针的 ID 不是不触摸屏幕的指针。如果它是 app 选择另一个指针活动并且保存其当前的 X Y 坐标。因为当前保存的指针在 ACTION_MOVE 被用于计算在屏幕上的移动距离,app 将总是使用正确的指针计算移动距离。
// The ‘active pointer’ is the one currently moving our object.privateint mActivePointerId = INVALID_POINTER_ID;@OverridepublicbooleanonTouchEvent(MotionEvent ev) {// Let the ScaleGestureDetector inspect all events.mScaleDetector.onTouchEvent(ev);finalint action =MotionEventCompat.getActionMasked(ev);switch (action) {caseMotionEvent.ACTION_DOWN: {finalint pointerIndex =MotionEventCompat.getActionIndex(ev);finalfloat x =MotionEventCompat.getX(ev, pointerIndex);finalfloat 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; }caseMotionEvent.ACTION_MOVE: {// Find the index of the active pointer and fetch its positionfinalint pointerIndex =MotionEventCompat.findPointerIndex(ev, mActivePointerId);finalfloat x =MotionEventCompat.getX(ev, pointerIndex);finalfloat y =MotionEventCompat.getY(ev, pointerIndex);// Calculate the distance movedfinalfloat dx = x - mLastTouchX;finalfloat dy = y - mLastTouchY; mPosX += dx; mPosY += dy;invalidate();// Remember this touch position for the next move event mLastTouchX = x; mLastTouchY = y;break; }caseMotionEvent.ACTION_UP: { mActivePointerId = INVALID_POINTER_ID;break; }caseMotionEvent.ACTION_CANCEL: { mActivePointerId = INVALID_POINTER_ID;break; }caseMotionEvent.ACTION_POINTER_UP: {finalint pointerIndex =MotionEventCompat.getActionIndex(ev);finalint pointerId =MotionEventCompat.getPointerId(ev, pointerIndex);if (pointerId == mActivePointerId) {// This was our active pointer going up. Choose a new// active pointer and adjust accordingly.finalint newPointerIndex = pointerIndex ==0?1:0; mLastTouchX =MotionEventCompat.getX(ev, newPointerIndex); mLastTouchY =MotionEventCompat.getY(ev, newPointerIndex); mActivePointerId =MotionEventCompat.getPointerId(ev, newPointerIndex); }break; } }returntrue;}
二、 拖动平移
上一节展示了一个在屏幕上拖动对象的例子。另一种常见的情境是平移,也就是当用户拖动事件发生的时候对象在 X Y 轴方向均发生滚动。上面的代码片段中直接拦截 MotionEvent 活动实现拖动。本节代码片段得益于平台对常见手势的支持。覆写了 GestureDetector.SimpleOnGestureListener.onScroll()。
为了提供更多的上下文,当用户使用手指拖动内容平移的时候 onScroll() 被调用。onScroll() 仅当手指按着的时候被调用;只要手指离开屏幕手势事件结束,或者 fling 手势被调用(如果手指在它离开前加速移动了)。关于移动滚动的更多讨论:Animating a Scroll Gesture。
代码片段摘自 onScroll():
// The current viewport. This rectangle represents the currently visible// chart domain and range.privateRectF mCurrentViewport =newRectF(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.privateRect mContentRect;privatefinalGestureDetector.SimpleOnGestureListener mGestureListener=new GestureDetector.SimpleOnGestureListener() {...@OverridepublicbooleanonScroll(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);...returntrue;}
实现 onScroll() 滚动视图,响应触摸手势:
/** * 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. */privatevoidsetViewportBottomLeft(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);}
privateScaleGestureDetector mScaleDetector;privatefloat mScaleFactor =1.f;publicMyCustomView(Context mContext){...// View code goes here... mScaleDetector =newScaleGestureDetector(context,new ScaleListener());}@OverridepublicbooleanonTouchEvent(MotionEvent ev) {// Let the ScaleGestureDetector inspect all events.mScaleDetector.onTouchEvent(ev);returntrue;}@OverridepublicvoidonDraw(Canvas canvas) { super.onDraw(canvas);canvas.save();canvas.scale(mScaleFactor, mScaleFactor);...// onDraw() code goes here...canvas.restore();}privateclassScaleListenerextendsScaleGestureDetector.SimpleOnScaleGestureListener { @OverridepublicbooleanonScale(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();returntrue; }}
@OverrideprivateRectF mCurrentViewport =newRectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);privateRect mContentRect;privateScaleGestureDetector mScaleGestureDetector;...publicbooleanonTouchEvent(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. */privatefinalScaleGestureDetector.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. */privatePointF viewportFocus =newPointF();privatefloat lastSpanX;privatefloat lastSpanY;// Detects that new pointers are going down. @OverridepublicbooleanonScaleBegin(ScaleGestureDetector scaleGestureDetector) { lastSpanX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector); lastSpanY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector);returntrue; } @OverridepublicbooleanonScale(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;returntrue; }};