Implementar o recurso de arrastar e soltar com visualizações

É possível implementar o processo de arrastar e soltar em visualizações respondendo a eventos que podem acionar um início de arrastar e respondendo e consumindo eventos de soltar.

Iniciar uma ação de arrastar

O usuário inicia uma ação de arrastar com um gesto, geralmente tocando ou clicando e mantendo pressionado um item que quer arrastar.

Para lidar com isso em um View, crie um objeto ClipData e um objeto ClipData.Item para os dados que estão sendo movidos. Como parte do ClipData, forneça metadados armazenados em um objeto ClipDescription dentro do ClipData. Para uma operação de arrastar e soltar que não representa movimentação de dados, use null em vez de um objeto real.

Por exemplo, o snippet de código abaixo mostra como responder a um gesto de tocar e manter pressionado em uma ImageView, criando um objeto ClipData que contém a tag (ou o identificador) de uma 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;
});

Responder ao início de uma ação de arrastar

Durante a operação de arrastar, o sistema envia eventos de arrastar para os listeners de eventos de arrastar dos objetos View do layout atual. Os listeners reagem chamando DragEvent.getAction() para receber o tipo de ação. No início de uma ação de arrastar, esse método retorna ACTION_DRAG_STARTED.

Em resposta a um evento com o tipo de ação ACTION_DRAG_STARTED, um listener de eventos de arrastar precisa fazer o seguinte:

  1. Chame DragEvent.getClipDescription() e use os métodos do tipo MIME no ClipDescription retornado para ver se o listener pode aceitar os dados que estão sendo arrastados.

    Se a operação de arrastar e soltar não representar movimento de dados, isso poderá ser desnecessário.

  2. Se o listener de eventos de arrastar puder aceitar uma ação de soltar, ele precisará retornar true para dizer ao sistema para continuar a enviar eventos de arrastar ao listener. Se o listener não puder aceitar uma ação de soltar, ele precisará retornar false e o sistema para de enviar eventos de arrastar ao listener até que ele envie ACTION_DRAG_ENDED para concluir a operação de arrastar e soltar.

Para um evento ACTION_DRAG_STARTED, os seguintes métodos DragEvent não são válidos: getClipData(), getX(), getY() e getResult().

Gerenciar eventos durante a ação de arrastar

Durante a ação de arrastar, os listeners de eventos de arrastar que retornam true em resposta ao evento de arrastar ACTION_DRAG_STARTED continuam recebendo eventos de arrastar. Os tipos de eventos de arrastar que um listener recebe durante a ação de arrastar dependem do local da ação de arrastar e da visibilidade da View do listener. Os listeners usam os eventos de arrastar principalmente para decidir se precisam mudar a aparência da View.

Durante a ação de arrastar, DragEvent.getAction() retorna um dos três valores:

  • ACTION_DRAG_ENTERED: o listener recebe esse tipo de ação de evento quando o ponto de contato (o ponto na tela sob o dedo ou o mouse do usuário) entra na caixa delimitadora do View do listener.
  • ACTION_DRAG_LOCATION: depois que o listener recebe um evento ACTION_DRAG_ENTERED, ele recebe um novo evento ACTION_DRAG_LOCATION sempre que o ponto de contato se move até receber um evento ACTION_DRAG_EXITED. Os métodos getX() e getY() retornam as coordenadas X e Y do ponto de contato.
  • ACTION_DRAG_EXITED: esse tipo de ação de evento é enviado a um listener que já recebe ACTION_DRAG_ENTERED. O evento é enviado quando o ponto de contato da ação de arrastar é movido de dentro da caixa delimitadora da View do listener para fora dessa caixa.

O listener de eventos de arrastar não precisa reagir a nenhum desses tipos de ação. Se o listener retornar um valor para o sistema, ele será ignorado.

Veja abaixo algumas diretrizes para responder a cada um desses tipos de ação:

  • Em resposta a ACTION_DRAG_ENTERED ou ACTION_DRAG_LOCATION, o listener pode mudar a aparência da View para indicar que a visualização é um possível destino de soltar.
  • Um evento com o tipo de ação ACTION_DRAG_LOCATION contém dados válidos para getX() e getY() correspondentes ao local do ponto de contato. O listener pode usar essas informações para mudar a aparência do View no ponto de contato ou determinar a posição exata em que o usuário pode soltar o conteúdo.
  • Em resposta a ACTION_DRAG_EXITED, o listener precisa redefinir todas as mudanças de aparência aplicadas em resposta a ACTION_DRAG_ENTERED ou ACTION_DRAG_LOCATION. Isso indica ao usuário que a View deixou de ser um destino para uma ação de soltar iminente.

Responder a uma ação de soltar

Quando o usuário solta a sombra de arraste sobre uma View, e o View informa anteriormente que pode aceitar o conteúdo que está sendo arrastado, o sistema envia um evento de arrastar para a View com o tipo de ação ACTION_DROP.

O listener de eventos de arrastar precisa fazer o seguinte:

  1. Chame getClipData() para receber o objeto ClipData fornecido originalmente na chamada de startDragAndDrop() e processe os dados. Se a operação de arrastar e soltar não representar movimento de dados, isso será desnecessário.

  2. Retornará o valor booleano true para indicar que a ação de soltar foi processada ou false se não for. O valor retornado se torna o valor retornado por getResult() para um possível evento ACTION_DRAG_ENDED. Se o sistema não enviar um evento ACTION_DROP, o valor retornado por getResult() para um evento ACTION_DRAG_ENDED será false.

Para um evento ACTION_DROP, getX() e getY() usam o sistema de coordenadas da View que recebe a queda para retornar as posições X e Y do ponto de contato no momento da queda.

Embora o usuário possa soltar a sombra de arraste sobre uma View cujo listener de eventos de arrastar não está recebendo eventos de arrastar, regiões vazias da interface do seu app ou até mesmo sobre áreas fora do aplicativo, o Android não enviará um evento com o tipo de ação ACTION_DROP, apenas um evento ACTION_DRAG_ENDED.

Responder ao fim de uma ação de arrastar

Imediatamente após o usuário soltar a sombra da ação de arrastar, o sistema envia um evento de arrastar com um tipo de ação ACTION_DRAG_ENDED para todos os listeners de eventos de arrastar no aplicativo. Isso indica que a operação de arrastar foi concluída.

Cada listener de eventos de arrastar precisa fazer o seguinte:

  1. Se o listener mudar de aparência durante a operação, será necessário redefinir para a aparência padrão como uma indicação visual ao usuário de que a operação foi concluída.
  2. O listener pode chamar o método getResult() para saber mais sobre a operação. Se um listener retornar true em resposta a um evento do tipo de ação ACTION_DROP, getResult() retornará o booleano true. Em todos os outros casos, getResult() retorna o booleano false, inclusive quando o sistema não envia um evento ACTION_DROP.
  3. Para indicar a conclusão bem-sucedida da operação de soltar, o listener precisa retornar o booleano true ao sistema. Ao não retornar false, um sinal visual mostrando a sombra projetada retornando à origem pode sugerir ao usuário que a operação não foi bem-sucedida.

Responder a eventos de arrastar: um exemplo

Todos os eventos de arrastar são acessados pelo método de evento de arrastar ou pelo listener. O snippet de código abaixo é um exemplo de como responder a eventos de arrastar:

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;

});

Personalizar uma sombra da ação de arrastar

Você pode definir um myDragShadowBuilder personalizado substituindo os métodos em View.DragShadowBuilder. O snippet de código abaixo cria uma sombra de arrastar pequena, retangular e cinza para uma 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);
    }
}