Bu dokümanda, dokunma etkinliklerine müdahale etmek için onTouchEvent()
kullanılarak ekrandaki nesneleri sürükleme ve ölçeklendirmek üzere dokunma hareketlerinin nasıl kullanılacağı açıklanmaktadır.
Nesneyi sürükleme
Dokunma hareketi için yaygın olarak kullanılan bir işlem, hareketi kullanarak ekran boyunca bir nesneyi sürüklemektir.
Sürükleme veya kaydırma işleminde, ekrana başka parmaklar dokunsa bile uygulamanın orijinal işaretçiyi takip etmesi gerekir. Örneğin, resmi sürüklerken kullanıcının dokunmatik ekrana ikinci bir parmağını yerleştirdiğini ve ilk parmağını kaldırdığını düşünün. Uygulamanız yalnızca işaretçileri ayrı ayrı izliyorsa ikinci işaretçiyi varsayılan olarak dikkate alır ve resmi bu konuma taşır.
Bunun olmasını önlemek için uygulamanızın orijinal işaretçi ile sonraki işaretçileri ayırt etmesi gerekir. Bunu yapmak için ACTION_POINTER_DOWN
ve ACTION_POINTER_UP
etkinliklerini Çoklu dokunma hareketlerini işleme bölümünde açıklandığı şekilde izler.
İkincil işaretçi aşağı veya yukarı doğru gittiğinde, ACTION_POINTER_DOWN
ve ACTION_POINTER_UP
onTouchEvent()
geri çağırmasına aktarılır.
ACTION_POINTER_UP
durumunda, bu dizini çıkarıp etkin işaretçi kimliğinin artık ekrana dokunmayan bir işaretçiyi belirtmediğinden emin olabilirsiniz. İşaretliyse etkin olması için farklı bir işaretçi seçebilir
ve geçerli X ve Y konumunu kaydedebilirsiniz. Ekrandaki nesneyi taşıyacağınız mesafeyi hesaplamak için ACTION_MOVE
durumundaki bu kayıtlı konumu kullanın. Böylece, uygulama her zaman doğru işaretçiden alınan verileri kullanarak taşınacak mesafeyi hesaplar.
Aşağıdaki kod snippet'i, kullanıcının ekranda bir nesneyi sürüklemesini sağlar. Etkin işaretçinin ilk konumunu kaydeder, işaretçinin katettiği mesafeyi hesaplar ve nesneyi yeni konuma taşır. Ayrıca, ek işaretçilerin olma ihtimalini de doğru şekilde yönetir.
Snippet, getActionMasked()
yöntemini kullanır. MotionEvent
işlemini almak için her zaman bu yöntemi kullanın.
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; }
Kaydırmak için sürükleyin
Önceki bölümde, ekrandaki bir nesneyi sürüklemeye ilişkin örnek gösterilmektedir.
Yaygın olarak karşılaşılan bir diğer senaryo da, kullanıcının hareketini sürüklemenin hem X hem de Y ekseninde kaydırmaya neden olması anlamına gelir. Önceki snippet, sürükleme uygulamak için MotionEvent
işlemlerini doğrudan engeller. Bu bölümdeki snippet, GestureDetector.SimpleOnGestureListener
içindeki onScroll()
öğesini geçersiz kılarak platformun yaygın hareketler için sağladığı yerleşik destekten yararlanır.
Daha fazla bağlam sağlamak amacıyla, kullanıcı içeriği kaydırmak için parmağını sürüklediğinde onScroll()
çağrılır. onScroll()
yalnızca parmak
aşağıda değilken çağrılır. Parmak kaldırıldıktan hemen önce parmak
kaldırılıyorsa hareket sonlandırılır veya bir çırpma hareketi başlar. Kaydırma ve hızlıca kaydırma hakkında daha fazla bilgi için Kaydırma hareketini canlandırma konusuna bakın.
Aşağıda onScroll()
için kod snippet'i verilmiştir:
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()
uygulandığında, dokunma hareketine yanıt olarak
görüntü alanı kaydırılır:
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); }
Ölçeklendirme için dokunmayı kullan
Yaygın hareketleri algılama bölümünde açıklandığı gibi,
Android'in
GestureDetector
kaydırma, sallama, dokunup basılı tutma gibi yaygın olarak kullandığı hareketleri algılamak için
kullanın. Android, ölçeklendirme için ScaleGestureDetector
sunar.
Bir görünümün ek hareketleri tanımasını istediğinizde GestureDetector
ve ScaleGestureDetector
öğelerini birlikte kullanabilirsiniz.
Algılanan hareket etkinliklerini bildirmek için hareket algılayıcıları, oluşturucularına iletilen işleyici nesnelerini kullanır. ScaleGestureDetector
, ScaleGestureDetector.OnScaleGestureListener
kullanıyor.
Android, raporlanan etkinliklerin tümüne ihtiyacınız yoksa ScaleGestureDetector.SimpleOnScaleGestureListener
kullanabileceğiniz bir yardımcı sınıf olarak sunulur.
Temel ölçeklendirme örneği
Aşağıdaki snippet'te, ölçeklendirmede kullanılan temel öğeler gösterilmektedir.
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; } }
Daha karmaşık ölçeklendirme örneği
Aşağıda, Kaydırma hareketini canlandırma bölümünde gösterilen InteractiveChart
örneğinden daha karmaşık bir örnek verilmiştir.
InteractiveChart
örneği, ScaleGestureDetector
aralığı (getCurrentSpanX
ve
getCurrentSpanY
) ile "odak" (getFocusX
ve getFocusY
) özelliklerini kullanarak birden fazla parmakla kaydırma, kaydırma ve ölçeklendirmeyi destekler.
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; } };
Ek kaynaklar
Giriş etkinlikleri, sensörler ve özel görünümleri etkileşimli hale getirme hakkında daha fazla bilgi için aşağıdaki referanslara bakın.