Optimiser votre application Android pour Chrome OS

Puisqu'il est désormais possible d'exécuter des applications Android sur les Chromebooks, les utilisateurs se voient proposer un vaste écosystème d'applications et une foule de nouvelles fonctionnalités. Il s'agit là d'une excellente nouvelle pour les développeurs. Cependant, certaines optimisations sont nécessaires pour répondre aux attentes des utilisateurs et garantir une expérience utilisateur optimale. Cet atelier de programmation présente les optimisations les plus courantes.

f60cd3eb5b298d5d.png

Objectif de cet atelier

Vous allez créer une application Android fonctionnelle qui présente les bonnes pratiques et les optimisations nécessaires pour Chrome OS. Cette application offre les possibilités suivantes :

Gestion de la saisie au clavier

  • Touche Entrée
  • Touches fléchées
  • Raccourcis avec les touches Ctrl et Ctrl+Maj
  • Retour visuel pour les éléments sélectionnés

Gestion de la saisie à la souris

  • Clic droit
  • Effets de survol avec le pointeur de la souris
  • Info-bulles
  • Glisser-déposer

Utilisation des composants d'architecture

  • Conservation de l'état
  • Mise à jour automatique de l'interface utilisateur

52240dc3e68f7af8.png

Points abordés

  • Bonnes pratiques concernant la gestion de la saisie au clavier et à la souris dans Chrome OS
  • Optimisations spécifiques à Chrome OS
  • Implémentation de base des composants d'architecture ViewModel et LiveData

Prérequis

Clonez le dépôt depuis GitHub

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

… ou téléchargez un fichier ZIP du dépôt et extrayez-le.

Télécharger le fichier ZIP

Importer le projet

  • Ouvrez Android Studio.
  • Sélectionnez Importer un projet ou Fichier > Nouveau > Importer un projet.
  • Accédez à l'emplacement où vous avez cloné ou extrait le projet.
  • Importez le projet optimized-for-chromeos.
  • Notez qu'il existe deux modules : start et complete.

Essayer l'application

  • Créez et exécutez le module start.
  • Pour commencer, n'utilisez que le pavé tactile.
  • Cliquez sur les dinosaures.
  • Envoyez des messages secrets.
  • Déplacez le texte "Drag Me" (Faites-moi glisser) ou déposez un fichier dans la zone "Drop Things Here" (Déposez quelque chose ici).
  • Utilisez le clavier pour parcourir les messages et en envoyer.
  • Essayez d'utiliser l'application en mode tablette.
  • Faites pivoter l'appareil ou redimensionnez la fenêtre.

Qu'en pensez-vous ?

Même si cette application est assez rudimentaire et que les éléments qui semblent défectueux sont faciles à corriger, l'expérience utilisateur est catastrophique. Remédions à ce problème !

a40270071a9b5ac3.png

Si vous avez saisi quelques messages secrets à l'aide du clavier, vous aurez remarqué que la touche Entrée n'a aucune utilité, ce qui est frustrant pour l'utilisateur.

L'exemple de code ci-dessous et la documentation Handling Keyboard Actions (Gérer les actions du clavier) devraient faire l'affaire.

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
})

Faites un essai ! Avoir la possibilité d'envoyer des messages en utilisant uniquement le clavier garantit une expérience utilisateur bien plus agréable.

Ne serait-il pas intéressant de pouvoir utiliser cette application en se servant uniquement du clavier ? En l'état, l'expérience utilisateur est médiocre. Lorsqu'un utilisateur a un clavier à disposition, ne pas pouvoir s'en servir dans une application peut être frustrant.

Pour qu'il soit possible de parcourir les vues au moyen des touches fléchées et de la touche de tabulation, la méthode la plus simple consiste à les rendre sélectionnables.

Examinez les fichiers de mise en page, et analysez les balises Button et ImageView. Notez que l'attribut focusable est défini sur "false". Remplacez cette valeur par "true" dans le code XML :

activity_main.xml

android:focusable="true"

Ou par programmation :

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)

Faites un essai ! Vous devriez être en mesure d'utiliser les touches fléchées et la touche Entrée pour sélectionner des dinosaures. Cependant, en fonction de la version de votre système d'exploitation, de votre écran et de la luminosité, vous ne verrez peut-être pas quel élément est actuellement sélectionné. Pour ce faire, définissez la ressource d'arrière-plan des images sur 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)

En règle générale, Android est très efficace pour déterminer quelle View se trouve au-dessus, en dessous, à gauche ou à droite de la View actuellement sélectionnée. Qu'en est-il dans cette application ? Veillez à tester les touches fléchées et la touche de tabulation. Essayez de passer du champ de message au bouton d'envoi, et inversement, à l'aide des touches fléchées. Maintenant, sélectionnez le tricératops et appuyez sur la touche de tabulation. L'application sélectionne-t-elle la vue à laquelle vous vous attendiez ?

Dans cet exemple, les éléments sont volontairement légèrement décentrés. Pour l'utilisateur, ces petits bugs au niveau du retour de saisie peuvent être très désagréables.

Pour corriger manuellement le comportement des touches fléchées ou de la touche de tabulation, vous pouvez utiliser le code suivant :

Touches fléchées

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

Touche de tabulation

android:nextFocusForward="@id/next_view"

Ou par programmation :

Touches fléchées

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

Touche de tabulation

myView.nextFocusForwardId - R.id.next_view

Dans cet exemple, l'ordre de sélection peut être corrigé à l'aide du code suivant :

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

Vous pouvez maintenant sélectionner des dinosaures. Cependant, en fonction de votre écran, des conditions d'éclairage, de la vue et de votre vision, il peut s'avérer difficile de voir la mise en surbrillance des éléments sélectionnés. Dans l'image ci-dessous, par exemple, le paramètre par défaut est "gris sur gris".

c0ace19128e548fe.png

Pour offrir à vos utilisateurs un retour visuel plus marqué, ajoutez les éléments suivants à res/values/styles.xml sous AppTheme :

res/values/styles.xml

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

23a53d405efe5602.png

On peut aimer le rose, mais la mise en surbrillance utilisée dans cette image est peut-être trop agressive pour le résultat que vous souhaitez obtenir et peut donner un rendu brouillon si toutes les images n'ont pas exactement la même dimension. Un objet StateListDrawable vous permet de créer une bordure drawable qui ne s'affiche que lorsqu'un élément est sélectionné.

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>

Remplacez maintenant les lignes highlightValue/setBackgroundResource de l'étape précédente par cette nouvelle ressource d'arrière-plan 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

Toute personne qui travaille avec un clavier s'attend à pouvoir utiliser des raccourcis avec la touche Ctrl. C'est pourquoi vous allez maintenant ajouter les raccourcis Annuler (Ctrl+Z) et Rétablir (Ctrl+Maj+Z) dans l'application.

Commencez par créer une simple pile d'historique des clics. Supposons qu'un utilisateur ait effectué cinq actions et appuyé deux fois sur Ctrl+Z, de sorte que les actions 4 et 5 se trouvent sur la pile de rétablissement, et les actions 1, 2 et 3 sur la pile d'annulation. Si l'utilisateur appuie à nouveau sur Ctrl+Z, l'action 3 passe de la pile d'annulation à la pile de rétablissement. S'il appuie ensuite sur Ctrl+Maj+Z, l'action 3 repasse de la pile de rétablissement à la pile d'annulation.

9d952ca72a5640d7.png

En haut de votre classe principale, définissez les différentes actions de clic, puis créez les piles à l'aide de 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

Chaque fois qu'un message est envoyé ou qu'un utilisateur clique sur un dinosaure, ajoutez l'action correspondante à la pile d'annulation. Lorsqu'une nouvelle action est effectuée, effacez la pile de rétablissement. Mettez à jour vos écouteurs de clics comme suit :

MainActivity.kt

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

...

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

Procédez maintenant au mappage proprement dit des touches de raccourci. Vous pouvez utiliser dispatchKeyShortcutEvent pour activer la prise en charge des commandes avec la touche Ctrl et, sur Android O ou version ultérieure, des commandes avec les touches Alt et Maj.

MainActivity.kt (dispatchKeyShortcutEvent)

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

Dans le cas présent, nous allons être un petit peu plus pointilleux. Pour insister sur le fait que seul le raccourci Ctrl+Z déclenche le rappel, et nonAlt+Z ouMaj+Z, utilisez hasModifiers. Les opérations de la pile d'annulation sont renseignées ci-dessous.

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)
}

Faites un essai. Le résultat est-il conforme à vos attentes ? Ajoutez maintenant Ctrl+Maj-Z en utilisant OR avec les indicateurs de modification.

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
    }
}

Les utilisateurs partent du principe que lorsqu'ils effectuent un clic droit à l'aide de la souris ou qu'ils appuient deux fois sur un pavé tactile, un menu contextuel s'affiche. Il s'agit, en effet, du comportement standard dans la majorité des interfaces. Avec cette application, l'objectif est de proposer ce menu contextuel pour permettre aux utilisateurs d'envoyer ces images de dinosaures à un ami.

8b8c4a377f5e743b.png

Lors de la création d'un menu contextuel, la fonctionnalité de clic droit est automatiquement incluse. Dans bien des cas, c'est tout ce dont vous avez besoin. Cette configuration se compose de trois parties :

Indiquer à l'interface utilisateur qu'un menu contextuel est disponible dans cette vue

Utilisez registerForContextMenu dans chaque vue pour laquelle vous souhaitez proposer un menu contextuel ; dans ce cas les quatre images.

MainActivity.kt

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

Définir l'apparence du menu contextuel

Créez un menu au format XML contenant toutes les options contextuelles dont vous avez besoin. Pour cela, ajoutez simplement "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>

Ensuite, dans votre classe d'activité principale, ignorez onCreateContextMenu et transmettez le fichier 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)
}

Définir les actions à effectuer lorsqu'un élément spécifique est sélectionné

Pour terminer, définissez l'action à effectuer en ignorant onContextItemSelected. Dans cet exemple, affichez simplement un court message Snackbar pour informer l'utilisateur que l'image a bien été partagée.

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)
    }
}

Faites un essai ! Si vous effectuez un clic droit sur une image, le menu contextuel doit normalement s'afficher.

MainActivity.kt

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

Ajouter un texte d'info-bulle qui s'affiche lorsque le pointeur de la souris passe sur un élément permet aux utilisateurs de comprendre le fonctionnement de votre interface ou de fournir des informations supplémentaires.

17639493329a9d1a.png

Ajoutez des info-bulles pour chacune des images portant le nom d'un dinosaure à l'aide de la méthode 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))

Il peut être utile d'ajouter un effet de retour visuel à certains affichages lorsque vous les survolez à l'aide d'un dispositif de pointage.

Pour ajouter un effet de ce type, utilisez le code ci-dessous. Le bouton Send (Envoyer) devient vert lorsque le pointeur de la souris passe dessus.

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
})

Ajoutez un autre effet de survol : modifiez l'image de fond associée à l'élément TextView déplaçable, afin que l'utilisateur sache que ce texte peut être déplacé.

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
})

Faites un essai ! Normalement, une image représentant une grande main doit s'afficher lorsque vous passez la souris sur le texte "Drag Me!". Cet effet n'est certes pas des plus subtils, mais il présente l'avantage de rendre l'expérience utilisateur plus tactile.

Pour en savoir plus, consultez la documentation View.OnHoverListener et MotionEvent.

Sur un ordinateur de bureau, glisser-déposer des éléments dans une application est une opération naturelle, en particulier depuis le gestionnaire de fichiers de Chrome OS. Au cours de cette étape, vous allez configurer une cible de dépôt pouvant recevoir des fichiers ou des éléments en texte brut. Dans la section suivante de l'atelier de programmation, nous allons implémenter un élément déplaçable.

cfbc5c9d8d28e5c5.gif

Commencez par créer un élément OnDragListener vide. Examinez sa structure avant de commencer à coder :

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
            }
        }
    }
}

La méthode onDrag() est appelée chaque fois que l'un des événements de déplacement se produit : lorsque vous commencez à déplacer un élément, passez la souris sur une zone de dépôt ou déposez un élément. Voici un résumé des différents événements de déplacement :

  • L'événement ACTION_DRAG_STARTED est déclenché lorsqu'un élément est déplacé. Votre cible doit rechercher les éléments valides qu'elle peut recevoir et indiquer visuellement qu'elle est prête à l'emploi.
  • Les événements ACTION_DRAG_ENTERED et ACTION_DRAG_EXITED sont déclenchés lorsqu'un élément est déplacé et qu'il pénètre dans la zone de dépôt ou la quitte. Vous devez fournir un retour visuel pour indiquer à l'utilisateur qu'il peut déposer l'élément.
  • L'événement ACTION_DROP est déclenché lorsque l'élément est déposé. Traitez l'élément à cet endroit.
  • L'événement ACTION_DRAG_ENDED est déclenché lorsque l'action de dépôt est soit effectuée correctement, soit annulée. Rétablissez l'interface utilisateur dans son état normal.

ACTION_DRAG_STARTED

Cet événement est déclenché chaque fois que l'utilisateur commence à déplacer un élément. Indiquez ici si une cible peut recevoir un élément spécifique (valeur renvoyée "true") ou non (valeur renvoyée "false") et informez-en visuellement l'utilisateur. L'événement de déplacement comprend une classe ClipDescription contenant des informations sur l'élément en cours de déplacement.

Pour déterminer si cet écouteur de déplacement peut recevoir un élément, examinez son type MIME. Dans cet exemple, indiquez que la cible est valide en attribuant une teinte vert clair à l'arrière-plan.

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 et ENDED

La logique de retour visuel/haptique se trouve dans les événements ENTERED et EXITED. Dans cet exemple, choisissez un vert plus foncé lorsque l'élément est placé au-dessus de la zone cible, pour indiquer à l'utilisateur qu'il peut le déposer. Dans l'événement ENDED, rétablissez l'interface utilisateur dans son état normal, c'est-à-dire sans opération de déplacement ni de dépôt.

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

Cet événement se produit lorsque l'élément est déposé sur la cible. C'est ici que le traitement s'effectue.

Remarque : Vous devez accéder aux fichiers Chrome OS à l'aide de ContentResolver.

Dans cette démo, la cible peut recevoir un objet en texte brut ou un fichier. Dans le cas de l'objet en texte brut, le texte est affiché dans la vue Texte. S'il s'agit d'un fichier, copiez les 200 premiers caractères et affichez-les.

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

Maintenant que la classe DropTargetListener est configurée, associez-la à la vue dans laquelle vous souhaitez recevoir les éléments déposés.

MainActivity.kt

text_drop.setOnDragListener(DropTargetListener(this))

Faites un essai ! Pour rappel, vous devrez faire glisser des fichiers depuis le gestionnaire de fichiers de Chrome OS. Vous pouvez créer un fichier texte à l'aide de l'éditeur de texte de Chrome OS ou télécharger un fichier image sur Internet.

Configurez maintenant un élément déplaçable dans votre application. En règle générale, un processus de déplacement est déclenché par un appui prolongé sur une vue. Pour signaler qu'un élément peut être déplacé, créez un LongClickListener qui fournit au système les données transférées et indique de quel type il s'agit. C'est également à cet endroit que vous pouvez configurer l'apparence de l'élément lorsqu'il est déplacé.

Configurez un élément de déplacement de texte brut qui extrait une chaîne d'un TextView. Définissez le type MIME du contenu sur ClipDescription.MIMETYPE_TEXT_PLAIN.

Pour proposer une apparence visuelle pendant le déplacement, utilisez l'objet DragShadowBuilder intégré afin d'obtenir un effet de déplacement translucide standard. Pour un exemple plus complexe, consultez la section Starting a Drag (Commencer une action de déplacement) dans la documentation.

N'oubliez pas de définir l'indicateur DRAG_FLAG_GLOBAL pour indiquer que cet élément peut être déplacé dans d'autres applications.

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
    }
}

Ajoutez maintenant l'objet LongClickListener à l'élément TextView déplaçable.

MainActivity.kt (onCreate)

text_drag.setOnLongClickListener(TextViewLongClickListener())

Faites un essai ! Est-il possible de faire glisser le texte à partir de TextView ?

Votre application devrait maintenant être compatible avec la souris et le clavier, sans oublier les dinosaures ! Toutefois, sur un ordinateur de bureau, il est fréquent que les utilisateurs redimensionnent l'application, agrandissent et réduisent la fenêtre, passent en mode tablette et changent l'orientation. Qu'advient-il alors des éléments déposés, du compteur de messages envoyés et du compteur de clics ?

Le cycle de vie d'une activité est un facteur dont il faut tenir compte lorsque l'on crée des applications Android. À mesure que les applications deviennent plus complexes, la gestion des états du cycle de vie peut s'avérer difficile. Heureusement, les composants d'architecture permettent de gérer efficacement les problèmes ayant trait au cycle de vie. Dans cet atelier de programmation, nous allons nous intéresser à ViewModel et LiveData pour conserver l'état de l'application.

ViewModel permet de gérer les données liées à l'interface utilisateur lors des modifications du cycle de vie. LiveData fonctionne comme un observateur pour mettre à jour automatiquement les éléments de l'interface utilisateur.

Prenez en compte les données dont vous souhaitez effectuer le suivi dans cette application :

  • Compteur de messages envoyés (ViewModel, LiveData)
  • Compteur d'images sur lesquelles l'utilisateur a cliqué (ViewModel, LiveData)
  • Texte de la cible de dépôt actuelle (ViewModel, LiveData)
  • Piles d'annulation/de rétablissement (ViewModel)

Examinez le code de la classe ViewModel qui définit cette configuration. Il contient principalement un getter et des setters qui utilisent un modèle 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
    }
}

Dans votre activité principale, procurez-vous la classe ViewModel à l'aide de ViewModelProvider. Cette classe permet de gérer facilement le cycle de vie. Ainsi, les piles d'annulation et de rétablissement conservent automatiquement leur état lors d'un redimensionnement, d'un changement d'orientation et d'une modification de la mise en page.

MainActivity.kt (onCreate)

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

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

Pour les variables LiveData, créez et associez des objets Observer, puis indiquez à l'interface utilisateur comment réagir lors de la modification des variables.

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
})

Une fois ces observateurs en place, le code de tous les rappels de clic peut être simplifié de manière à ne modifier que les données variables de ViewModel.

Le code ci-dessous montre qu'il n'est pas nécessaire de manipuler directement les objets TextView. Tous les éléments de l'interface utilisateur associés à des observateurs LiveData sont mis à jour automatiquement.

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)
    }
}

Pour terminer, mettez à jour les commandes d'annulation et de rétablissement afin d'utiliser ViewModel et LiveData au lieu de manipuler directement l'interface utilisateur.

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")
}

Faites un essai ! Qu'en est-il du redimensionnement ? Êtes-vous tombé sous le charme des composants d'architecture ?

Pour une explication plus détaillée des composants d'architecture, reportez-vous à l'atelier de programmation sur les cycles de vie Android. Cet article de blog constitue une excellente ressource pour comprendre comment fonctionnent et interagissent ViewModel et onSavedInstanceState.

Bravo ! Bien joué ! Vous avez, à présent, une bonne idée des difficultés que rencontrent généralement les développeurs pour optimiser des applications Android pour Chrome OS.

52240dc3e68f7af8.png

Exemple de code source

Clonez le dépôt depuis GitHub

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

… ou téléchargez-le sous la forme d'un fichier ZIP.

Télécharger le fichier ZIP