Plugin UI Mobil

Gunakan plugin car-ui-lib untuk membuat implementasi lengkap penyesuaian komponen di car-ui-lib daripada menggunakan runtime resource overlay (RRO). RRO memungkinkan Anda untuk mengubah hanya sumber daya XML komponen car-ui-lib , yang membatasi sejauh mana Anda dapat menyesuaikan.

Membuat plugin

Plugin car-ui-lib adalah APK yang berisi kelas yang mengimplementasikan sekumpulan API Plugin . API Plugin terletak di packages/apps/Car/libs/car-ui-lib/oem-apis dan dapat dikompilasi menjadi plugin sebagai pustaka statis.

Lihat contoh Soong dan dalam Gradle di bawah ini:

segera

Perhatikan contoh Soong ini:

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

Gradle

Lihat file build.gradle ini:

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

Plugin harus memiliki penyedia konten yang dideklarasikan dalam manifesnya yang memiliki atribut berikut:

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

android:authorities="com.android.car.ui.plugin" membuat plugin dapat ditemukan oleh car-ui-lib . Penyedia harus diekspor sehingga dapat ditanyakan saat runtime. Juga, jika atribut yang enabled disetel ke false , implementasi default akan digunakan sebagai ganti implementasi plugin. Kelas penyedia konten tidak harus ada. Dalam hal ini, pastikan untuk menambahkan tools:ignore="MissingClass" ke definisi penyedia. Lihat contoh entri manifes di bawah ini:

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

Terakhir, sebagai tindakan keamanan, Tanda tangani aplikasi Anda .

Memasang plugin

Setelah Anda membuat plugin, plugin dapat dipasang seperti aplikasi lainnya, seperti menambahkannya ke PRODUCT_PACKAGES atau menggunakan adb install . Namun, jika ini adalah pemasangan plugin yang baru dan baru, aplikasi harus dimulai ulang agar perubahan diterapkan. Ini dapat dilakukan dengan melakukan adb reboot penuh, atau adb shell am force-stop package.name untuk aplikasi tertentu.

Jika Anda memperbarui plugin car-ui-lib yang ada di sistem, aplikasi apa pun yang menggunakan plugin itu akan menutup secara otomatis dan, setelah dibuka kembali oleh pengguna, memiliki perubahan yang diperbarui. Ini terlihat seperti crash jika aplikasi berada di latar depan pada saat itu. Jika aplikasi tidak berjalan, saat berikutnya dijalankan, plugin akan diperbarui.

Saat memasang plugin dengan Android Studio, ada beberapa pertimbangan tambahan yang perlu dipertimbangkan. Pada saat penulisan, terdapat bug dalam proses penginstalan aplikasi Android Studio yang menyebabkan pembaruan pada plugin tidak berlaku. Ini dapat diperbaiki dengan memilih opsi Always install with package manager (nonaktifkan pengoptimalan penerapan di Android 11 dan yang lebih baru) di konfigurasi build plugin.

Selain itu, saat memasang plugin, Android Studio melaporkan kesalahan bahwa tidak dapat menemukan aktivitas utama untuk diluncurkan. Hal ini diharapkan, karena plugin tidak memiliki aktivitas apa pun (kecuali maksud kosong yang digunakan untuk menyelesaikan maksud). Untuk menghilangkan kesalahan, ubah opsi Luncurkan ke Tidak Ada dalam konfigurasi build.

Konfigurasi plugin Android Studio Gambar 1. Konfigurasi plugin Android Studio

Menerapkan API plugin

Titik masuk utama ke plugin adalah kelas com.android.car.ui.plugin.PluginVersionProviderImpl . Semua plugin harus menyertakan kelas dengan nama persis dan nama paket ini. Kelas ini harus memiliki konstruktor default dan mengimplementasikan antarmuka PluginVersionProviderOEMV1 .

Plugin CarUi harus bekerja dengan aplikasi yang lebih lama atau lebih baru dari plugin. Untuk memfasilitasi ini, semua API plugin diversi dengan V# di akhir nama kelasnya. Jika versi baru car-ui-lib dirilis dengan fitur baru, fitur tersebut merupakan bagian dari komponen versi V2 . car-ui-lib melakukan yang terbaik untuk membuat fitur baru bekerja dalam lingkup komponen plugin yang lebih lama. Misalnya, dengan mengubah jenis tombol baru di bilah alat menjadi MenuItems .

Namun, aplikasi lama dengan car-ui-lib versi lama tidak dapat beradaptasi dengan plugin baru yang ditulis dengan API yang lebih baru. Untuk mengatasi masalah ini, kami mengizinkan plugin untuk mengembalikan implementasi yang berbeda berdasarkan versi OEM API yang didukung oleh aplikasi.

PluginVersionProviderOEMV1 memiliki satu metode di dalamnya:

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

Metode ini mengembalikan objek yang mengimplementasikan versi tertinggi PluginFactoryOEMV# yang didukung oleh plugin, sementara masih kurang dari atau sama dengan maxVersion . Jika sebuah plugin tidak memiliki implementasi PluginFactory yang lama, plugin mungkin mengembalikan null , dalam hal ini implementasi komponen CarUi yang di-statis digunakan.

PluginFactory adalah antarmuka yang membuat semua komponen CarUi lainnya. Ini juga menentukan versi antarmuka mana yang harus digunakan. Jika plugin tidak berusaha untuk mengimplementasikan salah satu komponen ini, plugin mungkin mengembalikan null dalam fungsi pembuatannya (dengan pengecualian toolbar, yang memiliki fungsi customizesBaseLayout() terpisah).

pluginFactory membatasi versi komponen CarUi mana yang dapat digunakan bersama. Misalnya, tidak akan pernah ada pluginFactory yang dapat membuat Toolbar versi 100 dan juga versi 1 dari RecyclerView , karena hanya ada sedikit jaminan bahwa berbagai versi komponen akan bekerja sama. Untuk menggunakan toolbar versi 100, pengembang diharapkan menyediakan implementasi versi pluginFactory yang membuat toolbar versi 100, yang kemudian membatasi opsi pada versi komponen lain yang dapat dibuat. Versi komponen lain mungkin tidak sama, misalnya pluginFactoryOEMV100 dapat membuat ToolbarControllerOEMV100 dan RecyclerViewOEMV70 .

Bilah Alat

Tata letak dasar

Bilah alat dan "tata letak dasar" sangat terkait erat, oleh karena itu fungsi yang membuat bilah alat disebut installBaseLayoutAround . Tata letak dasar adalah konsep yang memungkinkan bilah alat diposisikan di mana saja di sekitar konten aplikasi, untuk memungkinkan bilah alat di bagian atas/bawah aplikasi, secara vertikal di sepanjang sisi, atau bahkan bilah alat melingkar yang menutupi seluruh aplikasi. Ini dicapai dengan meneruskan View ke installBaseLayoutAround untuk toolbar/tata letak dasar untuk membungkus.

Plugin harus mengambil Tampilan yang disediakan, melepaskannya dari induknya, mengembang tata letak plugin sendiri dalam indeks induk yang sama dan dengan LayoutParams yang sama dengan tampilan yang baru saja dilepas, lalu memasang kembali Tampilan di suatu tempat di dalam tata letak yang tadi hanya meningkat. Tata letak yang meningkat akan berisi bilah alat, jika diminta oleh aplikasi.

Aplikasi dapat meminta tata letak dasar tanpa bilah alat. Jika ya, installBaseLayoutAround harus mengembalikan null. Untuk sebagian besar plugin, itu saja yang perlu terjadi, tetapi jika pembuat plugin ingin menerapkan misalnya hiasan di sekitar tepi aplikasi, itu masih bisa dilakukan dengan tata letak dasar. Dekorasi ini sangat berguna untuk perangkat dengan layar non-persegi panjang, karena dapat mendorong aplikasi ke ruang persegi panjang dan menambahkan transisi bersih ke ruang non-persegi panjang.

installBaseLayoutAround juga melewati Consumer<InsetsOEMV1> . Konsumen ini dapat digunakan untuk berkomunikasi dengan aplikasi bahwa plugin menutupi sebagian konten aplikasi (dengan bilah alat atau lainnya). Aplikasi kemudian akan tahu untuk terus menggambar di ruang ini, tetapi jauhkan semua komponen penting yang dapat berinteraksi dengan pengguna darinya. Efek ini digunakan dalam desain referensi kami, untuk membuat bilah alat semi-transparan, dan memiliki daftar gulir di bawahnya. Jika fitur ini tidak diterapkan, item pertama dalam daftar akan tertahan di bawah bilah alat dan tidak dapat diklik. Jika efek ini tidak diperlukan, plugin dapat mengabaikan Konsumen.

Konten bergulir di bawah bilah alat Gambar 2. Konten bergulir di bawah bilah alat

Dari sudut pandang aplikasi, ketika plugin mengirimkan sisipan baru, plugin akan menerimanya melalui aktivitas/fragmen apa pun yang mengimplementasikan InsetsChangedListener . Berikut adalah contoh snippit implementasi yang menerapkan inset sebagai padding pada recyclerview di aplikasi:

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

Terakhir, plugin diberikan petunjuk fullscreen , yang digunakan untuk menunjukkan apakah Tampilan yang harus dibungkus menggunakan seluruh aplikasi atau hanya sebagian kecil. Ini dapat digunakan untuk menghindari penerapan beberapa dekorasi di sepanjang tepi yang hanya masuk akal jika muncul di sepanjang tepi seluruh layar. Contoh aplikasi yang menggunakan tata letak dasar non-layar penuh adalah Pengaturan, di mana setiap panel tata letak dua panel memiliki bilah alatnya sendiri.

Karena installBaseLayoutAround diharapkan mengembalikan null ketika toolbarEnabled adalah false , agar plugin menunjukkan bahwa ia tidak ingin menyesuaikan tata letak dasar, plugin harus mengembalikan false dari customizesBaseLayout .

Tata letak dasar harus berisi FocusParkingView dan FocusArea untuk sepenuhnya mendukung kontrol putar. Tampilan ini dapat dihilangkan pada perangkat yang tidak mendukung putar. FocusParkingView/FocusAreas diimplementasikan di perpustakaan CarUi statis, jadi setRotaryFactories digunakan untuk menyediakan pabrik untuk membuat tampilan dari konteks.

Konteks yang digunakan untuk membuat tampilan Fokus harus konteks sumber, bukan konteks plugin. FocusParkingView harus sedekat mungkin dengan tampilan pertama di pohon, karena itulah yang difokuskan ketika seharusnya tidak ada fokus yang terlihat oleh pengguna. FocusArea harus membungkus toolbar dalam tata letak dasar untuk menunjukkan bahwa itu adalah zona dorongan putar. Jika FocusArea tidak disediakan, pengguna tidak dapat menavigasi ke tombol apa pun di toolbar dengan pengontrol putar.

Pengontrol bilah alat

ToolbarController sebenarnya dikembalikan harus jauh lebih mudah diterapkan daripada tata letak dasar. Tugasnya adalah mengambil informasi yang diteruskan ke setternya dan menampilkannya di tata letak dasar. Lihat Javadoc untuk informasi tentang sebagian besar metode. Beberapa metode yang lebih kompleks dibahas di bawah ini.

getImeSearchInterface digunakan untuk menampilkan hasil pencarian di jendela IME (keyboard). Ini dapat berguna untuk menampilkan/menganimasikan hasil pencarian di samping keyboard, misalnya jika keyboard hanya menempati separuh layar. Sebagian besar fungsi diimplementasikan di perpustakaan CarUi statis, antarmuka pencarian di plugin hanya menyediakan metode untuk perpustakaan statis untuk mendapatkan callback TextView dan onPrivateIMECommand . Untuk mendukung ini, plugin harus menggunakan subkelas TextView yang menggantikan onPrivateIMECommand dan meneruskan panggilan ke pendengar yang disediakan sebagai TextView bilah pencariannya.

setMenuItems hanya menampilkan MenuItems di layar, tetapi akan sering dipanggil secara mengejutkan. Karena API plugin untuk MenuItems tidak dapat diubah, setiap kali MenuItem diubah, panggilan setMenuItems yang sama sekali baru akan terjadi. Ini bisa terjadi untuk sesuatu yang sepele seperti pengguna mengklik sakelar MenuItem, dan klik itu menyebabkan sakelar beralih. Untuk alasan kinerja dan animasi, karena itu dianjurkan untuk menghitung perbedaan antara daftar MenuItems lama dan baru, dan hanya memperbarui Tampilan yang benar-benar berubah. MenuItems menyediakan bidang key yang dapat membantu dengan ini, karena kuncinya harus sama di berbagai panggilan ke setMenuItems untuk MenuItem yang sama.

Tampilan Gaya Aplikasi

AppStyledView adalah wadah untuk Tampilan yang tidak dikustomisasi sama sekali. Ini dapat digunakan untuk memberikan batas di sekitar Tampilan yang membuatnya menonjol dari aplikasi lainnya, dan menunjukkan kepada pengguna bahwa ini adalah jenis antarmuka yang berbeda. Tampilan yang dibungkus oleh AppStyledView diberikan dalam setContent . AppStyledView juga dapat memiliki tombol kembali atau tutup seperti yang diminta oleh aplikasi.

AppStyledView tidak segera memasukkan Tampilannya ke dalam hierarki Tampilan seperti yang dilakukan installBaseLayoutAround , melainkan hanya mengembalikan tampilannya ke perpustakaan statis melalui getView , yang kemudian melakukan penyisipan. Posisi dan ukuran AppStyledView juga dapat dikontrol dengan menerapkan getDialogWindowLayoutParam .

konteks

Plugin harus berhati-hati saat menggunakan Konteks, karena ada konteks plugin dan "sumber". Konteks plugin diberikan sebagai argumen untuk getPluginFactory , dan merupakan satu-satunya konteks yang dijamin memiliki sumber daya plugin di dalamnya. Ini berarti ini adalah satu-satunya konteks yang dapat digunakan untuk mengembangkan tata letak di plugin.

Namun, konteks plugin mungkin tidak memiliki set Konfigurasi yang benar. Untuk mendapatkan konfigurasi yang benar, kami menyediakan konteks sumber dalam metode yang membuat komponen. Konteks sumber biasanya adalah Aktivitas, tetapi dalam beberapa kasus juga dapat berupa Layanan atau komponen Android lainnya. Untuk menggunakan konfigurasi dari konteks sumber dengan sumber daya dari konteks plugin, konteks baru harus dibuat menggunakan createConfigurationContext . Jika Konfigurasi yang benar tidak digunakan, akan ada pelanggaran mode ketat Android, dan tampilan yang digelembungkan mungkin tidak memiliki dimensi yang benar.

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

Perubahan mode

Beberapa plugin dapat mendukung beberapa mode untuk komponennya, seperti mode olahraga atau mode ramah lingkungan yang terlihat berbeda secara visual. Tidak ada dukungan bawaan untuk fungsionalitas seperti itu di CarUi, tetapi tidak ada yang menghentikan plugin untuk mengimplementasikannya sepenuhnya secara internal. Plugin dapat memantau kondisi apa pun yang diinginkan untuk mengetahui kapan harus beralih mode, seperti mendengarkan siaran. Plugin tidak dapat memicu perubahan konfigurasi untuk mengubah mode, tetapi tetap tidak disarankan untuk mengandalkan perubahan konfigurasi, karena memperbarui tampilan setiap komponen secara manual lebih lancar bagi pengguna dan juga memungkinkan transisi yang tidak mungkin dilakukan dengan perubahan konfigurasi.

Komposisi Jetpack

Plugin dapat diimplementasikan menggunakan Jetpack Compose, tetapi ini adalah fitur tingkat alfa dan tidak boleh dianggap stabil.

Plugin dapat menggunakan ComposeView untuk membuat permukaan yang mendukung Compose untuk dirender. ComposeView ini akan menjadi apa yang dikembalikan dari ke aplikasi dari metode getView dalam komponen.

Salah satu masalah utama dengan menggunakan ComposeView adalah ia menetapkan tag pada tampilan root di tata letak untuk menyimpan variabel global yang dibagikan di berbagai ComposeViews dalam hierarki. Karena id sumber daya plugin tidak diberi namespace terpisah dari aplikasi, hal ini dapat menyebabkan konflik saat aplikasi dan plugin menyetel tag pada Tampilan yang sama. ComposeViewWithLifecycle khusus yang memindahkan variabel global ini ke ComposeView disediakan di bawah ini. Sekali lagi, ini tidak boleh dianggap stabil.

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