普通手势
参考地址: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响应触摸手势来显示一个滚动效果。
Scroller和OverScroller很类似,都可以针对Touch Event产生一个滚动动画。但是OverScroller包含了一些方法可以告诉用户他们已经到了内容边缘,InteractiveChart 例子中的EdgeEffectCompat类就展示了一个绚丽的用户到达内容边缘的效果。
注意:建议使用OverScroller而不是Scroller类来进行滚动动画。OverScroller有很好的向后兼容性。还要注意,一般你只在实现滚动自己的情况下,只需要实现scrollers。而ScrollView 和HorizontalScrollView对它们内嵌的布局中已经做了所有的事情。
理解滚动术语
滚动在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中通过每一个手指的index和ID来跟踪每一根手指。 - 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楼caoshangpa9小时前
- 学习了