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 baliseapplication
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'optionshared-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.
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 :
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)
// }
}