ลากและปรับขนาด

ลองใช้วิธีการเขียน
Jetpack Compose เป็นชุดเครื่องมือ UI ที่แนะนำสำหรับ Android ดูวิธีใช้การแตะและการป้อนข้อมูลในการเขียน

เอกสารนี้อธิบายวิธีใช้ท่าทางแตะสัมผัสเพื่อลากและปรับขนาดบนหน้าจอ โดยใช้ onTouchEvent() เพื่อสกัดกั้นการสัมผัสได้

ลากวัตถุ

การทำงานทั่วไปสำหรับท่าทางสัมผัสคือการใช้ท่าทางสัมผัสเพื่อลากวัตถุ หน้าจอ

ในการดำเนินการลากหรือเลื่อน แอปจะต้องติดตามต้นฉบับเดิม เคอร์เซอร์เมาส์แม้ว่าจะมีนิ้วอื่นๆ แตะหน้าจอก็ตาม ตัวอย่างเช่น สมมติว่า ขณะลากรูปภาพ ผู้ใช้สามารถวางนิ้วที่ 2 บนหน้าจอสัมผัส และยกนิ้วแรกขึ้น หากแอปติดตามเฉพาะตัวชี้แต่ละรายการ พิจารณาตัวชี้ที่สองเป็นค่าเริ่มต้นและย้ายรูปภาพไปยังจุดนั้น ตำแหน่งนั้น

เพื่อป้องกันไม่ให้เกิดเหตุการณ์เช่นนี้ แอปของคุณต้องแยกความแตกต่างระหว่าง เคอร์เซอร์เดิมและตัวชี้ต่อๆ ไป ซึ่งทำการติดตาม 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 ได้ เมื่อต้องการให้มุมมองจดจำท่าทางสัมผัสเพิ่มเติม

ตัวตรวจจับท่าทางสัมผัสจะใช้ออบเจ็กต์ Listener เพื่อรายงานเหตุการณ์ท่าทางสัมผัสที่ตรวจพบ ส่งให้กับผู้สร้าง 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;
    }
};

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับเหตุการณ์อินพุตได้ในข้อมูลอ้างอิงต่อไปนี้ เซ็นเซอร์ และการทำให้มุมมองที่กำหนดเองเป็นแบบอินเทอร์แอกทีฟ