Arrastar e soltar

O framework de arrastar e soltar do Android permite que você adicione ao seu app recursos interativos que usam esse gesto. Com isso, os usuários podem copiar ou mover textos, imagens, objetos e qualquer conteúdo que possa ser representado por um URI de uma View para outra 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 a fim de permitir a transferência de dados, o framework pode ser usado para outras ações da IU. 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 deste guia descreve o framework de arrastar e soltar no contexto da transferência de dados.

Visão geral

Uma operação de arrastar e soltar começa quando o usuário faz um gesto de IU que o app reconhece como um sinal para começar a arrastar dados. Em resposta, o app notifica o sistema sobre a inicialização de uma operação de arrastar e soltar. O sistema chama o app de volta para receber uma representação dos dados que estão sendo arrastados, uma ação de arrastar. Conforme o usuário move a ação de arrastar sobre o layout do app, o sistema envia eventos de arrastar para os listeners de eventos de arrastar e os métodos de callback associados aos objetos View no layout. Se o usuário soltar a ação de arrastar sobre uma visualização que pode aceitar os dados (um destino de soltar), o sistema envia os dados para o destino. A operação de arrastar e soltar termina quando o usuário solta a ação de arrastar, mesmo que ela não esteja sobre um destino de soltar.

Crie um listener de eventos de arrastar implementando um View.OnDragListener. Defina o listener para 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 diz ao sistema para começar 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 o método 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 (uma View que aceita os dados), o sistema vai enviar um objeto de evento de arrastar contendo os dados ao listener de eventos de arrastar do destino de arrastar ou ao método de callback.

Listeners de eventos de arrastar e métodos de callback

Uma View recebe eventos de arrastar com um listener de eventos de arrastar que implementa um View.OnDragListener ou com o método de callback da visualização onDragEvent(). 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 a implementar como uma classe in-line anônima ou uma expressão lambda. Para definir o listener de um objeto View, chame o método setOnDragListener().

Como alternativa, a implementação padrão de onDragEvent() pode ser alterada sem substituir o método. Se você definir um OnReceiveContentListener em uma visualização (consulte setOnReceiveContentListener()), o método onDragEvent() vai ter este comportamento padrão:

  • Retorna verdadeiro em resposta à chamada para startDragAndDrop()
  • Chama 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 conteúdo

Defina o OnReceiveContentListener para processar os dados específicos do seu app. Se quiser oferecer compatibilidade com versões anteriores até a API nível 24, use a versão do OnReceiveContentListener do Jetpack.

Você pode 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 dos métodos onDragEvent() e View.OnDragListener é análoga à combinação de onTouchEvent() e View.OnTouchListener usados com eventos de toque.

Processo de arrastar e soltar

Existem basicamente quatro etapas ou estados no processo de arrastar e soltar: iniciado, em andamento, descartado e encerrado.

Iniciado

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

  • Os dados a serem arrastados
  • Um callback para mostrar a ação de arrastar
  • Metadados que descrevem os dados arrastados

Primeiro, o sistema responde chamando o aplicativo para extrair uma ação de arrastar. Em seguida, o sistema mostra a ação de arrastar no dispositivo.

Depois, o sistema envia um evento de arrastar com o tipo de ação ACTION_DRAG_STARTED para o listener de eventos de arrastar de todas as Views no layout atual. Para continuar a receber eventos de arrastar, incluindo um possível evento de soltar, é necessário que um listener de eventos de arrastar retorne true. Isso registra o listener junto ao sistema. Somente listeners registrados continuam a receber eventos de arrastar. Neste 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 recebe 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 ele não tem interesse na operação de arrastar e soltar e não quer aceitar os dados arrastados.

Em andamento

O usuário continua a ação de arrastar. Conforme a 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 optar por mudar a aparência do destino de soltar View em resposta ao evento. Por exemplo, se o evento indicar que a ação de arrastar entrou na caixa delimitadora do destino de soltar (tipo de ação ACTION_DRAG_ENTERED), o listener pode reagir destacando a View.

Solto

O usuário solta a 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 evento de arrastar contém os dados que foram transmitidos ao sistema na chamada para o método startDragAndDrop() que iniciou a operação. Espera-se que o listener retorne o booleano true ao sistema se o processamento dos dados descartados for concluído pelo listener.

Essa etapa só ocorre se o usuário soltar a ação de arrastar dentro da caixa delimitadora de uma View cujo listener está registrado para receber eventos de arrastar (um destino de soltar). Se o usuário soltar a ação de arrastar em qualquer outra situação, nenhum evento de arrastar ACTION_DROP é enviado.

Encerrado

Depois que o usuário solta e ação de arrastar e após o sistema enviar um evento de arrastar com tipo de ação ACTION_DROP (se necessário), o sistema envia 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 independente de onde o usuário tenha soltado a ação de arrastar. O evento é enviado a todos os listeners registrados para receber eventos de arrastar, mesmo se o listener também tiver recebido o evento ACTION_DROP.

Cada uma dessas quatro etapas é descrita em mais detalhes em 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 acontece 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.

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

Tipo de ação Significado
ACTION_DRAG_STARTED O aplicativo chamou startDragAndDrop() e recebeu uma ação de arrastar. Se o listener quiser continuar recebendo eventos de arrastar para a operação, é necessário retornar o valor booleano true ao sistema.
ACTION_DRAG_ENTERED A ação de arrastar acabou de entrar 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 ação de arrastar entra na caixa delimitadora.
ACTION_DRAG_LOCATION Depois de um evento ACTION_DRAG_ENTERED, a ação de arrastar ainda está dentro da caixa delimitadora da View do listener de eventos de arrastar.
ACTION_DRAG_EXITED Após a ação ACTION_DRAG_ENTERED e pelo menos um evento ACTION_DRAG_LOCATION, a ação de arrastar foi movida para fora da caixa delimitadora da View do listener de eventos de arrastar.
ACTION_DROP A ação de arrastar foi solta na View do listener de eventos de arrastar. Esse tipo de ação é enviado ao listener de um objeto View somente se o listener tiver retornado o valor booleano true em resposta ao evento de arrastar ACTION_DRAG_STARTED. Esse tipo de ação não é enviado se o usuário solta a ação de arrastar em uma View cujo listener não está registrado ou se ele solta a ação de arrastar em algo que não faz parte do layout atual.

Espera-se que o listener retorne valores booleanos true se ele processar o ato de soltar. Caso contrário, ele retorna 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 enviou um evento ACTION_DROP, o recebimento do tipo de ação ACTION_DRAG_ENDED não implica que a operação de soltar foi concluída. O listener precisa chamar getResult() (consulte a tabela 2) para receber o valor retornado em resposta à ação ACTION_DROP. Se um evento ACTION_DROP não foi enviado, getResult() retorna false.

O objeto DragEvent também contém os dados e metadados que o aplicativo forneceu ao sistema na chamada do método 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 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 de 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 retorna 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 ação de arrastar. Você pode a criar usando 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 que você definiu em View.DragShadowBuilder para ter uma 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 a View (se houver) 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ê vê uma ação de arrastar com a mesma aparência da View que é transmitida como argumento, centralizada no local em que o usuário está tocando na tela.

View.DragShadowBuilder()

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

A classe View.DragShadowBuilder tem dois métodos que, juntos, criam a 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 ação de arrastar fica na variável x e a altura em y.
outShadowTouchPoint
Um objeto Point. O ponto de contato é a área dentro da ação de arrastar que precisa estar sob o dedo do usuário durante a ação de arrastar. A posição X é armazenada em x e a posição Y 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 dos parâmetros fornecidos em onProvideShadowMetrics(). O método exibe a ação de arrastar na Canvas fornecida.

A fim de melhorar a performance, use um tamanho pequeno para a ação de arrastar. Para um único item, recomendamos usar um ícone. Para uma seleção múltipla, recomendamos usar ícones em uma pilha em vez de imagens completas espalhadas pela tela.

Uma operação de arrastar e soltar

Esta seção mostra de forma detalhada 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 arraste, normalmente tocar e manter pressionado, em um objeto View. Em resposta, o app precisa:

  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 representa 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(this).apply {
        // Sets the bitmap for the ImageView from an icon bit map (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(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 was 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(this);
    
    // Set the bitmap for the ImageView from an icon bit map (defined elsewhere).
    imageView.setImageBitmap(iconBitmap);
    
    // Set the tag.
    imageView.setTag(IMAGEVIEW_TAG);
    
    // Sets 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 was handled.
        return true;
    });
    
  2. O snippet de código abaixo define o myDragShadowBuilder substituindo os métodos em View.DragShadowBuilder. O código cria uma ação de arrastar pequena, cinza e retangular para uma TextView:

    Kotlin

    private class MyDragShadowBuilder(v: View) : View.DragShadowBuilder(v) {
    
        private val shadow = ColorDrawable(Color.LTGRAY)
    
        // Defines 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. This sets 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)
        }
    
        // Defines 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 v) {
    
            // Stores the View parameter.
            super(v);
    
            // Creates a draggable image that fills the Canvas provided by the system.
            shadow = new ColorDrawable(Color.LTGRAY);
        }
    
        // Defines a callback that sends the drag shadow dimensions and touch point
        // back to the system.
        @Override
        public void onProvideShadowMetrics (Point size, Point touch) {
    
            // Defines 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. This sets 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);
        }
    
        // Defines 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. A reação dos listeners é chamar DragEvent#getAction() para extrair 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:

  1. Chamar DragEvent#getClipDescription() e usar os métodos do tipo MIME no ClipDescription retornado para ver se o listener pode aceitar os dados arrastados.

    Se a operação de arrastar e soltar não representar movimento de dados, talvez isso não seja necessário.

  2. Se o listener de eventos de arrastar puder aceitar uma ação de soltar, ele retorna true para informar ao sistema que continua a enviar eventos de arrastar ao listener. Se o listener não puder aceitar uma ação de soltar, ele retorna false e o sistema interrompe o envio de 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, estes métodos de 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 retornaram true em resposta ao evento de arrastar ACTION_DRAG_STARTED continuarão 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 e da visibilidade da View do listener. Os listeners usam os eventos de arrastar principalmente para decidir se mudarão a aparência da View deles.

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 da View do listener.
  • ACTION_DRAG_LOCATION: depois que o listener receber um evento ACTION_DRAG_ENTERED e antes de receber um evento ACTION_DRAG_EXITED, ele recebe um novo evento ACTION_DRAG_LOCATION sempre que o ponto de contato se mover. 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 para um listener que já recebeu 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 ao sistema, ele é 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 de getX() e getY(), que correspondem ao local do ponto de contato. O listener pode usar essas informações para alterar a aparência da View no ponto de contato ou determinar a posição exata em que o usuário pode soltar a ação de arrastar (ou seja, soltar os dados).
  • Em resposta à ação ACTION_DRAG_EXITED, é necessário que o listener redefina todas as mudanças de aparência aplicadas em resposta à 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 ação de arrastar sobre uma View, e a View já informou que pode aceitar o conteúdo sendo arrastado, o sistema envia um evento de arrastar à View com o tipo de ação ACTION_DROP.

O listener de eventos de arrastar precisa:

  1. Chamar getClipData() para receber o objeto ClipData que foi fornecido originalmente na chamada do método startDragAndDrop() e processar os dados.

    Se a operação de arrastar e soltar não representar o movimento de dados, isso não é necessário.

  2. Retornar o valor booleano true para indicar que a ação de soltar foi processada, ou false em caso de falha. 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 é false.

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

O sistema permite que o usuário solte a ação de arrastar sobre uma View cujo listener de eventos de arrastar não está recebendo esses eventos. Ele também permite que o usuário solte a ação de arrastar sobre regiões vazias da IU do aplicativo ou sobre áreas fora do app. 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 ação de arrastar, o sistema envia um evento de arrastar com um tipo de ação de ACTION_DRAG_ENDED a todos os listeners de eventos de arrastar no 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 tiver mudado a aparência do objeto View durante a operação, ele precisa 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 retornou true em resposta a um evento de tipo de ação ACTION_DROP, o método getResult() retorna o booleano true. Em todos os outros casos, getResult() retorna o booleano false, incluindo o caso em que o sistema não enviou um evento ACTION_DROP.
  3. Para indicar que a operação de arrastar e soltar teve sucesso, o listener retorna 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 ->

    // Handles each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determines if this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example of what your application might do, applies 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()

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

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

            // Returns true; the value is ignored.
            true
        }

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

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

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

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

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

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

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

            // Returns true. DragEvent.getResult() will return true.
            true
        }

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

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

            // Does a getResult(), and displays what happened.
            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()

            // Returns true; the value is ignored.
            true
        }
        else -> {
            // An unknown action type was 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) -> {

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

        case DragEvent.ACTION_DRAG_STARTED:

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

                // As an example of what your application might do, applies 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();

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

            }

            // Returns false to indicate that, during the current drag and drop operation,
            // this View will not receive events again until ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

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

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

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

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

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

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

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

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

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

            // Turns off any color tints.
            ((ImageView)v).clearColorFilter();

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

            // Returns true. DragEvent.getResult() will return true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turns off any color tinting.
            ((ImageView)v).clearColorFilter();

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

            // Does a getResult(), and displays what happened.
            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();
            }

            // Returns true; the value is ignored.
            return true;

        // An unknown action type was 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 mais recentes oferecem suporte ao modo de várias janelas. Isso permite que os usuários movam dados de um app para outro usando uma operação de arrastar e soltar. Consulte Suporte a várias janelas.

O app de origem fornece os dados. A operação de arrastar e soltar é iniciada no app de origem. O app de destino recebe os dados. A operação de arrastar e soltar termina no app de destino.

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 atravessam os limites do app, eles compartilham o acesso aos dados usando um URI de conteúdo:

  • O app de origem precisa definir uma ou ambas as sinalizações DRAG_FLAG_GLOBAL_URI_READ e DRAG_FLAG_GLOBAL_URI_WRITE, dependendo do acesso de leitura/gravação aos dados que o app de origem quer conceder ao app de destino.
  • O app de destino precisa chamar o método requestDragAndDropPermissions() imediatamente antes de processar os dados que o usuário arrasta para o aplicativo. Se o app de destino não precisar mais de acesso aos dados da ação de arrastar e soltar, ele pode chamar release() no objeto que foi retornado do método requestDragAndDropPermissions(). Caso contrário, as permissões são liberadas quando a atividade que as contém for destruída.

O snippet de código abaixo mostra como liberar o acesso somente leitura para arrastar e soltar dados imediatamente após a operação de arrastar e soltar. Consulte o exemplo de DragAndDrop (link em inglês) no GitHub para ver uma demonstração mais completa.

Atividade de arrastar e soltar de origem

Kotlin

// Drag a file stored in internal storage. The file is in an "images/" directory.
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 allow for 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 under 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 allow for 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 for 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 afterwards because it's
            // no longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type was 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 afterwards 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.

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 de 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 EditTexts fica vazia. Consulte Configuração de destino de soltar abaixo.

Configuração de destino de soltar

A classe interna DropHelper.Options permite a configuração de destinos de soltar. Você fornece uma instância da classe ao método DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener). Consulte Destinos de soltar acima.

Destaque de destino de soltar

O DropHelper configura destinos de soltar para exibir um destaque enquanto os usuários arrastam o conteúdo sobre os destinos. O DropHelper oferece um estilo padrão, mas as DropHelper.Options permitem 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 de DropHelper.Options e definir opções de configuração, por exemplo:

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

Componentes de EditText em destinos de soltar

O 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 a DropHelper.Options.Builder#addInnerEditTexts(EditText...) para garantir que o destaque do destino de soltar e o processamento de dados de texto funcionem corretamente.

O DropHelper impede que componentes EditText na hierarquia de visualização do destino de soltar roubem o foco da visualização contida 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 nesta ordem de precedência:

  1. O EditText em que o ClipData foi solto
  2. O EditText que contém o cursor de texto (circunflexo)
  3. O primeiro EditText fornecido à chamada do 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();

Processamento de dados de destino 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. Há dados de texto no objeto. Objetos de mídia, como imagens, são representados por URIs.

O OnReceiveContentListener também processa os 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 o método DropHelper#configureView() é usado para configurar os tipos de visualizações abaixo:

  • Todas as visualizações, se o usuário estiver usando o Android 12 ou mais recente
  • AppCompatEditText até o Android 7.0

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

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

O DropHelper solicita todas as permissões de acesso aos URIs de conteúdo contidos no ClipData de arrastar e soltar (consulte DragAndDropPermissions). As permissões resolvem 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. Confira se há valores nulos e verifique se os dados resolvidos estão corretos.

Outros recursos