Stosowanie przeciągania i upuszczania w widokach

Proces przeciągania i upuszczania możesz wdrożyć w widokach, odpowiadając na zdarzenia, które mogą powodować rozpoczęcie przeciągania, reagowanie na zdarzenia i odpowiadanie na zdarzenia upuszczania.

Rozpocznij przeciąganie

Użytkownik rozpoczyna przeciąganie gestem, zwykle poprzez dotknięcie lub kliknięcie i przytrzymanie elementu, który chce przeciągnąć.

Aby obsłużyć to w View, utwórz obiekt ClipData i obiekt ClipData.Item na potrzeby przenoszonych danych. W ramach ClipData podaj metadane, które są przechowywane w obiekcie ClipDescription w obrębie ClipData. W przypadku operacji przeciągania i upuszczania, która nie odzwierciedla przenoszenia danych, można użyć elementu null zamiast rzeczywistego obiektu.

Ten fragment kodu pokazuje na przykład, jak odpowiedzieć na gest dotknięcia i przytrzymania elementu ImageView za pomocą utworzenia obiektu ClipData zawierającego tag (lub etykietę) obiektu ImageView:

Kotlin

// Create a string for the ImageView label.
val IMAGEVIEW_TAG = "icon bitmap"
...
val imageView = ImageView(context).apply {
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    setImageBitmap(iconBitmap)
    tag = IMAGEVIEW_TAG
    setOnLongClickListener { v ->
        // Create a new ClipData. This is done in two steps to provide
        // clarity. The convenience method ClipData.newPlainText() can
        // create a plain text ClipData in one step.

        // Create a new ClipData.Item from the ImageView object's tag.
        val item = ClipData.Item(v.tag as? CharSequence)

        // Create a new ClipData using the tag as a label, the plain text
        // MIME type, and the already-created item. This creates a new
        // ClipDescription object within the ClipData and sets its MIME type
        // to "text/plain".
        val dragData = ClipData(
            v.tag as? CharSequence,
            arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
            item)

        // Instantiate the drag shadow builder. We use this imageView object
        // to create the default builder.
        val myShadow = View.DragShadowBuilder(view: this)

        // Start the drag.
        v.startDragAndDrop(dragData,  // The data to be dragged.
                            myShadow,  // The drag shadow builder.
                            null,      // No need to use local data.
                            0          // Flags. Not currently used, set to 0.
        )

        // Indicate that the long-click is handled.
        true
    }
}

Java

// Create a string for the ImageView label.
private static final String IMAGEVIEW_TAG = "icon bitmap";
...
// Create a new ImageView.
ImageView imageView = new ImageView(context);

// Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
imageView.setImageBitmap(iconBitmap);

// Set the tag.
imageView.setTag(IMAGEVIEW_TAG);

// Set a long-click listener for the ImageView using an anonymous listener
// object that implements the OnLongClickListener interface.
imageView.setOnLongClickListener( v -> {

    // Create a new ClipData. This is done in two steps to provide clarity. The
    // convenience method ClipData.newPlainText() can create a plain text
    // ClipData in one step.

    // Create a new ClipData.Item from the ImageView object's tag.
    ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());

    // Create a new ClipData using the tag as a label, the plain text MIME type,
    // and the already-created item. This creates a new ClipDescription object
    // within the ClipData and sets its MIME type to "text/plain".
    ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);

    // Instantiate the drag shadow builder. We use this imageView object
    // to create the default builder.
    View.DragShadowBuilder myShadow = new View.DragShadowBuilder(imageView);

    // Start the drag.
    v.startDragAndDrop(dragData,  // The data to be dragged.
                            myShadow,  // The drag shadow builder.
                            null,      // No need to use local data.
                            0          // Flags. Not currently used, set to 0.
    );

    // Indicate that the long-click is handled.
    return true;
});

Reakcja na rozpoczęcie przeciągania

Podczas przeciągania system wysyła zdarzenia przeciągania do odbiorników obiektów View w bieżącym układzie. Słuchacze reagują, wywołując DragEvent.getAction(), aby określić typ działania. Na początku przeciągania ta metoda zwraca ACTION_DRAG_STARTED.

W odpowiedzi na zdarzenie o typie działania ACTION_DRAG_STARTED detektor zdarzeń przeciągania musi wykonać te czynności:

  1. Wywołaj metodę DragEvent.getClipDescription() i użyj metod typu MIME w zwróconym obiekcie ClipDescription, aby sprawdzić, czy detektor może zaakceptować przeciągnięte dane.

    Jeśli przeciąganie i upuszczanie nie odzwierciedla przenoszenia danych, może być niepotrzebne.

  2. Jeśli detektor zdarzeń przeciągania może zaakceptować upuszczanie, musi zwrócić wartość true, by poinformować system, że ma kontynuować wysyłanie tych zdarzeń. Jeśli detektor nie może zaakceptować upuszczania, musi zwrócić false, a system przestanie wysyłać zdarzenia przeciągania do detektora, dopóki system nie wyśle metody ACTION_DRAG_ENDED w celu zakończenia operacji przeciągania i upuszczania.

W przypadku zdarzenia ACTION_DRAG_STARTED nieprawidłowe są te metody DragEvent: getClipData(), getX(), getY() i getResult().

Obsługa zdarzeń podczas przeciągania

Podczas przeciągania elementów detektory, które w odpowiedzi na zdarzenie przeciągania ACTION_DRAG_STARTED zwracają wartość true, nadal otrzymują zdarzenia przeciągania. Rodzaje zdarzeń przeciągania odbieranych przez odbiornik podczas przeciągania zależą od lokalizacji przeciąganego cienia i widoczności View detektora. Detektory używają zdarzeń przeciągania przede wszystkim do określenia, czy muszą zmienić wygląd View.

Podczas przeciągania funkcja DragEvent.getAction() zwraca jedną z trzech wartości:

  • ACTION_DRAG_ENTERED: detektor odbiera ten typ działania zdarzenia, gdy punkt styczności z klientem (punkt na ekranie pod palcem lub myszą użytkownika) wpisze się w ramkę ograniczającą View detektora.
  • ACTION_DRAG_LOCATION: gdy detektor otrzyma zdarzenie ACTION_DRAG_ENTERED, otrzyma nowe zdarzenie ACTION_DRAG_LOCATION za każdym razem, gdy punkt styczności zostanie przesunięty, aż otrzyma zdarzenie ACTION_DRAG_EXITED. Metody getX() i getY() zwracają współrzędne X i Y punktu styczności z klientem.
  • ACTION_DRAG_EXITED: ten typ działania związanego ze zdarzeniem jest wysyłany do detektora, który wcześniej odbierał ACTION_DRAG_ENTERED. Zdarzenie jest wysyłane, gdy punkt styku cienia przeciągania zmieni się z ramki ograniczającej w elemencie View odbiornika i wyjdzie poza tę ramkę.

Detektor zdarzeń przeciągnięcia nie musi reagować na żaden z tych typów działań. Jeśli detektor zwróci wartość do systemu, zostanie ona zignorowana.

Oto kilka wskazówek dotyczących reagowania na te działania:

  • W odpowiedzi na polecenie ACTION_DRAG_ENTERED lub ACTION_DRAG_LOCATION odbiornik może zmienić wygląd wskaźnika View, aby wskazać, że dane wyświetlenie może potencjalnie skutkować porzuceniem.
  • Zdarzenie o typie działania ACTION_DRAG_LOCATION zawiera prawidłowe dane dla zdarzeń getX() i getY() odpowiadające lokalizacji punktu styczności z klientem. Na podstawie tych informacji odbiorca może zmienić wygląd elementu View w punkcie styczności z klientem lub określić dokładną pozycję, w której użytkownik może upuścić treści.
  • W odpowiedzi na polecenie ACTION_DRAG_EXITED detektor musi zresetować wszelkie zmiany wyglądu wprowadzone w odpowiedzi na działanie ACTION_DRAG_ENTERED lub ACTION_DRAG_LOCATION. Informuje to użytkownika, że View nie jest już celem bezpośredniego spadku.

Reagowanie na spadek

Gdy użytkownik zwolni cień przeciągania na element View, a View wcześniej zgłosi, że może zaakceptować przeciągnięte treści, system wyśle do obiektu View zdarzenie przeciągania z typem działania ACTION_DROP.

Detektor zdarzeń przeciągania musi:

  1. Wywołaj getClipData(), aby uzyskać obiekt ClipData, który został pierwotnie przekazany w wywołaniu startDragAndDrop(), i przetworzyć dane. Jeśli przeciąganie i upuszczanie nie odzwierciedla przenoszenia danych, nie jest to konieczne.

  2. Zwraca wartość logiczną true, aby wskazać, że spadek został przetworzony prawidłowo, lub false, jeśli nie został przetworzony. Zwrócona wartość staje się wartością zwracaną przez funkcję getResult() dla ostatecznego zdarzenia ACTION_DRAG_ENDED. Jeśli system nie wysyła zdarzenia ACTION_DROP, wartość zwrócona przez funkcję getResult() w przypadku zdarzenia ACTION_DRAG_ENDED to false.

W przypadku zdarzenia ACTION_DROP funkcje getX() i getY() używają układu współrzędnych View, który otrzymuje spadek, aby zwrócić pozycje X i Y punktu styczności z klientem w momencie spadku.

Użytkownik może zdjąć cień na element View, którego detektor zdarzeń przeciągania nie otrzymuje zdarzeń przeciągania, puste obszary interfejsu aplikacji, a nawet obszary poza aplikacją. Android nie wyśle zdarzenia z działaniem typu ACTION_DROP, a jedynie zdarzenie ACTION_DRAG_ENDED.

Reakcja na koniec przeciągania

Natychmiast po zwolnieniu cienia przez użytkownika system wysyła zdarzenie przeciągania z typem działania ACTION_DRAG_ENDED do wszystkich detektorów zdarzeń przeciągania w Twojej aplikacji. Oznacza to, że operacja przeciągania została zakończona.

Każdy detektor zdarzeń przeciągania musi:

  1. Jeśli detektor zmieni swój wygląd podczas wykonywania operacji, powinien zostać przywrócony do stanu domyślnego, aby pokazać użytkownikowi, że operacja się zakończyła.
  2. Detektor może opcjonalnie wywołać metodę getResult(), by dowiedzieć się więcej o tej operacji. Jeśli detektor zwraca w odpowiedzi na zdarzenie typu działania ACTION_DROP wartość true, wtedy getResult() zwraca wartość logiczną true. We wszystkich innych przypadkach getResult() zwraca wartość logiczną false, nawet wtedy, gdy system nie wysyła zdarzenia ACTION_DROP.
  3. Aby wskazać pomyślne zakończenie operacji usuwania, detektor powinien zwrócić do systemu wartość logiczną true. Jeśli nie zwracasz właściwości false, graficzny sygnał pokazujący cień powracający do źródła może sugerować użytkownikowi, że operacja się nie powiodła.

Reagowanie na zdarzenia przeciągania – przykład

Wszystkie zdarzenia przeciągania są odbierane przez metodę lub detektor zdarzeń przeciągania. Ten fragment kodu jest przykładem reagowania na zdarzenia przeciągania:

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handle each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determine whether this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate()

                // Return true to indicate that the View can accept the dragged
                // data.
                true
            } else {
                // Return false to indicate that, during the current drag and
                // drop operation, this View doesn't receive events again until
                // ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Apply a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Reset the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Get the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Get the text data from the item.
            val dragData = item.text

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turn off color tints.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Return true. DragEvent.getResult() returns true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turn off color tinting.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Do a getResult() and display what happens.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Return true. The value is ignored.
            true
        }
        else -> {
            // An unknown action type is received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handle each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determine whether this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate();

                // Return true to indicate that the View can accept the dragged
                // data.
                return true;

            }

            // Return false to indicate that, during the current drag-and-drop
            // operation, this View doesn't receive events again until
            // ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Apply a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Reset the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

            // Get the item containing the dragged data.
            ClipData.Item item = e.getClipData().getItemAt(0);

            // Get the text data from the item.
            CharSequence dragData = item.getText();

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

            // Turn off color tints.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Return true. DragEvent.getResult() returns true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turn off color tinting.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Do a getResult() and displays what happens.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Return true. The value is ignored.
            return true;

        // An unknown action type is received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;

});

Dostosowywanie cienia przeciągania

Możesz zdefiniować niestandardową wartość myDragShadowBuilder, zastępując metody w View.DragShadowBuilder. Ten fragment kodu tworzy mały, prostokątny, szary cień do przeciągania elementu TextView:

Kotlin

private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {

    private val shadow = ColorDrawable(Color.LTGRAY)

    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    override fun onProvideShadowMetrics(size: Point, touch: Point) {

            // Set the width of the shadow to half the width of the original
            // View.
            val width: Int = view.width / 2

            // Set the height of the shadow to half the height of the original
            // View.
            val height: Int = view.height / 2

            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height)

            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height)

            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2)
    }

    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    override fun onDrawShadow(canvas: Canvas) {

            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
    }
}

Java

private static class MyDragShadowBuilder extends View.DragShadowBuilder {

    // The drag shadow image, defined as a drawable object.
    private static Drawable shadow;

    // Constructor.
    public MyDragShadowBuilder(View view) {

            // Store the View parameter.
            super(view);

            // Create a draggable image that fills the Canvas provided by the
            // system.
            shadow = new ColorDrawable(Color.LTGRAY);
    }

    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    @Override
    public void onProvideShadowMetrics (Point size, Point touch) {

            // Define local variables.
            int width, height;

            // Set the width of the shadow to half the width of the original
            // View.
            width = getView().getWidth() / 2;

            // Set the height of the shadow to half the height of the original
            // View.
            height = getView().getHeight() / 2;

            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height);

            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height);

            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2);
    }

    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    @Override
    public void onDrawShadow(Canvas canvas) {

            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
    }
}