Ativar o recurso de arrastar e soltar

O framework de arrastar e soltar do Android permite adicionar recursos interativos de arrastar e soltar ao app. Com esse recurso, os usuários podem copiar ou mover texto, imagens, objetos e qualquer conteúdo que possa ser representado por um URI, de uma View para outra dentro de um app ou entre apps no modo de várias janelas.

String de texto e imagem sendo arrastadas e soltas em um app. String de texto e imagem sendo arrastadas e soltas entre apps no modo de tela dividida.
Figura 1. Arrastar e soltar em um app.
Figura 2. Arrastar e soltar entre apps.

O framework inclui uma classe de eventos de arrastar, listeners de arrastar e classes e métodos auxiliares. Embora tenha sido projetado principalmente para permitir a transferência de dados, você pode usar o framework para outras ações da interface. Por exemplo, você pode criar um app que mistura cores quando o usuário arrasta um ícone colorido sobre outro ícone. No entanto, o restante do documento descreve o framework de arrastar e soltar no contexto da transferência de dados.

Visão geral

Uma operação de arrastar e soltar é iniciada quando o usuário faz um gesto na interface que o app reconhece como um sinal para começar a arrastar dados. Em resposta, o app notifica o sistema de que uma operação de arrastar e soltar está sendo iniciada. O sistema chama o app para receber uma representação dos dados que estão sendo arrastados, chamada de sombra de arrastar.

À medida que o usuário move a sombra da ação de arrastar sobre o layout do app, o sistema envia eventos de arrastar para os listeners de eventos de arrastar e métodos de callback associados aos objetos View no layout. Se o usuário soltar a sombra da ação de arrastar sobre uma visualização que pode aceitar os dados (um destino de soltar), o sistema enviará os dados ao destino. A operação de arrastar e soltar termina quando o usuário libera a sombra da ação de arrastar, mesmo que ela esteja ou não sobre um destino de soltar.

Crie um listener de eventos de arrastar implementando View.OnDragListener. Defina o listener de um destino de soltar com o método setOnDragListener() do objeto View. Cada visualização no layout também tem um método de callback onDragEvent().

O aplicativo notifica o sistema para iniciar uma operação de arrastar e soltar chamando o método startDragAndDrop(), que instrui o sistema a enviar eventos de arrastar. O método também fornece ao sistema os dados que o usuário está arrastando e os metadados que os descrevem. Você pode chamar startDragAndDrop() em qualquer View no layout atual. O sistema usa o objeto View apenas para ter acesso às configurações globais no layout.

Durante a operação de arrastar e soltar, o sistema envia eventos de arrastar para os listeners de eventos de arrastar ou métodos de callback dos objetos View no layout. Os listeners ou métodos de callback usam os metadados para decidir se querem aceitar os dados quando eles são soltos. Se o usuário soltar os dados em um destino de soltar, ou seja, uma View que aceita os dados, o sistema vai enviar um objeto de evento de arrastar contendo os dados para o listener de eventos de arrastar do destino de soltar ou para o método de callback.

Listeners de eventos de arrastar e métodos de callback

Um View recebe eventos de arrastar com um listener de eventos de arrastar que implementa View.OnDragListener ou com o método de callback onDragEvent() da visualização. Quando o sistema chama o método ou listener, ele fornece um argumento DragEvent.

Na maioria dos casos, é preferível usar um listener que um método de callback. Ao projetar IUs, você normalmente não cria subclasses de View, mas usar o método de callback força a criação de subclasses para substituir o método. Por comparação, você pode implementar uma classe de listener e a usar com vários objetos View diferentes. Também é possível implementá-la como uma classe in-line anônima ou expressão lambda. Para definir o listener de um objeto View, chame setOnDragListener().

Como alternativa, é possível mudar a implementação padrão de onDragEvent() sem modificar o método. Defina um OnReceiveContentListener em uma visualização. Para mais detalhes, consulte setOnReceiveContentListener(). Por padrão, o método onDragEvent() faz o seguinte:

  • Retorna verdadeiro em resposta à chamada para startDragAndDrop().
  • Chamar performReceiveContent() se os dados de arrastar e soltar forem soltos na visualização. Os dados são transmitidos para o método como um objeto ContentInfo. O método invoca o OnReceiveContentListener.

  • Retorna verdadeiro se os dados de arrastar e soltar forem soltos na visualização e o OnReceiveContentListener consumir qualquer parte do conteúdo.

Defina o OnReceiveContentListener para processar os dados especificamente para seu app. Para compatibilidade com versões anteriores até o nível 24 da API, use a versão do Jetpack de OnReceiveContentListener.

É possível ter um listener de eventos de arrastar e um método de callback para um objeto View. Nesse caso, o sistema chama o listener primeiro. O sistema não chama o método de callback, a menos que o listener retorne false.

A combinação do método onDragEvent() e View.OnDragListener é análoga à combinação de onTouchEvent() e View.OnTouchListener usados com eventos de toque.

Processo de arrastar e soltar

Há quatro etapas ou estados no processo de arrastar e soltar: iniciado, continuante, solto e encerrado.

Iniciada

Em resposta ao gesto de arrastar de um usuário, seu aplicativo chama startDragAndDrop() para instruir o sistema a iniciar uma operação de arrastar e soltar. Os argumentos do método fornecem o seguinte:

  • Os dados a serem arrastados.
  • Um callback para mostrar a ação de arrastar
  • Metadados que descrevem os dados arrastados: o sistema responde chamando o aplicativo de volta para receber uma sombra da ação de arrastar. Em seguida, o sistema mostra a ação de arrastar no dispositivo. : Em seguida, o sistema envia um evento de arrastar com o tipo de ação ACTION_DRAG_STARTED ao listener de eventos de arrastar de todos os objetos View no layout atual. Para continuar recebendo eventos de arrastar, incluindo um possível evento de soltar, o listener de eventos de arrastar precisa retornar true. Isso registra o listener no sistema. Somente listeners registrados continuam a receber eventos de arrastar. Nesse ponto, os listeners também podem mudar a aparência do objeto View do destino de soltar para mostrar que a visualização pode aceitar um evento de soltar. : se o listener de eventos de arrastar retornar false, ele não receberá eventos de arrastar para a operação atual até que o sistema envie um evento de arrastar com o tipo de ação ACTION_DRAG_ENDED. Ao retornar false, o listener informa ao sistema que não está interessado na operação de arrastar e soltar e não quer aceitar os dados arrastados.
Em andamento
O usuário continua a arrastar. À medida que a sombra da ação de arrastar cruza a caixa delimitadora de um destino de soltar, o sistema envia um ou mais eventos de arrastar ao listener de eventos de arrastar do destino. O listener pode mudar a aparência do destino de soltar View em resposta ao evento. Por exemplo, se o evento indicar que a sombra da ação de arrastar entra na caixa delimitadora do destino de soltar (tipo de ação ACTION_DRAG_ENTERED), o listener pode reagir destacando o View.
Solto
O usuário libera a sombra da ação de arrastar dentro da caixa delimitadora de um destino de soltar. O sistema envia ao listener do destino de soltar um evento de arrastar com o tipo de ação ACTION_DROP. O objeto de evento de arrastar contém os dados que são transmitidos ao sistema na chamada startDragAndDrop() que inicia a operação. Espera-se que o listener retorne o booleano true ao sistema se processar com êxito os dados descartados. : esta etapa só vai ocorrer se o usuário soltar a sombra da ação de arrastar dentro da caixa delimitadora de uma View com um listener registrado para receber eventos de arrastar (um destino de soltar). Se o usuário soltar a sombra da ação de arrastar em qualquer outra situação, nenhum evento de arrastar ACTION_DROP será enviado.
Encerrado

Após o usuário soltar a sombra da ação de arrastar e após o sistema enviar

um evento de arrastar com o tipo de ação ACTION_DROP. Se necessário, o sistema vai enviar um evento de arrastar com o tipo de ação ACTION_DRAG_ENDED para indicar que a operação de arrastar e soltar acabou. Isso é feito independentemente de onde o usuário solta a sombra da ação de arrastar. O evento é enviado a todos os listeners registrados para receber eventos de arrastar, mesmo que o listener também receba o evento ACTION_DROP.

Cada uma dessas etapas é descrita em mais detalhes na seção Uma operação de arrastar e soltar.

Eventos de arrastar

O sistema envia um evento de arrastar na forma de um objeto DragEvent, que contém um tipo de ação que descreve o que está acontecendo no processo de arrastar e soltar. Dependendo do tipo de ação, o objeto também pode conter outros dados.

Os listeners de eventos de arrastar recebem o objeto DragEvent. Para ver o tipo de ação, os listeners chamam DragEvent.getAction(). Há seis valores possíveis, definidos por constantes na classe DragEvent, que são descritos na tabela 1:

Tabela 1. Tipos de ação de DragEvent.

Tipo de ação Significado
ACTION_DRAG_STARTED O aplicativo chama startDragAndDrop() e recebe uma sombra de arraste. Se o listener quiser continuar recebendo eventos de arrastar para a operação, será necessário retornar o booleano true ao sistema.
ACTION_DRAG_ENTERED A sombra de arraste entra na caixa delimitadora da View do listener de eventos de arrastar. Esse é o primeiro tipo de ação de evento que o listener recebe quando a sombra da ação de arrastar entra na caixa delimitadora.
ACTION_DRAG_LOCATION Depois de um evento ACTION_DRAG_ENTERED, a sombra da ação de arrastar ainda está dentro da caixa delimitadora do View do listener de eventos de arrastar.
ACTION_DRAG_EXITED Após um ACTION_DRAG_ENTERED e pelo menos um evento ACTION_DRAG_LOCATION, a sombra da ação de arrastar se move para fora da caixa delimitadora do View do listener de eventos de arrastar.
ACTION_DROP A sombra de arraste é liberada sobre a View do listener de eventos de arrastar. Esse tipo de ação é enviado ao listener de um objeto View somente se o listener retornar o booleano true em resposta ao evento de arrastar ACTION_DRAG_STARTED. Esse tipo de ação não será enviado se o usuário liberar a sombra da ação de arrastar sobre uma View cujo listener não está registrado ou se o usuário liberar a sombra sobre algo que não faz parte do layout atual.

O listener retornará o valor booleano true se processar a ação de soltar. Caso contrário, ele precisa retornar false.

ACTION_DRAG_ENDED O sistema está finalizando a operação de arrastar e soltar. Esse tipo de ação não é necessariamente precedido por um evento ACTION_DROP. Se o sistema enviar uma ACTION_DROP, o recebimento do tipo de ação ACTION_DRAG_ENDED não implica que a ação de soltar foi concluída. O listener precisa chamar getResult(), conforme mostrado na Tabela 2, para receber o valor retornado em resposta a ACTION_DROP. Se um evento ACTION_DROP não for enviado, getResult() retornará false.

O objeto DragEvent também contém os dados e metadados que o aplicativo fornece ao sistema na chamada para startDragAndDrop(). Alguns dados são válidos apenas para determinados tipos de ação, conforme resumido na Tabela 2. Para mais informações sobre eventos e os dados associados, consulte a seção Uma operação de arrastar e soltar.

Tabela 2. Dados válidos de DragEvent por tipo de ação

getAction()
valor
getClipDescription()
valor
getLocalState()
valor
getX()
valor
getY()
valor
getClipData()
valor
getResult()
valor
ACTION_DRAG_STARTED ✓ ✓ ✓ ✓    
ACTION_DRAG_ENTERED ✓ ✓        
ACTION_DRAG_LOCATION ✓ ✓ ✓ ✓    
ACTION_DRAG_EXITED ✓ ✓        
ACTION_DROP ✓ ✓ ✓ ✓ ✓  
ACTION_DRAG_ENDED   ✓       ✓

Os métodos DragEvent getAction(), describeContents(), writeToParcel() e toString() sempre retornam dados válidos.

Se um método não contiver dados válidos para um tipo de ação específico, ele vai retornar null ou 0, dependendo do tipo de resultado.

Ação de arrastar

Durante uma operação de arrastar e soltar, o sistema exibe uma imagem que o usuário arrasta. Para o movimento de dados, essa imagem representa os dados sendo arrastados. Para outras operações, a imagem representa algum aspecto da operação de arrastar.

A imagem é chamada de sombra de arrastar. Você pode criá-la com os métodos que declara para um objeto View.DragShadowBuilder. Você transmite o builder ao sistema quando inicia uma operação de arrastar e soltar usando startDragAndDrop(). Como parte da resposta a startDragAndDrop(), o sistema invoca os métodos de callback definidos em View.DragShadowBuilder para acessar uma sombra da ação de arrastar.

A classe View.DragShadowBuilder tem dois construtores:

View.DragShadowBuilder(View)

Esse construtor aceita todos os objetos View do aplicativo. O construtor armazena o objeto View no objeto View.DragShadowBuilder para que os callbacks possam o acessar a fim de construir a ação de arrastar. A visualização não precisa ser uma View selecionada pelo usuário para iniciar a operação de arrastar.

Se você usar esse construtor, não vai precisa estender o View.DragShadowBuilder nem substituir os métodos dele. Por padrão, você tem uma sombra de arrastar com a mesma aparência da View transmitida como argumento, centrada no local em que o usuário toca na tela.

View.DragShadowBuilder()

Se você usar esse construtor, nenhum objeto View vai estar disponível no objeto View.DragShadowBuilder. O campo está definido como null. É necessário estender View.DragShadowBuilder e substituir os métodos. Caso contrário, uma ação de arrastar invisível vai aparecer. O sistema não gera um erro.

A classe View.DragShadowBuilder tem dois métodos que, juntos, criam a sombra da ação de arrastar:

onProvideShadowMetrics()

O sistema chama esse método imediatamente após startDragAndDrop() ser chamado. Use o método para enviar as dimensões e o ponto de contato da ação de arrastar ao sistema. O método tem dois parâmetros:

outShadowSize:um objeto Point. A largura da sombra de arraste aparece em x, e a altura em y.

outShadowTouchPoint:um objeto Point. O ponto de contato é o local dentro da sombra da ação de arrastar que precisa estar sob o dedo do usuário durante a ação. Sua posição X vai em x, e sua posição Y vai em y.

onDrawShadow()

Imediatamente após a chamada do método onProvideShadowMetrics(), o sistema chama onDrawShadow() para criar a ação de arrastar. O método tem um único argumento, um objeto Canvas que o sistema cria usando os parâmetros fornecidos em onProvideShadowMetrics(). O método desenha a sombra da ação de arrastar no Canvas fornecido.

Para melhorar o desempenho, mantenha o tamanho da sombra da ação de arrastar pequeno. Para um único item, você pode usar um ícone. Para uma seleção de vários itens, convém usar ícones em uma pilha em vez de imagens completas espalhadas pela tela.

Uma operação de arrastar e soltar

Esta seção mostra em detalhes como iniciar uma ação de arrastar, responder a eventos durante a ação, responder a um evento de soltar e encerrar a operação de arrastar e soltar.

Iniciar uma ação de arrastar

O usuário inicia uma ação de arrastar com um gesto de arrastar, geralmente tocar e manter pressionado, em um objeto View. Em resposta, seu app precisa fazer o seguinte:

  1. Criar 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 no ClipData. Para uma operação de arrastar e soltar que não represente a movimentação de dados, recomendamos usar 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.
            val myShadow = MyDragShadowBuilder(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.
    View.DragShadowBuilder myShadow = new MyDragShadowBuilder(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;
    });
    
  2. Defina myDragShadowBuilder 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);
    }
    }
    

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 informar 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 vai parar de enviar eventos de arrastar ao listener até que o sistema 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 das 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: quando 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 da View no ponto de contato ou determinar a posição exata em que o usuário pode soltar a sombra da ação de arrastar, ou seja, soltar os dados.
  • 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 libera a sombra da ação de arrastar sobre uma View e a 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. Retorna o 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.

O sistema permite que o usuário solte a sombra da ação de arrastar sobre uma View cujo listener de eventos de arrastar não está recebendo eventos de arrastar. Ele também permite que o usuário solte a sombra da ação de arrastar sobre regiões vazias da interface do aplicativo ou sobre áreas fora do aplicativo. Em todos esses casos, o sistema não envia um evento com o tipo de ação ACTION_DROP, embora o sistema envie 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 de ACTION_DRAG_ENDED para todos os listeners de eventos de arrastar no seu aplicativo. Isso indica que a operação de arrastar e soltar acabou.

Cada listener de eventos de arrastar precisa fazer o seguinte:

  1. Se o listener mudar a aparência do objeto View durante a operação, ele precisará redefinir a View para a aparência padrão. Essa é uma indicação visual ao usuário de que a operação acabou.
  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 que a operação de arrastar e soltar foi concluída, o listener precisa retornar o booleano true ao sistema.

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

});

Arrastar e soltar no modo de várias janelas

Os dispositivos com o Android 7.0 (nível 24 da API) ou versões mais recentes oferecem suporte ao modo de várias janelas, que permite que os usuários movam dados de um app para outro usando uma operação de arrastar e soltar. Para mais informações, consulte Suporte a várias janelas.

O app de origem, em que a operação de arrastar e soltar é iniciada, fornece os dados. O app de destino, em que a operação de arrastar e soltar termina, recebe os dados.

Ao iniciar a operação de arrastar e soltar, o app de origem precisa definir a sinalização DRAG_FLAG_GLOBAL para indicar que o usuário pode arrastar dados a outro app.

Como os dados se movem pelos limites do app, eles compartilham o acesso aos dados usando um URI de conteúdo. Isso exige o seguinte:

  • O app de origem precisa definir uma ou ambas as flags DRAG_FLAG_GLOBAL_URI_READ e DRAG_FLAG_GLOBAL_URI_WRITE, dependendo do acesso de leitura ou gravação aos dados que o app de origem quer conceder ao app de destino.
  • O app de destino precisa chamar requestDragAndDropPermissions() imediatamente antes de processar os dados que o usuário arrasta para o app. Se o app de destino não precisar mais de acesso aos dados de arrastar e soltar, ele poderá chamar release() no objeto retornado de requestDragAndDropPermissions(). Caso contrário, as permissões são liberadas quando a atividade que as contém for destruída. Se a implementação envolver o início de uma nova atividade para processar os URIs descartados, você precisará conceder as mesmas permissões à nova atividade. Você precisa definir os dados de clipe e uma sinalização:

    Kotlin

    intent.setClipData(clipData)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    

    Java

    intent.setClipData(clipData);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    

Os snippets de código abaixo demonstram como liberar o acesso somente leitura para arrastar e soltar dados imediatamente após a operação de arrastar e soltar. Consulte o exemplo DragAndDrop (link em inglês) no GitHub para ver um exemplo mais completo.

Atividade de arrastar e soltar de origem

Kotlin

// Drag a file stored in an images/ directory in internal storage.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored in an images/ directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

Atividade de arrastar e soltar de destino

Kotlin

// Container where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type is received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

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

    return false;
});

DropHelper para operação de arrastar e soltar simplificada

A classe DropHelper simplifica a implementação de recursos de arrastar e soltar. Um membro da biblioteca DragAndDrop do Jetpack, o DropHelper, oferece compatibilidade com versões anteriores até o nível 24 da API.

Use o DropHelper para especificar destinos de soltar, personalizar o destaque do destino de soltar e definir como os dados descartados são processados.

Especificar destinos de soltar

DropHelper.configureView() é um método estático e sobrecarregado que permite especificar destinos de soltar. Os parâmetros incluem:

Por exemplo, para criar um destino de soltar que aceite imagens, use uma das chamadas de método abaixo:

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

A segunda chamada omite as opções de configuração do destino de soltar. Nesse caso, a cor de destaque do destino de soltar é definida como a cor secundária (ou de destaque) do tema, o raio do canto de destaque é definido como 16 dp e a lista de componentes EditText está vazia. Consulte a próxima seção para mais informações.

Configurar destinos de soltar

A classe interna DropHelper.Options permite configurar destinos de soltar. Forneça uma instância da classe para o método DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener. Consulte a seção anterior para saber mais.

Personalizar o destaque do destino de soltar

O DropHelper configura destinos de soltar para exibir um destaque enquanto os usuários arrastam o conteúdo sobre os destinos. DropHelper oferece um estilo padrão, e DropHelper.Options permite definir a cor do destaque e especificar o raio do canto do retângulo do destaque.

Use a classe DropHelper.Options.Builder para criar uma instância DropHelper.Options e definir opções de configuração, conforme mostrado no exemplo a seguir:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

Processar componentes EditText em destinos de soltar

DropHelper também controla o foco no destino de soltar quando o destino contém campos de texto editáveis.

Os destinos de soltar podem ser uma visualização única ou uma hierarquia de visualização. Se a hierarquia de visualização do destino de soltar contiver um ou mais componentes EditText, forneça uma lista dos componentes para DropHelper.Options.Builder.addInnerEditTexts(EditText...) para garantir que o destaque do destino de soltar e o processamento de dados de texto funcionem corretamente.

DropHelper impede que componentes EditText na hierarquia de visualização do destino de soltar roubem o foco da visualização que a contém durante interações de arrastar.

Além disso, se o ClipData de arrastar e soltar incluir dados de texto e URI, o DropHelper seleciona um dos componentes EditText no destino de soltar para processar dados de texto. A seleção é baseada na seguinte ordem de precedência:

  1. O EditText em que o ClipData é solto.
  2. O EditText que contém o cursor de texto (circunflexo).
  3. O primeiro EditText fornecido à chamada para DropHelper.Options.Builder.addInnerEditTexts(EditText...).

Para definir um EditText como o gerenciador de dados de texto padrão, transmita o EditText como o primeiro argumento da chamada para o DropHelper.Options.Builder.addInnerEditTexts(EditText...). Por exemplo, se o destino de soltar processar imagens, mas tiver campos de texto editáveis T1, T2 e T3, torne o T2 o padrão desta maneira:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

Processar dados em destinos de soltar

O método DropHelper.configureView() aceita um OnReceiveContentListener que você cria para processar o ClipData de arrastar e soltar. Os dados de arrastar e soltar são fornecidos ao listener em um objeto ContentInfoCompat. Dados de texto estão presentes no objeto. As mídias, como imagens, são representadas por URIs.

O OnReceiveContentListener também processa dados fornecidos ao destino de soltar por interações do usuário que não sejam de arrastar e soltar, como copiar e colar, quando a DropHelper.configureView() é usada para configurar estes tipos de visualização:

  • Todas as visualizações, se o usuário estiver usando o Android 12 ou mais recente.
  • AppCompatEditText, se o usuário estiver executando uma versão do Android anterior à 7.0.

Tipos MIME, permissões e validação de conteúdo

A verificação do tipo MIME feito por DropHelper é baseada na ação de arrastar e soltar ClipDescription, que é criada pelo app que fornece os dados de arrastar e soltar. Valide o ClipDescription para garantir que os tipos MIME estejam definidos corretamente.

DropHelper solicita todas as permissões de acesso aos URIs de conteúdo contidos no ClipData de arrastar e soltar. Para saber mais, consulte DragAndDropPermissions. As permissões permitem resolver os URIs de conteúdo ao processar os dados de arrastar e soltar.

O DropHelper não valida os dados retornados pelos provedores de conteúdo ao resolver URIs nos dados soltos. Verifique se há nulo e confirme a exatidão dos dados resolvidos.