Android最佳实践之触摸手势

来源:互联网 发布:淘宝mk著名原单店 编辑:程序博客网 时间:2024/05/21 11:26

普通手势

参考地址:http://developer.android.com/training/gestures/detector.html#data
当用户一根或多根手指在屏幕上运动的时候,就开始产生了手势事件,我们用onTouchEvent()回调方法来处理。

为Activity 或View捕捉触摸事件

使用getActionMasked()来提取event中的action。

public class MainActivity extends Activity {...// This example shows an Activity, but you would use the same approach if// you were subclassing a View.@Overridepublic boolean onTouchEvent(MotionEvent event){     int action = MotionEventCompat.getActionMasked(event);    switch(action) {        case (MotionEvent.ACTION_DOWN) :            Log.d(DEBUG_TAG,"Action was DOWN");            return true;        case (MotionEvent.ACTION_MOVE) :            Log.d(DEBUG_TAG,"Action was MOVE");            return true;        case (MotionEvent.ACTION_UP) :            Log.d(DEBUG_TAG,"Action was UP");            return true;        case (MotionEvent.ACTION_CANCEL) :            Log.d(DEBUG_TAG,"Action was CANCEL");            return true;        case (MotionEvent.ACTION_OUTSIDE) :            Log.d(DEBUG_TAG,"Movement occurred outside bounds " +                    "of current screen element");            return true;              default :             return super.onTouchEvent(event);    }      }

为一个View添加触摸事件

你可以使用View的View.OnTouchListener监听器为任意的View注册监听事件,而不需要继承View重写onTouchEvent()。

View myView = findViewById(R.id.my_view); myView.setOnTouchListener(new OnTouchListener() {    public boolean onTouch(View v, MotionEvent event) {        // ... Respond to touch events               return true;    }});

注意,如果在ACTION_DOWN事件中return false,那么接下来的ACTION_MOVE 和 ACTION_UP事件都不会被回调

监测所有支持的手势

public class MainActivity extends Activity implements         GestureDetector.OnGestureListener,        GestureDetector.OnDoubleTapListener{    private static final String DEBUG_TAG = "Gestures";    private GestureDetectorCompat mDetector;     // Called when the activity is first created.     @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // Instantiate the gesture detector with the        // application context and an implementation of        // GestureDetector.OnGestureListener        mDetector = new GestureDetectorCompat(this,this);        // Set the gesture detector as the double tap        // listener.        mDetector.setOnDoubleTapListener(this);    }    @Override     public boolean onTouchEvent(MotionEvent event){         this.mDetector.onTouchEvent(event);        // Be sure to call the superclass implementation        return super.onTouchEvent(event);    }    @Override    public boolean onDown(MotionEvent event) {         Log.d(DEBUG_TAG,"onDown: " + event.toString());         return true;    }    @Override    public boolean onFling(MotionEvent event1, MotionEvent event2,             float velocityX, float velocityY) {        Log.d(DEBUG_TAG, "onFling: " + event1.toString()+event2.toString());        return true;    }    @Override    public void onLongPress(MotionEvent event) {        Log.d(DEBUG_TAG, "onLongPress: " + event.toString());     }    @Override    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,            float distanceY) {        Log.d(DEBUG_TAG, "onScroll: " + e1.toString()+e2.toString());        return true;    }    @Override    public void onShowPress(MotionEvent event) {        Log.d(DEBUG_TAG, "onShowPress: " + event.toString());    }    @Override    public boolean onSingleTapUp(MotionEvent event) {        Log.d(DEBUG_TAG, "onSingleTapUp: " + event.toString());        return true;    }    @Override    public boolean onDoubleTap(MotionEvent event) {        Log.d(DEBUG_TAG, "onDoubleTap: " + event.toString());        return true;    }    @Override    public boolean onDoubleTapEvent(MotionEvent event) {        Log.d(DEBUG_TAG, "onDoubleTapEvent: " + event.toString());        return true;    }    @Override    public boolean onSingleTapConfirmed(MotionEvent event) {        Log.d(DEBUG_TAG, "onSingleTapConfirmed: " + event.toString());        return true;    }}

监测支持手势的子集

如果你只想处理一些简单手势的,那么可以继承GestureDetector.SimpleOnGestureListener而不用实现GestureDetector.OnGestureListener监听。
同样,在onDown() 中return false,那么接下来的所有事件都不会发生了。

public class MainActivity extends Activity {     private GestureDetectorCompat mDetector;     @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mDetector = new GestureDetectorCompat(this, new MyGestureListener());    }    @Override     public boolean onTouchEvent(MotionEvent event){         this.mDetector.onTouchEvent(event);        return super.onTouchEvent(event);    }    class MyGestureListener extends GestureDetector.SimpleOnGestureListener {        private static final String DEBUG_TAG = "Gestures";         @Override        public boolean onDown(MotionEvent event) {             Log.d(DEBUG_TAG,"onDown: " + event.toString());             return true;        }        @Override        public boolean onFling(MotionEvent event1, MotionEvent event2,                 float velocityX, float velocityY) {            Log.d(DEBUG_TAG, "onFling: " + event1.toString()+event2.toString());            return true;        }    }}

跟踪运动速度

原文地址:http://developer.android.com/training/gestures/movement.html

有一些不同的方法来跟踪一个手势的运动,根据不同程序的需要:

  • 移动的开始和结束位置(例如,将一个屏幕上的物体从a点移动到B点)。
  • 根据x、y坐标决定移动的方向
  • 历史点。通过getHistorySize()的方法得到手势历史的大小。getHistorical方法可以获得每个点的位置、大小、时间和压力
  • 点的运动速速

跟踪速度

Android提供 VelocityTracker类和 Support Library中提供的VelocityTrackerCompat类来帮助我们跟踪触摸事件的速度。

public class MainActivity extends Activity {    private static final String DEBUG_TAG = "Velocity";        ...    private VelocityTracker mVelocityTracker = null;    @Override    public boolean onTouchEvent(MotionEvent event) {        int index = event.getActionIndex();        int action = event.getActionMasked();        int pointerId = event.getPointerId(index);        switch(action) {            case MotionEvent.ACTION_DOWN:                if(mVelocityTracker == null) {                    // Retrieve a new VelocityTracker object to watch the velocity of a motion.                    mVelocityTracker = VelocityTracker.obtain();                }                else {                    // Reset the velocity tracker back to its initial state.                    mVelocityTracker.clear();                }                // Add a user's movement to the tracker.                mVelocityTracker.addMovement(event);                break;            case MotionEvent.ACTION_MOVE:                mVelocityTracker.addMovement(event);                // When you want to determine the velocity, call                 // computeCurrentVelocity(). Then call getXVelocity()                 // and getYVelocity() to retrieve the velocity for each pointer ID.                 mVelocityTracker.computeCurrentVelocity(1000);                // Log velocity of pixels per second                // Best practice to use VelocityTrackerCompat where possible.                Log.d("", "X velocity: " +                         VelocityTrackerCompat.getXVelocity(mVelocityTracker,                         pointerId));                Log.d("", "Y velocity: " +                         VelocityTrackerCompat.getYVelocity(mVelocityTracker,                        pointerId));                break;            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                // Return a VelocityTracker object back to be re-used by others.                mVelocityTracker.recycle();                break;        }        return true;    }}

注意:请注意,你应该在ACTION_MOVE事件后计算速度,而不是ACTION_UP之后。因为ACTION_UP后,X和Y速度为0。

滚动手势

参考地址:http://developer.android.com/training/gestures/scroll.html
本文介绍使用scrollers响应触摸手势来显示一个滚动效果。
ScrollerOverScroller很类似,都可以针对Touch Event产生一个滚动动画。但是OverScroller包含了一些方法可以告诉用户他们已经到了内容边缘,InteractiveChart 例子中的EdgeEffectCompat类就展示了一个绚丽的用户到达内容边缘的效果。

注意:建议使用OverScroller而不是Scroller类来进行滚动动画。OverScroller有很好的向后兼容性。还要注意,一般你只在实现滚动自己的情况下,只需要实现scrollers。而ScrollViewHorizontalScrollView对它们内嵌的布局中已经做了所有的事情。

理解滚动术语

滚动在X和Y两个方向进行时,称为“panning”。滚动有两种类型,拖拽(Draging)和快速滑动(Flinging)。

  • Dragging :简单的dragging需要实现GestureDetector.OnGestureListener中的onScroll()方法。
  • Flinging :Flinging是用户拖拽一个view并进行快速的上下滑动。需要实现GestureDetector.OnGestureListener中的onFling()方法,并使用scroller 对象。这就是本文的主题。

实现基于触摸的滚动

InteractiveChart 例子,显示了一个可以缩放、左右滚动的图表。它里面实现了GestureDetector.SimpleOnGestureListener 中的onFling()方法,并使用了OverScroller来跟踪快速滑动手势。当用户滑到了内容边缘,则产生一个绚丽的效果。下面时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) {        // Initiates the decay phase of any active edge effects.        releaseEdgeEffects();        mScrollerStartViewport.set(mCurrentViewport);        // Aborts any active scroll animations and invalidates.        mScroller.forceFinished(true);        ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);        return true;    }    ...    @Override    public boolean onFling(MotionEvent e1, MotionEvent e2,             float velocityX, float velocityY) {        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);}

当onFling()调用postInvalidateOnAnimation(),它触发computeScroll()来更新x和y的值,这通常是使用scroller 对象来对View进行滚动时发生的。
大多数View通过scroller对象的x和y坐标直接传给scrollTo(),下面computeScroll()的实现,采用了一个不同的方法,调用computeScrollOffset()方法来获取当前位置的x、y值。当达到了overscroll 的条件(放大的视图,并且x、y超出边界,并且还没有显示overscroll),代码中将产生一个 overscroll的绚丽效果,并调用postInvalidateOnAnimation()来刷新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;@Overridepublic void computeScroll() {    super.computeScroll();    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();        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 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;        }        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;        }        ...    }

下面是执行缩放的代码:

// Custom object that is functionally similar to ScrollerZoomer 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);}

就是这个computeScrollSurfaceSize()方法,它计算了当前的滚动平面的像素尺寸。比如:如果整个图表都可见,那就返回mContentRect的当前尺寸。如果图表在两个方向都放大了200%,则返回的值在水平和竖直方向都返回2倍的大小。

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()));}

处理多点触摸手势

参考地址:http://developer.android.com/training/gestures/multi.html

跟踪多个手指

当多个手指(Pointer)同时触摸屏幕,系统生成下面的触摸事件:

  • ACTION_DOWN:当第一个手指触摸了屏幕,就触发了。在MotionEvent中它指向的手指index永远为0。
  • ACTION_POINTER_DOWN:除了第一个手指,另外的手指戳到屏幕触发此事件,通过调用getActionIndex()方法返回手指的index。
  • ACTION_MOVE
  • ACTION_POINTER_UP:当非第一个手指Up的时候调用。
  • ACTION_UP:当最后一根手指离开屏幕调用
    你要在MotionEvent中通过每一个手指的indexID来跟踪每一根手指。
  • Index:MotionEvent使用一个数组存储了每一根手指的信息。手指的index就是这个数组的position。大多数情况下你要使用MotionEvent 来和手指交互,都是取Index。
  • ID:每个手指也有一个ID,保证在整个触摸事件允许跟踪单个手指。
    index是会变得,但ID不会变,只要手指是活跃的。使用getPointerId() 来获得一个手指的ID。因此,对于连续的触摸事件,通过findPointerIndex() 方法传入一个固定的ID值,就可以返回这个手指的index。
private int mActivePointerId;public boolean onTouchEvent(MotionEvent event) {    ....    // Get the pointer ID    mActivePointerId = event.getPointerId(0);    // ... Many touch events later...    // Use the pointer ID to find the index of the active pointer     // and fetch its position    int pointerIndex = event.findPointerIndex(mActivePointerId);    // Get the pointer's current position    float x = event.getX(pointerIndex);    float y = event.getY(pointerIndex);}

获取事件的Action

你要使用getActionMasked()(也许更好的是兼容的Support Library中提供的MotionEventCompat.getActionMasked()方法)方法获取事件的action。不像老版的getAction()方法,getActionMasked()是为多点触摸设计的。你可以使用getActionIndex()方法返回这个action对应的手指index。

int action = MotionEventCompat.getActionMasked(event);// Get the index of the pointer associated with the action.int index = MotionEventCompat.getActionIndex(event);int xPos = -1;int yPos = -1;Log.d(DEBUG_TAG,"The action is " + actionToString(action));if (event.getPointerCount() > 1) {    Log.d(DEBUG_TAG,"Multitouch event");     // The coordinates of the current screen contact, relative to     // the responding View or Activity.      xPos = (int)MotionEventCompat.getX(event, index);    yPos = (int)MotionEventCompat.getY(event, index);} else {    // Single touch event    Log.d(DEBUG_TAG,"Single touch event");     xPos = (int)MotionEventCompat.getX(event, index);    yPos = (int)MotionEventCompat.getY(event, index);}...// Given an action int, returns a string descriptionpublic static String actionToString(int action) {    switch (action) {        case MotionEvent.ACTION_DOWN: return "Down";        case MotionEvent.ACTION_MOVE: return "Move";        case MotionEvent.ACTION_POINTER_DOWN: return "Pointer Down";        case MotionEvent.ACTION_UP: return "Up";        case MotionEvent.ACTION_POINTER_UP: return "Pointer Up";        case MotionEvent.ACTION_OUTSIDE: return "Outside";        case MotionEvent.ACTION_CANCEL: return "Cancel";    }    return "";}

拖拽和缩放

参考地址:http://developer.android.com/training/gestures/scale.html#pan

如果你的设备是Android 3.0 及以上版本,可以使用View.OnDragListener来处理拖拽事件。

拖拽一个对象

在拖拽事件中,即使加入了新的手指,app也不得不跟踪最开始的手指。例如,在屏幕上拖拽一个图片,然后放入第二根手指,并松开第一个手指。如果app单独处理单个手指的话,那么图片就会停在那里不动了。
为了防止这样的事发生,在ACTION_POINTER_UP事件中,提取了index并确保激活的手指id不再指向离开了屏幕的手指。这样,app选择了一个不同的手指来保存x、y坐标,因为这个坐标是ACTION_MOVE事件中计算移动对象的距离的,所以app一直都在使用正确的手指计算移动数据。

// The ‘active pointer’ is the one currently moving our object.private int mActivePointerId = INVALID_POINTER_ID;@Overridepublic 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;}

Drag to Pan

当用户用手指拖拽内容时,onScroll()只在手指按下时被调用,一旦手指离开屏幕,手势要么结束,要么onFling(离开前快速滑动)调用。

// 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() {...@Overridepublic 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;}/** * 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);}

使用触摸执行缩放

Android提供ScaleGestureDetector.OnScaleGestureListener来处理触摸的缩放。基本缩放的代码如下:

private ScaleGestureDetector mScaleDetector;private float mScaleFactor = 1.f;public MyCustomView(Context mContext){    ...    // View code goes here    ...    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());}@Overridepublic boolean onTouchEvent(MotionEvent ev) {    // Let the ScaleGestureDetector inspect all events.    mScaleDetector.onTouchEvent(ev);    return true;}@Overridepublic 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;    }}

在InteractiveChart 例子中提供了更加复杂的缩放例子:
InteractiveChart例子支持多手指的滚动和缩放:

@Overrideprivate 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;    }};

处理ViewGroup中的触摸事件

参考地址:http://developer.android.com/training/gestures/viewgroup.html
ViewGroup中,为确保每个子View能接收到触摸事件,覆盖onInterceptTouchEvent()方法。

当ViewGroup及其子View发生触摸事件都会调用onInterceptTouchEvent()方法。如果onInterceptTouchEvent()返回true,MotionEvent就会被拦截,意味着它不会传递到子View,而是执行父View的onTouchEvent()。
onInterceptTouchEvent()方法给了父View一个机会看它的子View执行之前的触摸事件。如果你onInterceptTouchEvent()返回true,那么你的子View准备处理触摸事件的现在会接收ACTION_CANCEL事件,并将手指的事件传递到父View的onTouchEvent()中;因为通常onInterceptTouchEvent()方法会返回false,并将事件通过View Hierarchy传递到子View,让每个子View执行它自己的onTouchEvent()
下面的代码中,MyViewGroup继承ViewGroup,MyViewGroup有多个子View。如果水平拖动一个子View,子View不再响应Touch events,这时MyViewGroup应该处理滚动这些内容的事件;然而,如果你按下一个按钮,或垂直滚动子View,MyViewGroup就不应该拦截这些事件,因为子View需要响应这些事件。在这些情况下,onInterceptTouchEvent()将返回false,MyViewGroup 的onTouchEvent()方法不会被调用。

public class MyViewGroup extends ViewGroup {    private int mTouchSlop;    ...    ViewConfiguration vc = ViewConfiguration.get(view.getContext());    mTouchSlop = vc.getScaledTouchSlop();    ...    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        /*         * This method JUST determines whether we want to intercept the motion.         * If we return true, onTouchEvent will be called and we do the actual         * scrolling there.         */        final int action = MotionEventCompat.getActionMasked(ev);        // Always handle the case of the touch gesture being complete.        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {            // Release the scroll.            mIsScrolling = false;            return false; // Do not intercept touch event, let the child handle it        }        switch (action) {            case MotionEvent.ACTION_MOVE: {                if (mIsScrolling) {                    // We're currently scrolling, so yes, intercept the                     // touch event!                    return true;                }                // If the user has dragged her finger horizontally more than                 // the touch slop, start the scroll                // left as an exercise for the reader                final int xDiff = calculateDistanceX(ev);                 // Touch slop should be calculated using ViewConfiguration                 // constants.                if (xDiff > mTouchSlop) {                     // Start scrolling!                    mIsScrolling = true;                    return true;                }                break;            }            ...        }        // In general, we don't want to intercept touch events. They should be         // handled by the child view.        return false;    }    @Override    public boolean onTouchEvent(MotionEvent ev) {        // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE,         // scroll this container).        // This method will only be called if the touch event was intercepted in         // onInterceptTouchEvent        ...    }}

ViewGroup还提供了一个requestDisallowInterceptTouchEvent()方法,当它的一个子View不想它的父View或祖先View使用onInterceptTouchEvent()拦截它的touch event事件时调用。.

使用ViewConfiguration常量

在Android系统中使用 ViewConfiguration类可以访问通用的距离、速度和时间。
Touch slop典型的用处时为了防止用户在进行一些其他操作意外的滚动。ViewConfiguration中另外两个很常见的方法是getScaledMinimumFlingVelocity()getScaledMaximumFlingVelocity()。这2个方法返回最小和最大速度(像素每秒)初始化一个快速滑动:

ViewConfiguration vc = ViewConfiguration.get(view.getContext());private int mSlop = vc.getScaledTouchSlop();private int mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();private int mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();...case MotionEvent.ACTION_MOVE: {    ...    float deltaX = motionEvent.getRawX() - mDownX;    if (Math.abs(deltaX) > mSlop) {        // A swipe occurred, do something    }...case MotionEvent.ACTION_UP: {    ...    } if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity            && velocityY < velocityX) {        // The criteria have been satisfied, do something    }}

扩展子View的可触区域

Android提供了TouchDelegate类让父View扩展子View的触摸区域,这个在子View非常小而又非要一个较大触摸区域时很有用。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/parent_layout"     android:layout_width="match_parent"     android:layout_height="match_parent"     tools:context=".MainActivity" >     <ImageButton android:id="@+id/button"          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:background="@null"          android:src="@drawable/icon" /></RelativeLayout>

在RelativeLayout中扩展ImageButton的触摸区域:

public class MainActivity extends Activity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // Get the parent view        View parentView = findViewById(R.id.parent_layout);        parentView.post(new Runnable() {            // Post in the parent's message queue to make sure the parent            // lays out its children before you call getHitRect()            @Override            public void run() {                // The bounds for the delegate view (an ImageButton                // in this example)                Rect delegateArea = new Rect();                ImageButton myButton = (ImageButton) findViewById(R.id.button);                myButton.setEnabled(true);                myButton.setOnClickListener(new View.OnClickListener() {                    @Override                    public void onClick(View view) {                        Toast.makeText(MainActivity.this,                                 "Touch occurred within ImageButton touch region.",                                 Toast.LENGTH_SHORT).show();                    }                });                // The hit rectangle for the ImageButton                myButton.getHitRect(delegateArea);                // Extend the touch area of the ImageButton beyond its bounds                // on the right and bottom.                delegateArea.right += 100;                delegateArea.bottom += 100;                // Instantiate a TouchDelegate.                // "delegateArea" is the bounds in local coordinates of                 // the containing view to be mapped to the delegate view.                // "myButton" is the child view that should receive motion                // events.                TouchDelegate touchDelegate = new TouchDelegate(delegateArea,                         myButton);                // Sets the TouchDelegate on the parent view, such that touches                 // within the touch delegate bounds are routed to the child.                if (View.class.isInstance(myButton.getParent())) {                    ((View) myButton.getParent()).setTouchDelegate(touchDelegate);                }            }        });    }}
1 0