يوضّح هذا المستند كيفية استخدام إيماءات اللمس لسحب
العناصر على الشاشة وتغيير حجمها، باستخدام
onTouchEvent()
لاعتراض أحداث اللمس.
سحب عنصر
من العمليات الشائعة لإيماءة اللمس هي استخدامها لسحب عنصر على سطح الشاشة.
في عملية السحب أو التمرير، على التطبيق تتبُّع المؤشر الأصلي، حتى إذا لمست أصابع إضافية الشاشة. على سبيل المثال، تخيل أنّه أثناء سحب الصورة، يضع المستخدم إصبعًا ثانيًا على الشاشة اللمسية ويرفع الإصبع الأول. إذا كان تطبيقك يتتبّع مؤشرات فردية فقط، سيُعتبر المؤشر الثاني هو التلقائي وينقل الصورة إلى هذا الموقع.
لمنع حدوث ذلك، يجب أن يميز تطبيقك بين
المؤشر الأصلي وأي مؤشرات لاحقة. ولإجراء ذلك، يتتبّع التطبيق حدثَي
ACTION_POINTER_DOWN
و
ACTION_POINTER_UP
كما هو موضّح في مقالة معالجة إيماءات اللمس المتعدّد.
يتم تمرير ACTION_POINTER_DOWN
وACTION_POINTER_UP
إلى دالة الاستدعاء onTouchEvent()
كلما تحرك المؤشر الثانوي
للأسفل أو للأعلى.
في الحالة ACTION_POINTER_UP
، يمكنك استخراج هذا الفهرس و
التأكّد من أنّ معرّف المؤشر النشط لا يشير إلى مؤشر لم يعد
يلمس الشاشة. إذا كان الأمر كذلك، يمكنك اختيار مؤشر مختلف ليكون نشطًا
وحفظ موضعَي X وY الحاليَين. استخدِم هذا الموضع المحفوظ في حالة
ACTION_MOVE
لحساب المسافة لنقل الجسم المعروض على الشاشة. بهذه الطريقة، يحسب التطبيق
دائمًا المسافة التي يجب التنقّل إليها باستخدام البيانات الواردة من المؤشر الصحيح.
يتيح مقتطف الرمز البرمجي التالي للمستخدم سحب عنصر على الشاشة. ويُسجِّل الإجراء الموضع الأولي للمؤشر النشط، ويحسب المسافة التي يقطعها المؤشر، وينقل الكائن إلى الموضع الجديد. ويدير أيضًا بشكلٍ صحيح إمكانية استخدام مؤشرات إضافية.
يستخدم المقتطف الطريقة
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
span
(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; } };
مصادر إضافية
اطّلِع على المراجع التالية للحصول على مزيد من المعلومات عن أحداث الإدخال والأجهزة الاستشعار وجعل العروض المخصّصة تفاعلية.