Utilizza i plug-in della libreria dell'interfaccia utente dell'auto per creare implementazioni complete delle personalizzazioni dei componenti nella libreria dell'interfaccia utente dell'auto anziché utilizzare overlay di risorse di runtime (RRO). Gli RRO ti consentono di modificare solo le risorse XML dei componenti della libreria dell'interfaccia utente dell'auto, il che limita l'entità della personalizzazione.
Crea un plug-in
Un plug-in della libreria dell'interfaccia utente dell'auto è un APK che contiene classi che implementano un insieme di API plug-in. Le API dei plug-in possono essere compilate in un plug-in come libreria statica.
Vedi esempi in Soong e Gradle:
Soong
Considera questo esempio di 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
Consulta questo file 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')
Il plug-in deve avere un content provider dichiarato nel manifest con i seguenti attributi:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin"
rende il plug-in rilevabile
dalla libreria dell'interfaccia utente dell'auto. Il fornitore deve essere esportato in modo che possa essere interrogato in fase di runtime. Inoltre, se l'attributo enabled
è impostato su false
, verrà utilizzata l'implementazione predefinita anziché quella del plug-in. La classe del provider di contenuti non deve esistere. In questo caso, assicurati di aggiungere
tools:ignore="MissingClass"
alla definizione del fornitore. Vedi la voce del manifest
di esempio di seguito:
<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>
Infine, come misura di sicurezza, firma la tua app.
Plugin come libreria condivisa
A differenza delle librerie statiche Android, che vengono compilate direttamente nelle app, le librerie condivise Android vengono compilate in un APK autonomo a cui fanno riferimento altre app in fase di runtime.
Le classi dei plug-in implementati come libreria condivisa Android vengono aggiunte automaticamente al class loader condiviso tra le app. Quando un'app che utilizza la libreria dell'interfaccia utente dell'auto specifica una dipendenza di runtime dalla libreria condivisa del plug-in, il suo class loader può accedere alle classi della libreria condivisa del plug-in. I plug-in implementati come normali app per Android (non una libreria condivisa) possono influire negativamente sui tempi di avvio a freddo delle app.
Implementare e creare librerie condivise
Lo sviluppo con le librerie condivise Android è molto simile a quello delle normali app Android, con alcune differenze fondamentali.
- Utilizza il tag
library
sotto il tagapplication
con il nome del pacchetto del plug-in nel file manifest dell'app del plug-in:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- Configura la regola di build di Soong
android_app
(Android.bp
) con il flag AAPTshared-lib
, che viene utilizzato per creare una libreria condivisa:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
Dipendenze dalle librerie condivise
Per ogni app sul sistema che utilizza la libreria dell'interfaccia utente dell'auto, includi il tag
uses-library
nel manifest dell'app sotto il tag
application
con il nome del pacchetto del plug-in:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
Installare un plug-in
I plug-in DEVONO essere preinstallati nella partizione di sistema includendo il modulo
in PRODUCT_PACKAGES
. Il pacchetto preinstallato può essere aggiornato in modo simile a
qualsiasi altra app installata.
Se aggiorni un plug-in esistente sul sistema, tutte le app che lo utilizzano vengono chiuse automaticamente. Una volta riaperto dall'utente, il file conterrà le modifiche aggiornate. Se l'app non era in esecuzione, la volta successiva che viene avviata avrà il plug-in aggiornato.
Quando installi un plug-in con Android Studio, ci sono alcune considerazioni aggiuntive da tenere in considerazione. Al momento della stesura, esiste un bug nel processo di installazione dell'app Android Studio che impedisce l'applicazione degli aggiornamenti a un plug-in. Questo problema può essere risolto selezionando l'opzione Installa sempre con il gestore pacchetti (disattiva le ottimizzazioni della distribuzione su Android 11 e versioni successive) nella configurazione di build del plug-in.
Inoltre, durante l'installazione del plug-in, Android Studio segnala un errore che non riesce a trovare un'attività principale da avviare. Questo è previsto, in quanto il plug-in non ha attività (tranne l'intent vuoto utilizzato per risolvere un intent). Per eliminare l'errore, imposta l'opzione Avvia su Niente nella configurazione della build.
Figura 1. Configurazione del plug-in Android Studio
Plug-in proxy
La personalizzazione delle app che utilizzano la libreria Car UI richiede un RRO che abbia come target ogni app specifica da modificare, anche quando le personalizzazioni sono identiche tra le app. Ciò significa che è necessario un RRO per app. Scopri quali app utilizzano la libreria dell'interfaccia utente dell'auto.
Il plug-in proxy della libreria dell'interfaccia utente dell'auto è un esempio di libreria condivisa di plug-in che delega le implementazioni dei componenti alla versione statica della libreria dell'interfaccia utente dell'auto. Questo plug-in può essere scelto come target con un RRO, che può essere utilizzato come unico punto di personalizzazione per le app che utilizzano la libreria dell'interfaccia utente dell'auto senza la necessità di implementare un plug-in funzionale. Per ulteriori informazioni sulle RRO, vedi Modificare il valore delle risorse di un'app in fase di runtime.
Il plug-in proxy è solo un esempio e un punto di partenza per eseguire la personalizzazione utilizzando un plug-in. Per la personalizzazione oltre alle RRO, è possibile implementare un sottoinsieme di componenti del plug-in e utilizzare il plug-in proxy per il resto oppure implementare tutti i componenti del plug-in completamente da zero.
Anche se il plug-in proxy fornisce un unico punto di personalizzazione RRO per le app, le app che disattivano l'utilizzo del plug-in richiedono comunque un RRO che miri direttamente all'app stessa.
Implementa le API del plug-in
Il punto di ingresso principale del plug-in è la classe
com.android.car.ui.plugin.PluginVersionProviderImpl
. Tutti i plug-in devono
includere una classe con questo nome e nome del pacchetto esatti. Questa classe deve avere un costruttore predefinito e implementare l'interfaccia PluginVersionProviderOEMV1
.
I plug-in CarUi devono funzionare con app precedenti o più recenti del plug-in. Per
facilitare questa operazione, tutte le API dei plug-in sono versionate con un V#
alla fine del nome
della classe. Se viene rilasciata una nuova versione della libreria Car UI con nuove funzionalità,
queste fanno parte della versione V2
del componente. La libreria dell'interfaccia utente dell'auto fa del suo meglio per far funzionare le nuove funzionalità nell'ambito di un componente plug-in precedente.
Ad esempio, convertendo un nuovo tipo di pulsante nella barra degli strumenti in MenuItems
.
Tuttavia, un'app con una versione precedente della libreria dell'interfaccia utente dell'auto non può adattarsi a un nuovo plug-in scritto in base ad API più recenti. Per risolvere questo problema, consentiamo ai plug-in di restituire implementazioni diverse di se stessi in base alla versione dell'API OEM supportata dalle app.
PluginVersionProviderOEMV1
contiene un metodo:
Object getPluginFactory(int maxVersion, Context context, String packageName);
Questo metodo restituisce un oggetto che implementa la versione più recente di
PluginFactoryOEMV#
supportata dal plug-in, pur essendo inferiore o
uguale a maxVersion
. Se un plug-in non ha un'implementazione di un PluginFactory
così vecchio, potrebbe restituire null
, nel qual caso vengono utilizzate le implementazioni collegate staticamente dei componenti CarUi.
Per mantenere la compatibilità con le versioni precedenti delle app compilate in base a versioni precedenti della libreria Car Ui statica, ti consigliamo di supportare maxVersion
di 2, 5 e versioni successive dall'implementazione del plug-in della classe PluginVersionProvider
. Le versioni 1, 3 e 4 non sono supportate. Per
maggiori informazioni, vedi
PluginVersionProviderImpl
.
PluginFactory
è l'interfaccia che crea tutti gli altri componenti CarUi. Definisce anche la versione delle interfacce da utilizzare. Se
il plug-in non tenta di implementare nessuno di questi componenti, può restituire
null
nella funzione di creazione (ad eccezione della barra degli strumenti, che ha
una funzione customizesBaseLayout()
separata).
pluginFactory
limita le versioni dei componenti CarUi che possono essere utilizzate
insieme. Ad esempio, non esisterà mai un pluginFactory
in grado di creare
la versione 100 di un Toolbar
e anche la versione 1 di un RecyclerView
, in quanto
non ci sarebbe alcuna garanzia che una vasta gamma di versioni dei componenti
funzionino insieme. Per utilizzare la versione 100 della barra degli strumenti, gli sviluppatori devono
fornire un'implementazione di una versione di pluginFactory
che crei una
versione 100 della barra degli strumenti, che limiti poi le opzioni delle versioni di altri
componenti che possono essere creati. Le versioni di altri componenti potrebbero non essere
uguali, ad esempio un pluginFactoryOEMV100
potrebbe creare un
ToolbarControllerOEMV100
e un RecyclerViewOEMV70
.
Barra degli strumenti
Layout di base
La barra degli strumenti e il "layout di base" sono strettamente correlati, per questo motivo la funzione
che crea la barra degli strumenti si chiama installBaseLayoutAround
. Il
layout di base
è un concetto che consente di posizionare la barra degli strumenti ovunque intorno ai contenuti dell'app, per consentire una barra degli strumenti nella parte superiore/inferiore dell'app, verticalmente lungo i lati o persino una barra degli strumenti circolare che racchiude l'intera app. Ciò si ottiene passando una visualizzazione a installBaseLayoutAround
per la barra degli strumenti/il layout di base da racchiudere.
Il plug-in deve prendere la visualizzazione fornita, staccarla dal relativo elemento principale, gonfiare
il proprio layout nello stesso indice dell'elemento principale e con lo stesso
LayoutParams
della visualizzazione appena staccata, quindi ricollegare la visualizzazione
in un punto qualsiasi all'interno del layout appena gonfiato. Il layout espanso
conterrà la barra degli strumenti, se richiesta dall'app.
L'app può richiedere un layout di base senza una barra degli strumenti. In caso contrario,
installBaseLayoutAround
deve restituire null. Per la maggior parte dei plug-in, questo è tutto ciò che
deve essere fatto, ma se l'autore del plug-in vuole applicare, ad esempio, una decorazione
sul bordo dell'app, può comunque farlo con un layout di base. Queste
decorazioni sono particolarmente utili per i dispositivi con schermi non rettangolari, in quanto
possono spostare l'app in uno spazio rettangolare e aggiungere transizioni pulite nello
spazio non rettangolare.
installBaseLayoutAround
viene passato anche un Consumer<InsetsOEMV1>
. Questo
consumer può essere utilizzato per comunicare all'app che il plug-in copre parzialmente
i contenuti dell'app (con la barra degli strumenti o in altro modo). L'app saprà
di continuare a disegnare in questo spazio, ma di tenere fuori i componenti
critici con cui l'utente può interagire. Questo effetto viene utilizzato nel nostro design di riferimento per rendere la
barra degli strumenti semitrasparente e per far scorrere gli elenchi sotto di essa. Se questa funzionalità non fosse stata implementata, il primo elemento di un elenco rimarrebbe bloccato sotto la barra degli strumenti e non sarebbe cliccabile. Se questo effetto non è necessario, il plug-in può ignorare
Consumer.
Figura 2. Scorrimento dei contenuti sotto la barra degli strumenti
Dal punto di vista dell'app, quando il plug-in invia nuovi inserti, li riceve
da qualsiasi attività o frammento che implementa InsetsChangedListener
. Se
un'attività o un fragment non implementa InsetsChangedListener
, la libreria Car Ui
gestirà gli inset per impostazione predefinita applicandoli come spaziatura interna al
Activity
o al FragmentActivity
contenente il fragment. Per impostazione predefinita, la libreria non
applica gli inset ai fragment. Ecco un esempio di snippet di un'implementazione che applica gli inserti come spaziatura interna a un RecyclerView
nell'app:
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());
}
}
Infine, al plug-in viene fornito un suggerimento fullscreen
, che viene utilizzato per indicare se
la visualizzazione da racchiudere occupa l'intera app o solo una piccola sezione.
Questa opzione può essere utilizzata per evitare di applicare alcune decorazioni lungo il bordo che
hanno senso solo se appaiono lungo il bordo dell'intero schermo. Un'app di esempio
che utilizza layout di base non a schermo intero è Impostazioni, in cui ogni riquadro del
layout a due riquadri ha la propria barra degli strumenti.
Poiché è previsto che installBaseLayoutAround
restituisca un valore null quando
toolbarEnabled
è false
, affinché il plug-in indichi che non
vuole personalizzare il layout di base, deve restituire false
da
customizesBaseLayout
.
Il layout di base deve contenere un FocusParkingView
e un FocusArea
per supportare completamente i controlli rotativi. Queste visualizzazioni possono essere omesse sui dispositivi che
non supportano la rotazione. FocusParkingView/FocusAreas
sono implementati nella libreria statica CarUi, quindi viene utilizzato un setRotaryFactories
per fornire factory per creare le visualizzazioni dai contesti.
I contesti utilizzati per creare le visualizzazioni Focus devono essere il contesto di origine, non il
contesto del plug-in. L'elemento FocusParkingView
deve essere il più vicino possibile alla prima visualizzazione
nell'albero, in quanto è l'elemento su cui viene messo a fuoco quando non deve
essere visibile alcun focus all'utente. FocusArea
deve racchiudere la barra degli strumenti nel layout di base per indicare che si tratta di una zona di notifica rotativa. Se FocusArea
non è
fornito, l'utente non può navigare tra i pulsanti della barra degli strumenti con il
controller rotativo.
Controller della barra degli strumenti
L'ToolbarController
effettivo restituito dovrebbe essere molto più semplice da
implementare rispetto al layout di base. Il suo compito è prendere le informazioni passate ai relativi
setter e visualizzarle nel layout di base. Consulta la documentazione Javadoc per informazioni sulla maggior parte dei metodi. Di seguito sono descritti alcuni dei metodi più complessi.
getImeSearchInterface
viene utilizzato per mostrare i risultati di ricerca nella finestra IME (tastiera). Ciò può essere utile per visualizzare/animare i risultati di ricerca accanto alla
tastiera, ad esempio se la tastiera occupa solo metà dello schermo. La maggior parte delle funzionalità è implementata nella libreria statica CarUi. L'interfaccia di ricerca nel plug-in fornisce solo metodi per la libreria statica per ottenere i callback TextView
e onPrivateIMECommand
. Per supportare questa funzionalità, il plug-in
deve utilizzare una sottoclasse TextView
che esegue l'override di onPrivateIMECommand
e passa
la chiamata al listener fornito come TextView
della barra di ricerca.
setMenuItems
visualizza semplicemente MenuItems sullo schermo, ma verrà chiamato
sorprendentemente spesso. Poiché l'API plug-in per MenuItems è immutabile, ogni volta che un
MenuItem viene modificato, viene eseguita una nuova chiamata setMenuItems
. Ciò potrebbe
accadere per un'azione banale come il clic di un utente su un elemento di menu di un interruttore, che
ha causato l'attivazione/disattivazione dell'interruttore. Per motivi di prestazioni e animazione, pertanto, è consigliabile calcolare la differenza tra l'elenco di MenuItem precedente e quello nuovo e aggiornare solo le visualizzazioni effettivamente modificate. MenuItems
fornisce un campo key
che può essere utile a questo scopo, in quanto la chiave deve essere la stessa
in diverse chiamate a setMenuItems
per lo stesso MenuItem.
AppStyledView
AppStyledView
è un contenitore per una visualizzazione non personalizzata. Può
essere utilizzato per fornire un bordo intorno a questa visualizzazione che la distingue dal
resto dell'app e indicare all'utente che si tratta di un tipo diverso di
interfaccia. La visualizzazione racchiusa da AppStyledView è fornita in
setContent
. La AppStyledView
può anche avere un pulsante Indietro o Chiudi come
richiesto dall'app.
AppStyledView
non inserisce immediatamente le sue visualizzazioni nella gerarchia delle visualizzazioni
come fa installBaseLayoutAround
, ma restituisce la sua visualizzazione alla
libreria statica tramite getView
, che poi esegue l'inserimento. La posizione e le dimensioni di AppStyledView
possono essere controllate anche implementando getDialogWindowLayoutParam
.
Contesti
Il plug-in deve prestare attenzione quando utilizza i contesti, in quanto esistono contesti plug-in e "origine". Il contesto del plug-in viene fornito come argomento a
getPluginFactory
ed è l'unico contesto che contiene le
risorse del plug-in. Ciò significa che è l'unico contesto che può essere utilizzato per
aumentare i layout nel plug-in.
Tuttavia, il contesto del plug-in potrebbe non avere la configurazione corretta. Per
ottenere la configurazione corretta, forniamo contesti di origine nei metodi che creano
componenti. Il contesto di origine è in genere un'attività, ma in alcuni casi può
essere anche un servizio o un altro componente Android. Per utilizzare la configurazione del contesto di origine con le risorse del contesto del plug-in, è necessario creare un nuovo contesto utilizzando createConfigurationContext
. Se non viene utilizzata la configurazione corretta, si verificherà una violazione della modalità rigorosa di Android e le visualizzazioni aumentate potrebbero non avere le dimensioni corrette.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
Modifiche alla modalità
Alcuni plug-in possono supportare più modalità per i loro componenti, ad esempio una modalità sport e una modalità eco che hanno un aspetto visivamente distinto. Non esiste un supporto integrato per questa funzionalità in CarUi, ma nulla impedisce al plug-in di implementarla internamente. Il plug-in può monitorare qualsiasi condizione per capire quando cambiare modalità, ad esempio l'ascolto di trasmissioni. Il plug-in non può attivare una modifica della configurazione per cambiare modalità, ma non è consigliabile fare affidamento sulle modifiche della configurazione in ogni caso, poiché l'aggiornamento manuale dell'aspetto di ogni componente è più fluido per l'utente e consente anche transizioni non possibili con le modifiche della configurazione.
Jetpack Compose
I plug-in possono essere implementati utilizzando Jetpack Compose, ma questa è una funzionalità di livello alpha e non deve essere considerata stabile.
I plug-in possono utilizzare
ComposeView
per creare una superficie compatibile con Compose in cui eseguire il rendering. Questo ComposeView
sarebbe
ciò che viene restituito all'app dal metodo getView
nei componenti.
Un problema importante dell'utilizzo di ComposeView
è che imposta i tag nella visualizzazione principale
nel layout per archiviare le variabili globali condivise tra
diverse ComposeView nella gerarchia. Poiché gli ID risorsa del plug-in non sono
spazi dei nomi separati da quelli dell'app, ciò potrebbe causare conflitti quando sia l'app sia il plug-in impostano tag sulla stessa vista. Di seguito è riportato un
ComposeViewWithLifecycle
personalizzato che sposta queste variabili globali in
ComposeView
. Anche in questo caso, non deve essere considerato stabile.
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)
// }
}