Plug-in UI auto

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 tag application 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 flag shared-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.

Configurazione del plug-in di Android Studio 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.

Scorrimento dei contenuti sotto la barra degli strumenti 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)
//  }
}