Tài liệu này mô tả cách sử dụng cử chỉ chạm để kéo và điều chỉnh tỷ lệ trên màn hình
đối tượng, đang sử dụng
onTouchEvent()
để chặn các sự kiện chạm.
Kéo một đối tượng
Một thao tác phổ biến của một cử chỉ chạm là dùng cử chỉ đó để kéo một đối tượng ngang màn hình.
Trong thao tác kéo hoặc cuộn, ứng dụng phải theo dõi bản gốc con trỏ, ngay cả khi có thêm ngón tay chạm vào màn hình. Ví dụ: hãy tưởng tượng rằng trong khi kéo hình ảnh, người dùng đặt ngón tay thứ hai lên màn hình cảm ứng rồi nhấc ngón tay đầu tiên lên. Nếu ứng dụng của bạn chỉ theo dõi con trỏ riêng lẻ, coi con trỏ thứ hai là con trỏ mặc định và di chuyển hình ảnh đến con trỏ đó vị trí.
Để ngăn điều này xảy ra, ứng dụng của bạn cần phân biệt giữa
con trỏ ban đầu và mọi con trỏ tiếp theo. Để thực hiện điều này, công cụ này sẽ theo dõi
ACTION_POINTER_DOWN
và
ACTION_POINTER_UP
các sự kiện như mô tả trong bài viết Xử lý cử chỉ nhiều điểm chạm.
Đã vượt qua ACTION_POINTER_DOWN
và ACTION_POINTER_UP
vào lệnh gọi lại onTouchEvent()
bất cứ khi nào con trỏ phụ chuyển đến
xuống hoặc lên.
Trong trường hợp ACTION_POINTER_UP
, bạn có thể trích xuất chỉ mục này và
đảm bảo rằng mã con trỏ đang hoạt động không tham chiếu đến con trỏ không còn
chạm vào màn hình. Nếu có, bạn có thể chọn một con trỏ khác để hoạt động
và lưu vị trí X và Y hiện tại của điểm đó. Sử dụng vị trí đã lưu này trong
ACTION_MOVE
trường hợp để tính khoảng cách di chuyển đối tượng trên màn hình. Bằng cách này, ứng dụng
luôn tính khoảng cách cần di chuyển bằng cách sử dụng dữ liệu từ con trỏ chính xác.
Đoạn mã sau đây cho phép người dùng kéo một đối tượng trên màn hình. Nó ghi lại vị trí ban đầu của con trỏ đang hoạt động, tính khoảng cách con trỏ di chuyển và di chuyển đối tượng đến vị trí mới. Điều này cũng đúng quản lý khả năng có thêm con trỏ.
Đoạn mã sử dụng
getActionMasked()
. Luôn sử dụng phương thức này để truy xuất hành động của
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; }
Kéo để xoay
Phần trước cho thấy một ví dụ về cách kéo một đối tượng trên màn hình.
Một tình huống phổ biến khác là kéo, tức là khi người dùng kéo chuyển động
khiến thao tác cuộn ở cả trục X và trục Y. Đoạn mã trước đó trực tiếp
chặn các thao tác MotionEvent
để triển khai thao tác kéo. Chiến lược phát hành đĩa đơn
trong phần này tận dụng tính năng hỗ trợ tích hợp sẵn của nền tảng dành cho
các cử chỉ phổ biến bằng cách ghi đè
onScroll()
inch
GestureDetector.SimpleOnGestureListener
Để cung cấp thêm ngữ cảnh, onScroll()
được gọi khi người dùng kéo
một ngón tay để xoay nội dung. onScroll()
chỉ được gọi khi
hướng ngón tay xuống. Ngay khi nhấc ngón tay ra khỏi màn hình,
kết thúc hoặc cử chỉ hất bắt đầu, nếu ngón tay đang di chuyển cùng với một số
ngay trước khi nhấc nó lên. Để biết thêm thông tin về cách cuộn so với
cử chỉ hất, hãy xem phần Tạo ảnh động cho cử chỉ cuộn.
Sau đây là đoạn mã cho 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; }
Quá trình triển khai onScroll()
sẽ cuộn khung nhìn trong
phản hồi với cử chỉ chạm:
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); }
Sử dụng thao tác chạm để chuyển tỷ lệ
Như đã thảo luận trong bài viết Phát hiện các cử chỉ phổ biến,
sử dụng
GestureDetector
để phát hiện các cử chỉ phổ biến mà Android sử dụng, chẳng hạn như cuộn, hất và
chạm và giữ. Để mở rộng quy mô, Android cung cấp
ScaleGestureDetector
.
Bạn có thể sử dụng GestureDetector
và ScaleGestureDetector
khi bạn muốn khung hiển thị nhận ra các cử chỉ bổ sung.
Để báo cáo các sự kiện cử chỉ đã phát hiện, trình phát hiện cử chỉ sẽ sử dụng các đối tượng trình nghe
được truyền đến hàm khởi tạo của chúng. ScaleGestureDetector
lần sử dụng
ScaleGestureDetector.OnScaleGestureListener
Android cung cấp
ScaleGestureDetector.SimpleOnScaleGestureListener
như một lớp trợ giúp mà bạn có thể mở rộng nếu không cần tất cả
các sự kiện.
Ví dụ về việc điều chỉnh tỷ lệ cơ bản
Đoạn mã sau minh hoạ các thành phần cơ bản liên quan đến việc điều chỉnh theo tỷ lệ.
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; } }
Ví dụ về việc điều chỉnh tỷ lệ phức tạp hơn
Sau đây là một ví dụ phức tạp hơn từ
InteractiveChart
mẫu xuất hiện trong
Tạo ảnh động cho một cử chỉ cuộn.
Chiến lược phát hành đĩa đơn
Mẫu InteractiveChart
hỗ trợ cuộn, kéo và chuyển tỷ lệ
bằng nhiều ngón tay, sử dụng phím ScaleGestureDetector
(getCurrentSpanX
)
và
getCurrentSpanY
)
và "lấy nét"
(getFocusX
)
và getFocusY
)
các tính năng AI mới.
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; } };
Tài nguyên khác
Hãy xem các tài liệu tham khảo sau đây để biết thêm thông tin về sự kiện đầu vào, cảm biến và làm cho các khung hiển thị tuỳ chỉnh có tính tương tác.
- Tổng quan về sự kiện đầu vào
- Tổng quan về cảm biến
- Tạo thành phần hiển thị tuỳ chỉnh có tính tương tác