Cara mendeskripsikan secara deskriptif toolbar yang runtuh



Saya ingin memperkenalkan solusi bagaimana CollapsingToolbar dapat dijelaskan, dengan penekanan pada keterbacaan kode. Artikel tidak akan menjelaskan apa dan bagaimana cara menulis CoordinatorLayout.Behavior Anda. Jika pembaca tertarik untuk memahami ini, ada banyak artikel, termasuk yang ada di Habr . Jika Anda tidak ingin mengerti, tidak apa-apa: Saya mencoba untuk mengeja CollapsingToolbar sehingga saya bisa abstrak dari CoordinatorLayout.Behavior dan OnOffsetChangedListener.

Ketentuan


  • Bilah Alat - sekumpulan tampilan yang ingin kami tampilkan di bagian atas layar (bukan android.widget.Toolbar).
  • NestedScroll - semua tampilan yang dapat digulir yang dapat dikaitkan dengan AppBarLayout (RecyclerView, NestedScrollView).

Mengapa Anda perlu menulis keputusan Anda?


Saya melihat beberapa pendekatan di "Internet", dan hampir semua dibangun sebagai berikut:

  1. Mengatur ketinggian tetap untuk AppBarLayout.
  2. CoordinatorLayout.Behavior ditulis, di mana, dengan beberapa perhitungan (tinggi tampilan cache ditambahkan ke bagian bawah tampilan lain dan minus margin dikalikan dengan gulir, dihitung di sini) mereka mengubah beberapa jenis tampilan.
  3. Tampilan lain berubah di OnOffsetChangedListener dari AppBarLayout.

Berikut adalah contoh Perilaku dengan pendekatan yang dijelaskan, 2.5k membintangi Github.

Menunggu

Realita: pakai OnePlus Anda

Anda dapat memperbaiki tata letak untuk solusi ini, tetapi ada hal lain yang membingungkan saya. Beberapa tampilan dikelola melalui OnOffsetChangedListener, beberapa melalui Behavior, sesuatu berfungsi di luar kotak. Untuk memahami keseluruhan gambar, pengembang harus membahas banyak kelas, dan jika untuk tampilan baru perlu menambahkan perilaku yang tergantung pada Perilaku lain dan pada pandangan yang berubah di OnOffsetChangedListener, kruk dan bug dapat tumbuh secara tiba-tiba

Selain itu, contoh ini tidak menunjukkan apa yang harus dilakukan jika elemen tambahan ditambahkan ke bilah alat yang memengaruhi ketinggian bilah alat ini.

Di gif di awal artikel, Anda dapat melihat bagaimana TextView disembunyikan dengan mengklik tombol - dan NestedScroll ditarik lebih tinggi sehingga tidak ada ruang kosong).

GIF lagi

Bagaimana cara melakukannya? Solusi yang muncul pertama kali adalah menulis CoordinatorLayout.Behavior lain untuk NestedScroll (menjaga logika AppBarLayout.Behavior yang mendasarinya) atau menempelkan bilah alat di AppBarLayout dan mengubahnya ke OnOffsetChangedListener. Saya mencoba kedua solusi, dan ternyata kode terikat dengan detail implementasi, yang akan sangat sulit bagi orang lain untuk mengerti dan tidak dapat digunakan kembali.

Saya akan senang jika seseorang membagikan contoh di mana logika seperti itu diterapkan "dengan bersih", tetapi untuk sekarang saya akan menunjukkan solusi saya. Idenya adalah untuk dapat mendeskripsikan secara deklaratif di satu tempat yang dilihat dan bagaimana mereka harus bersikap.

Seperti apa api itu?


Jadi, untuk membuat CoordinatorLayout.Behavior yang Anda butuhkan:

  • mewarisi BehaviorByRules;
  • mengganti metode yang mengembalikan AppBarLayout, CollapsingToolbarLayout dan panjang gulir (tinggi AppBarLayout).
  • menimpa metode setUpViews - menjelaskan aturan tentang bagaimana tampilan akan berperilaku ketika bilah gulir appBar berubah.

TopInfoBehavior untuk bilah alat dari gif di awal artikel akan terlihat seperti ini (nanti dalam artikel saya akan menjelaskan cara kerjanya):

Tata letak

TopInfoBehavior.kt
class TopInfoBehavior( context: Context?, attrs: AttributeSet? ) : BehaviorByRules(context, attrs) { override fun calcAppbarHeight(child: View): Int = with(child) { return (height + pixels(R.dimen.toolbar_height)).toInt() } override fun View.provideAppbar(): AppBarLayout = ablAppbar override fun View.provideCollapsingToolbar(): CollapsingToolbarLayout = ctlToolbar override fun View.setUpViews(): List<RuledView> = listOf( RuledView( viewGroupTopDetails, BRuleYOffset( min = pixels(R.dimen.zero), max = pixels(R.dimen.toolbar_height) ) ), RuledView( textViewTopDetails, BRuleAlpha(min = 0.6f, max = 1f) .workInRange(from = appearedUntil, to = 1f), BRuleXOffset( min = 0f, max = pixels(R.dimen.big_margin), interpolator = ReverseInterpolator(AccelerateInterpolator()) ), BRuleYOffset( min = pixels(R.dimen.zero), max = pixels(R.dimen.pad), interpolator = ReverseInterpolator(LinearInterpolator()) ), BRuleAppear(0.1f), BRuleScale(min = 0.8f, max = 1f) ), RuledView( textViewPainIsTheArse, BRuleAppear(isAppearedUntil = GONE_VIEW_THRESHOLD) ), RuledView( textViewCollapsedTop, BRuleAppear(0.1f, true) ), RuledView( textViewTop, BRuleAppear(isAppearedUntil = GONE_VIEW_THRESHOLD) ), buildRuleForIcon(ivTop, LinearInterpolator()), buildRuleForIcon(ivTop2, AccelerateInterpolator(0.7f)), buildRuleForIcon(ivTop3, AccelerateInterpolator()) ) private fun View.buildRuleForIcon( view: ImageView, interpolator: Interpolator ) = RuledView( view, BRuleYOffset( min = -(ivTop3.y - tvCollapsedTop.y), max = 0f, interpolator = DecelerateInterpolator(1.5f) ), BRuleXOffset( min = 0f, max = tvCollapsedTop.width.toFloat() + pixels(R.dimen.huge_margin), interpolator = ReverseInterpolator(interpolator) ) ) companion object { const val GONE_VIEW_THRESHOLD = 0.8f } } 


Tata letak Xml (menghapus atribut yang jelas untuk dibaca)
 <android.support.design.widget.CoordinatorLayout> <android.support.design.widget.AppBarLayout android:layout_height="wrap_content"> <android.support.design.widget.CollapsingToolbarLayout app:layout_scrollFlags="scroll|exitUntilCollapsed"> <android.support.v7.widget.Toolbar android:layout_height="@dimen/toolbar_height" app:layout_collapseMode="pin"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <!--  --> <RelativeLayout android:translationZ="5dp" app:layout_behavior="TopInfoBehavior"/> <android.support.v4.widget.NestedScrollView app:layout_behavior="@string/appbar_scrolling_view_behavior"> </android.support.v4.widget.NestedScrollView> <android.support.design.widget.FloatingActionButton app:layout_anchor="@id/nesteScroll" app:layout_anchorGravity="right"/> </android.support.design.widget.CoordinatorLayout> 


Bagaimana cara kerjanya


Tugasnya adalah menulis aturan:

 interface BehaviorRule { /** * @param view to be changed * @param details view's data when first attached * @param ratio in range [0, 1]; 0 when toolbar is collapsed */ fun manage(ratio: Float, details: InitialViewDetails, view: View) } 

Semuanya jelas di sini - nilai float dari 0 hingga 1 masuk, yang mencerminkan persentase pengguliran ActionBar, tampilan dan keadaan awalnya datang. Itu terlihat lebih menarik BaseBehaviorRule - aturan dari mana aturan dasar lainnya diwarisi.

 abstract class BaseBehaviorRule : BehaviorRule { abstract val interpolator: Interpolator abstract val min: Float abstract val max: Float final override fun manage( ratio: Float, details: InitialViewDetails, view: View ) { val interpolation = interpolator.getInterpolation(ratio) val offset = normalize( oldValue = interpolation, newMin = min, newMax = max ) perform(offset, details, view) } /** * @param offset normalized with range from [min] to [max] with [interpolator] */ abstract fun perform(offset: Float, details: InitialViewDetails, view: View) } /** * Affine transform value form one range into another */ fun normalize( oldValue: Float, newMin: Float, newMax: Float, oldMin: Float = 0f, oldMax: Float = 1f ): Float = newMin + ((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin) 

Untuk aturan dasar, rentang nilai (min, maks) dan interpolator ditentukan. Ini cukup untuk menggambarkan hampir semua perilaku.

Misalkan kita ingin mengatur alpha untuk tampilan kita di kisaran 0,5 hingga 0,9. Kami juga ingin tampilan gulir dengan cepat menjadi transparan terlebih dahulu, dan kemudian laju perubahan akan turun.
Aturannya akan terlihat seperti ini:

 BRuleAlpha(min = 0.5f, max = 0.9f, interpolator = DecelerateInterpolator()) 

Dan di sini adalah implementasi dari BRuleAlpha:

BRuleAlpha.kt
 /** * [min], [max] — values in range [0, 1] */ class BRuleAlpha( override val min: Float, override val max: Float, override val interpolator: Interpolator = LinearInterpolator() ) : BaseBehaviorRule() { override fun perform(offset: Float, details: InitialViewDetails, view: View) { view.alpha = offset } } 


Dan akhirnya, kode BehaviorByRules. Bagi mereka yang menulis Perilaku mereka, semuanya harus jelas (kecuali untuk apa yang ada di onMeasureChild, saya akan membicarakan ini di bawah):

BehaviorByRules.kt
 abstract class BehaviorByRules( context: Context?, attrs: AttributeSet? ) : CoordinatorLayout.Behavior<View>(context, attrs) { private var views: List<RuledView> = emptyList() private var lastChildHeight = -1 private var needToUpdateHeight: Boolean = true override fun layoutDependsOn( parent: CoordinatorLayout, child: View, dependency: View ): Boolean { return dependency is AppBarLayout } override fun onDependentViewChanged( parent: CoordinatorLayout, child: View, dependency: View ): Boolean { if (views.isEmpty()) views = child.setUpViews() val progress = calcProgress(parent) views.forEach { performRules(offsetView = it, percent = progress) } tryToInitHeight(child, dependency, progress) return true } override fun onMeasureChild( parent: CoordinatorLayout, child: View, parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int, heightUsed: Int ): Boolean { val canUpdateHeight = canUpdateHeight(calcProgress(parent)) if (canUpdateHeight) { parent.post { val newChildHeight = child.height if (newChildHeight != lastChildHeight) { lastChildHeight = newChildHeight setUpAppbarHeight(child, parent) } } } else { needToUpdateHeight = true } return super.onMeasureChild( parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed ) } /** * If you use fitsSystemWindows=true in your coordinator layout, * you will have to include statusBar height in the appbarHeight */ protected abstract fun calcAppbarHeight(child: View): Int protected abstract fun View.setUpViews(): List<RuledView> protected abstract fun View.provideAppbar(): AppBarLayout protected abstract fun View.provideCollapsingToolbar(): CollapsingToolbarLayout /** * You man not want to update height, if height depends on views, that are currently invisible */ protected open fun canUpdateHeight(progress: Float): Boolean = true private fun calcProgress(parent: CoordinatorLayout): Float { val appBar = parent.provideAppbar() val scrollRange = appBar.totalScrollRange.toFloat() val scrollY = Math.abs(appBar.y) val scroll = 1 - scrollY / scrollRange return when { scroll.isNaN() -> 1f else -> scroll } } private fun setUpAppbarHeight(child: View, parent: ViewGroup) { parent.provideCollapsingToolbar().setHeight(calcAppbarHeight(child)) } private fun tryToInitHeight(child: View, dependency: View, scrollPercent: Float) { if (needToUpdateHeight && canUpdateHeight(scrollPercent)) { setUpAppbarHeight(child, dependency as ViewGroup) needToUpdateHeight = false } } private fun performRules(offsetView: RuledView, percent: Float) { val view = offsetView.view val details = offsetView.details offsetView.rules.forEach { rule -> rule.manage(percent, details, view) } } } 


Jadi, ada apa dengan onMeasureChild?

Ini diperlukan untuk menyelesaikan masalah yang saya tulis di atas: jika beberapa bagian bilah alat menghilang, NestedScroll akan bergerak lebih tinggi. Untuk membuatnya naik lebih tinggi, Anda perlu mengurangi ketinggian CollapsingToolbarLayout.

Ada metode lain yang tidak jelas - canUpdateHeight. Ini diperlukan agar Anda dapat mengizinkan pewaris untuk menetapkan aturan saat Anda tidak dapat mengubah ketinggian. Misalnya, jika tampilan tempat bergantung ketinggian saat ini tersembunyi. Saya tidak yakin ini akan mencakup semua kasus, tetapi jika ada yang punya ide tentang bagaimana melakukan lebih baik, silakan tulis komentar atau pesan pribadi.

Rake yang bisa Anda injak ketika bekerja dengan CollapsingToolbarLayout


  • Saat mengubah tampilan, Anda harus menghindari onLayout. Misalnya, Anda tidak boleh mengubah layoutParams atau textSize di dalam BehaviorRule, jika tidak, kinerja akan berkurang secara drastis.
  • Jika Anda ingin bekerja dengan bilah alat melalui OnOffsetChangedListener, onLayout bahkan lebih berbahaya - metode onOffsetChanged akan memicu tanpa batas.
  • CoordinatorLayout.Behavior tidak harus bergantung pada tampilan (layoutDependsOn), yang dapat masuk ke visibilitas GONE. Ketika tampilan ini kembali ke View.VISIBLE, Perilaku tidak akan bereaksi.
  • Jika bilah alat berada di luar AppBarLayout, maka untuk mencegah pemblokiran bilah alat, Anda perlu menambahkan atribut android: translationZ = "5dp" ke induk ViewGroup pada bilah alat.

Kesimpulannya


Kami memiliki solusi yang memungkinkan Anda membuat sketsa CollapsingToolbarLayout Anda dengan logika yang relatif mudah dibaca dan dimodifikasi. Semua aturan dan dependensi dibentuk dalam kerangka kerja satu kelas - CoordinatorLayout.Behavior. Kode dapat dilihat di github .

Source: https://habr.com/ru/post/id426369/


All Articles