Tutorial ini menjelaskan cara menggunakan gestur sentuh untuk menarik dan menskalakan objek di layar, menggunakan onTouchEvent()
untuk mencegat peristiwa sentuhan.
Lihat referensi terkait berikut:
Menarik objek
Jika Anda menargetkan Android 3.0 atau versi lebih tinggi, Anda dapat menggunakan pemroses peristiwa tarik-lalu-lepas bawaan dengan View.OnDragListener
, seperti yang dijelaskan dalam Tarik lalu Lepas.
Operasi umum untuk gestur sentuh adalah menggunakannya untuk menarik objek ke layar. Cuplikan berikut memungkinkan pengguna menarik gambar di layar. Perhatikan hal berikut:
- Dalam operasi tarik (atau scroll), aplikasi harus melacak pointer asli (jari), meskipun jari lain berada di atas layar. Contohnya, bayangkan ketika menarik gambar, pengguna meletakkan jari kedua di layar sentuh, lalu mengangkat jari pertama. Jika aplikasi Anda hanya melacak satu pointer, aplikasi akan menganggap pointer kedua sebagai defaultnya dan memindahkan gambar ke lokasi tersebut.
- Untuk mencegah hal ini, aplikasi Anda perlu membedakan antara pointer asli dan pointer berikutnya. Untuk melakukannya, aplikasi akan melacak peristiwa
ACTION_POINTER_DOWN
danACTION_POINTER_UP
yang dijelaskan dalam Menangani Gestur Multi-Sentuh.ACTION_POINTER_DOWN
danACTION_POINTER_UP
diteruskan ke callbackonTouchEvent()
setiap kali pointer sekunder turun atau naik. - Dalam kasus
ACTION_POINTER_UP
, contoh tersebut akan mengekstrak indeks ini dan memastikan bahwa ID pointer aktif tidak merujuk pada pointer yang tidak lagi menyentuh layar. Jika demikian, aplikasi akan memilih pointer lain untuk tetap aktif dan menyimpan posisi X dan Y saat ini. Karena posisi yang disimpan ini digunakan dalam kasusACTION_MOVE
untuk menghitung jarak guna memindahkan objek di layar, aplikasi akan selalu menghitung jarak untuk bergerak menggunakan data dari pointer yang benar.
Cuplikan berikut memungkinkan pengguna untuk menarik objek di layar. Ini akan mencatat posisi awal pointer aktif, menghitung jarak tempuh pointer, dan memindahkan objek ke posisi baru. Cara ini akan mengelola kemungkinan adanya pointer tambahan dengan benar, seperti yang dijelaskan di atas.
Perhatikan bahwa cuplikan menggunakan metode getActionMasked()
.
Anda harus selalu menggunakan metode ini (atau sebaiknya, versi kompatibilitas MotionEventCompat.getActionMasked()
) untuk mengambil tindakan MotionEvent
. Tidak seperti metode getAction()
yang lebih lama, getActionMasked()
didesain agar berfungsi dengan beberapa pointer. Hal ini akan mengembalikan tindakan samaran yang dilakukan, tanpa menyertakan bit indeks pointer.
Kotlin
// The ‘active pointer’ is the one currently moving our 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 we started (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 was our active pointer going up. Choose a new // active pointer and adjust 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 currently moving our 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 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; }
Tarik untuk menggeser
Bagian sebelumnya menunjukkan contoh penarikan objek di layar. Skenario umum lainnya adalah menggeser, yaitu ketika gerakan tarik pengguna menyebabkan scrolling pada sumbu x dan y. Cuplikan di atas langsung mencegat tindakan MotionEvent
untuk mengimplementasikan penarikan. Cuplikan di bagian ini memanfaatkan dukungan bawaan platform untuk gestur umum. Ini mengganti onScroll()
di GestureDetector.SimpleOnGestureListener
.
Untuk memberikan konteks lebih banyak, onScroll()
dipanggil saat pengguna menarik jarinya untuk menggeser konten.
onScroll()
hanya dipanggil saat jari turun; segera setelah jari terangkat dari layar, gestur akan berakhir, atau gestur lempar dimulai (jika jari tersebut bergerak dengan kecepatan sesaat sebelum diangkat).
Untuk pembahasan lebih lanjut tentang scrolling vs melempar (flinging), lihat Animasi Gestur Scroll.
Berikut adalah cuplikan untuk onScroll()
:
Kotlin
// The current viewport. This rectangle represents the currently 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 should 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, refreshes the display. setViewportBottomLeft( mCurrentViewport.left + viewportOffsetX, mCurrentViewport.bottom + viewportOffsetY ) } return true } }
Java
// 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() { ... @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; }
Implementasi onScroll()
melakukan scrolling di area pandang sebagai respons terhadap gestur sentuh:
Kotlin
/** * 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 fun setViewportBottomLeft(x: Float, y: Float) { /* * 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. */ 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 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); }
Menggunakan sentuhan untuk melakukan penskalaan
Seperti yang dibahas dalam Mendeteksi Gestur Umum, GestureDetector
membantu Anda mendeteksi gestur umum yang digunakan oleh Android, seperti scrolling, lempar (flinging), dan tekan lama. Untuk penskalaan, Android menyediakan ScaleGestureDetector
. GestureDetector
dan ScaleGestureDetector
dapat digunakan bersamaan ketika Anda menginginkan tampilan untuk mengenali gestur tambahan.
Untuk melaporkan peristiwa gestur yang terdeteksi, pendeteksi gestur menggunakan objek pemroses yang diteruskan ke konstruktornya. ScaleGestureDetector
menggunakan
ScaleGestureDetector.OnScaleGestureListener
.
Android menyediakan ScaleGestureDetector.SimpleOnScaleGestureListener
sebagai class bantuan yang dapat Anda perluas jika semua peristiwa yang dilaporkan diabaikan.
Contoh penskalaan dasar
Berikut adalah cuplikan yang menggambarkan bahan dasar yang tercakup dalam penskalaan.
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; } }
Contoh penskalaan yang lebih kompleks
Berikut adalah contoh yang lebih kompleks dari contoh InteractiveChart
yang disediakan dengan class ini.
Contoh InteractiveChart
mendukung scrolling (penggeseran) dan penskalaan dengan beberapa jari, menggunakan fitur ScaleGestureDetector
"span" (getCurrentSpanX/Y
) dan "fokus" (getFocusX/Y
):
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. Could be a local * variable but kept here to minimize per-frame allocations. */ private val viewportFocus = PointF() private var lastSpanX: Float = 0f private var lastSpanY: Float = 0f // Detects that 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 // Makes sure that 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. 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; } };