Android insets: berurusan dengan ketakutan dan bersiap-siap untuk Android Q

Android Q adalah versi Android kesepuluh dengan API level 29. Salah satu ide utama dari versi baru ini adalah konsep edge-to-edge, ketika aplikasi menempati seluruh layar, dari bawah ke atas. Ini berarti bahwa Bilah Status dan Bilah Navigasi harus transparan. Tetapi, jika mereka transparan, maka tidak ada UI sistem - itu tumpang tindih komponen interaktif aplikasi. Masalah ini diselesaikan dengan insets.

Pengembang seluler menghindari inset, mereka menyebabkan rasa takut di dalamnya. Tetapi di Android Q itu tidak akan mungkin untuk berkeliling insets - Anda harus belajar dan menerapkannya. Bahkan, tidak ada yang rumit tentang insets: mereka menunjukkan elemen layar yang bersinggungan dengan antarmuka sistem dan menyarankan cara memindahkan elemen sehingga tidak bertentangan dengan sistem UI. Konstantin Tskhovrebov akan memberi tahu tentang cara kerja insets dan bagaimana mereka berguna.


Konstantin Tskhovrebov (terrakok) bekerja di Redmadrobot . Telah terlibat dalam Android selama 10 tahun dan telah mengumpulkan banyak pengalaman di berbagai proyek di mana tidak ada tempat untuk insets, mereka selalu berhasil berkeliling entah bagaimana. Konstantin akan menceritakan tentang sejarah panjang menghindari masalah inset, tentang belajar dan melawan Android. Dia akan mempertimbangkan tugas-tugas khas dari pengalamannya di mana insets dapat diterapkan, dan menunjukkan bagaimana berhenti takut pada keyboard, mengenali ukurannya dan merespons penampilan.

Catatan. Artikel ini ditulis berdasarkan laporan oleh Konstantin di Saint AppsConf 2019 . Laporan tersebut menggunakan bahan dari beberapa artikel tentang insets. Tautkan ke bahan-bahan ini di akhir.

Tugas khas


Bilah Status Warna. Dalam banyak proyek, perancang menggambar Bilah Status warna. Menjadi modis ketika Android 5 datang bersama dengan Desain Material baru.



Bagaimana cara melukis Status Bar? Dasar - tambahkan colorPrimaryDarkdan warnanya diganti.

 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="colorPrimaryDark">@color/colorAccent</item>
    ...
</style>

Untuk Android di atas versi kelima (API dari 21 ke atas), Anda dapat mengatur parameter khusus dalam topik:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="android:statusBarColor">@color/colorAccent</item>
    ...
</style>

Bilah Status Berwarna-warni . Terkadang desainer di layar yang berbeda menggambar Bilah Status dalam warna yang berbeda.



Tidak apa-apa, cara termudah untuk bekerja adalah dengan berbagai topik dalam berbagai aktivitas .

Cara yang lebih menarik adalah dengan mengubah warna secara langsung di runtime .

override fun onCreateView(...): View {
    requireActivity().window.statusBarColor = requireContext().getColor(R.color.colorPrimary)
    ...
}

Parameter diubah melalui bendera khusus. Namun hal utama adalah jangan lupa untuk mengubah warna kembali ketika pengguna keluar dari layar kembali.

Bilah Status Transparan. Ini lebih sulit. Paling sering, transparansi dikaitkan dengan peta, karena pada peta itulah transparansi paling baik dilihat. Dalam hal ini, seperti sebelumnya, kami menetapkan parameter khusus:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">    
    ...
    <item name="android:windowTranslucentStatus">true</item>
    ...
</style>

Di sini, tentu saja, ada trik terkenal - untuk membuat indentasi lebih tinggi, jika tidak, Status Bar akan ditumpangkan pada ikon dan itu akan jelek.


Tetapi di layar lain semuanya rusak.



Bagaimana cara mengatasi masalah tersebut? Metode pertama yang muncul dalam pikiran adalah kegiatan yang berbeda: kami memiliki topik yang berbeda, parameter berbeda, mereka bekerja secara berbeda.

Bekerja dengan keyboard. Kami menghindari insets tidak hanya dengan Bilah Status, tetapi juga ketika bekerja dengan keyboard.



Tidak ada yang suka opsi di sebelah kiri, tetapi untuk mengubahnya menjadi opsi di sebelah kanan, ada solusi sederhana.

<activity
    ...
    android:windowSoftInputMode="adjustResize">
    ...
</activity>

Aktivitas sekarang dapat mengubah ukuran saat keyboard muncul. Ia bekerja dengan sederhana dan berat. Tapi jangan lupa trik lain - bungkus semuanya ScrollView. Tiba-tiba keyboard akan menempati seluruh layar dan akan ada strip kecil di atas?

Ada saat-saat yang lebih sulit ketika kita ingin mengubah tata letak ketika keyboard muncul. Misalnya, jika tidak mengatur tombol atau menyembunyikan logo.



Kami melatih banyak kruk dengan keyboard dan Status Bar yang transparan, sekarang sulit untuk menghentikan kami. Pergi ke StackOverflow dan salin kode yang indah.

boolean isOpened = false;

public void setListenerToRootView() {
    final View activityRootView = getWindow().getDecorView().findViewById(android.R.id.content);
    activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
    
            int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();      
            if (heightDiff > 100) { // 99% of the time the height diff will be due to a keyboard.       
                Toast.makeText(getApplicationContext(), "Gotcha!!! softKeyboardup", 0).show();

                if (isOpened == false) {
                   //Do two things, make the view top visible and the editText smaller
                }
                isOpened = true;
            } else if (isOpened == true) {
                Toast.makeText(getApplicationContext(), "softkeyborad Down!!!", 0).show();
                isOpened = false;
            }    
        }
    });
}

Bahkan menghitung probabilitas munculnya keyboard. Kode berfungsi, di salah satu proyek kami bahkan menggunakannya, tetapi untuk waktu yang lama.

Banyak kruk dalam contoh terkait dengan penggunaan berbagai aktivitas. Tetapi mereka buruk tidak hanya karena mereka adalah kruk, tetapi juga karena alasan lain: masalah "mulai dingin", asinkron. Saya menggambarkan masalah secara lebih rinci dalam artikel " Lisensi untuk mengendarai mobil, atau mengapa aplikasi harus Single-Activity ". Selain itu, dokumentasi Google menunjukkan bahwa pendekatan yang disarankan adalah aplikasi Aktivitas Tunggal.

Apa yang kami lakukan sebelum Android 10


Kami (di Redmadrobot) sedang mengembangkan aplikasi yang cukup berkualitas, tetapi telah lama menghindari insets. Bagaimana kami berhasil menghindarinya tanpa kruk besar dan dalam satu aktivitas?

Catatan. Tangkapan layar dan kode diambil dari proyek kesayangan saya GitFox .

Bayangkan layar aplikasi. Ketika kami mengembangkan aplikasi kami, kami tidak pernah berpikir bahwa mungkin ada Bilah Navigasi transparan di bawah ini. Apakah ada bilah hitam di bawah ini? Jadi apa, pengguna sudah terbiasa.



Di bagian atas, kami awalnya menetapkan parameter bahwa Bilah Status hitam dengan transparansi. Bagaimana tampilannya dalam hal tata letak dan kode?



Abstraksi pada gambar: blok merah adalah aktivitas aplikasi, biru adalah fragmen dengan bot navigasi (dengan Tab), dan di dalamnya fragmen hijau dengan konten diaktifkan. Dapat dilihat bahwa Bilah Alat tidak berada di bawah Bilah Status. Bagaimana kita mencapai ini?

Android memiliki bendera yang rumit fitSystemWindow. Jika Anda menyetelnya ke true, wadah akan menambahkan padding ke dirinya sendiri sehingga tidak ada di dalamnya yang berada di bawah Status Bar. Saya percaya bendera ini adalah penopang resmi dari Google untuk mereka yang takut akan inset. Gunakan semuanya akan bekerja relatif baik tanpa insets.

Bendera FitSystemWindow=”true”menambahkan padding ke wadah yang Anda tentukan. Tetapi hierarki itu penting: jika salah satu dari orang tua menetapkan bendera ini menjadi "benar", maka distribusinya tidak akan diperhitungkan, karena wadah sudah menerapkan lekukan.

Bendera berfungsi, tetapi masalah lain muncul. Bayangkan layar dengan dua tab. Saat beralih, transaksi diluncurkan, yang menyebabkan replacefragmen satu sama lain, dan semuanya rusak.



Fragmen kedua juga memiliki set bendera FitSystemWindow=”true”, dan ini seharusnya tidak terjadi. Tetapi apa yang terjadi, mengapa? Jawabannya adalah ini adalah penopang, dan terkadang tidak berfungsi.

Tapi kami menemukan solusi pada Stack Overflow : gunakan root untuk bukan fragmen FrameLayout, tetapi CoordinatorLayout. Itu dibuat untuk tujuan lain, tetapi bekerja di sini.

Mengapa ini berhasil?


Mari kita lihat di sumber apa yang terjadi CoordinatorLayout.

@Override
public void onAttachedToWindow() {   
    super.onAttachedToWindow();
    ...    
    if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {         
        // We're set to fitSystemWindows but we haven't had any insets yet...
        // We should request a new dispatch of window insets
        ViewCompat.requestApplyInsets(this);
    }     
    ...
}

Kami melihat komentar indah bahwa insets diperlukan di sini, tetapi tidak. Kita perlu bertanya kembali ketika kita meminta jendela. Kami menemukan bahwa insets entah bagaimana bekerja di dalam, tetapi kami tidak ingin bekerja dengan mereka dan pergi Coordinator.

Pada musim semi 2019, kami mengembangkan aplikasi, di mana Google I / O baru saja lewat. Kami belum memikirkan semuanya, jadi kami terus berpegang pada prasangka. Tapi kami perhatikan bahwa beralih Tab di bagian bawah entah bagaimana lambat, karena kami memiliki tata letak yang kompleks, dimuat dengan UI. Kami menemukan cara sederhana untuk mengatasi ini - perubahan replaceke show/hidesehingga setiap kali Anda tidak membuat ulang tata letak.



Kami berubah, dan sekali lagi tidak ada yang berhasil - semuanya telah rusak! Tampaknya tidak mungkin untuk menyebarkan kruk, Anda harus mengerti mengapa mereka berhasil. Kami mempelajari kode dan ternyata setiap ViewGroup juga dapat bekerja dengan insets.


@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {    
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {         
        final int count = getChildCount();        
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);             
            if (insets.isConsumed()) {          
                break;
            }
        }
    }
    return insets;
}

Ada logika di dalamnya: jika setidaknya seseorang telah memproses inset, maka semua Tampilan selanjutnya di dalam ViewGroup tidak akan menerimanya. Apa artinya ini untuk kita? Mari saya tunjukkan contoh FrameLayout kami, yang mengganti Tab.



Di dalamnya ada fragmen pertama yang memiliki set bendera fitSystemWindow=”true”. Ini berarti bahwa fragmen pertama memproses insets. Setelah itu, kita sebut HIDEfragmen pertama dan yang SHOWkedua. Tapi yang pertama tetap dalam tata letak - View-nya dibiarkan, hanya disembunyikan.

Wadah melewati View-nya: mengambil fragmen pertama, memberinya inset, dan darinya fitSystemWindow=”true”- ia mengambilnya dan memprosesnya. Sempurna, FrameLayout melihat bahwa insets diproses, dan tidak memberikannya pada fragmen kedua. Semuanya berfungsi sebagaimana mestinya. Tapi ini tidak cocok untuk kita, apa yang harus kita lakukan?

Kami menulis ViewGroup kami sendiri


Kami datang dari Jawa, dan ada OOP dengan segala kemuliaan, jadi kami memutuskan untuk mewarisi. Kami menulis ViewGroup kami sendiri, dari mana kami mendefinisikan ulang metode dispatchApplyWindowInsets. Ini bekerja seperti yang kita butuhkan: selalu memberikan kembali kepada anak-anak inset yang datang, terlepas dari apakah kita memprosesnya atau tidak.

class WindowInsetFrameLayout @JvmOverloads constructor(
    context: Context,      
    attrs: AttributeSet? = null,     
    defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleRes) {

    override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { 
        for (child in (0 until childCount)) {
            getChildAt(child).dispatchApplyWindowInsets(insets)
        }
        return insets.consumeSystemWindowInsets()
    }
}

ViewGroup kustom berfungsi, proyek juga adalah semua yang kita butuhkan. Namun dia masih belum mencapai solusi umum. Ketika kami mengetahui apa yang mereka katakan di Google IO, kami menyadari bahwa kami tidak akan dapat bertindak seperti itu lagi.

Android 10


Android 10 menunjukkan kepada kita dua konsep UI penting yang sangat dianjurkan untuk dipatuhi: edge-to-edge dan Gestural Navigation.

Ujung ke ujung. Konsep ini menunjukkan bahwa konten aplikasi harus menempati semua ruang yang mungkin di layar. Bagi kami, sebagai pengembang, ini berarti bahwa aplikasi harus ditempatkan di bawah panel sistem Status Bar dan Navigation Bar.



Sebelumnya, kami dapat mengabaikan hal ini atau hanya ditempatkan di bawah Bilah Status.

Adapun daftar, mereka harus gulir tidak hanya ke elemen terakhir, tetapi juga lebih jauh agar tidak tetap di bawah Bilah Navigasi.


Navigasi Gerakan. Konsep penting kedua ini adalah navigasi gerakan . Ini memungkinkan Anda untuk mengontrol aplikasi dengan jari-jari Anda dari tepi layar. Mode gerakan tidak standar, semuanya terlihat berbeda, tetapi sekarang Anda tidak dapat beralih di antara dua Bilah Navigasi yang berbeda.

Pada saat ini, kami menyadari bahwa semuanya tidak begitu sederhana. Tidak mungkin menghindari inset lebih lanjut jika kita ingin mengembangkan aplikasi yang berkualitas. Saatnya mempelajari dokumentasi dan memahami apa itu insets.

Insets Apa yang perlu Anda ketahui tentang mereka?


Insets dibuat untuk menakuti pengembang.
Mereka telah melakukan ini dengan sempurna sejak saat itu muncul di Android 5.

Tentu saja, semuanya tidak begitu menakutkan. Konsep insets sederhana - mereka melaporkan overlay sistem UI pada layar aplikasi. Ini bisa menjadi Bar Navigasi, Bar Status atau keyboard. Keyboard juga merupakan sistem UI biasa, yang ditumpangkan di atas aplikasi. Tidak perlu mencoba memprosesnya dengan kruk, hanya insets.

Tujuan insets adalah untuk menyelesaikan konflik . Misalnya, jika ada elemen lain di atas tombol kami, kami dapat memindahkan tombol sehingga pengguna dapat terus menggunakan aplikasi.

Untuk menangani inset, gunakan Windowlnsetsuntuk Android 10 dan WindowInsetsCompatuntuk versi lainnya.

Ada 5 jenis insets di Android 10dan satu "bonus", yang disebut bukan inset, tetapi sebaliknya. Kami akan berurusan dengan semua jenis, karena kebanyakan hanya tahu satu hal - System Window Insets.

Insets jendela sistem


Diperkenalkan di Android 5.0 (API 21). Diperoleh dengan metode getSystemWindowInsets().

Ini adalah jenis utama dari insets yang perlu Anda pelajari cara bekerja, karena sisanya bekerja dengan cara yang sama. Mereka diperlukan untuk memproses Bilah Status, pertama-tama, dan kemudian Bilah Navigasi dan keyboard. Misalnya, mereka memecahkan masalah ketika Bilah Navigasi berada di atas aplikasi. Seperti pada gambar: tombol tetap di bawah Bilah Navigasi, pengguna tidak dapat mengkliknya dan sangat tidak senang.



Insets elemen yang dapat disentuh


Hanya muncul di Android 10. Diperoleh dengan metode getTappableElementInsets().

Inset ini hanya berguna untuk menangani berbagai mode Bar Navigasi . Seperti yang dikatakan Chris Bane sendiri , Anda bisa melupakan jenis inset ini dan hanya berkeliling di System Window Insets. Tetapi jika Anda ingin aplikasi menjadi 100% keren, bukan 99,9% keren, Anda harus menggunakannya.

Lihat gambarnya.



Di atas, warna merah menunjukkan Insets Window Sistem, yang akan datang dalam mode yang berbeda dari Bilah Navigasi. Dapat dilihat bahwa di sebelah kanan dan di sebelah kiri mereka sama dengan Bilah Navigasi.

Ingat cara kerja navigasi gerakan: dalam mode yang tepat, kami tidak pernah mengklik panel baru, tetapi selalu menyeret jari kami dari bawah ke atas. Ini berarti bahwa tombol tidak dapat dihapus. Kita dapat terus menekan tombol FAB (Floating Action Button) kami, tidak ada yang akan mengganggu. Oleh karena itu, TappableElementInsetsakan kosong karena FAB tidak perlu bergerak. Tetapi jika kita bergerak sedikit lebih tinggi, tidak apa-apa.

Perbedaannya hanya muncul dengan navigasi gerakan dan Navigasi Bar transparan (adaptasi warna). Ini bukan situasi yang sangat menyenangkan.



Semuanya akan berfungsi, tetapi terlihat tidak menyenangkan. Pengguna mungkin bingung oleh kedekatan elemen. Dapat dijelaskan bahwa satu untuk gerakan dan yang lainnya untuk menekan, tetapi masih tidak cantik. Karena itu, naikkan FAB lebih tinggi atau tinggalkan di sebelah kanan.

Insets gerakan sistem


Hanya muncul di Android 10. Diperoleh dengan metode getSystemGestureInsets().

Insets ini terkait dengan navigasi gerakan. Awalnya, diasumsikan bahwa sistem UI ditarik di atas aplikasi, tetapi System Gesture Insets mengatakan bahwa itu bukan UI yang ditarik, tetapi sistem itu sendiri yang menangani gerakan. Mereka menggambarkan di mana sistem akan menangani gerakan secara default.

Area-area inset ini kira-kira ditandai dengan warna kuning pada diagram.



Tetapi saya ingin memperingatkan Anda - mereka tidak akan selalu seperti itu. Kita tidak akan pernah tahu apa layar baru Samsung dan produsen lain akan muncul. Sudah ada perangkat di mana layar ada di semua sisi. Mungkin insets tidak akan berada di tempat yang kita harapkan sama sekali. Oleh karena itu, Anda perlu bekerja dengan mereka seperti dengan beberapa abstraksi: ada insets seperti itu di System Gesture Insets di mana sistem itu sendiri memproses gerakan.

Insets ini dapat diganti. Misalnya, Anda sedang mengembangkan editor foto. Di beberapa tempat, Anda sendiri ingin menangani gerakan, meskipun mereka berada di dekat tepi layar. Tunjukkan ke sistem bahwa Anda akan memproses titik pada layar di sudut foto sendiri. Itu dapat didefinisikan ulang dengan memberi tahu sistem: "Saya akan selalu memproses alun-alun sendiri."

Insets gesture sistem wajib


Muncul hanya di Android 10. Ini adalah subtipe dari System Gesture Insets, tetapi tidak dapat ditimpa oleh aplikasi.

Kami menggunakan metode getMandatorySystemGestureInsets()untuk menentukan area di mana mereka tidak akan bekerja. Ini dilakukan dengan sengaja sehingga tidak mungkin untuk mengembangkan aplikasi "putus asa": mendefinisikan kembali gerakan navigasi dari bawah ke atas, yang memungkinkan Anda untuk keluar dari aplikasi ke layar utama. Di sini, sistem selalu memproses gerakan itu sendiri .


Tidak harus itu akan di bagian bawah perangkat dan belum tentu sebesar ini. Bekerja dengannya sebagai abstraksi.

Ini adalah jenis insets yang relatif baru. Tetapi ada yang muncul bahkan sebelum Android 5 dan dipanggil secara berbeda.

Inset yang stabil


Diperkenalkan dengan Android 5.0 (API 21). Diperoleh dengan metode getStableInsets().

Bahkan pengembang yang berpengalaman tidak selalu dapat mengatakan mengapa mereka dibutuhkan. Saya akan memberi tahu Anda rahasia: insets ini hanya berguna untuk aplikasi layar penuh: pemutar video, game. Misalnya, dalam mode pemutaran, semua UI sistem disembunyikan, termasuk Bilah Status, yang bergerak di luar tepi layar. Tapi ada baiknya menyentuh layar, karena Bilah Status akan muncul di atas.

Jika Anda meletakkan tombol KEMBALI di bagian atas layar, saat ia memproses System Window Insets dengan benar, maka dengan setiap Tab, sebuah UI muncul di layar dari atas, dan tombol itu melompat ke bawah. Jika Anda tidak menyentuh layar untuk sementara waktu, Bilah Status menghilang, tombol memantul karena Insets Jendela Sistem telah menghilang.

Untuk kasus seperti itu, Stable Insets tepat. Mereka mengatakan bahwa sekarang tidak ada elemen yang ditarik pada aplikasi Anda, tetapi dapat melakukannya. Dengan insets ini, Anda dapat mengetahui terlebih dahulu nilai dari Bilah Status dalam mode ini, misalnya, dan posisikan tombol di mana Anda inginkan.

Catatan. Metode ini getStableInsets()hanya muncul dengan API 21. Sebelumnya, ada beberapa metode untuk setiap sisi layar. 

Kami memeriksa 5 jenis inset, tetapi ada satu lagi yang tidak berlaku untuk inset secara langsung. Ini membantu menangani poni dan guntingan.

Poni dan garis leher


Layar tidak lagi persegi. Mereka lonjong, dengan satu atau lebih guntingan untuk kamera dan poni di semua sisi.



Hingga 28 API kami tidak dapat memprosesnya. Saya harus menebak melalui insets apa yang terjadi di sana. Tetapi dengan 28 API dan seterusnya (dari Android sebelumnya), kelas secara resmi muncul DisplayCutout. Ada dalam insets yang sama dari mana Anda bisa mendapatkan semua jenis lainnya. Kelas ini memungkinkan Anda untuk mengetahui lokasi dan ukuran artefak.

Selain informasi lokasi, satu set bendera y disediakan untuk pengembang WindowManager.LayoutParams. Mereka memungkinkan Anda untuk memasukkan berbagai perilaku di sekitar guntingan. Konten Anda dapat ditampilkan di sekitarnya, atau mungkin tidak ditampilkan: dalam mode lansekap dan potret dengan berbagai cara.

Benderawindow.attributes.layoutInDisplayCutoutMode =

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. Dalam potret - ada, dan dalam lanskap - bilah hitam secara default.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER. Tidak sama sekali - garis hitam.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - selalu begitu.

Layar dapat dikontrol, tetapi kami bekerja dengan mereka dengan cara yang sama dengan Insets lainnya.

insets.displayCutout
    .boundingRects
    .forEach { rect -> ... }

Panggilan balik dengan insets dengan array datang kepada kami displayCutout. Lalu kita bisa menjalankannya, memproses semua potongan dan poni yang ada di aplikasi. Anda dapat mempelajari lebih lanjut tentang cara bekerja dengan mereka di artikel .

Dipahami dengan 6 jenis insets. Sekarang mari kita bicara tentang cara kerjanya.

Bagaimana itu bekerja


Insets diperlukan, pertama-tama, ketika sesuatu digambar di atas aplikasi, misalnya, Bilah Navigasi.



Tanpa insets, aplikasi tidak akan memiliki Bilah Navigasi dan Bilah Status yang transparan. Jangan kaget bahwa Anda tidak datang SystemWindowInsets, ini terjadi.

Sebarkan insets


Seluruh hierarki UI terlihat seperti pohon dengan Tampilan di ujungnya. Node biasanya merupakan ViewGroup. Mereka juga mewarisi dari View, jadi insets datang kepada mereka dengan cara khusus dispatchApplyWindowInsets. Mulai dari tampilan root, sistem mengirim insets ke seluruh pohon. Mari kita lihat bagaimana View bekerja dalam hal ini.



Sistem menyebutnya metode dispatchApplyWindowInsetsyang digunakan insets. Kembali tampilan ini harus mengembalikan sesuatu. Apa yang harus dilakukan untuk menangani perilaku ini?

Untuk kesederhanaan, kami hanya mempertimbangkan WindowInsets.

Menangani insets


Pertama-tama, redefinisi metode dispatchApplyWindowInsetsdan implementasi di dalam logikanya sendiri tampak logis : insets datang, kami mengimplementasikan semua yang ada di dalamnya.

Jika Anda membuka kelas Lihat dan melihat Javadoc yang ditulis di atas metode ini, Anda dapat melihat: "Jangan menimpa metode ini!"
Kami menangani Insets melalui pendelegasian atau pewarisan.
Gunakan delegasi . Google memberikan kesempatan untuk mengekspos delegasinya sendiri, yang bertanggung jawab untuk menangani inset. Anda dapat menginstalnya melalui metode ini setOnApplyWindowInsetsListener(listener). Kami menetapkan callback yang menangani inset ini.

Jika karena alasan tertentu ini tidak sesuai dengan kami, Anda dapat mewarisi dari Lihat dan mengganti metode lain onApplyWindowInsets(WindowInsets insets) . Kami mengganti insets kita sendiri ke dalamnya.

Google tidak memilih antara delegasi dan warisan, tetapi diizinkan untuk melakukan semuanya bersama. Ini hebat karena kami tidak perlu mendefinisikan ulang semua Toolbar atau ListView untuk menangani insets. Kita dapat mengambil View yang sudah jadi, bahkan yang sudah ada di perpustakaan, dan mengatur delegasi di sana, yang akan menangani insets tanpa redefinisi.

Apa yang akan kami kembalikan?

Kenapa mengembalikan sesuatu?


Untuk melakukan ini, Anda perlu memahami cara distribusi insets, mis., ViewGroup, berfungsi. Apa yang dia lakukan di dalam dirinya. Mari kita lihat lebih dekat perilaku default menggunakan contoh yang saya tunjukkan sebelumnya.

Insets sistem datang di bagian atas dan bawah, misalnya, masing-masing 100 piksel. ViewGroup mengirimkannya ke Tampilan pertama yang dimilikinya. Tampilan ini menanganinya dan mengembalikan insets: di atasnya berkurang menjadi 0, tetapi di bagian bawah ia meninggalkan. Di bagian atas, dia menambahkan padding atau margin, tetapi di bagian bawah dia tidak menyentuh dan melaporkan ini. Selanjutnya, ViewGroup meneruskan insets ke View kedua.



Lihat selanjutnya memproses dan merender insets. Sekarang ViewGroup melihat bahwa insets diproses di atas dan di bawah - tidak ada yang tersisa, semua parameternya nol.

Pandangan ketiga bahkan tidak tahu bahwa ada beberapa inset dan sesuatu sedang terjadi. ViewGroup akan mengembalikan insets kembali kepada orang yang mengekspos mereka.

Ketika kami mulai berurusan dengan topik ini, kami menuliskan ide untuk kami sendiri - selalu mengembalikan insets yang sama yang datang. Kami ingin menghindari situasi seperti itu ketika beberapa View bahkan tidak mengenali bahwa ada insets. Idenya terlihat logis. Tapi ternyata tidak. Google belum menambahkan perilaku sia-sia untuk insets, seperti dalam contoh.

Ini diperlukan agar insets selalu mencapai seluruh hierarki Tampilan dan Anda selalu dapat bekerja dengannya. Dalam hal ini, tidak akan ada situasi ketika kami mengganti dua fragmen, satu telah diproses, dan yang kedua belum diterima. Di bagian latihan, kami akan kembali ke titik ini.

Praktek


Teori sudah selesai. Mari kita lihat tampilannya dalam kode, karena secara teori semuanya selalu mulus. Kami akan menggunakan AndroidX.

Catatan. Di GitFox, semuanya sudah diimplementasikan dalam kode, dukungan penuh untuk semua barang dari Android baru. Agar tidak memeriksa versi Android dan tidak mencari tipe Insets yang diperlukan, selalu gunakan view.setOnApplyWindowInsetsListener { v, insets -> ... }versi dari AndroidX - sebagai gantinya ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> ... }.

Ada layar dengan Bar Navigasi gelap. Itu tidak tumpang tindih elemen apa pun di atas. Semuanya adalah cara kita terbiasa.


Tetapi ternyata sekarang kita perlu membuatnya transparan. Bagaimana?

Nyalakan transparansi


Hal yang paling sederhana adalah menunjukkan dalam subjek bahwa Status Bar dan Navigation Bar transparan.

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>
</style>

Pada saat ini, semuanya mulai saling tumpang tindih baik dari atas maupun dari bawah.

Menariknya, ada dua bendera, dan selalu ada tiga opsi. Jika Anda menentukan transparansi untuk Bilah Navigasi, Bilah Status akan menjadi transparan dengan sendirinya - pembatasan seperti itu. Anda tidak dapat menulis baris pertama, tetapi saya selalu suka kejelasan, jadi saya menulis 2 baris sehingga pengikut dapat memahami apa yang terjadi, dan tidak menggali di dalamnya.

Jika Anda memilih metode ini, Bilah Navigasi dan Bilah Status akan berubah menjadi hitam dengan transparansi. Jika aplikasi berwarna putih, maka Anda dapat menambahkan warna hanya dari kode. Bagaimana cara melakukannya?

Nyalakan transparansi dengan warna


Ada baiknya kami memiliki aplikasi Aktivitas Tunggal, jadi kami menempatkan dua bendera dalam satu aktivitas.

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or  
    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <!-- Set the navigation bar to 50% translucent white -->
    <item name="android:navigationBarColor">#80FFFFFF</item> 
</style>

Ada banyak bendera di sana, tetapi keduanya inilah yang akan membantu membuat transparansi di Bilah Navigasi dan Bilah Status. Warna dapat ditentukan melalui tema.

Ini adalah perilaku aneh Android: sesuatu melalui tema, dan sesuatu melalui bendera. Tapi kita bisa menentukan parameter dalam kode dan subjek. Ini bukan Android begitu buruk, hanya pada Android versi lama bendera yang ditentukan dalam topik akan diabaikan. navigationBarColorIni akan menyoroti bahwa di Android 5 ini tidak, tetapi semuanya akan datang bersama. Di GitFox, dari kode itulah saya mengatur warna Bilah Navigasi.

Kami melakukan penipuan - menunjukkan bahwa Bilah Navigasi berwarna putih dengan transparansi. Sekarang aplikasi terlihat seperti ini.


Apa yang bisa lebih mudah daripada menangani insets?


Memang dasar.

ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->  
        view.updatePadding(bottom = insets.systemWindowInsetBottom) 
        insets
    }

Kami mengambil metode setOnApplyWindowInsetsListenerdan meneruskannya ke dalamnya bottomNav. Kami mengatakan bahwa ketika insets datang, instal padding bawah yang masuk systemWindowInsetBottom. Kami menginginkannya di bawah, tetapi semua elemen yang dapat diklik berada di atas. Kami mengembalikan sepenuhnya insets sehingga semua View lain dalam hierarki kami juga menerimanya.

Semuanya tampak hebat.


Tapi ada yang menangkap. Jika dalam tata letak kami menunjukkan semacam padding di Bilah Navigasi di bawah ini, maka di sini dihapus - setel updatePaddinginset yang sama. Tata letak kami tidak seperti yang kami inginkan.

Untuk menyimpan nilai dari tata letak, Anda harus terlebih dahulu menyimpan apa yang ada di padding bawah. Kemudian, ketika insets tiba, tambahkan dan atur nilai yang dihasilkan.

val bottomNavBottomPadding = bottomNav.paddingBottom
ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->                 
        view.updatePadding(
        bottom = bottomNavBottomPadding + insets.systemWindowInsetBottom
    )
    Insets
}

Tidak nyaman untuk menulis dengan cara ini: di mana pun dalam kode tempat Anda menggunakan inset, Anda perlu menyimpan nilai dari tata letak, lalu menambahkannya ke UI. Tapi ada Kotlin, dan ini luar biasa - Anda dapat menulis ekstensi Anda sendiri, yang akan melakukan semua ini untuk kami.

Tambahkan Kotlin!


Kami ingat initialPaddingdan memberikannya kepada pawang ketika insets baru datang (bersama mereka). Ini akan membantu mereka entah bagaimana menambahkan bersama atau membangun semacam logika dari atas.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {    

    val initialPadding = recordInitialPaddingForView(this)

    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
    
    requestApplyInsetsWhenAttached()
}

private fun recordInitialPaddingForView(view: View) =
   Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

  .

bottomNav.doOnApplyWindowInsets { view, insets, padding ->   
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    ) 
    insets
}

Kita harus mendefinisikan kembali lambda. Tidak hanya memiliki insets, tetapi juga paddings. Kami dapat menambahkan mereka tidak hanya dari bawah, tetapi juga dari atas, jika itu adalah Bilah Alat yang memproses Bilah Status.

Sesuatu dilupakan!


Ada panggilan ke metode yang tidak bisa dipahami.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {

    val initialPadding = recordInitialPaddingForView(this)
     
    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
      
    requestApplyInsetsWhenAttached() 
}

private fun recordInitialPaddingForView(view: View) =
      Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

Ini disebabkan oleh fakta bahwa Anda tidak bisa hanya berharap bahwa sistem akan mengirimi Anda inset. Jika kita membuat tampilan dari kode atau menginstal InsetsListenersedikit lebih lama, sistem itu sendiri mungkin tidak melewati nilai terakhir.

Kami harus memeriksa bahwa ketika Lihat di layar, kami memberi tahu sistem: "Insets telah tiba, kami ingin memprosesnya." Kami mengatur Pendengar dan harus membuat permintaan requestsApplyInsets. "

fun View.requestApplyInsetsWhenAttached() {    
    if (isAttachedToWindow) {        
        requestApplyInsets()
    } else {
        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            
            override fun onViewAttachedToWindow(v: View) {
                v.removeOnAttachStateChangeListener(this)
                v.requestApplyInsets()           
            }

            override fun onViewDetachedFromWindow(v: View) = Unit
        })
    }
}

Jika kami membuat kode Tampilan dan belum melampirkannya ke tata letak kami, maka kami harus berlangganan waktu kapan kami melakukannya. Hanya kemudian kami meminta insets.

Tapi sekali lagi, ada Kotlin: mereka menulis ekstensi sederhana dan kita tidak perlu memikirkannya lagi.

Memperbaiki RecyclerView


Sekarang mari kita bicara tentang menyelesaikan RecyclerView. Elemen terakhir jatuh di bawah Bilah Navigasi dan tetap di bawah - jelek. Juga tidak nyaman untuk mengkliknya. Jika ini bukan panel baru, tapi yang lama, yang besar, maka secara umum seluruh elemen mungkin hilang di bawahnya. Apa yang harus dilakukan?



Gagasan pertama adalah menambahkan elemen di bawah ini, dan mengatur ketinggian agar sesuai dengan insets. Tetapi jika kita memiliki aplikasi dengan ratusan daftar, maka entah bagaimana kita harus berlangganan setiap daftar ke insets, tambahkan elemen baru di sana dan atur tingginya. Selain itu, RecyclerView asynchronous, tidak diketahui kapan akan bekerja. Ada banyak masalah.

Tetapi ada peretasan resmi lainnya. Anehnya, ini bekerja secara efisien.

<androidx.recyclerview.widget.RecyclerView 
        android:id="@+id/recyclerView"        
    android:layout_width="match_parent"    
    android:layout_height="match_parent"             
    android:clipToPadding="false" />

recyclerView.doOnApplyWindowInsets { view, insets, padding ->    
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    )
}

Ada bendera seperti itu di tata letak clipToPadding. Secara default, selalu benar. Ini menunjukkan bahwa Anda tidak perlu menggambar elemen yang muncul di mana bantalan diekspos. Tetapi jika diatur clipToPadding="false", maka Anda bisa menggambar.

Sekarang, jika Anda mengatur padding ke bawah, di RecyclerView itu akan berfungsi seperti ini: padding diatur ke bawah, dan elemen digambar di atasnya sampai kita gulir. Setelah mencapai akhir, RecyclerView akan menggulir elemen ke posisi yang kita butuhkan.

Dengan mengatur satu flag seperti itu, kita dapat bekerja dengan RecyclerView seperti dengan Tampilan normal. Jangan berpikir bahwa ada elemen yang menggulir - cukup setel bantalan di bawah ini, seperti, misalnya, di Bilah Navigasi Bawah.

Sekarang kami selalu mengembalikan insets seolah-olah tidak diproses. Seluruh inset datang kepada kami, kami melakukan sesuatu dengan mereka, mengatur bantalan dan mengembalikan semua inset lagi. Ini diperlukan agar setiap ViewGroup selalu meneruskan inset ini ke semua View. Berhasil.

Bug aplikasi


Di banyak aplikasi di Google Play yang sudah memproses insets, saya perhatikan ada bug kecil. Sekarang saya akan menceritakan tentang dia.

Ada layar dengan navigasi di ruang bawah tanah. Di sebelah kanan adalah layar yang sama yang menunjukkan hierarki. Fragmen hijau menampilkan konten di layar, di dalamnya ada RecyclerView.



Siapa yang menangani inset di sini? Bilah Alat: ia menerapkan lapisan di atas sehingga kontennya bergerak di bawah Bilah Status. Dengan demikian, Bilah Navigasi Bawah menerapkan insets dari bawah dan naik di atas Bilah Navigasi. Tetapi RecyclerView tidak menangani insets dengan cara apa pun, tidak termasuk di dalamnya, tidak perlu memproses insets - semuanya dilakukan dengan benar.



Tapi di sini ternyata kita ingin menggunakan fragmen RecyclerView hijau di tempat lain di mana Bilah Navigasi Bawah tidak lagi ada. Pada titik ini, RecyclerView sudah mulai menangani insets dari bawah. Kita perlu menerapkan pelapis untuk menggulir dengan benar dari bawah Bilah Navigasi. Oleh karena itu, dalam RecyclerView, kami juga menambahkan pemrosesan inset.



Kami kembali ke layar kami, tempat semua orang memproses insets. Ingat, tidak ada yang melaporkan bahwa dia memprosesnya?



Kita melihat situasi ini: RecyclerView memproses insets dari bawah, meskipun ia tidak mencapai Bilah Navigasi - ruang muncul di bagian bawah. Saya perhatikan ini dalam aplikasi, dan cukup besar dan populer.



Jika ini masalahnya, kami ingat bahwa kami mengembalikan semua inset untuk diproses. Jadi (ternyata!) Penting untuk melaporkan bahwa insets diproses: Bar Navigasi harus melaporkan bahwa insets diproses. Mereka seharusnya tidak sampai ke RecyclerView.



Untuk melakukan ini, instal Bilah Navigasi Bawah InsetsListener. Kurangi di dalam bottom + insetsbahwa mereka tidak diproses, kembalikan 0.

doOnApplyWindowInsets { view, insets, initialPadding ->    
    view.updatePadding(
        bottom = initialPadding.bottom + insets.systemWindowInsetBottom
    )
    insets.replaceSystemWindowInsets(
        Rect(
            insets.systemWindowInsetLeft,            
            insets.systemWindowInsetTop,            
            insets.systemWindowInsetRight,
            0
        )
    )
}

Kami mengembalikan insets baru, di mana semua parameter lama sama, tetapi bottom + insetssama dengan 0. Tampaknya semuanya baik-baik saja. Kami mulai dan lagi omong kosong - RecyclerView masih menangani inset karena beberapa alasan.



Saya tidak segera mengerti bahwa masalahnya adalah wadah untuk ketiga Tampilan ini adalah LinearLayout. Di dalamnya ada Bilah Alat, cuplikan dengan RecyclerView dan di bagian bawah Bilah Navigasi Bawah. LinearLayout mengambil anak-anaknya secara berurutan dan menerapkan insets kepada mereka.

Ternyata Bilah Navigasi Bawah akan menjadi yang terakhir. Dia mengatakan kepada seseorang bahwa dia memproses semua inset, tetapi sudah terlambat. Semua inset diproses dari atas ke bawah, RecyclerView telah menerimanya, dan ini tidak menyelamatkan kita.



Apa yang harus dilakukan? LinearLayout tidak berfungsi seperti yang saya inginkan, itu mentransfer mereka dari atas ke bawah, dan saya harus mendapatkan yang bawah terlebih dahulu.

Tetapkan Semua


Pengembang Java bermain dalam diri saya - semuanya perlu didefinisikan ulang ! Nah, sekarang dispatchApplyWindowInsetsdefinisikan ulang, atur yLinearLayout, yang akan selalu pergi dari bawah ke atas. Dia pertama-tama akan mengirim insets Bottom Navigation Bar, dan kemudian ke orang lain.

@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {     
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {        
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);
            if (insets.isConsumed()) {
                break;
            }
        }
    }
    return insets;
}

Tetapi saya berhenti tepat waktu, mengingat komentar bahwa tidak perlu mendefinisikan kembali metode ini.

Di sini, sedikit lebih tinggi adalah keselamatan: ViewGroup memeriksa delegasi yang memproses insets, kemudian memanggil metode super pada View dan memungkinkan pemrosesan sendiri. Dalam kode ini, kita mendapatkan insets, dan jika belum diproses, kita mulai logika standar untuk anak-anak kita.

Saya menulis ekstensi sederhana. Hal ini memungkinkan, berlaku InsetsListeneruntuk satu Tampilan, untuk mengatakan kepada siapa insets ini lulus.

fun View.addSystemBottomPadding(    
    targetView: View = this
) {
    doOnApplyWindowInsets { _, insets, initialPadding ->           
        targetView.updatePadding(
            bottom = initialPadding.bottom + insets.systemWindowInsetBottom
        )
        insets.replaceSystemWindowInsets(
            Rect(
                insets.systemWindowInsetLeft,
                insets.systemWindowInsetTop,     
                insets.systemWindowInsetRight,
                0
            )
        )
    }
}

Ini targetViewsama dengan default untuk tampilan yang sama di atas yang kami terapkan addSystemBottomPadding. Kita bisa mendefinisikannya kembali.

Di LinearLayout saya, saya menggantung handler, melewati targetView- ini adalah Bottom Navigation Bar saya.

  • Pertama dia akan memberikan insets Bottom Navigation Bar. 
  • Itu akan memproses insets, akan mengembalikan nol bawah.
  • Selanjutnya, secara default, mereka akan pergi dari atas ke bawah: Toolbar, fragmen dengan RecyclerView.
  • Lalu, mungkin, dia akan kembali mengirim insets Bottom Navigation Bar. Tapi ini tidak lagi penting, semuanya akan bekerja dengan baik.

Saya mencapai apa yang saya inginkan: semua inset diproses sesuai urutan yang dibutuhkan.



Penting


Beberapa hal penting yang perlu diingat.

Keyboard adalah UI sistem di atas aplikasi Anda. Tidak perlu memperlakukannya dengan cara khusus. Jika Anda melihat keyboard Google, itu bukan Layout dengan tombol yang bisa Anda klik. Ada ratusan mode untuk keyboard ini: mencari gif dan meme, input suara, mengubah ukuran dari 10 piksel hingga ukuran layar. Jangan berpikir tentang keyboard, tetapi berlangganan insets.

Laci Navigasi masih tidak mendukung navigasi gerakan. Di Google IO, mereka berjanji bahwa semuanya akan berjalan baik. Tetapi di Android 10, Navigasi Drawer masih tidak mendukung ini. Jika Anda meningkatkan ke Android 10 dan mengaktifkan navigasi dengan gerakan, Drawer Navigasi akan jatuh. Sekarang Anda perlu mengklik menu hamburger untuk membuatnya muncul, atau kombinasi keadaan acak memungkinkan Anda untuk merentangkannya.

Di Android versi pra-alfa, Navigasi Drawer berfungsi, tetapi saya tidak berani memperbarui - ini adalah pra-alfa. Oleh karena itu, bahkan jika Anda menginstal versi terbaru GitFox dari repositori, ada Drawer Navigasi di sana, tetapi tidak dapat ditarik. Segera setelah dukungan resmi keluar, saya akan memperbarui dan semuanya akan berfungsi dengan baik.

Daftar Periksa Persiapan Android 10


Tetapkan transparansi dari Bilah Navigasi dan Bilah Status dari awal proyek . Google sangat menyarankan mempertahankan Bilah Navigasi transparan. Bagi kami, ini adalah bagian praktis yang penting. Jika proyek berhasil, tetapi Anda tidak menyalakannya, pilih waktu untuk mendukung Android 10. Aktifkan terlebih dahulu dalam subjek, seperti tembus cahaya, dan perbaiki tata letak di mana proyek itu rusak.

Tambahkan ekstensi ke Kotlin - lebih mudah dengan mereka.

Tambahkan ke semua Bilah Alat di atas padding. Bilah alat selalu ada di atas, dan itulah yang perlu Anda lakukan.

Untuk semua RecyclerViews, tambahkan padding dari bawah dan kemampuan untuk menggulir clipToPadding="false".

Pikirkan semua tombol di tepi layar (FAB) . FAB kemungkinan besar tidak berada di tempat yang Anda harapkan.

Jangan mengganti atau membuat implementasi Anda sendiri untuk semua LinearLayout dan kasus serupa lainnya. Lakukan triknya, seperti di GitFox, atau ambil ekstensi yang sudah jadi untuk membantu.

Periksa gerakan di tepi layar untuk Lihat kustom . Laci Navigasi tidak mendukung ini. Tetapi tidak begitu sulit untuk mendukungnya dengan tangan Anda, untuk mendefinisikan kembali inset untuk gerakan di layar dengan Navigasi Drawer. Mungkin Anda memiliki editor gambar tempat gestur bekerja.

Menggulir di ViewPager tidak bekerja dari tepi, tetapi hanya dari tengah ke kanan dan kiri . Google mengatakan ini normal. Jika Anda menarik ViewPager dari tepi, maka itu dianggap sebagai menekan tombol "Kembali".

Dalam proyek baru, segera sertakan semua transparansi. Anda dapat memberi tahu desainer bahwa Anda tidak memiliki Bilah Navigasi dan Bilah Status. Seluruh kotak adalah konten aplikasi Anda. Dan pengembang sudah akan mencari tahu di mana dan apa yang harus dibesarkan.

Tautan yang bermanfaat:

  • @rmr_spb dalam telegram - catatan mitaps internal Redmadrobot SPb.
  • Semua kode sumber dalam artikel ini dan bahkan lebih banyak lagi di GitFox . Ada cabang Android 10 yang terpisah, di mana Anda dapat melihat dengan mengkomit bagaimana saya mendapatkan semua ini dan bagaimana saya mendukung Android baru.
  • Perpustakaan Insetter Chris Bane. Ini berisi 5-6 ekstensi yang saya tunjukkan. Untuk digunakan dalam proyek Anda, hubungi perpustakaan, kemungkinan besar itu akan pindah ke Android Jetpack. Saya mengembangkan mereka dalam proyek saya dan, menurut saya, mereka menjadi lebih baik.
  • Artikel Chris Bane.
  • FunCorn, , .
  • «Why would I want to fitsSystemWindows?» .

AppConf, youtube- . , - . AppsConf , . , , stay tuned!

All Articles