Ziehen und skalieren

Schreiben Sie jetzt
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Weitere Informationen zur Verwendung von Berührungen und Eingaben in „Schreiben“

In diesem Dokument wird beschrieben, wie Sie Touch-Gesten verwenden, um Objekte auf dem Bildschirm zu ziehen und zu skalieren. Dabei wird onTouchEvent() verwendet, um Touch-Ereignisse abzufangen.

Objekt ziehen

Eine gängige Aktion besteht darin, ein Objekt über den Bildschirm zu ziehen.

Beim Ziehen oder Scrollen muss die App den ursprünglichen Zeiger verfolgen, auch wenn weitere Finger den Bildschirm berühren. Stellen Sie sich beispielsweise vor, dass der Nutzer beim Ziehen des Bildes einen zweiten Finger auf den Touchscreen setzt und den ersten Finger hebt. Wenn Ihre App nur einzelne Zeiger verfolgt, wird der zweite Zeiger als Standard betrachtet und das Bild an diesen Ort verschoben.

Um dies zu verhindern, muss Ihre Anwendung zwischen dem ursprünglichen und allen nachfolgenden Zeiger unterscheiden. Dazu werden die Ereignisse ACTION_POINTER_DOWN und ACTION_POINTER_UP erfasst, wie unter Multi-Touch-Gesten verarbeiten beschrieben. ACTION_POINTER_DOWN und ACTION_POINTER_UP werden an den onTouchEvent()-Callback übergeben, wenn ein sekundärer Zeiger nach unten oder oben geht.

Im Fall ACTION_POINTER_UP können Sie diesen Index extrahieren und dafür sorgen, dass sich die aktive Zeiger-ID nicht auf einen Zeiger bezieht, der den Bildschirm nicht mehr berührt. Ist dies der Fall, können Sie einen anderen aktiven Cursor auswählen und seine aktuelle X- und Y-Position speichern. Verwenden Sie diese gespeicherte Position im ACTION_MOVE-Case, um die Entfernung zum Verschieben des Bildschirmobjekts zu berechnen. Auf diese Weise berechnet die App immer die zu bewegende Strecke anhand von Daten vom richtigen Zeiger.

Mit dem folgenden Code-Snippet können Nutzer ein Objekt auf dem Bildschirm ziehen. Dabei wird die Anfangsposition des aktiven Zeigers erfasst, die Strecke berechnet, die der Zeiger zurücklegt, und das Objekt wird an die neue Position verschoben. Es verwaltet auch die Möglichkeit zusätzlicher Hinweise.

Im Snippet wird die Methode getActionMasked() verwendet. Verwenden Sie diese Methode immer, um die Aktion eines MotionEvents abzurufen.

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;
}

Zum Schwenken ziehen

Der vorherige Abschnitt zeigt ein Beispiel für das Ziehen eines Objekts auf dem Bildschirm. Ein weiteres häufiges Szenario ist das Schwenken, bei dem die Ziehbewegung eines Nutzers zum Scrollen auf der X- und der Y-Achse führt. Das vorherige Snippet fängt die MotionEvent-Aktionen zur Implementierung des Ziehens direkt ab. Das Snippet in diesem Abschnitt nutzt die integrierte Unterstützung der Plattform für gängige Gesten, indem es onScroll() in GestureDetector.SimpleOnGestureListener überschreibt.

Für mehr Kontext wird onScroll() aufgerufen, wenn ein Nutzer einen Finger zieht, um den Inhalt zu schwenken. onScroll() wird nur aufgerufen, wenn ein Finger auf dem Display liegt. Sobald der Finger vom Bildschirm genommen wird, wird die Geste entweder beendet oder es wird ein Flügelgeste gestartet, wenn sich der Finger kurz vor dem Anheben mit einer gewissen Geschwindigkeit bewegt. Weitere Informationen zum Scrollen und zum Ziehen findest du unter Scroll-Geste animieren.

Hier ist das Code-Snippet für 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;
}

Bei der Implementierung von onScroll() wird als Reaktion auf die Touch-Geste der Darstellungsbereich gescrollt:

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);
}

Zum Skalieren berühren

Wie unter Häufig verwendete Touch-Gesten erkennen beschrieben, kannst du mit GestureDetector gängige Android-Gesten wie Scrollen, Wischen und Berühren und Halten erkennen. Zur Skalierung bietet Android ScaleGestureDetector. Sie können GestureDetector und ScaleGestureDetector zusammen verwenden, wenn eine Ansicht zusätzliche Gesten erkennen soll.

Um erkannte Gestenereignisse zu melden, verwenden die Gestendetektoren Listener-Objekte, die an ihre Konstruktoren übergeben werden. ScaleGestureDetector verwendet ScaleGestureDetector.OnScaleGestureListener. Android bietet ScaleGestureDetector.SimpleOnScaleGestureListener als Hilfsklasse, die Sie erweitern können, wenn Sie nicht alle gemeldeten Ereignisse benötigen.

Beispiel für einfache Skalierung

Das folgende Snippet veranschaulicht die grundlegenden Elemente der Skalierung.

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;
    }
}

Komplexeres Skalierungsbeispiel

Das folgende komplexere Beispiel stammt aus dem InteractiveChart-Beispiel, das unter Scroll-Geste animieren gezeigt wird. Das Beispiel InteractiveChart unterstützt Scrollen, Schwenken und Skalieren mit mehreren Fingern unter Verwendung der Spannen ScaleGestureDetector (getCurrentSpanX und getCurrentSpanY) und „Fokus“ (getFocusX und 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;
    }
};

Zusätzliche Ressourcen

Weitere Informationen zu Eingabeereignissen, Sensoren und zur interaktiven Gestaltung benutzerdefinierter Ansichten finden Sie in den folgenden Referenzen.