Otimizar o app Android para o Chrome OS

Com o recurso de execução de apps Android em Chromebooks, um enorme ecossistema de apps e muitas funcionalidades novas agora estão disponíveis para os usuários. Embora essa seja uma ótima notícia para os desenvolvedores, algumas otimizações de apps são necessárias para atender às expectativas de usabilidade e oferecer uma excelente experiência aos usuários. Este codelab explica as otimizações mais comuns.

f60cd3eb5b298d5d.png

O que você criará

Você criará um app Android funcional que demonstrará as práticas recomendadas e otimizações do Chrome OS. Esse app vai:

Gerenciar a entrada do teclado, incluindo

  • tecla Enter;
  • teclas de seta;
  • atalhos Ctrl- e Ctrl-Shift-;
  • feedback visual para itens selecionados.

Gerenciar a entrada do mouse, incluindo

  • clicar com o botão direito;
  • efeitos de passar o cursor sobre um item;
  • dicas;
  • arrastar e soltar.

Usar componentes de arquitetura para

  • manter o estado;
  • atualizar a IU automaticamente.

52240dc3e68f7af8.png

O que você aprenderá

  • Práticas recomendadas para gerenciar entradas de teclado e do mouse no Chrome OS.
  • Otimizações específicas do Chrome OS.
  • Implementação básica dos componentes de arquitetura ViewModel e LiveData.

Pré-requisitos

Clone o repositório do GitHub

git clone https://github.com/googlecodelabs/optimized-for-chromeos

… ou faça o download de um arquivo ZIP do repositório e extraia-o.

Download do ZIP

Importar o projeto

  • Abra o Android Studio.
  • Selecione Import Project ou File > New > Import Project.
  • Acesse o local em que você clonou ou extraiu o projeto.
  • Importe o projeto optimized-for-chromeos.
  • Você verá dois módulos: start e complete.

Testar o app

  • Crie e execute o módulo start.
  • Comece usando apenas o trackpad.
  • Clique nos dinossauros.
  • Envie algumas mensagens secretas.
  • Tente arrastar o texto "Drag Me" ou soltar um arquivo na área "Drop Things Here".
  • Tente usar o teclado para navegar e enviar mensagens.
  • Tente usar o app no modo tablet.
  • Tente girar o dispositivo ou redimensionar a janela.

O que você acha?

Ainda que esse app seja básico e as partes que parecem estar corrompidas sejam fáceis de resolver, a experiência do usuário é péssima. Vamos melhorá-la.

a40270071a9b5ac3.png

Se você digitou algumas mensagens secretas usando o teclado, perceberá que a tecla Enter não faz nada. Isso é frustrante para o usuário.

O exemplo de código abaixo e a documentação Processar ações do teclado devem ser suficientes.

MainActivity.kt (onCreate)

// Enter key listener
edit_message.setOnKeyListener(View.OnKeyListener { v, keyCode, keyEvent ->
    if (keyEvent.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) {
        button_send.performClick()
        return@OnKeyListener true
    }
    false
})

Faça o teste. Poder enviar mensagens usando apenas o teclado é uma experiência muito melhor para os usuários.

Não seria ótimo navegar nesse app usando apenas o teclado? A experiência atual é insatisfatória. Quando o usuário tem um teclado para usar, é frustrante o aplicativo não responder a ele.

Uma das maneiras mais fáceis de tornar as visualizações navegáveis usando as teclas de seta e a tecla Tab é transformá-las em focalizáveis.

Analise os arquivos de layout e observe as tags Button e ImageView. Perceba que o atributo focusable está definido como "false". Mude-o para "true" no XML:

activity_main.xml

android:focusable="true"

Ou, programaticamente:

MainActivity.kt

button_send.setFocusable(true)
image_dino_1.setFocusable(true)
image_dino_2.setFocusable(true)
image_dino_3.setFocusable(true)
image_dino_4.setFocusable(true)

Faça o teste. Você poderá usar as teclas de seta e a tecla Enter para selecionar dinossauros, mas, dependendo da versão do SO, da tela e da iluminação, talvez não seja possível ver qual item está selecionado no momento. Para resolver isso, defina o recurso de plano de fundo das imagens como R.attr.selectableItemBackground.

MainActivity.kt (onCreate)

val highlightValue = TypedValue()
theme.resolveAttribute(R.attr.selectableItemBackground, highlightValue, true)

image_dino_1.setBackgroundResource(highlightValue.resourceId)
image_dino_2.setBackgroundResource(highlightValue.resourceId)
image_dino_3.setBackgroundResource(highlightValue.resourceId)
image_dino_4.setBackgroundResource(highlightValue.resourceId)

Normalmente, o Android faz um bom trabalho para decidir qual View está acima, abaixo, à esquerda ou à direita da View em foco. Isso funciona neste app? Teste as teclas de seta e a tecla Tab. Navegue entre o campo de mensagem e o botão de envio com as teclas de seta. Selecione o tricerátops e pressione Tab. O foco muda para a visualização esperada?

As coisas estão (intencionalmente) um pouco desajustadas neste exemplo. Para os usuários, essas pequenas falhas no feedback de entrada podem ser muito frustrantes.

No geral, para ajustar manualmente o comportamento da tecla de seta/Tab, você pode usar o seguinte:

Teclas de seta

android:nextFocusLeft="@id/view_to_left"
android:nextFocusRight="@id/view_to_right"
android:nextFocusUp="@id/view_above"
android:nextFocusDown="@id/view_below"

Tecla Tab

android:nextFocusForward="@id/next_view"

Ou, programaticamente:

Teclas de seta

myView.nextFocusLeftId = R.id.view_to_left
myView.nextFocusRightId = R.id.view_to_right
myView.nextFocusTopId = R.id.view_above
myView.nextFocusBottomId = R.id.view_below

Tecla Tab

myView.nextFocusForwardId - R.id.next_view

Neste exemplo, a ordem de foco pode ser corrigida com:

MainActivity.kt

edit_message.nextFocusForwardId = R.id.button_send
edit_message.nextFocusRightId = R.id.button_send
button_send.nextFocusForwardId = R.id.image_dino_1
button_send.nextFocusLeftId = R.id.edit_message
image_dino_2.nextFocusForwardId = R.id.image_dino_3
image_dino_3.nextFocusForwardId = R.id.image_dino_4

Agora você pode selecionar dinossauros, mas, dependendo da tela, das condições de iluminação, da visualização e da sua visão, pode ser difícil ver o destaque dos itens selecionados. Por exemplo, na imagem abaixo, o padrão é cinza sobre cinza.

c0ace19128e548fe.png

Para fornecer um feedback visual com mais destaque para seus usuários, adicione o seguinte a res/values/styles.xml em AppTheme:

res/values/styles.xml

<item name="colorControlHighlight">@color/colorAccent</item>

23a53d405efe5602.png

Digamos que você adora esse rosa, mas o destaque na imagem acima pode ser muito agressivo para o que você quer e fica desorganizado se as imagens não têm exatamente a mesma dimensão. Usando um drawable da lista de estados, você pode criar um drawable de borda que aparecerá somente quando um item for selecionado.

res/drawable/box_border.xml

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:state_focused="true">
       <shape android:padding="2dp">
           <solid android:color="#FFFFFF" />
           <stroke android:width="1dp" android:color="@color/colorAccent" />
           <padding android:left="2dp" android:top="2dp" android:right="2dp"
               android:bottom="2dp" />
       </shape>
   </item>
</selector>

Agora, substitua as linhas highlightValue/setBackgroundResource da etapa anterior pelo novo recurso de plano de fundo box_border:

MainActivity.kt (onCreate)

image_dino_1.setBackgroundResource(R.drawable.box_border)
image_dino_2.setBackgroundResource(R.drawable.box_border)
image_dino_3.setBackgroundResource(R.drawable.box_border)
image_dino_4.setBackgroundResource(R.drawable.box_border)

77ac1e50cdfbea01.png

631df359631b28bb.png

Os usuários de teclado esperam que atalhos comuns baseados em Ctrl funcionem. Agora, você adicionará os atalhos Desfazer (Ctrl-Z) e Refazer (Ctrl-Shift-Z) ao app.

Primeiro, crie uma pilha simples de histórico de cliques. Imagine que um usuário tenha feito cinco ações e pressione Ctrl-Z duas vezes para que as ações 4 e 5 estejam na pilha de refazer e 1, 2 e 3 na pilha de desfazer. Se o usuário pressionar Ctrl-Z novamente, a ação 3 irá da pilha de desfazer para a de refazer. Se depois ele pressionar Ctrl-Shift-Z, a ação 3 irá da pilha de refazer para a de desfazer.

9d952ca72a5640d7.png

Na parte superior da classe principal, defina as diferentes ações de clique e crie as pilhas usando ArrayDeque.

MainActivity.kt

private var undoStack = ArrayDeque<Int>()
private var redoStack = ArrayDeque<Int>()

private val UNDO_MESSAGE_SENT = 1
private val UNDO_DINO_CLICKED = 2

Sempre que uma mensagem for enviada ou o usuário clicar em um dinossauro, adicione essa ação à pilha de desfazer. Quando uma nova ação for realizada, limpe a pilha de refazer. Atualize seus listeners de clique da seguinte forma:

MainActivity.kt

//In button_send onClick listener
undoStack.push(UNDO_MESSAGE_SENT)
redoStack.clear()

...

//In ImageOnClickListener
undoStack.push(UNDO_DINO_CLICKED)
redoStack.clear()

Agora, mapeie as teclas de atalho. A compatibilidade com comandos Ctrl- e, no Android O e versões mais recentes, com comandos Alt- e Shift-, pode ser adicionada usando dispatchKeyShortcutEvent.

MainActivity.kt (dispatchKeyShortcutEvent)

override fun dispatchKeyShortcutEvent(event: KeyEvent): Boolean {
    if (event.getKeyCode() == KeyEvent.KEYCODE_Z) {
        // Undo action
        return true
    }
    return super.dispatchKeyShortcutEvent(event)
}

Vamos ser minuciosos aqui. Para insistir que apenas Ctrl+Z acione o callback, e nãoAlt-Z ouShift+Z, use hasModifiers. As operações de desfazer da pilha estão preenchidas abaixo.

MainActivity.kt (dispatchKeyShortcutEvent)

override fun dispatchKeyShortcutEvent(event: KeyEvent): Boolean {
    // Ctrl-z == Undo
    if (event.keyCode == KeyEvent.KEYCODE_Z && event.hasModifiers(KeyEvent.META_CTRL_ON)) {
        val lastAction = undoStack.poll()
        if (null != lastAction) {
            redoStack.push(lastAction)

            when (lastAction) {
                UNDO_MESSAGE_SENT -> {
                    messagesSent--
                    text_messages_sent.text = (Integer.toString(messagesSent))
                }

                UNDO_DINO_CLICKED -> {
                    dinosClicked--
                    text_dinos_clicked.text = Integer.toString(dinosClicked)
                }

                else -> Log.d("OptimizedChromeOS", "Error on Ctrl-z: Unknown Action")
            }

            return true
        }
    }
    return super.dispatchKeyShortcutEvent(event)
}

Faça o teste. Tudo está funcionando de acordo com o esperado? Agora, adicione Ctrl-Shift-Z usando OR com as sinalizações de modificador.

MainActivity.kt (dispatchKeyShortcutEvent)

// Ctrl-Shift-z == Redo
if (event.keyCode == KeyEvent.KEYCODE_Z &&
    event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)) {
    val prevAction = redoStack.poll()
    if (null != prevAction) {
        undoStack.push(prevAction)

        when (prevAction) {
            UNDO_MESSAGE_SENT -> {
                messagesSent++
                text_messages_sent.text = (Integer.toString(messagesSent))
            }

            UNDO_DINO_CLICKED -> {
                dinosClicked++
                text_dinos_clicked.text = Integer.toString(dinosClicked)
            }

            else -> Log.d("OptimizedChromeOS", "Error on Ctrl-Shift-z: Unknown Action")
        }

        return true
    }
}

Em muitas interfaces, os usuários presumem que clicar com o botão direito do mouse ou tocar duas vezes em um trackpad exibirá um menu de contexto. Neste app, queremos apresentar esse menu de contexto para que os usuários possam enviar fotos legais de dinossauros para um amigo.

8b8c4a377f5e743b.png

A criação de um menu de contexto inclui automaticamente a funcionalidade de clique com o botão direito do mouse. Em muitos casos, isso é tudo o que você precisa. Há três partes nesta configuração:

Informar à IU que essa visualização tem um menu de contexto

Use registerForContextMenu em cada visualização em que você quer usar um menu de contexto. Neste caso, as quatro imagens.

MainActivity.kt

registerForContextMenu(image_dino_1)
registerForContextMenu(image_dino_2)
registerForContextMenu(image_dino_3)
registerForContextMenu(image_dino_4)

Definir a aparência do menu de contexto

Crie um menu em XML que contenha todas as opções de contexto necessárias. Para isso, basta adicionar "Share".

res/menu/context_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_item_share_dino"
        android:icon="@android:drawable/ic_menu_share"
        android:title="@string/menu_share" />
</menu>

Em seguida, na classe de atividade principal, substitua onCreateContextMenu e transmita o arquivo XML.

MainActivity.kt

override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
    super.onCreateContextMenu(menu, v, menuInfo)
    val inflater = menuInflater
    inflater.inflate(R.menu.context_menu, menu)
}

Definir as ações a serem realizadas quando um item específico for escolhido

Por fim, defina a ação que será realizada ao substituir onContextItemSelected. Aqui, basta mostrar um Snackbar rápido, informando ao usuário que a imagem foi compartilhada.

MainActivity.kt

override fun onContextItemSelected(item: MenuItem): Boolean {
    if (R.id.menu_item_share_dino == item.itemId) {
        Snackbar.make(findViewById(android.R.id.content),
            getString(R.string.menu_shared_message), Snackbar.LENGTH_SHORT).show()
        return true
    } else {
        return super.onContextItemSelected(item)
    }
}

Faça o teste. Se você clicar com o botão direito do mouse em uma imagem, o menu de contexto será exibido.

MainActivity.kt

myView.setOnContextClickListener {
    // Display right-click options
    true
}

A adição de um texto de dica que é exibido ao passar o cursor sobre um item é uma maneira fácil de ajudar os usuários a entenderem como a IU funciona ou de fornecer mais informações.

17639493329a9d1a.png

Adicione dicas com o nome do dinossauro para cada uma das fotos usando o método setTootltipText().

MainActivity.kt

// Add dino tooltips
TooltipCompat.setTooltipText(image_dino_1, getString(R.string.name_dino_hadrosaur))
TooltipCompat.setTooltipText(image_dino_2, getString(R.string.name_dino_triceratops))
TooltipCompat.setTooltipText(image_dino_3, getString(R.string.name_dino_nodosaur))
TooltipCompat.setTooltipText(image_dino_4, getString(R.string.name_dino_afrovenator))

Pode ser útil adicionar um efeito de feedback visual para certas visualizações quando um dispositivo apontador passar o cursor sobre elas.

Para adicionar esse tipo de feedback, use o código abaixo. O botão Send ficará verde quando o mouse passar sobre ele.

MainActivity.kt (onCreate)

button_send.setOnHoverListener(View.OnHoverListener { v, event ->
    val action = event.actionMasked

    when (action) {
        ACTION_HOVER_ENTER -> {
            val buttonColorStateList = ColorStateList(
                arrayOf(intArrayOf()),
                intArrayOf(Color.argb(127, 0, 255, 0))
            )
            button_send.setBackgroundTintList(buttonColorStateList)
            return@OnHoverListener true
        }

        ACTION_HOVER_EXIT -> {
            button_send.setBackgroundTintList(null)
            return@OnHoverListener true
        }
    }

    false
})

Adicione mais um efeito de passar o cursor: mude a imagem de plano de fundo associada à TextView arrastável para que o usuário saiba que o texto é arrastável.

MainActivity.kt (onCreate)

text_drag.setOnHoverListener(View.OnHoverListener { v, event ->
    val action = event.actionMasked

    when (action) {
        ACTION_HOVER_ENTER -> {
            text_drag.setBackgroundResource(R.drawable.hand)
            return@OnHoverListener true
        }

        ACTION_HOVER_EXIT -> {
            text_drag.setBackgroundResource(0)
            return@OnHoverListener true
        }
    }

    false
})

Faça o teste. Uma imagem grande de uma mão deve aparecer quando o mouse passar sobre o texto "Drag Me!". Até mesmo esse feedback extravagante torna a experiência do usuário mais tátil.

Para mais informações, consulte a documentação View.OnHoverListener e MotionEvent.

Em um ambiente de área de trabalho, é natural arrastar e soltar itens em um app, especialmente no gerenciador de arquivos do Chrome OS. Nesta etapa, configure um destino para soltar que possa receber arquivos ou itens de texto simples. Na próxima seção do codelab, implementaremos um item arrastável.

cfbc5c9d8d28e5c5.gif

Primeiro, crie um OnDragListener vazio. Confira a estrutura dele antes de começar a programar:

MainActivity.kt

protected inner class DropTargetListener(private val activity: AppCompatActivity
) : View.OnDragListener {
    override fun onDrag(v: View, event: DragEvent): Boolean {
        val action = event.action

        when (action) {
            DragEvent.ACTION_DRAG_STARTED -> {
                    return true
            }

            DragEvent.ACTION_DRAG_ENTERED -> {
                return true
            }

            DragEvent.ACTION_DRAG_EXITED -> {
                return true
            }

            DragEvent.ACTION_DRAG_ENDED -> {
                return true
            }

            DragEvent.ACTION_DROP -> {
                return true
            }

            else -> {
                Log.d("OptimizedChromeOS", "Unknown action type received by DropTargetListener.")
                return false
            }
        }
    }
}

O método onDrag() é chamado sempre que um ou vários eventos de arrastar diferentes ocorrem: ao iniciar uma ação de arrastar, passar o cursor sobre uma zona onde o objeto pode ser solto ou ao soltar um item. Veja um resumo dos diferentes eventos de arrastar:

  • ACTION_DRAG_STARTED é acionado quando algum item é arrastado. O destino precisa procurar itens válidos que ele pode receber e fornecer uma indicação visual de que esse é um destino disponível.
  • ACTION_DRAG_ENTERED e ACTION_DRAG_EXITED são acionados quando um item está sendo arrastado e entra/sai da zona onde ele pode ser solto. Forneça um feedback visual para que o usuário saiba que pode soltar o item.
  • ACTION_DROP é acionado quando o item é solto. Processe o item aqui.
  • ACTION_DRAG_ENDED é acionado quando a ação de soltar é concluída ou cancelada. Retorne a IU ao estado normal.

ACTION_DRAG_STARTED

Esse evento é acionado sempre que uma ação de arrastar é iniciada. Indique aqui se um destino pode receber um item específico (retorna um valor verdadeiro) ou não (retorna um valor falso) e informe isso visualmente ao usuário. O evento de arrastar conterá uma ClipDescription com informações sobre o item que está sendo arrastado.

Para determinar se esse listener de arrastar pode receber um item, examine o Tipo MIME do item. Neste exemplo, indique que o destino é válido, colorindo o plano de fundo em verde-claro.

MainActivity.kt

DragEvent.ACTION_DRAG_STARTED -> {
    // Limit the types of items that can be received
    if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
        event.clipDescription.hasMimeType("application/x-arc-uri-list")) {

        // Greenify background colour so user knows this is a target
        v.setBackgroundColor(Color.argb(55, 0, 255, 0))
        return true
    }

    // If the dragged item is of an unrecognized type, indicate this is not a valid target
    return false
}

ENTERED, EXITED e ENDED

ENTERED e EXITED são os locais em que a lógica de retorno tátil/visual será colocada. Neste exemplo, intensifique o verde quando o item passar sobre a zona de destino para indicar que o usuário pode soltá-lo. Em ENDED, redefina a IU para o estado normal dela, sem a ação de arrastar e soltar.

MainActivity.kt

DragEvent.ACTION_DRAG_ENTERED -> {
    // Increase green background colour when item is over top of target
    v.setBackgroundColor(Color.argb(150, 0, 255, 0))
    return true
}

DragEvent.ACTION_DRAG_EXITED -> {
    // Less intense green background colour when item not over target
    v.setBackgroundColor(Color.argb(55, 0, 255, 0))
    return true
}

DragEvent.ACTION_DRAG_ENDED -> {
    // Restore background colour to transparent
    v.setBackgroundColor(Color.argb(0, 255, 255, 255))
    return true
}

ACTION_DROP

Esse é o evento que ocorre ao soltar o item no destino. É aqui que o processamento é feito.

Observação: os arquivos do Chrome OS precisam ser acessados usando o ContentResolver.

Nesta demonstração, o destino pode receber um arquivo ou um objeto de texto simples. Para texto simples, mostre o texto na TextView. Se for um arquivo, copie os primeiros 200 caracteres e mostre-os.

MainActivity.kt

DragEvent.ACTION_DROP -> {
    requestDragAndDropPermissions(event) // Allow items from other applications
    val item = event.clipData.getItemAt(0)
    val textTarget = v as TextView

    if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
        // If this is a text item, simply display it in a new TextView.
        textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
        textTarget.text = item.text
        // In STEP 10, replace line above with this
        // dinoModel.setDropText(item.text.toString())
    } else if (event.clipDescription.hasMimeType("application/x-arc-uri-list")) {
        // If a file, read the first 200 characters and output them in a new TextView.

        // Note the use of ContentResolver to resolve the ChromeOS content URI.
        val contentUri = item.uri
        val parcelFileDescriptor: ParcelFileDescriptor?
        try {
            parcelFileDescriptor = contentResolver.openFileDescriptor(contentUri, "r")
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
            Log.e("OptimizedChromeOS", "Error receiving file: File not found.")
            return false
        }

        if (parcelFileDescriptor == null) {
            textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
            textTarget.text = "Error: could not load file: " + contentUri.toString()
            // In STEP 10, replace line above with this
            // dinoModel.setDropText("Error: could not load file: " + contentUri.toString())
            return false
        }

        val fileDescriptor = parcelFileDescriptor.fileDescriptor

        val MAX_LENGTH = 5000
        val bytes = ByteArray(MAX_LENGTH)

        try {
            val `in` = FileInputStream(fileDescriptor)
            try {
                `in`.read(bytes, 0, MAX_LENGTH)
            } finally {
                `in`.close()
            }
        } catch (ex: Exception) {
        }

        val contents = String(bytes)

        val CHARS_TO_READ = 200
        val content_length = if (contents.length > CHARS_TO_READ) CHARS_TO_READ else 0

        textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
        textTarget.text = contents.substring(0, content_length)
        // In STEP 10, replace line above with this
        // dinoModel.setDropText(contents.substring(0, content_length))
    } else {
        return false
    }
    return true
}

OnDragListener

Agora que o DropTargetListener está configurado, anexe-o à visualização em que você quer receber os itens soltos.

MainActivity.kt

text_drop.setOnDragListener(DropTargetListener(this))

Faça o teste. Lembre-se de que você precisará arrastar os itens do gerenciador de arquivos do Chrome OS. Você pode criar um arquivo de texto usando o editor de texto do Chrome OS ou fazer o download de um arquivo de imagem da Internet.

Agora, configure um item arrastável no app. Um processo de arrastar normalmente é acionado por uma ação de tocar em uma visualização e mantê-la pressionada. Para indicar que um item pode ser arrastado, crie um LongClickListener que forneça ao sistema os dados a serem transferidos e indique o tipo de dado. É aqui que você configura a aparência do item enquanto ele está sendo arrastado.

Configure um item de arrastar com texto simples que extraia uma string de uma TextView. Defina o Tipo MIME do conteúdo como ClipDescription.MIMETYPE_TEXT_PLAIN.

Para a apresentação visual durante a ação de arrastar, use o DragShadowBuilder integrado para aplicar um visual translúcido padrão. Consulte Iniciar uma ação de arrastar na documentação para ver um exemplo mais complexo.

Lembre-se de definir a sinalização DRAG_FLAG_GLOBAL para indicar que esse item pode ser arrastado para outros apps.

MainActivity.kt

protected inner class TextViewLongClickListener : View.OnLongClickListener {
    override fun onLongClick(v: View): Boolean {
        val thisTextView = v as TextView
        val dragContent = "Dragged Text: " + thisTextView.text

        //Set the drag content and type
        val item = ClipData.Item(dragContent)
        val dragData = ClipData(dragContent, arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), item)

        //Set the visual look of the dragged object
        //Can be extended and customized. We use the default here.
        val dragShadow = View.DragShadowBuilder(v)

        // Starts the drag, note: global flag allows for cross-application drag
        v.startDragAndDrop(dragData, dragShadow, null, View.DRAG_FLAG_GLOBAL)

        return false
    }
}

Agora, adicione o LongClickListener à TextView arrastável.

MainActivity.kt (onCreate)

text_drag.setOnLongClickListener(TextViewLongClickListener())

Faça um teste. Você consegue arrastar o texto da TextView?

Seu app está muito bom: compatível com o uso de teclado e de mouse e dinossauros. No entanto, em um ambiente de área de trabalho, os usuários redimensionam o app com frequência, maximizando-o, voltando ao padrão, alternando para o modo tablet e mudando a orientação. O que acontece com os itens arrastados, o contador de mensagens e o contador de cliques?

É importante entender o ciclo de vida da atividade ao criar apps Android. À medida que os apps ficam mais complicados, gerenciar os estados do ciclo de vida pode ser difícil. Felizmente, os componentes de arquitetura facilitam o gerenciamento de questões relacionadas ao ciclo de vida de uma maneira robusta. Neste codelab, vamos nos concentrar no uso de ViewModel e LiveData para preservar o estado do app.

ViewModel ajuda a manter os dados relacionados à IU em todas as mudanças do ciclo de vida. LiveData funciona como um observador para atualizar automaticamente elementos da IU.

Considere os dados que queremos acompanhar neste app:

  • Contador de mensagens enviadas (ViewModel, LiveData)
  • Contador de imagens clicadas (ViewModel, LiveData)
  • Texto atual para soltar no destino (ViewModel, LiveData)
  • Pilhas de desfazer/refazer (ViewModel)

Examine o código da classe ViewModel que configura esses itens. Essencialmente, ele contém getter e setters usando um padrão Singleton.

DinoViewModel.kt

class DinoViewModel : ViewModel() {
    private val undoStack = ArrayDeque<Int>()
    private val redoStack = ArrayDeque<Int>()

    private val messagesSent = MutableLiveData<Int>().apply { value = 0 }
    private val dinosClicked = MutableLiveData<Int>().apply { value = 0 }
    private val dropText = MutableLiveData<String>().apply { value = "Drop Things Here!" }

    fun getUndoStack(): ArrayDeque<Int> {
        return undoStack
    }

    fun getRedoStack(): ArrayDeque<Int> {
        return redoStack
    }

    fun getDinosClicked(): LiveData<Int> {
        return dinosClicked
    }

    fun getDinosClickedInt(): Int {
        return dinosClicked.value ?: 0
    }

    fun setDinosClicked(newNumClicks: Int): LiveData<Int> {
        dinosClicked.value = newNumClicks
        return dinosClicked
    }

    fun getMessagesSent(): LiveData<Int> {
        return messagesSent
    }

    fun getMessagesSentInt(): Int {
        return messagesSent.value ?: 0
    }

    fun setMessagesSent(newMessagesSent: Int): LiveData<Int> {
        messagesSent.value = newMessagesSent
        return messagesSent
    }

    fun getDropText(): LiveData<String> {
        return dropText
    }

    fun setDropText(newDropText: String): LiveData<String> {
        dropText.value = newDropText
        return dropText
    }
}

Na sua atividade principal, adquira o ViewModel usando ViewModelProvider. Isso fará toda a mágica do ciclo de vida. Por exemplo, as pilhas de desfazer e refazer manterão o estado automaticamente durante as mudanças de redimensionamento, orientação e layout.

MainActivity.kt (onCreate)

// Get the persistent ViewModel
dinoModel = ViewModelProviders.of(this).get(DinoViewModel::class.java)

// Restore our stacks
undoStack = dinoModel.getUndoStack()
redoStack = dinoModel.getRedoStack()

Para as variáveis de LiveData, crie e anexe objetos Observer e informe à IU como mudar quando as variáveis forem modificadas.

MainActivity.kt (onCreate)

// Set up data observers
dinoModel.getMessagesSent().observe(this, androidx.lifecycle.Observer { newCount ->
    text_messages_sent.setText(Integer.toString(newCount))
})

dinoModel.getDinosClicked().observe(this, androidx.lifecycle.Observer { newCount ->
    text_dinos_clicked.setText(Integer.toString(newCount))
})

dinoModel.getDropText().observe(this, androidx.lifecycle.Observer { newString ->
    text_drop.text = newString
})

Depois que esses observadores são posicionados, o código em todos os callbacks de clique pode ser simplificado para modificar apenas os dados da variável ViewModel.

O código abaixo mostra como você não precisa manipular diretamente os objetos TextView. Todos os elementos da IU com observadores LiveData são atualizados automaticamente.

MainActivity.kt

internal inner class SendButtonOnClickListener(private val sentCounter: TextView) : View.OnClickListener {
    override fun onClick(v: View?) {
        undoStack.push(UNDO_MESSAGE_SENT)
        redoStack.clear()
        edit_message.getText().clear()

        dinoModel.setMessagesSent(dinoModel.getMessagesSentInt() + 1)
    }
}

internal inner class ImageOnClickListener(private val clickCounter: TextView) : View.OnClickListener {
    override fun onClick(v: View) {
        undoStack.push(UNDO_DINO_CLICKED)
        redoStack.clear()

        dinoModel.setDinosClicked(dinoModel.getDinosClickedInt() + 1)
    }
}

Por fim, atualize os comandos de desfazer/refazer para usar o ViewModel e o LiveData em vez de manipular diretamente a IU.

MainActivity.kt

when (lastAction) {
    UNDO_MESSAGE_SENT -> {
        dinoModel.setMessagesSent(dinoModel.getMessagesSentInt() - 1)
    }

    UNDO_DINO_CLICKED -> {
        dinoModel.setDinosClicked(dinoModel.getDinosClickedInt() - 1)
    }

    else -> Log.d("OptimizedChromeOS", "Error on Ctrl-z: Unknown Action")
}

...

when (prevAction) {
    UNDO_MESSAGE_SENT -> {
        dinoModel.setMessagesSent(dinoModel.getMessagesSentInt() + 1)
    }

    UNDO_DINO_CLICKED -> {
        dinoModel.setDinosClicked(dinoModel.getDinosClickedInt() + 1)
    }

    else -> Log.d("OptimizedChromeOS", "Error on Ctrl-Shift-z: Unknown Action")
}

Faça um teste. Como o redimensionamento é feito agora? Você não adorou os componentes de arquitetura?

Confira o codelab Ciclos de vida no Android para ver uma explicação mais detalhada sobre os componentes de arquitetura. Esta postagem do blog (link em inglês) é um ótimo recurso para entender como o ViewModel e o onSavedInstanceState funcionam e interagem.

Você conseguiu! Bom trabalho! Você percorreu um longo caminho para entender os problemas mais comuns que os desenvolvedores enfrentam ao otimizar apps Android para Chrome OS.

52240dc3e68f7af8.png

Exemplo de código-fonte

Clone o repositório do GitHub

git clone https://github.com/googlecodelabs/optimized-for-chromeos

… ou faça o download do repositório como um arquivo ZIP.

Download do ZIP