Plug-ins d'UI pour voitures

Utiliser les plug-ins de la bibliothèque Car UI pour créer des implémentations complètes de composants personnalisations dans la bibliothèque Car UI au lieu d'utiliser des superpositions de ressources d'exécution (RRO). Les RRO vous permettent de ne modifier que les ressources XML de la bibliothèque Car UI ce qui limite les possibilités de personnalisation.

Créer un plug-in

Un plug-in de bibliothèque Car UI est un APK qui contient des classes qui implémentent un ensemble de API de plug-in. Les API de plug-in peuvent être compilées dans un en tant que bibliothèque statique.

Consultez des exemples dans Soong et Gradle:

Søong

Prenons l'exemple de Soong:

android_app {
    name: "my-plugin",

    min_sdk_version: "28",
    target_sdk_version: "30",
    aaptflags: ["--shared-lib"],
    sdk_version: "current",

    manifest: "src/main/AndroidManifest.xml",
    srcs: ["src/main/java/**/*.java"],
    resource_dirs: ["src/main/res"],
    static_libs: [
        "car-ui-lib-oem-apis",
    ],
    // Disable optimization is mandatory to prevent R.java class from being
    // stripped out
    optimize: {
        enabled: false,
    },

    certificate: ":my-plugin-certificate",
}

Gradle

Consultez ce fichier build.gradle:

apply plugin: 'com.android.application'

android {
  compileSdkVersion 30

  defaultConfig {
    minSdkVersion 28
    targetSdkVersion 30
  }

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  signingConfigs {
    debug {
      storeFile file('chassis_upload_key.jks')
      storePassword 'chassis'
      keyAlias 'chassis'
      keyPassword 'chassis'
    }
  }
}

dependencies {
  implementation project(':oem-apis')
  // Or use the following if you'd like to use the maven artifact
  // implementation 'com.android.car.ui:car-ui-lib-plugin-apis:1.0.0'
}

Settings.gradle :

// You can remove the ':oem-apis' if you're using the maven artifact.
include ':oem-apis'
project(':oem-apis').projectDir = new File('./path/to/oem-apis')
include ':my-plugin'
project(':my-plugin').projectDir = new File('./my-plugin')

Le fournisseur de contenu du plug-in doit être déclaré dans son fichier manifeste avec le les attributs suivants:

  android:authorities="com.android.car.ui.plugin"
  android:enabled="true"
  android:exported="true"

android:authorities="com.android.car.ui.plugin" rend le plug-in visible à la bibliothèque Car UI. Le fournisseur doit être exporté pour pouvoir être interrogé de l'environnement d'exécution. De plus, si l'attribut enabled est défini sur false, la valeur par défaut sera utilisée à la place de l'implémentation du plug-in. Le contenu la classe du fournisseur n'a pas besoin d'exister. Dans ce cas, assurez-vous d'ajouter tools:ignore="MissingClass" à la définition du fournisseur. Voir l'exemple du fichier manifeste ci-dessous:

    <application>
        <provider
            android:name="com.android.car.ui.plugin.PluginNameProvider"
            android:authorities="com.android.car.ui.plugin"
            android:enabled="false"
            android:exported="true"
            tools:ignore="MissingClass"/>
    </application>

Enfin, par mesure de sécurité, Signez votre application.

Plug-ins en tant que bibliothèque partagée

Contrairement aux bibliothèques statiques Android qui sont compilées directement dans des applications, Les bibliothèques partagées Android sont compilées dans un APK autonome référencé par d'autres applications au moment de l'exécution.

Les plug-ins implémentés en tant que bibliothèques partagées Android ont des classes est automatiquement ajouté au ClassLoader partagé entre les applications. Lorsqu'une application qui utilise la bibliothèque Car UI spécifie dépendance d'exécution sur la bibliothèque partagée du plug-in, classloader peut accéder aux classes de la bibliothèque partagée du plug-in. Plug-ins implémentés car les applications Android standards (et non une bibliothèque partagée) peuvent avoir un impact négatif sur le froid de l'application les heures de début.

Implémenter et créer des bibliothèques partagées

Le développement avec les bibliothèques partagées Android ressemble beaucoup à celui d'Android standard applications, à quelques différences près.

  • Utiliser la balise library sous la balise application avec le package de plug-in dans le fichier manifeste d'application de votre plug-in:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Configurer votre règle de compilation android_app Soong (Android.bp) avec l'AAPT L'option shared-lib, qui permet de créer une bibliothèque partagée:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Dépendances à des bibliothèques partagées

Pour chaque application du système qui utilise la bibliothèque Car UI, incluez le paramètre Balise uses-library dans le fichier manifeste de l'application sous l'élément application par le nom du package du plug-in:

<manifest>
  <application
      android:name=".MyApp"
      ...>
    <uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
    ...
  </application>
</manifest>

Installer un plug-in

Les plug-ins DOIVENT être préinstallés sur la partition du système en incluant le module dans PRODUCT_PACKAGES. Le pack préinstallé peut être mis à jour de la même manière que : toute autre application installée.

Si vous mettez à jour un plug-in existant sur le système, toutes les applications qui l'utilisent se fermer automatiquement. Une fois rouverts par l'utilisateur, les modifications sont appliquées. Si l'application n'était pas en cours d'exécution, la version mise à jour .

Lorsque vous installez un plug-in avec Android Studio, quelques éléments à prendre en compte. Au moment de la rédaction de ce document, il y a un bug dans Le processus d'installation de l'application Android Studio qui entraîne la mise à jour d'un plug-in de sorte qu'elles ne soient pas prises en compte. Vous pouvez résoudre ce problème en sélectionnant l'option Toujours installer avec le gestionnaire de packages (désactive les optimisations de déploiement sur Android 11 et versions ultérieures). dans la configuration de compilation du plug-in.

De plus, lors de l'installation du plug-in, Android Studio signale une erreur indiquant qu'il impossible de trouver une activité principale à lancer. C'est normal, car le plug-in n'inclut aucune activité (à l'exception de l'intent vide utilisé pour résoudre un intent). À supprimez l'erreur, définissez l'option Launch (Lancer) sur Nothing (Rien) dans le build. configuration.

Configuration du plug-in Android Studio Figure 1 : Configuration du plug-in Android Studio

Plug-in de proxy

Personnalisation de les applications utilisant la bibliothèque Car UI nécessite une RRO qui cible chaque application spécifique à modifier, y compris lorsque les personnalisations sont identiques d'une application à l'autre. Cela signifie qu'une RRO app est requise. Découvrez quelles applications utilisent la bibliothèque Car UI.

Le plug-in de proxy de la bibliothèque Car UI en est un exemple. bibliothèque partagée de plug-in qui délègue ses implémentations de composants à l'instance de la bibliothèque Car UI. Ce plug-in peut être ciblé à l'aide d'une RRO, qui peut être utilisé comme point de personnalisation unique pour les applications qui utilisent la bibliothèque Car UI sans qu'il soit nécessaire d'implémenter un plug-in fonctionnel. Pour en savoir plus sur Pour les RRO, consultez la section Modifier la valeur des ressources d'une application à environnement d'exécution.

Le plug-in proxy n'est qu'un exemple et un point de départ pour la personnalisation à l'aide de un plug-in. Pour une personnalisation au-delà des RRO, il est possible d'implémenter un sous-ensemble de plug-ins et utilisez le plug-in proxy pour le reste, ou implémentez tous les plug-ins les composants à partir de zéro.

Bien que le plug-in proxy fournisse un point unique de personnalisation de la RRO pour les applications, les applications qui choisissent de ne pas utiliser le plug-in nécessiteront tout de même une RRO qui cible l'application elle-même.

Implémenter les API de plug-in

Le point d'entrée principal du plug-in est com.android.car.ui.plugin.PluginVersionProviderImpl. Tous les plug-ins doivent inclure une classe avec ce nom exact et ce nom de package. Cette classe doit comporter un élément constructeur par défaut et implémenter l'interface PluginVersionProviderOEMV1.

Les plug-ins CarUi doivent fonctionner avec des applications plus anciennes ou plus récentes que le plug-in. À Pour ce faire, toutes les API de plug-in sont gérées par version avec V# à la fin de leur classname. Si une nouvelle version de la bibliothèque Car UI est publiée avec de nouvelles fonctionnalités, ils font partie de la version V2 du composant. La bibliothèque Car UI fait de faire fonctionner les nouvelles fonctionnalités dans le cadre d'un composant de plug-in plus ancien. Par exemple, vous pouvez convertir un nouveau type de bouton de la barre d'outils en MenuItems.

Toutefois, une application avec une ancienne version de la bibliothèque Car UI ne peut pas s'adapter à une nouvelle le plug-in écrit sur des API plus récentes. Pour résoudre ce problème, les plug-ins renvoyer différentes implémentations d'elles-mêmes en fonction de la version de l'API OEM ; pris en charge par les applications.

PluginVersionProviderOEMV1 contient une méthode:

Object getPluginFactory(int maxVersion, Context context, String packageName);

Cette méthode renvoie un objet qui implémente la version la plus élevée PluginFactoryOEMV# pris en charge par le plug-in, tout en étant inférieur à ou égal à maxVersion. Si un plug-in ne dispose pas d'une implémentation PluginFactory à cette ancienne valeur, il peut renvoyer null, auquel cas l'opérateur statique- une implémentation liée des composants CarUi.

Pour assurer la rétrocompatibilité avec les applications compilées d'anciennes versions de la bibliothèque Car UI statique, nous vous recommandons de prendre en charge maxVersion de 2, 5 ou plus à partir de l'implémentation de votre plug-in de la classe PluginVersionProvider. Les versions 1, 3 et 4 ne sont pas compatibles. Pour Pour en savoir plus, consultez PluginVersionProviderImpl

PluginFactory est l'interface qui crée toutes les autres CarUi composants. Elle définit également la version de leurs interfaces à utiliser. Si le plug-in ne cherche pas à implémenter ces composants, il peut renvoyer null dans leur fonction de création (à l'exception de la barre d'outils, qui contient une fonction customizesBaseLayout() distincte).

Le pluginFactory limite les versions des composants CarUi pouvant être utilisées ensemble. Par exemple, aucun pluginFactory ne pourra créer la version 100 d'une Toolbar et la version 1 d'une RecyclerView, car ne garantit pas qu'une grande variété de versions de composants fonctionnent ensemble. Pour utiliser la version 100 de la barre d'outils, les développeurs doivent : fournir une implémentation d'une version de pluginFactory qui crée un la version 100 de la barre d'outils, ce qui limite les options des versions d'autres composants qui peuvent être créés. Il est possible que les versions des autres composants ne soient pas égal à. Par exemple, un pluginFactoryOEMV100 peut créer ToolbarControllerOEMV100 et RecyclerViewOEMV70.

Barre d'outils

Mise en page de base

La barre d'outils et la "mise en page de base" sont étroitement liés, d'où la fonction qui crée la barre d'outils s'appelle installBaseLayoutAround. La mise en page de base est un concept qui permet de positionner la barre d'outils n'importe où autour du contenu, pour permettre l'ajout d'une barre d'outils en haut/bas de l'application, verticalement sur les côtés, ou même une barre d'outils circulaire englobant l'ensemble de l'application. C'est obtenu en transmettant une vue à installBaseLayoutAround pour la barre d'outils/la base mise en page à envelopper.

Le plug-in doit utiliser la vue fournie, la dissocier de son parent et gonfler la mise en page du plug-in dans le même index que le parent et avec la même LayoutParams comme vue qui vient d'être dissociée, puis réassocier la vue quelque part dans la mise en page qui a été simplement gonflé. La mise en page gonflée contiennent la barre d'outils, si l'application le demande.

L'application peut demander une mise en page de base sans barre d'outils. Si c’est le cas, installBaseLayoutAround doit renvoyer la valeur "null". Pour la plupart des plug-ins, c'est tout ce que doit être appliqué, mais si l'auteur du plug-in souhaite l'appliquer (par exemple, une décoration autour du bord de l'application, cela peut toujours être fait avec une mise en page de base. Ces les décorations sont particulièrement utiles pour les appareils avec des écrans non rectangulaires, comme il peut pousser l'application dans un espace rectangulaire et ajouter des transitions propres dans l'espace non rectangulaire.

installBaseLayoutAround reçoit également un Consumer<InsetsOEMV1>. Ce consommateur peut être utilisé pour indiquer à l'application que le plug-in est partiellement recouvrir le contenu de l'application (avec la barre d'outils ou autre). L'application va vous savez qu'il faut continuer à dessiner dans cet espace, mais que les utilisateurs doivent pouvoir interagir des composants. Cet effet est utilisé dans notre conception de référence, pour rendre semi-transparente et faire défiler les listes en dessous. Si cette fonctionnalité était n'est pas implémenté, le premier élément d'une liste reste bloqué sous la barre d'outils. et non cliquables. Si cet effet n'est pas nécessaire, le plug-in peut ignorer les Consommateur :

Contenu défilant sous la barre d&#39;outils Figure 2 : Contenu défilant sous la barre d'outils

Du point de vue de l'application, lorsque le plug-in envoie de nouveaux encarts, il reçoit de toutes les activités ou tous les fragments qui implémentent InsetsChangedListener. Si une activité ou un fragment n'implémente pas InsetsChangedListener, l'UI Car. gère les encarts par défaut en les appliquant comme remplissage à la Activity ou FragmentActivity contenant le fragment. La bibliothèque n'a pas appliquer les encarts par défaut aux fragments. Voici un exemple d'extrait de code qui applique les encarts en tant que marge intérieure à un RecyclerView dans application:

public class MainActivity extends Activity implements InsetsChangedListener {
  @Override
  public void onCarUiInsetsChanged(Insets insets) {
    CarUiRecyclerView rv = requireViewById(R.id.recyclerview);
    rv.setPadding(insets.getLeft(), insets.getTop(),
                  insets.getRight(), insets.getBottom());
  }
}

Enfin, le plug-in reçoit une suggestion fullscreen, qui permet d'indiquer si la vue à encapsuler occupe toute l'application ou seulement une petite section. Cela permet d'éviter d'appliquer des décorations sur le bord qui n'ont de sens que si elles apparaissent sur le bord de l'ensemble de l'écran. Exemple qui utilise des mises en page de base qui ne sont pas en plein écran est "Paramètres", dans lequel chaque volet du la mise en page à double volet possède sa propre barre d'outils.

Comme installBaseLayoutAround doit renvoyer une valeur nulle lorsque toolbarEnabled est false, pour que le plug-in indique qu'il n'a pas que vous souhaitez personnaliser la mise en page de base, elle doit renvoyer false à partir de customizesBaseLayout

La mise en page de base doit contenir un élément FocusParkingView et un élément FocusArea pour que sont compatibles avec les commandes par dispositif rotatif. Ces vues peuvent être omises sur les appareils ne sont pas compatibles avec le dispositif rotatif. Les FocusParkingView/FocusAreas sont implémentés dans le bibliothèque CarUi statique. Ainsi, un setRotaryFactories est utilisé pour fournir des fabriques à créer des vues à partir de contextes.

Les contextes utilisés pour créer des vues de focus doivent être le contexte source, et non les le contexte du plug-in. FocusParkingView doit être le plus proche de la première vue. dans l'arborescence aussi raisonnablement que possible, car c'est ce qui est axé ne doit pas être visible par l'utilisateur. L'élément FocusArea doit encapsuler la barre d'outils dans le de base pour indiquer qu'il s'agit d'une zone de déplacement par rotation. Si FocusArea n'est pas l'utilisateur ne peut accéder à aucun bouton de la barre d'outils à l'aide du bouton à un contrôleur rotatif.

Contrôleur de barre d'outils

La valeur ToolbarController réelle renvoyée devrait être beaucoup plus simple à que la mise en page de base. Son rôle est de récupérer les informations transmises des setters et de l'afficher dans la mise en page de base. Consultez la documentation Javadoc pour en savoir plus la plupart des méthodes. Certaines des méthodes les plus complexes sont décrites ci-dessous.

getImeSearchInterface permet d'afficher les résultats de recherche dans l'IME (clavier) fenêtre. Cela peut être utile pour afficher/animer des résultats de recherche à côté de clavier, par exemple s'il n'occupait que la moitié de l'écran. La plupart des la fonctionnalité est implémentée dans la bibliothèque CarUi statique, la fonction de recherche du plug-in ne fournit que des méthodes permettant à la bibliothèque statique d'obtenir Rappels TextView et onPrivateIMECommand Pour ce faire, le plug-in doit utiliser une sous-classe TextView qui remplace onPrivateIMECommand et transmet l'appel de l'écouteur fourni en tant que TextView de sa barre de recherche.

setMenuItems affiche simplement les éléments MenuItems à l'écran, mais ils sont appelés étonnamment souvent. L'API du plug-in pour MenuItems étant immuable, chaque fois qu'un MenuItem a été modifié, un nouvel appel setMenuItems va avoir lieu. Cela pourrait se produire pour quelque chose d'aussi simple qu'un utilisateur qui a cliqué sur un bouton "Changer" a déclenché l'activation/la désactivation du bouton. Pour des raisons de performances et d'animation, nous vous conseillons de calculer la différence entre l'ancienne et la nouvelle MenuItems liste et ne met à jour que les vues qui ont réellement changé. MenuItems Fournissez un champ key qui peut vous aider, car la clé doit être la même entre différents appels à setMenuItems pour le même MenuItem.

Vue AppStyledView

AppStyledView est un conteneur pour une vue qui n'est pas personnalisée du tout. Il peut être utilisée pour ajouter une bordure autour de cette vue qui la différencie le reste de l'application, et d'indiquer à l'utilisateur qu'il s'agit d'un autre type de commande. La vue encapsulée par l'AppStyledView est fournie dans setContent AppStyledView peut également comporter un bouton "Retour" ou "Fermer" comme suit : demandé par l'application.

AppStyledView n'insère pas immédiatement ses vues dans la hiérarchie des vues. comme le fait installBaseLayoutAround, il renvoie simplement l'affichage bibliothèque statique via getView, qui effectue ensuite l'insertion. La position et la taille de AppStyledView peut également être contrôlée en implémentant getDialogWindowLayoutParam

Contexts (Contextes)

Le plug-in doit faire attention lorsque vous utilisez des contextes, car il existe à la fois des plug-ins et "source" différents contextes. Le contexte du plug-in est fourni en tant qu'argument à getPluginFactory. Il s'agit du seul contexte présentant le paramètre et les ressources du plug-in. Cela signifie que c'est le seul contexte qui peut être utilisé pour gonfler les mises en page dans le plug-in.

Toutefois, il est possible que le contexte du plug-in ne soit pas correctement configuré. À obtenir la configuration correcte, nous fournissons des contextes sources dans des méthodes qui créent composants. Le contexte source est généralement une activité, mais dans certains cas, ou un service ou un autre composant Android. Pour utiliser la configuration le contexte source avec les ressources du contexte du plug-in, un nouveau contexte doit être créé à l'aide de createConfigurationContext. Si la configuration correcte n'est pas le mode strict d'Android ne respecte pas les règles, et les vues gonflées peuvent ne sont pas aux bonnes dimensions.

Context layoutInflationContext = pluginContext.createConfigurationContext(
        sourceContext.getResources().getConfiguration());

Changements de mode

Certains plug-ins acceptent plusieurs modes pour leurs composants, par exemple un mode Sport ou un mode Éco qui se distinguent visuellement. Il n'y a aucun la prise en charge intégrée de cette fonctionnalité dans CarUi, mais rien ne s'arrête au plug-in de l'implémenter entièrement en interne. Le plug-in peut surveiller les conditions qu'il souhaite déterminer quand il doit changer de mode, comme à écouter les annonces. Le plug-in ne peut pas déclencher de modification de configuration de changer de mode, mais il n'est pas recommandé de compter sur les modifications de configuration car la mise à jour manuelle de l'apparence de chaque composant est plus fluide pour l'utilisateur et permet des transitions qui ne sont pas possibles avec les modifications de configuration.

Jetpack Compose

Les plug-ins peuvent être implémentés à l'aide de Jetpack Compose, mais il s'agit d'une version alpha. et ne doivent pas être considérées comme stables.

Les plug-ins peuvent utiliser ComposeView pour créer une surface compatible avec Compose dans laquelle effectuer le rendu. Ce ComposeView serait ce qui est renvoyé à l'application par la méthode getView dans les composants.

L'un des principaux problèmes liés à l'utilisation de ComposeView est qu'il définit des balises sur la vue racine. dans la mise en page afin de stocker les variables globales différentes ComposeViews dans la hiérarchie. Étant donné que les ID de ressource du plug-in séparément de celui de l'application, cela peut entraîner des conflits lorsque les deux l'application et le plug-in définissent des balises dans la même vue. Une configuration personnalisée ComposeViewWithLifecycle, qui déplace ces variables globales vers le bas ComposeView est indiqué ci-dessous. Là encore, cela ne doit pas être considéré comme stable.

ComposeViewWithLifecycle :

class ComposeViewWithLifecycle @JvmOverloads constructor(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
    LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {

  private val lifeCycle = LifecycleRegistry(this)
  private val modelStore = ViewModelStore()
  private val savedStateRegistryController = SavedStateRegistryController.create(this)
  private var composeView: ComposeView? = null
  private var content = @Composable {}

  init {
    ViewTreeLifecycleOwner.set(this, this)
    ViewTreeViewModelStoreOwner.set(this, this)
    ViewTreeSavedStateRegistryOwner.set(this, this)
    compositionContext = createCompositionContext()
  }

  fun setContent(content: @Composable () -> Unit) {
    this.content = content
    composeView?.setContent(content)
  }

  override fun getLifecycle(): Lifecycle {
    return lifeCycle
  }

  override fun getViewModelStore(): ViewModelStore {
    return modelStore
  }

  override fun getSavedStateRegistry(): SavedStateRegistry {
    return savedStateRegistryController.savedStateRegistry
  }

  override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    savedStateRegistryController.performRestore(Bundle())
    lifeCycle.currentState = Lifecycle.State.RESUMED
    composeView = ComposeView(context)
    composeView?.setContent(content)
    addView(composeView, LayoutParams(
      LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
  }

  override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    lifeCycle.currentState = Lifecycle.State.DESTROYED
    modelStore.clear()
    removeAllViews()
    composeView = null
  }

  // Exact copy of View.createCompositionContext() in androidx's WindowRecomposer.android.kt
  private fun createCompositionContext(): CompositionContext {
    val currentThreadContext = AndroidUiDispatcher.CurrentThread
    val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
      PausableMonotonicFrameClock(it).apply { pause() }
    }
    val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
    val recomposer = Recomposer(contextWithClock)
    val runRecomposeScope = CoroutineScope(contextWithClock)
    val viewTreeLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
      "ViewTreeLifecycleOwner not found from $this"
    }
    viewTreeLifecycleOwner.lifecycle.addObserver(
      LifecycleEventObserver { _, event ->
        @Suppress("NON_EXHAUSTIVE_WHEN")
        when (event) {
          Lifecycle.Event.ON_CREATE ->
            // Undispatched launch since we've configured this scope
            // to be on the UI thread
            runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
              recomposer.runRecomposeAndApplyChanges()
            }
          Lifecycle.Event.ON_START -> pausableClock?.resume()
          Lifecycle.Event.ON_STOP -> pausableClock?.pause()
          Lifecycle.Event.ON_DESTROY -> {
            recomposer.cancel()
          }
        }
      }
    )
    return recomposer
  }

//  TODO: ComposeViewWithLifecycle should handle saving state and other lifecycle things
//  override fun onSaveInstanceState(): Parcelable? {
//    val superState = super.onSaveInstanceState()
//    val bundle = Bundle()
//    savedStateRegistryController.performSave(bundle)
//  }
}