Auto-UI-Plugins

Mit Sammlungen den Überblick behalten Sie können Inhalte basierend auf Ihren Einstellungen speichern und kategorisieren.

Verwenden Sie car-ui-lib Plug -ins, um vollständige Implementierungen von Komponentenanpassungen in car-ui-lib zu erstellen, anstatt Runtime Resource Overlays (RROs) zu verwenden. RROs ermöglichen es Ihnen, nur die XML-Ressourcen von car-ui-lib Komponenten zu ändern, was den Umfang dessen, was Sie anpassen können, einschränkt.

Plugin erstellen

Ein car-ui-lib Plug-in ist ein APK, das Klassen enthält, die eine Reihe von Plug-in-APIs implementieren. Die Plugin-APIs befinden sich in packages/apps/Car/libs/car-ui-lib/oem-apis und können als statische Bibliothek in ein Plugin kompiliert werden.

Siehe die Soong- und In-Gradle-Beispiele unten:

Bald

Betrachten Sie dieses Song-Beispiel:

android_app {
    name: "my-plugin",

    min_sdk_version: "28",
    target_sdk_version: "30",
    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",
    ],

    optimize: {
        enabled: false,
    },

    certificate: ":my-plugin-certificate",

Gradl

Siehe diese build.gradle -Datei:

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 skip the 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')

Das Plugin muss einen Inhaltsanbieter in seinem Manifest deklariert haben, der die folgenden Attribute hat:

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

android:authorities="com.android.car.ui.plugin" macht das Plugin für car-ui-lib auffindbar. Der Provider muss exportiert werden, damit er zur Laufzeit abgefragt werden kann. Auch wenn das Attribut enabled auf false gesetzt ist, wird die Standardimplementierung anstelle der Plugin-Implementierung verwendet. Die Inhaltsanbieterklasse muss nicht vorhanden sein. Stellen Sie in diesem Fall sicher, dass Sie tools:ignore="MissingClass" zur Anbieterdefinition hinzufügen. Sehen Sie sich den Beispiel-Manifesteintrag unten an:

    <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>

Als Sicherheitsmaßnahme signieren Sie schließlich Ihre App .

Installieren eines Plugins

Nachdem Sie das Plug-in erstellt haben, kann es wie jede andere App installiert werden, indem Sie es beispielsweise zu PRODUCT_PACKAGES oder adb install verwenden. Wenn es sich jedoch um eine neue, frische Installation eines Plugins handelt, müssen die Apps neu gestartet werden, damit die Änderungen wirksam werden. Dies kann durch Ausführen eines vollständigen adb reboot oder einer adb shell am force-stop package.name für eine bestimmte App erfolgen.

Wenn Sie ein vorhandenes car-ui-lib Plug-in auf dem System aktualisieren, werden alle Apps, die dieses Plug-in verwenden, automatisch geschlossen und verfügen nach dem erneuten Öffnen durch den Benutzer über die aktualisierten Änderungen. Das sieht nach einem Absturz aus, wenn die Apps gerade im Vordergrund sind. Wenn die App nicht ausgeführt wurde, verfügt sie beim nächsten Start über das aktualisierte Plugin.

Bei der Installation eines Plugins mit Android Studio müssen einige zusätzliche Überlegungen berücksichtigt werden. Zum Zeitpunkt des Verfassens dieses Artikels gibt es einen Fehler im Installationsprozess der Android Studio-App, der dazu führt, dass Aktualisierungen eines Plug-ins nicht wirksam werden. Dies kann behoben werden, indem in der Build-Konfiguration des Plugins die Option Always install with package manager (deaktiviert Deployment-Optimierungen auf Android 11 und höher) ausgewählt wird.

Darüber hinaus meldet Android Studio bei der Installation des Plugins einen Fehler, dass es keine Hauptaktivität zum Starten finden kann. Dies ist zu erwarten, da das Plugin keine Aktivitäten hat (außer der leeren Absicht, die zum Auflösen einer Absicht verwendet wird). Um den Fehler zu beheben, ändern Sie in der Build-Konfiguration die Launch- Option auf Nothing .

Plugin-Android-Studio-Konfiguration Abbildung 1. Konfiguration des Plugins für Android Studio

Implementieren der Plugin-APIs

Der Haupteinstiegspunkt für das Plugin ist die Klasse com.android.car.ui.plugin.PluginVersionProviderImpl . Alle Plugins müssen eine Klasse mit genau diesem Namen und Paketnamen enthalten. Diese Klasse muss über einen Standardkonstruktor verfügen und die PluginVersionProviderOEMV1 -Schnittstelle implementieren.

CarUi-Plugins müssen mit Apps funktionieren, die älter oder neuer als das Plugin sind. Um dies zu erleichtern, sind alle Plugin-APIs mit einem V# am Ende ihres Klassennamens versioniert. Wenn eine neue Version von car-ui-lib mit neuen Funktionen veröffentlicht wird, sind diese Teil der V2 -Version der Komponente. car-ui-lib tut sein Bestes, um neue Funktionen im Rahmen einer älteren Plugin-Komponente zum Laufen zu bringen. Beispielsweise durch Konvertieren eines neuen Schaltflächentyps in der Symbolleiste in MenuItems .

Eine alte App mit einer alten Version von car-ui-lib kann sich jedoch nicht an ein neues Plugin anpassen, das für neuere APIs geschrieben wurde. Um dieses Problem zu lösen, erlauben wir Plugins, verschiedene Implementierungen von sich selbst zurückzugeben, basierend auf der Version der OEM-API, die von den Apps unterstützt wird.

PluginVersionProviderOEMV1 enthält eine Methode:

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

Diese Methode gibt ein Objekt zurück, das die höchste Version von PluginFactoryOEMV# implementiert, die vom Plugin unterstützt wird, während es immer noch kleiner oder gleich maxVersion . Wenn ein Plugin keine so alte Implementierung einer PluginFactory hat, kann es null zurückgeben, in diesem Fall wird die statisch gelinkte Implementierung von CarUi-Komponenten verwendet.

Die PluginFactory ist die Schnittstelle, die alle anderen CarUi-Komponenten erstellt. Es definiert auch, welche Version ihrer Schnittstellen verwendet werden soll. Wenn das Plug-in keine dieser Komponenten zu implementieren versucht, kann es in ihrer Erstellungsfunktion null zurückgeben (mit Ausnahme der Symbolleiste, die eine separate customizesBaseLayout() Funktion hat).

Die pluginFactory begrenzt, welche Versionen von CarUi-Komponenten zusammen verwendet werden können. Beispielsweise wird es niemals eine pluginFactory , die Version 100 einer Toolbar und auch Version 1 einer RecyclerView erstellen kann, da es wenig Garantie dafür gäbe, dass eine Vielzahl von Versionen von Komponenten zusammenarbeiten würden. Um die Symbolleistenversion 100 zu verwenden, wird von Entwicklern erwartet, dass sie eine Implementierung einer Version von pluginFactory , die eine Symbolleistenversion 100 erstellt, die dann die Optionen auf die Versionen anderer Komponenten beschränkt, die erstellt werden können. Die Versionen anderer Komponenten sind möglicherweise nicht gleich, beispielsweise könnte eine pluginFactoryOEMV100 einen ToolbarControllerOEMV100 und einen RecyclerViewOEMV70 erstellen.

Symbolleiste

Basislayout

Die Symbolleiste und das "Basislayout" sind sehr eng miteinander verwandt, daher heißt die Funktion, die die Symbolleiste erstellt, installBaseLayoutAround . Das Basislayout ist ein Konzept, das es ermöglicht, die Symbolleiste an beliebiger Stelle um den Inhalt der App herum zu positionieren, um eine Symbolleiste oben/unten in der App, vertikal an den Seiten oder sogar eine kreisförmige Symbolleiste zu ermöglichen, die die gesamte App umschließt. Dies wird erreicht, indem eine Ansicht an installBaseLayoutAround wird, damit das Symbolleisten-/Basislayout umbrochen wird.

Das Plugin sollte die bereitgestellte Ansicht nehmen, sie von ihrem übergeordneten Element trennen, das eigene Layout des Plugins im selben Index des übergeordneten Elements und mit denselben LayoutParams wie die gerade getrennte Ansicht aufblasen und die Ansicht dann irgendwo innerhalb des Layouts neu anfügen nur aufgeblasen. Das aufgeblasene Layout enthält die Symbolleiste, wenn dies von der App angefordert wird.

Die App kann ein Basislayout ohne Symbolleiste anfordern. Wenn dies der Fall ist, sollte installBaseLayoutAround null zurückgeben. Für die meisten Plugins ist das alles, was passieren muss, aber wenn der Plugin-Autor zB eine Dekoration um den Rand der App herum anbringen möchte, könnte dies immer noch mit einem Basislayout erfolgen. Diese Dekorationen sind besonders nützlich für Geräte mit nicht rechteckigen Bildschirmen, da sie die App in einen rechteckigen Bereich verschieben und saubere Übergänge in den nicht rechteckigen Bereich einfügen können.

installBaseLayoutAround wird auch ein Consumer<InsetsOEMV1> . Dieser Verbraucher kann verwendet werden, um der App mitzuteilen, dass das Plugin den Inhalt der App teilweise abdeckt (mit der Symbolleiste oder auf andere Weise). Die App wird dann wissen, dass sie in diesem Bereich weiter zeichnen muss, aber alle kritischen Komponenten, die mit dem Benutzer interagierbar sind, davon fernhalten. Dieser Effekt wird in unserem Referenzdesign verwendet, um die Symbolleiste halbtransparent zu machen und Listen darunter scrollen zu lassen. Wenn diese Funktion nicht implementiert wäre, würde das erste Element in einer Liste unter der Symbolleiste hängen bleiben und nicht anklickbar sein. Wenn dieser Effekt nicht benötigt wird, kann das Plugin den Consumer ignorieren.

Scrollen von Inhalten unter der Symbolleiste Abbildung 2. Scrollen des Inhalts unter der Symbolleiste

Wenn das Plug-in neue Insets sendet, empfängt es aus Sicht der App diese über alle Aktivitäten/Fragmente, die InsetsChangedListener implementieren. Hier ist ein Beispiel-Snippit einer Implementierung, die die Einschübe als Padding auf eine Recycler-Ansicht in der App anwendet:

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());
  }
}

Abschließend erhält das Plugin einen fullscreen Hinweis, mit dem angegeben wird, ob die zu umschließende Ansicht die gesamte App oder nur einen kleinen Ausschnitt einnimmt. Dies kann verwendet werden, um zu vermeiden, dass einige Dekorationen am Rand angebracht werden, die nur dann sinnvoll sind, wenn sie am Rand des gesamten Bildschirms erscheinen. Eine Beispiel-App, die Nicht-Vollbild-Basislayouts verwendet, ist Einstellungen, in der jeder Bereich des Layouts mit zwei Bereichen über eine eigene Symbolleiste verfügt.

Da erwartet wird, dass installBaseLayoutAround null zurückgibt, wenn toolbarEnabled false ist, muss das Plug-in false von customizesBaseLayout zurückgeben, damit es angeben kann, dass es das Basislayout nicht anpassen möchte.

Das Basislayout muss eine FocusParkingView und eine FocusArea enthalten, um Drehregler vollständig zu unterstützen. Diese Ansichten können auf Geräten, die Rotation nicht unterstützen, weggelassen werden. Die FocusParkingView/FocusAreas sind in der statischen CarUi-Bibliothek implementiert, sodass ein setRotaryFactories verwendet wird, um Fabriken bereitzustellen, um die Ansichten aus Kontexten zu erstellen.

Die zum Erstellen von Fokusansichten verwendeten Kontexte müssen der Quellkontext sein, nicht der Kontext des Plugins. Die FocusParkingView sollte der ersten Ansicht in der Struktur möglichst nahe kommen, da sie fokussiert wird, wenn für den Benutzer kein Fokus sichtbar sein sollte. Die FocusArea muss die Symbolleiste im Basislayout umschließen, um anzugeben, dass es sich um eine Rotationsschubzone handelt. Wenn die FocusArea nicht bereitgestellt wird, kann der Benutzer mit dem Drehregler zu keinen Schaltflächen in der Symbolleiste navigieren.

Toolbar-Controller

Der tatsächlich zurückgegebene ToolbarController sollte viel einfacher zu implementieren sein als das Basislayout. Seine Aufgabe ist es, Informationen, die an seine Setter weitergegeben werden, aufzunehmen und im Basislayout anzuzeigen. Informationen zu den meisten Methoden finden Sie im Javadoc. Einige der komplexeren Verfahren werden unten diskutiert.

getImeSearchInterface wird verwendet, um Suchergebnisse im IME-Fenster (Tastaturfenster) anzuzeigen. Dies kann nützlich sein, um Suchergebnisse neben der Tastatur anzuzeigen/animieren, beispielsweise wenn die Tastatur nur die Hälfte des Bildschirms einnimmt. Die meisten Funktionen sind in der statischen CarUi-Bibliothek implementiert, die Suchschnittstelle im Plugin stellt nur Methoden für die statische Bibliothek bereit, um die TextView und onPrivateIMECommand Callbacks abzurufen. Um dies zu unterstützen, sollte das Plug-in eine TextView Unterklasse verwenden, die onPrivateIMECommand überschreibt und den Aufruf als TextView seiner Suchleiste an den bereitgestellten Listener weiterleitet.

setMenuItems zeigt einfach MenuItems auf dem Bildschirm an, wird aber überraschend oft aufgerufen. Da die Plugin-API für MenuItems unveränderlich ist, wird immer dann, wenn ein MenuItem geändert wird, ein ganz neuer setMenuItems -Aufruf ausgeführt. Dies könnte bei etwas so Trivialem passieren, wenn ein Benutzer auf einen Schalter MenuItem klickt und dieser Klick bewirkt, dass der Schalter umgeschaltet wird. Sowohl aus Performance- als auch aus Animationsgründen wird daher empfohlen, die Differenz zwischen der alten und der neuen MenuItems-Liste zu berechnen und nur die Ansichten zu aktualisieren, die sich tatsächlich geändert haben. Die MenuItems bieten ein key , das dabei helfen kann, da der Schlüssel bei verschiedenen Aufrufen von setMenuItems für dasselbe MenuItem gleich sein sollte.

AppStyledView

Die AppStyledView ist ein Container für eine Ansicht, die überhaupt nicht angepasst ist. Es kann verwendet werden, um diese Ansicht mit einem Rahmen zu versehen, der sie vom Rest der App abhebt, und dem Benutzer anzuzeigen, dass dies eine andere Art von Schnittstelle ist. Die von AppStyledView umschlossene Ansicht wird in setContent angegeben. Die AppStyledView kann je nach Anforderung der App auch eine Zurück- oder Schließen-Schaltfläche haben.

Die AppStyledView fügt ihre Ansichten nicht sofort in die Ansichtshierarchie ein, wie dies bei installBaseLayoutAround der Fall ist, sondern gibt ihre Ansicht stattdessen einfach über getView an die statische Bibliothek zurück, die dann die Einfügung durchführt. Die Position und Größe der AppStyledView kann auch durch die Implementierung von getDialogWindowLayoutParam gesteuert werden.

Kontexte

Das Plugin muss bei der Verwendung von Kontexten vorsichtig sein, da es sowohl Plugin- als auch "Quell"-Kontexte gibt. Der Plugin-Kontext wird als Argument an getPluginFactory und ist der einzige Kontext, der garantiert die Ressourcen des Plugins enthält. Dies bedeutet, dass dies der einzige Kontext ist, der zum Aufblasen von Layouts im Plugin verwendet werden kann.

Für den Plugin-Kontext ist jedoch möglicherweise nicht die richtige Konfiguration festgelegt. Um die richtige Konfiguration zu erhalten, stellen wir Quellkontexte in Methoden bereit, die Komponenten erstellen. Der Quellkontext ist normalerweise eine Aktivität, kann aber in einigen Fällen auch ein Dienst oder eine andere Android-Komponente sein. Um die Konfiguration aus dem Quellkontext mit den Ressourcen aus dem Plugin-Kontext zu verwenden, muss mit createConfigurationContext ein neuer Kontext erstellt werden. Wenn nicht die richtige Konfiguration verwendet wird, tritt ein Verstoß gegen den strengen Android-Modus auf, und die vergrößerten Ansichten haben möglicherweise nicht die richtigen Abmessungen.

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

Moduswechsel

Einige Plugins können mehrere Modi für ihre Komponenten unterstützen, wie z. B. einen Sportmodus oder einen Eco-Modus , die optisch unterschiedlich aussehen. Es gibt keine integrierte Unterstützung für solche Funktionen in CarUi, aber nichts hindert das Plugin daran, es vollständig intern zu implementieren. Das Plugin kann beliebige Bedingungen überwachen, um herauszufinden, wann der Modus gewechselt werden muss, z. B. das Abhören von Sendungen. Das Plugin kann keine Konfigurationsänderung in Änderungsmodi auslösen, aber es wird sowieso nicht empfohlen, sich auf Konfigurationsänderungen zu verlassen, da das manuelle Aktualisieren des Erscheinungsbilds jeder Komponente für den Benutzer reibungsloser ist und auch Übergänge ermöglicht, die mit Konfigurationsänderungen nicht möglich sind.

Jetpack komponieren

Plugins können mit Jetpack Compose implementiert werden, aber dies ist eine Alpha-Level-Funktion und sollte nicht als stabil betrachtet werden.

Plug-ins können ComposeView verwenden, um eine Compose-fähige Oberfläche zum Rendern zu erstellen. Diese ComposeView wäre das, was von der getView Methode in Komponenten an die App zurückgegeben wird.

Ein Hauptproblem bei der Verwendung von ComposeView besteht darin, dass Tags in der Stammansicht im Layout festgelegt werden, um globale Variablen zu speichern, die von verschiedenen ComposeViews in der Hierarchie gemeinsam genutzt werden. Da die Ressourcen-IDs des Plug-ins nicht getrennt von denen der App benannt werden, könnte dies zu Konflikten führen, wenn sowohl die App als auch das Plug-in Tags für dieselbe Ansicht festlegen. Ein benutzerdefiniertes ComposeViewWithLifecycle , das diese globalen Variablen nach unten in die ComposeView , wird unten bereitgestellt. Auch dies sollte nicht als stabil angesehen werden.

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)
//  }
}