Usa los complementos de la biblioteca de IU del vehículo para crear implementaciones completas de los componentes personalizadas en la biblioteca de la IU del vehículo en lugar de usar superposiciones de recursos de tiempo de ejecución (RRO). Las RRO te permiten cambiar solo los recursos XML de la biblioteca de la IU del vehículo componentes de seguridad, lo que limita la medida en que se puede personalizar.
Cómo crear un complemento
Un complemento de la biblioteca de IU del vehículo es un APK que contiene clases que implementan un conjunto de APIs de complementos. Las APIs de complementos pueden compilarse en un complemento como una biblioteca estática.
Consulta ejemplos en Soong y Gradle:
Soong
Considera este ejemplo de Soong:
android_app {
name: "my-plugin",
min_sdk_version: "28",
target_sdk_version: "30",
aaptflags: ["--shared-lib"],
sdk_version: "current",
manifest: "src/main/AndroidManifest.xml",
srcs: ["src/main/java/**/*.java"],
resource_dirs: ["src/main/res"],
static_libs: [
"car-ui-lib-oem-apis",
],
// Disable optimization is mandatory to prevent R.java class from being
// stripped out
optimize: {
enabled: false,
},
certificate: ":my-plugin-certificate",
}
Gradle
Consulta este archivo 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')
El complemento debe tener un proveedor de contenido declarado en su manifiesto que incluya siguientes atributos:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin"
hace que el complemento sea detectable.
a la biblioteca de IU del vehículo. Se debe exportar el proveedor para que se pueda consultar en
tiempo de ejecución. Además, si el atributo enabled
se establece en false
, el valor predeterminado
en lugar de la del complemento. El contenido
no es necesario que exista. En ese caso, asegúrate de agregar
tools:ignore="MissingClass"
a la definición del proveedor. Ver la muestra
manifiesto a continuación:
<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>
Por último, como medida de seguridad, Firma la app.
Complementos como biblioteca compartida
A diferencia de las bibliotecas estáticas de Android, que se compilan directamente en apps, Las bibliotecas compartidas de Android se compilan en un APK independiente al que se hace referencia. por otras apps en el tiempo de ejecución.
Los complementos que se implementan como una biblioteca compartida de Android tienen sus clases. automáticamente al cargador de clases compartido entre apps. Cuando una app que usa la biblioteca de IU del vehículo especifica dependencia del tiempo de ejecución en la biblioteca compartida del complemento, su classloader puede acceder a las clases de la biblioteca compartida del complemento. Complementos implementados ya que las apps para Android normales (no una biblioteca compartida) pueden afectar negativamente el frío de la app. horas de inicio.
Cómo implementar y compilar bibliotecas compartidas
Desarrollar con bibliotecas compartidas de Android es muy similar al uso de aplicaciones normales de Android con algunas diferencias clave.
- Usa la etiqueta
library
debajo de la etiquetaapplication
con el paquete del complemento nombre en el manifiesto de la app del complemento:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- Configura tu regla de compilación de
android_app
de Soong (Android.bp
) con la AAPT la marcashared-lib
, que se usa para compilar una biblioteca compartida:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
Dependencias de las bibliotecas compartidas
Para cada app del sistema que use la biblioteca de IU del vehículo, incluye la
uses-library
en el manifiesto de la app, en la etiqueta
Etiqueta application
con el nombre del paquete del complemento:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
Instala un complemento
Los complementos DEBEN estar preinstalados en la partición del sistema incluyendo el módulo
en PRODUCT_PACKAGES
. El paquete preinstalado se puede actualizar de manera similar a lo siguiente:
ninguna otra app instalada.
Si actualizas un complemento existente en el sistema, todas las apps que lo usen se cierran automáticamente. Una vez que el usuario vuelve a abrirlo, tiene los cambios actualizados. Si la app no se estaba ejecutando, la próxima vez que se inicie, tendrá el archivo .
Cuando instalas un complemento con Android Studio, debes tener en cuenta y consideraciones clave para tener en cuenta. Al momento de esta redacción, hay un error en el proceso de instalación de la app de Android Studio que actualiza un complemento en caso de que no surta efecto. Para solucionar este problema, selecciona la opción Instalar siempre con el administrador de paquetes (inhabilita las optimizaciones de implementación en Android 11 y versiones posteriores) en la configuración de compilación del complemento.
Además, al instalar el complemento, Android Studio informa un error que indica no puede encontrar una actividad principal para iniciar. Esto es esperable, ya que el complemento no tener actividades (excepto el intent vacío que se usa para resolver un intent) Para eliminar el error, cambia la opción Launch a Nothing en la compilación configuración.
Figura 1: Configuración del complemento de Android Studio
Complemento de proxy
Personalización de apps que usan la biblioteca de la IU del vehículo requiere una RRO que se dirija a cada app específica que se modificará incluso cuando las personalizaciones son idénticas en todas las apps. Esto significa que una RRO por es obligatoria. Consulta qué apps usan la biblioteca de la IU del vehículo.
El complemento del proxy de la biblioteca de la IU del vehículo es un ejemplo. de complementos que delega las implementaciones de sus componentes al entorno de la biblioteca de la IU del vehículo. Este complemento se puede orientar con una RRO, que puede ser se usa como un punto único de personalización para apps que usan la biblioteca de IU del vehículo. sin tener que implementar un complemento funcional. Para obtener más información RRO, consulta Cambia el valor de los recursos de una app en entorno de ejecución.
El complemento del proxy es solo un ejemplo y punto de partida para hacer la personalización usando un complemento. Para la personalización más allá de los RRO, se puede implementar un subconjunto de complementos los componentes y usa el complemento proxy para el resto, o implementa todas los componentes completamente desde cero.
Aunque el complemento de proxy proporciona un punto único de personalización de RRO para las apps, las apps que rechacen el uso del complemento seguirán requiriendo una RRO que se orienta a la app en sí.
Cómo implementar las APIs del complemento
El punto de entrada principal al complemento es el
Clase com.android.car.ui.plugin.PluginVersionProviderImpl
. Todos los complementos deben
Incluye una clase con este nombre exacto y este nombre de paquete. Esta clase debe tener un
el constructor predeterminado y, luego, implementarás la interfaz PluginVersionProviderOEMV1
.
Los complementos de CarUi deben funcionar con apps anteriores o nuevas que el complemento. Para
para facilitar esto, todas las APIs del complemento tienen control de versiones con un V#
al final de su
de clase. Si se lanza una nueva versión de la biblioteca de la IU del vehículo con funciones nuevas,
son parte de la versión V2
del componente. La biblioteca de IU del vehículo
Es la mejor opción para que las funciones nuevas funcionen dentro del alcance de un componente de complemento anterior.
Por ejemplo, puedes convertir un nuevo tipo de botón de la barra de herramientas en MenuItems
.
Sin embargo, una app con una versión anterior de la biblioteca de la IU del vehículo no puede adaptarse a una nueva. complemento escrito con APIs más nuevas. Para resolver este problema, permitimos que los complementos devolver implementaciones propias basadas en la versión de la API de OEM compatibles con las apps.
PluginVersionProviderOEMV1
tiene un método:
Object getPluginFactory(int maxVersion, Context context, String packageName);
Este método muestra un objeto que implementa la versión más alta de
PluginFactoryOEMV#
es compatible con el complemento, y sigue siendo menor que o
igual a maxVersion
. Si un complemento no tiene una implementación de un
PluginFactory
tan antiguo, es posible que muestre null
. En ese caso, el comando
implementación vinculada de componentes CarUi.
Para mantener la retrocompatibilidad con apps que se compilan
versiones anteriores de la biblioteca estática de Car UI, se recomienda admitir
maxVersion
de 2, 5 y superiores desde la implementación de tu complemento de
la clase PluginVersionProvider
. Las versiones 1, 3 y 4 no son compatibles. Para
más información, consulta
PluginVersionProviderImpl
PluginFactory
es la interfaz que crea todos los demás CarUi
o los componentes de la solución. También define qué versión de sus interfaces se debe usar. Si
el complemento no busca implementar ninguno de estos componentes, es posible que
null
en su función de creación (excepto la barra de herramientas, que tiene
una función customizesBaseLayout()
independiente).
El pluginFactory
limita las versiones de los componentes de CarUi que se pueden usar.
entre sí. Por ejemplo, nunca habrá un pluginFactory
que pueda crear
la versión 100 de un Toolbar
y también la versión 1 de un RecyclerView
, ya que
no garantizaría que una amplia variedad de versiones de componentes
trabajan juntos. Para usar la versión 100 de la barra de herramientas, se espera que los desarrolladores
proporcionan una implementación de una versión de pluginFactory
que crea un
barra de herramientas 100, que limita las opciones en las versiones de otras
componentes que se pueden crear. Es posible que las versiones de otros componentes no se
igual; por ejemplo, un pluginFactoryOEMV100
podría crear una
ToolbarControllerOEMV100
y RecyclerViewOEMV70
.
Barra de herramientas
Diseño básico
La barra de herramientas y el "diseño base" están muy relacionados, por lo que la función
que crea la barra de herramientas se llama installBaseLayoutAround
. El
diseño básico
es un concepto que permite que la barra de herramientas se ubique en cualquier lugar alrededor del
contenido, para permitir una barra de herramientas en la parte superior/inferior de la app, verticalmente
a los lados, o incluso una barra de herramientas circular que encierra toda la app. Este es
lo cual se logra pasando una vista a installBaseLayoutAround
para la barra de herramientas o base
diseño para envolver.
El complemento debe tomar la vista proporcionada, separarla de su elemento superior, aumentarla
el diseño propio del complemento en el mismo índice del elemento superior y con la misma
LayoutParams
como la vista que se acaba de desconectar y, luego, vuelve a conectarla
en algún lugar dentro del diseño que se acaba de aumentar. El diseño inflado
contener la barra de herramientas, si la aplicación lo solicita.
La app puede solicitar un diseño base sin una barra de herramientas. Si lo hace,
installBaseLayoutAround
debe mostrar un valor nulo. Para la mayoría de los complementos, eso es todo.
debe suceder, pero si el autor del complemento desea aplicar, p.ej., una decoración
alrededor del borde de la app, lo que aún podría hacerse con un diseño base. Estos
decoraciones son particularmente útiles para dispositivos con pantallas no rectangulares, como
pueden empujar la app a un espacio rectangular y agregar transiciones claras
el espacio no rectangular.
installBaseLayoutAround
también recibe un Consumer<InsetsOEMV1>
. Esta
consumidor se puede usar para comunicar a la aplicación que el complemento está parcialmente
que cubre el contenido de la aplicación (con la barra de herramientas u otro modo). La app hará lo siguiente
sé que deben seguir dibujando en este espacio, pero mantengan todas las críticas
los componentes de ella. Este efecto se usa en nuestro diseño de referencia para que la
semitransparente, y verás que las listas se desplazan debajo de ella. Si este atributo fuera
implementado, el primer elemento de una lista estaría atascado debajo de la barra de herramientas.
y no se puede hacer clic. Si este efecto no es necesario, el complemento puede ignorar el
Consumidor
Figura 2: Desplazamiento del contenido debajo de la barra de herramientas
Desde la perspectiva de la app, cuando el complemento envíe nuevas inserciones, recibirá
desde cualquier actividad o fragmento que implemente InsetsChangedListener
. Si
una actividad o un fragmento no implementa InsetsChangedListener
, la IU del vehículo
controlará las inserciones de forma predeterminada aplicando las inserciones como relleno al
Activity
o FragmentActivity
que contienen el fragmento. La biblioteca no
aplica las inserciones a los fragmentos de forma predeterminada. Este es un fragmento de ejemplo de un
implementación que aplica las inserciones como relleno en un elemento RecyclerView
en el
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());
}
}
Por último, el complemento recibe una sugerencia fullscreen
, que se usa para indicar si
la vista que se debe unir ocupa toda la app o solo una pequeña sección.
Esto se puede usar para evitar aplicar algunas decoraciones en el borde que
solo tienen sentido si aparecen a lo largo de toda la pantalla. Una muestra
que usa diseños básicos que no son de pantalla completa es Configuración, en la que cada panel
el diseño de panel doble tiene su propia barra de herramientas.
Dado que se espera que installBaseLayoutAround
muestre un valor nulo cuando
toolbarEnabled
es false
, para que el complemento indique que no lo hace.
deseas personalizar el diseño base, debe mostrar false
de
customizesBaseLayout
El diseño base debe contener un FocusParkingView
y un FocusArea
para completar
admitir controles rotativos. Estas vistas se pueden omitir en dispositivos que
no son compatibles con el sistema rotativo. Las FocusParkingView/FocusAreas
se implementan en la
biblioteca CarUi estática, por lo que se usa un setRotaryFactories
para proporcionar fábricas a
crear las vistas a partir de contextos.
Los contextos que se usan para crear vistas de enfoque deben ser el contexto de origen, no el
contexto del complemento. El valor de FocusParkingView
debe ser el más cercano a la primera vista
en el árbol de la forma más razonablemente posible, ya que es lo que se enfoca cuando debería
ningún enfoque visible para el usuario. El elemento FocusArea
debe unir la barra de herramientas en el
para indicar que es una zona de empuje rotativo. Si la FocusArea
no es
el usuario no puede navegar a ningún botón de la barra de herramientas con la
control rotativo.
Controlador de la barra de herramientas
El ToolbarController
real que se muestra debería ser mucho más sencillo de
implementar que el diseño base. Su trabajo es llevar la información que se pasa
métodos set y los mostrará en el diseño base. Consulta el Javadoc para obtener más información sobre
de la mayoría de los métodos. A continuación, se analizan algunos de los métodos más complejos.
getImeSearchInterface
se usa para mostrar los resultados de la búsqueda en el IME (teclado).
en la ventana modal. Esto puede ser útil para mostrar/animar los resultados de la búsqueda junto al
teclado, por ejemplo, si ocupaba solo la mitad de la pantalla. La mayoría de
se implementa la funcionalidad en la biblioteca estática CarUi, la biblioteca
del complemento solo proporciona métodos para que la biblioteca estática obtenga
Devoluciones de llamada TextView
y onPrivateIMECommand
Para ello, el complemento
debes usar una subclase TextView
que anule a onPrivateIMECommand
y pase
la llamada al objeto de escucha proporcionado como el TextView
de su barra de búsqueda.
setMenuItems
simplemente muestra MenuItems en la pantalla, pero se lo llamará
sorprendentemente a menudo. Como la API del complemento para MenuItems es inmutable, cada vez que un
Se cambió MenuItem, se realizará una llamada a setMenuItems
completamente nueva. Esto podría
suceden por algo tan trivial como que
un usuario hace clic en un switch MenuItem,
clic en el interruptor para activarlo. Por razones de rendimiento y animación,
por lo tanto, se recomienda calcular la diferencia entre el modelo de
MenuItems y solo actualizar las vistas que realmente cambiaron. El elemento MenuItems
proporciona un campo key
que pueda ayudarte con esto, ya que la clave debe ser la misma
en diferentes llamadas a setMenuItems
para el mismo MenuItem.
Vista de estilo de la aplicación
AppStyledView
es un contenedor para una vista que no está personalizada en absoluto. Integra
se puede usar para proporcionar un borde alrededor de esa vista que haga que se destaque
el resto de la app e indicarle al usuario que este es un tipo diferente de
interfaz de usuario. La vista que une AppStyledView se proporciona en
setContent
El elemento AppStyledView
también puede tener un botón Atrás o Cerrar.
solicitada por la aplicación.
AppStyledView
no inserta de inmediato sus vistas en la jerarquía de vistas.
como lo hace installBaseLayoutAround
, en cambio, solo devuelve su vista al
biblioteca estática a través de getView
, que luego realiza la inserción. La posición y
el tamaño de AppStyledView
también se puede controlar implementando
getDialogWindowLayoutParam
Contextos
El complemento debe tener cuidado cuando se usan contextos, ya que existen plugin y
"fuente" diferentes. El contexto del complemento se proporciona como un argumento para
getPluginFactory
y es el único contexto que tiene el elemento
los recursos del complemento en ella. Esto significa que es el único contexto que se puede usar
aumentar los diseños en el complemento.
Sin embargo, es posible que el contexto del complemento no tenga la configuración correcta establecida. Para
obtener la configuración correcta, proporcionamos contextos de origen en los métodos que crean
o los componentes de la solución. El contexto de origen suele ser una actividad, pero en algunos casos puede
ser también un servicio u otro componente de Android. Para usar la configuración del
un contexto de origen con los recursos del contexto del complemento, se debe crear
creado con createConfigurationContext
. Si la configuración correcta no es
si se usa, se considerará una infracción del Modo estricto de Android y es posible que las vistas infladas
pero no tienen las dimensiones correctas.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
Cambios de modo
Algunos complementos pueden admitir varios modos para sus componentes, como un modo deportivo o un modo Eco que se vean visualmente distintos. No hay compatibilidad integrada para esa funcionalidad en CarUi, pero nada que el complemento lo implemente por completo internamente. El complemento puede supervisar las condiciones en las que quiera determinar cuándo cambiar de modo, por ejemplo, escuchando transmisiones. El complemento no puede activar un cambio de configuración. para cambiar los modos, pero no se recomienda depender de los cambios de configuración de todas formas, ya que actualizar manualmente el aspecto de cada componente es más para el usuario y también permite transiciones que no son posibles con cambios de configuración.
Jetpack Compose
Los complementos se pueden implementar con Jetpack Compose, pero este es un nivel alfa y no debe considerarse estable.
Los complementos pueden usar
ComposeView
para crear una plataforma habilitada para Compose en la que renderizar. Este ComposeView
sería
lo que se muestra a la app desde el método getView
en los componentes
Un problema importante con el uso de ComposeView
es que establece etiquetas en la vista raíz.
en el diseño para almacenar variables globales que se comparten
diferentes ComposeViews en la jerarquía. Como los IDs de recurso del complemento no son
espacios de nombres separados de los de la app, esto podría causar conflictos cuando
la app y las etiquetas del conjunto de complementos en la misma vista. Un
ComposeViewWithLifecycle
que mueve estas variables globales al
A continuación, se proporciona ComposeView
. Una vez más, esto no debe considerarse estable.
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)
// }
}