Utilizza i plugin della libreria dell'interfaccia utente dell'auto per creare implementazioni complete del componente personalizzazioni nella libreria di UI dell'auto anziché utilizzare gli overlay delle risorse di runtime (RRO). Gli RRO ti consentono di modificare solo le risorse XML della libreria dell'UI dell'auto il che limita la possibilità di personalizzazione.
Creare un plug-in
Un plug-in della libreria dell'interfaccia utente dell'auto è un APK che contiene classi che implementano un set di API dei plug-in. Le API dei plug-in possono essere compilate in come libreria statica.
Vedi esempi in Presto e Gradle:
Presto
Considera questo esempio di Presto:
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
Visualizza 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')
Nel file manifest del plug-in deve essere dichiarato un fornitore di contenuti che includa il parametro 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
alla libreria dell'UI dell'auto. È necessario esportare il provider in modo da poter eseguire query su
runtime. Inoltre, se l'attributo enabled
è impostato su false
il valore predefinito
verrà utilizzata al posto dell'implementazione del plug-in. I contenuti
non è necessario che esista una classe provider. In tal caso, assicurati di aggiungere
tools:ignore="MissingClass"
alla definizione del provider. Guarda l'esempio
voce nel file manifest riportata 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 l'app.
Plug-in come libreria condivisa
A differenza delle librerie statiche di Android, che vengono compilate direttamente nelle app, Le librerie condivise di Android sono compilate in un APK autonomo a cui viene fatto riferimento da altre app in fase di runtime.
I plug-in implementati come libreria condivisa di Android hanno le proprie classi. aggiunti automaticamente al classloader condiviso tra le app. Quando un'app utilizza la libreria UI dell'auto specifica dipendenza di runtime dalla libreria condivisa del plug-in, classloader può accedere alle classi della libreria condivisa del plug-in. Plug-in implementati poiché le normali app per Android (non una libreria condivisa) possono influire negativamente sul blocco delle app all'ora di inizio.
Implementa e crea librerie condivise
Lo sviluppo con le librerie condivise di Android è molto simile a quello di Android di Google Cloud, con alcune differenze fondamentali.
- Utilizza il tag
library
sotto il tagapplication
con il pacchetto di plug-in nel file manifest dell'app del plug-in:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- Configura la tua regola di build
android_app
apprezzata (Android.bp
) con l'AAPT flagshared-lib
, che viene utilizzato per creare una libreria condivisa:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
Dipendenze dalle librerie condivise
Per ogni app nel sistema che utilizza la libreria dell'UI dell'auto, includi i seguenti elementi:
Tag uses-library
nel file manifest dell'app nella sezione
Tag application
con il nome del pacchetto 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 sulla partizione di sistema includendo il modulo
nel seguente paese: PRODUCT_PACKAGES
. Il pacchetto preinstallato può essere aggiornato in modo simile
di qualsiasi altra app installata.
Se aggiorni un plug-in esistente sul sistema, tutte le app che lo utilizzano si chiudono automaticamente. Una volta riaperte, gli utenti hanno le modifiche aggiornate. Se l'app non era in esecuzione, al successivo avvio avrà aggiornato .
Quando installi un plug-in con Android Studio, esistono alcune considerazioni da tenere in considerazione. Al momento della stesura di questo documento, c'è un bug la procedura di installazione dell'app Android Studio che attiva gli aggiornamenti di un plug-in per non avere effetto. Puoi risolvere il problema selezionando l'opzione Installa sempre con gestore di pacchetti (disattiva le ottimizzazioni del deployment su Android 11 e versioni successive) nella configurazione di compilazione del plug-in.
Inoltre, al momento dell'installazione del plug-in, Android Studio segnala un errore che indica che non riesci a trovare un'attività principale da avviare. Si tratta di un comportamento previsto, in quanto il plug-in non Avere attività (tranne l'intent vuoto usato per risolvere un intent). A eliminare l'errore, cambiare l'opzione Launch (Avvia) in Nothing nella build configurazione.
Figura 1. Configurazione del plug-in di Android Studio
Plug-in proxy
Personalizzazione di app che usano la raccolta UI dell'auto richiede un RRO che abbia come target ogni app specifica da modificare, anche quando le personalizzazioni sono identiche tra le app. Ciò significa un RRO per è obbligatoria. Scoprire quali app utilizzano la raccolta UI dell'auto.
Un esempio è il plug-in proxy per la libreria dell'interfaccia utente dell'auto libreria condivisa con plug-in, che delega le implementazioni dei componenti all'ambiente dell'interfaccia utente dell'auto. Questo plug-in può essere scelto come target con un RRO, che usato come singolo punto di personalizzazione per le app che usano la libreria UI dell'auto. senza dover implementare un plug-in funzionale. Per ulteriori informazioni RRO, consulta l'articolo Modificare il valore delle risorse di un'app all'indirizzo tempo di esecuzione.
Il plug-in proxy è solo un esempio e il punto di partenza per eseguire la personalizzazione utilizzando un plug-in. Per la personalizzazione oltre agli RRO, è possibile implementare un sottoinsieme di plug-in e utilizzare il plug-in proxy per gli altri componenti o implementare tutti i plug-in componenti completamente da zero.
Sebbene il plug-in proxy fornisca un unico punto di personalizzazione RRO per le app, per le app che disattivano l'utilizzo del plug-in sarà comunque necessario un RRO che sceglie come target l'app stessa.
Implementazione delle API dei plug-in
Il punto di ingresso principale del plug-in è
com.android.car.ui.plugin.PluginVersionProviderImpl
corso. Tutti i plug-in devono
includi una classe con questo nome esatto e questo nome del pacchetto. Questo corso deve avere un
costruttore predefinito e implementare l'interfaccia PluginVersionProviderOEMV1
.
I plug-in CarUi devono funzionare con app meno recenti o più recenti. A
Per facilitare questa operazione, viene eseguito il controllo delle versioni delle API dei plug-in con un V#
alla fine del
nome classe. Se viene rilasciata una nuova versione
della libreria di UI dell'auto con nuove funzionalità,
fanno parte della versione V2
del componente. La libreria UI dell'auto svolge le sue
è il modo migliore per far funzionare le nuove funzionalità nell'ambito di un componente plug-in meno recente.
Ad esempio, puoi convertire in MenuItems
un nuovo tipo di pulsante della barra degli strumenti.
Tuttavia, un'app con una versione precedente della libreria di UI dell'auto non può adattarsi a una nuova basato su API più recenti. Per risolvere questo problema, consentiamo ai plug-in di restituiscono implementazioni diverse in base alla versione dell'API dell'OEM supportate dalle app.
PluginVersionProviderOEMV1
contiene un metodo:
Object getPluginFactory(int maxVersion, Context context, String packageName);
Questo metodo restituisce un oggetto che implementa la versione più alta di
PluginFactoryOEMV#
supportato dal plug-in, pur essendo inferiore a o
uguale a maxVersion
. Se un plug-in non prevede l'implementazione di un
PluginFactory
quella precedente, potrebbe restituire null
, nel qual caso il valore
dell'implementazione collegata dei componenti CarUi.
Per mantenere la compatibilità con le versioni precedenti delle app che vengono compilate in
precedenti della libreria statica di UI dell'auto, ti consigliamo di supportare
maxVersion
di 2, 5 e successive nell'implementazione del plug-in di
la classe PluginVersionProvider
. Le versioni 1, 3 e 4 non sono supportate. Per
ulteriori informazioni, vedi
PluginVersionProviderImpl
PluginFactory
è l'interfaccia che crea tutte le altre CarUi
componenti. Inoltre, definisce la versione delle interfacce da utilizzare. Se
il plug-in non cerca 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).
Il pluginFactory
limita le versioni dei componenti CarUi che possono essere utilizzate
in sinergia. Ad esempio, non ci sarà mai un pluginFactory
che può creare
versione 100 di un Toolbar
e anche la versione 1 di RecyclerView
, poiché
non sarebbe certo che un'ampia varietà di versioni dei componenti
possono funzionare in sinergia. Per utilizzare la versione 100 della barra degli strumenti, gli sviluppatori devono:
fornisce un'implementazione di una versione di pluginFactory
che crea un
barra degli strumenti versione 100, che limita quindi le opzioni sulle versioni di altri
componenti che possono essere creati. Le versioni degli altri componenti potrebbero non essere
uguale, ad esempio pluginFactoryOEMV100
potrebbe creare un
ToolbarControllerOEMV100
e RecyclerViewOEMV70
.
Barra degli strumenti
Layout di base
La barra degli strumenti e il "layout di base" sono strettamente correlate, quindi la funzione
che crea la barra degli strumenti si chiama installBaseLayoutAround
. La
layout di base
è un concetto che consente alla barra degli strumenti di essere posizionata ovunque intorno al
contenuti, per consentire una barra degli strumenti nella parte superiore o inferiore dell'app, verticalmente
ai lati o persino una barra degli strumenti circolare che racchiude l'intera app. Questo è
ottenuta passando una vista a installBaseLayoutAround
per la barra degli strumenti/la base
un layout avvolgente.
Il plug-in deve prendere la visualizzazione fornita, scollegarla da quella principale,
layout del plug-in nello stesso indice dell'elemento principale e con lo stesso
LayoutParams
come vista appena scollegata, quindi allega di nuovo la vista
in qualche punto all'interno del layout. Il layout gonfiato
contengono la barra degli strumenti, se richiesta dall'app.
L'app può richiedere un layout di base senza una barra degli strumenti. Se sì,
installBaseLayoutAround
deve restituire un valore null. Per la maggior parte dei plug-in, è tutto
deve verificarsi, ma se l'autore del plug-in desidera applicarlo, ad es. una decorazione
attorno al perimetro dell'app, si può fare comunque con un layout di base. Questi
le decorazioni sono particolarmente utili per i dispositivi con schermi non rettangolari, come
possono spingere l'app in uno spazio rettangolare e aggiungere transizioni pulite
lo spazio non rettangolare.
Anche installBaseLayoutAround
ha superato un Consumer<InsetsOEMV1>
. Questo
consumer può essere utilizzato per comunicare all'app che il plug-in è parzialmente
che coprono i contenuti dell'app (con la barra degli strumenti o in altro modo). L'app
sapete di continuare a disegnare questo spazio, mantenendo però
ogni utente fondamentale
componenti. Questo effetto viene utilizzato nella nostra progettazione di riferimento, per
barra degli strumenti semitrasparente e sotto di essa gli elenchi scorrono. Se questa funzionalità era
non implementato, il primo elemento di un elenco rimane bloccato sotto la barra degli strumenti
e non cliccabili. Se questo effetto non è necessario, il plug-in può ignorare la richiesta
Consumatore.
Figura 2. Scorrimento dei contenuti sotto la barra degli strumenti
Dal punto di vista dell'app, quando il plug-in invia nuovi riquadri,
da qualsiasi attività o frammento che implementano InsetsChangedListener
. Se
un'attività o un frammento non implementano InsetsChangedListener
, l'interfaccia utente dell'auto
gestirà gli inserti per impostazione predefinita applicandoli come spaziatura interna al
Activity
o FragmentActivity
contenente il frammento. La libreria non
e applicare gli inserti ai frammenti per impostazione predefinita. Ecco un esempio di snippet di
implementazione che applica i riquadri come spaziatura interna su un RecyclerView
nel
dell'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 hint fullscreen
, che viene utilizzato per indicare se
la vista da includere occupa l'intera app o solo una piccola sezione.
In questo modo è possibile evitare l'applicazione di alcune decorazioni lungo il bordo che
ha senso solo se appaiono lungo il bordo dell'intero schermo. Esempio
che utilizza layout di base non a schermo intero è Impostazioni, in cui ogni riquadro
a due riquadri ha la propria barra degli strumenti.
Poiché è previsto che installBaseLayoutAround
restituisca un valore nullo quando
toolbarEnabled
è false
, perché il plug-in indica che non
personalizzare il layout di base, deve restituire false
da
customizesBaseLayout
.
Il layout di base deve contenere FocusParkingView
e FocusArea
per completare
supportano i controlli rotanti. Queste visualizzazioni possono essere omesse sui dispositivi che
le macchine rotative. Le FocusParkingView/FocusAreas
sono implementate nel
libreria CarUi statica, quindi un setRotaryFactories
viene utilizzato per fornire alle fabbriche
creare le viste dai contesti.
I contesti utilizzati per creare viste dell'elemento attivo devono essere il contesto di origine, non
contesto del plug-in. FocusParkingView
deve essere il più vicino alla prima vista
nell'albero nel modo più ragionevolmente possibile, in quanto è ciò che viene definito quando
non deve essere visibile all'utente. FocusArea
deve disporre la barra degli strumenti nella
per indicare che si tratta di una zona di sollecitazione rotatoria. Se FocusArea
non è
fornita, l'utente non è in grado di accedere ai pulsanti della barra degli strumenti con
rotativo.
Controller della barra degli strumenti
Il valore effettivo di ToolbarController
restituito dovrebbe essere molto più semplice
da implementare rispetto al layout di base. Il suo compito è raccogliere le informazioni passate
setter e visualizzarlo nel layout di base. Vedi il Javadoc per informazioni su
con la maggior parte dei metodi. Di seguito vengono descritti alcuni dei metodi più complessi.
getImeSearchInterface
viene utilizzato per mostrare i risultati di ricerca nell'IME (tastiera)
finestra. Può essere utile per visualizzare/animare i risultati di ricerca insieme al
tastiera, ad esempio se occupava solo metà dello schermo. La maggior parte di
la funzionalità è implementata nella libreria statica CarUi, il motore
nel plug-in fornisce solo metodi affinché la libreria statica
Callback TextView
e onPrivateIMECommand
. A questo scopo, il plug-in
deve utilizzare una sottoclasse TextView
che esegue l'override di onPrivateIMECommand
e supera
la chiamata al listener fornito come TextView
della barra di ricerca.
setMenuItems
mostra semplicemente le voci di menu sullo schermo, ma si chiama
sorprendentemente spesso. Poiché l'API plugin per MenuItems è immutabile, ogni volta che un
La voce di menu è cambiata e verrà effettuata una chiamata setMenuItems
completamente nuova. Questo potrebbe
per qualcosa di banale, ad esempio se un utente ha fatto clic su un'altra voce di menu.
il clic ha provocato l'attivazione/disattivazione. Per motivi legati alle prestazioni
e all'animazione,
è quindi consigliabile calcolare la differenza tra i vecchi e i nuovi
delle voci di menu e aggiorna solo le visualizzazioni effettivamente cambiate. Voci di menu
fornisci un campo key
che possa aiutarti a eseguire questa operazione, poiché la chiave deve essere la stessa
in diverse chiamate a setMenuItems
per la stessa voce di menu.
AppStyledView
AppStyledView
è un contenitore per una vista non personalizzata. it
può essere utilizzato per creare un bordo attorno all'immagine che la distingua
per il resto dell'app e segnalare all'utente che si tratta di un tipo diverso
a riga di comando. La vista sottoposta a wrapping da AppStyledView è indicata in
setContent
. AppStyledView
può anche avere un pulsante Indietro o Chiudi
richiesto dall'app.
AppStyledView
non inserisce immediatamente le proprie visualizzazioni nella gerarchia delle visualizzazioni
come fa installBaseLayoutAround
, restituisce solo la sua visualizzazione
libreria statica tramite getView
, che esegue poi l'inserimento. La posizione e
la dimensione di AppStyledView
può essere controllata anche implementando
getDialogWindowLayoutParam
.
Contesti
Il plug-in deve fare attenzione quando utilizzi i contesti, in quanto esistono sia il plug-in che
"fonte" i contesti. Il contesto del plug-in viene fornito come argomento
getPluginFactory
, ed è l'unico contesto con le
risorse del plug-in al suo interno. Questo significa che è l'unico contesto che può essere utilizzato
gonfiano i layout nel plug-in.
Tuttavia, nel contesto del plug-in potrebbe non essere impostata la configurazione corretta. A
la configurazione corretta, forniamo contesti di origine nei metodi che creano
componenti. Il contesto della fonte è solitamente un'attività, ma in alcuni casi potrebbe
anche un Servizio o un altro componente Android. Per utilizzare la configurazione
contesto di origine con le risorse dal contesto del plug-in, un nuovo contesto deve essere
creato utilizzando createConfigurationContext
. Se la configurazione non è corretta,
vi sarà una violazione della modalità con restrizioni di Android e il numero di visualizzazioni gonfiato potrebbe
non hanno le dimensioni corrette.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
Modifiche alla modalità
Alcuni plug-in possono supportare diverse modalità per i loro componenti, ad esempio: una modalità Sport o modalità Eco che abbiano un aspetto visivamente peculiare. Non sono presenti per queste funzionalità in CarUi, ma niente si interrompe il plug-in di implementarlo interamente internamente. Il plug-in può monitorare indipendentemente dalle condizioni in cui vuole capire quando passare da una modalità all'altra, ad esempio in ascolto delle trasmissioni. Il plug-in non può attivare una modifica alla configurazione cambiare modalità, ma è sconsigliato fare affidamento sulle modifiche alla configurazione comunque, poiché l'aggiornamento manuale dell'aspetto di ogni componente è più semplice all'utente e consente anche transizioni che non sono possibili modifiche alla configurazione.
Jetpack Compose
I plug-in possono essere implementati utilizzando Jetpack Compose, ma si tratta di un plug-in di livello alpha e non deve essere considerata stabile.
I plug-in possono utilizzare
ComposeView
per creare una piattaforma abilitata per Compose in cui eseguire il rendering. ComposeView
sarebbe
ciò che viene restituito all'app dal metodo getView
nei componenti.
Un importante problema dell'utilizzo di ComposeView
è che imposta i tag nella vista principale
nel layout per archiviare le variabili globali condivise
ComposeView diversi nella gerarchia. Poiché gli ID risorsa del plug-in non sono
con spazio dei nomi separato da quello dell'app, questo potrebbe causare conflitti quando
e il plug-in impostano i tag nella stessa vista. Un segmento di pubblico personalizzato
ComposeViewWithLifecycle
che sposta queste variabili globali verso il basso
ComposeView
è fornito di seguito. Anche in questo caso, questo aspetto 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)
// }
}