本文件說明如何使用觸控手勢在螢幕上拖曳及縮放
使用
onTouchEvent()
攔截觸控事件。
拖曳物件
觸控手勢的常見做法是用它來拖曳物件 。
在拖曳或捲動作業中,應用程式必須追蹤原始內容 指標,即使其他手指觸碰螢幕也沒問題。舉例來說 使用者拖曳圖片時,將第二指放在觸控螢幕上 然後放開第一指如果應用程式只會追蹤個別指標 將第二個指標視為預設,並將圖片移至該指標 或 HTTP/HTTPS 位置
為避免這種情況,您的應用程式需要區分
和任何後續指標因此會追蹤
ACTION_POINTER_DOWN
和
ACTION_POINTER_UP
事件,方法請參閱「處理多點觸控手勢」。
已通過 ACTION_POINTER_DOWN
和 ACTION_POINTER_UP
系統呼叫 onTouchEvent()
回呼時
向下或上下
以 ACTION_POINTER_UP
為例,您可以擷取這個索引,然後
確認使用中的指標 ID 未參照的指標已失效
請輕觸螢幕。如果是,您可以選取其他指標做為啟用
並儲存目前的 X 和 Y 位置將此儲存位置用於
ACTION_MOVE
大小寫,以計算在螢幕中移動物件的距離。如此一來
一律使用正確指標的資料計算移動距離。
下列程式碼片段可讓使用者將物件拖曳到畫面上。這項服務 記錄有效指標的初始位置,並計算 指標移動,並將物件移至新的位置。此外,這個 API 也能正確運作 可管理額外指標的可能性
程式碼片段使用
getActionMasked()
方法。一律使用這個方法來擷取
MotionEvent
。
Kotlin
// The "active pointer" is the one moving the object. private var mActivePointerId = INVALID_POINTER_ID override fun onTouchEvent(ev: MotionEvent): Boolean { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev) val action = MotionEventCompat.getActionMasked(ev) when (action) { MotionEvent.ACTION_DOWN -> { MotionEventCompat.getActionIndex(ev).also { pointerIndex -> // Remember where you start for dragging. mLastTouchX = MotionEventCompat.getX(ev, pointerIndex) mLastTouchY = MotionEventCompat.getY(ev, pointerIndex) } // Save the ID of this pointer for dragging. mActivePointerId = MotionEventCompat.getPointerId(ev, 0) } MotionEvent.ACTION_MOVE -> { // Find the index of the active pointer and fetch its position. val (x: Float, y: Float) = MotionEventCompat.findPointerIndex(ev, mActivePointerId).let { pointerIndex -> // Calculate the distance moved. MotionEventCompat.getX(ev, pointerIndex) to MotionEventCompat.getY(ev, pointerIndex) } mPosX += x - mLastTouchX mPosY += y - mLastTouchY invalidate() // Remember this touch position for the next move event. mLastTouchX = x mLastTouchY = y } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { mActivePointerId = INVALID_POINTER_ID } MotionEvent.ACTION_POINTER_UP -> { MotionEventCompat.getActionIndex(ev).also { pointerIndex -> MotionEventCompat.getPointerId(ev, pointerIndex) .takeIf { it == mActivePointerId } ?.run { // This is the active pointer going up. Choose a new // active pointer and adjust it accordingly. val newPointerIndex = if (pointerIndex == 0) 1 else 0 mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex) mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex) mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex) } } } } return true }
Java
// The "active pointer" is the one moving the object. private int mActivePointerId = INVALID_POINTER_ID; @Override public 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 the starting position of the pointer. 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 is the active pointer going up. Choose a new // active pointer and adjust it 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; }
拖曳即可平移
上一節展示瞭如何在螢幕上拖曳物件。
另一個常見的情況是平移,也就是使用者拖曳動作
會同時在 X 軸和 Y 軸上捲動。上述程式碼片段
會攔截 MotionEvent
動作以實作拖曳動作。
本節的程式碼片段利用平台內建的
可以覆寫設定
onScroll()
英吋
GestureDetector.SimpleOnGestureListener
。
如要提供更多背景資訊,系統會在使用者拖曳時呼叫 onScroll()
移動手指即可平移內容onScroll()
只有在
手指往下移。一旦手指從螢幕上舉起,
做出結束手勢或快速滑過手勢啟動 (如果手指隨某些東西移動)
就會進入下一個階段如要進一步瞭解捲動和捲動瀏覽
快速滑過,請參閱「為捲動手勢設定動畫」。
以下是 onScroll()
的程式碼片段:
Kotlin
// The current viewport. This rectangle represents the visible // chart domain and range. private val mCurrentViewport = 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 must be drawn. private val mContentRect: Rect? = null private val mGestureListener = object : GestureDetector.SimpleOnGestureListener() { ... override fun onScroll( e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { // Scrolling uses math based on the viewport, as opposed to math using // pixels. mContentRect?.apply { // Pixel offset is the offset in screen pixels, while viewport offset is the // offset within the current viewport. val viewportOffsetX = distanceX * mCurrentViewport.width() / width() val viewportOffsetY = -distanceY * mCurrentViewport.height() / height() // Updates the viewport and refreshes the display. setViewportBottomLeft( mCurrentViewport.left + viewportOffsetX, mCurrentViewport.bottom + viewportOffsetY ) } return true } }
Java
// The current viewport. This rectangle represents the 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 must 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; }
onScroll()
實作會將可視區域捲動
回應觸控手勢:
Kotlin
/** * Sets the current viewport, defined by mCurrentViewport, to the given * X and Y positions. The Y value represents the topmost pixel position, * and thus the bottom of the mCurrentViewport rectangle. */ private fun setViewportBottomLeft(x: Float, y: Float) { /* * Constrains within the scroll range. The scroll range is the viewport * extremes, such as AXIS_X_MAX, minus the viewport size. For example, if * the extremes are 0 and 10 and the viewport size is 2, the scroll range * is 0 to 8. */ val curWidth: Float = mCurrentViewport.width() val curHeight: Float = mCurrentViewport.height() val newX: Float = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth)) val newY: Float = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX)) mCurrentViewport.set(newX, newY - curHeight, newX + curWidth, newY) // Invalidates the View to update the display. ViewCompat.postInvalidateOnAnimation(this) }
Java
/** * 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 the viewport * extremes, such as AXIS_X_MAX, minus the viewport size. For example, if * the extremes are 0 and 10 and the viewport size is 2, the scroll range * is 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); }
使用觸控操作即可縮放大小
如「偵測常用手勢」一節中所述,
使用
GestureDetector
偵測 Android 使用的常用手勢,例如捲動、快速滑過和
按住。Android 提供
ScaleGestureDetector
。
您可以使用 GestureDetector
和 ScaleGestureDetector
您希望檢視畫面辨識其他手勢
為了回報偵測到的手勢事件,手勢偵測工具會使用事件監聽器物件
傳遞到各自的建構函式ScaleGestureDetector
使用
ScaleGestureDetector.OnScaleGestureListener
。
Android 提供
ScaleGestureDetector.SimpleOnScaleGestureListener
做為輔助類別,如果不需要所有報告
事件。
基本資源配置範例
以下程式碼片段說明與縮放相關的基本元素。
Kotlin
private var mScaleFactor = 1f private val scaleListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { mScaleFactor *= detector.scaleFactor // Don't let the object get too small or too large. mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)) invalidate() return true } } private val mScaleDetector = ScaleGestureDetector(context, scaleListener) override fun onTouchEvent(ev: MotionEvent): Boolean { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev) return true } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { save() scale(mScaleFactor, mScaleFactor) // onDraw() code goes here. restore() } }
Java
private ScaleGestureDetector mScaleDetector; private float mScaleFactor = 1.f; public MyCustomView(Context mContext){ ... // View code goes here. ... mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); } @Override public boolean onTouchEvent(MotionEvent ev) { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev); return true; } @Override public 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
範例支援捲動、平移和縮放功能
使用 ScaleGestureDetector
跨度
(getCurrentSpanX
)。
和
getCurrentSpanY
)。
和「焦點」
(getFocusX
和getFocusY
)
接著介紹網際網路通訊層
包括兩項主要的安全防護功能
Kotlin
private val mCurrentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX) private val mContentRect: Rect? = null ... override fun onTouchEvent(event: MotionEvent): Boolean { return mScaleGestureDetector.onTouchEvent(event) || mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event) } /** * The scale listener, used for handling multi-finger scale gestures. */ private val mScaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { /** * This is the active focal point in terms of the viewport. It can be a * local variable, but keep it here to minimize per-frame allocations. */ private val viewportFocus = PointF() private var lastSpanX: Float = 0f private var lastSpanY: Float = 0f // Detects new pointers are going down. override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean { lastSpanX = scaleGestureDetector.currentSpanX lastSpanY = scaleGestureDetector.currentSpanY return true } override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean { val spanX: Float = scaleGestureDetector.currentSpanX val spanY: Float = scaleGestureDetector.currentSpanY val newWidth: Float = lastSpanX / spanX * mCurrentViewport.width() val newHeight: Float = lastSpanY / spanY * mCurrentViewport.height() val focusX: Float = scaleGestureDetector.focusX val focusY: Float = scaleGestureDetector.focusY // Ensures the chart point is within the chart region. // See the sample for the implementation of hitTest(). hitTest(focusX, focusY, viewportFocus) mContentRect?.apply { mCurrentViewport.set( viewportFocus.x - newWidth * (focusX - left) / width(), viewportFocus.y - newHeight * (bottom - focusY) / height(), 0f, 0f ) } mCurrentViewport.right = mCurrentViewport.left + newWidth mCurrentViewport.bottom = mCurrentViewport.top + newHeight // Invalidates the View to update the display. ViewCompat.postInvalidateOnAnimation(this@InteractiveLineGraphView) lastSpanX = spanX lastSpanY = spanY return true } }
Java
private RectF mCurrentViewport = new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX); private Rect mContentRect; private ScaleGestureDetector mScaleGestureDetector; ... @Override 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. It can be a * local variable, but keep it here to minimize per-frame allocations. */ private PointF viewportFocus = new PointF(); private float lastSpanX; private float lastSpanY; // Detects 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(); // Ensures 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; } };
其他資源
如要進一步瞭解輸入事件,請參閱下列參考資料。 以及如何與自訂檢視畫面互動