
コードの読みやすさを重視して、CollapsingToolbarの記述方法のソリューションを紹介したいと思います。 この記事では、CoordinatorLayout.Behaviorの概要と作成方法については説明しません。 読者がこれを理解することに興味があるなら、
Habrの記事を含む多くの記事があります。 理解したくない場合は、大丈夫です。CoordinatorLayout.BehaviorおよびOnOffsetChangedListenerから抽象化できるように、CollapsingToolbarのスペルを取得しようとしました。
規約
- ツールバー-画面の上部に表示するビューのセット(android.widget.Toolbarではありません)。
- NestedScroll-AppBarLayoutに関連付けることができるスクロール可能なビュー(RecyclerView、NestedScrollView)。
なぜ決定を書く必要があったのですか
「インターネット」でいくつかのアプローチを検討しましたが、ほとんどすべてが次のように構築されました。
- AppBarLayoutの固定の高さを設定します。
- CoordinatorLayout.Behaviorは、いくつかの計算(キャッシュされたビューの高さを別のビューの下部に追加し、マージンにここで計算したスクロールを掛けたもの)で、ある種のビューを変更します。
- AppBarLayoutのOnOffsetChangedListenerで他のビューが変更されます。
以下に
、 Githubの2.5kの星で説明したアプローチ
での動作の例を示します。
このソリューションのレイアウトを修正できますが、他の何かが私を悩ませます。 一部のビューはOnOffsetChangedListenerによって管理され、一部のビューは動作によって管理されます。 全体像を理解するために、開発者は多くのクラスを通過する必要があり、新しいビューのために、他の動作とOnOffsetChangedListenerで変化するビューに依存する動作を追加する必要がある場合、松葉杖とバグは急成長する可能性があります
さらに、この例では、このツールバーの高さに影響する要素がツールバーに追加された場合の対処方法を示していません。
記事の冒頭のgifで、ボタンをクリックしてTextViewが非表示になっていることを確認できます。NestedScrollを引き上げると、空のスペースがなくなります。
どうやってやるの? 最初に思い浮かぶ解決策は、別のCoordinatorLayout.BehaviorをNestedScrollに書き込む(基になるAppBarLayout.Behaviorのロジックを保持する)か、AppBarLayoutにツールバーを貼り付けて、OnOffsetChangedListenerに変更することです。 両方のソリューションを試しましたが、実装の詳細に関連付けられたコードが判明しました。これは、他の誰かが理解するのは非常に難しく、再利用できませんでした。
そのようなロジックが「きれいに」実装されている例を誰かが共有してくれれば嬉しいが、今のところは私のソリューションを紹介する。 考え方は、どのビューをどのように動作させるかを
1か所で
宣言的に説明できるようにすることです。
APIはどのように見えますか?
したがって、CoordinatorLayout.Behaviorを作成するには次が必要です。
- BehaviorByRulesを継承
- AppBarLayout、CollapsingToolbarLayoutおよびスクロールの長さ(AppBarLayoutの高さ)を返すメソッドをオーバーライドします。
- setUpViewsメソッドを再定義します-appBarのスクロールバーが変更されたときのビューの動作規則を記述します。
記事の冒頭にあるgifのツールバーのTopInfoBehaviorは、次のようになります(記事の後半で、その仕組みを説明します)。
TopInfoBehavior.ktclass 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 } }
XMLレイアウト(読みやすいように明らかな属性を削除) <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>
仕組み
タスクはルールを書くことです:
interface BehaviorRule { fun manage(ratio: Float, details: InitialViewDetails, view: View) }
ここではすべてが明確です。フロート値は0から1で、ActionBarのスクロールの割合、ビュー、およびその初期状態を反映しています。 それはもっと興味深いBaseBehaviorRule-他の基本的なルールが継承されるルールに見えます。
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) } abstract fun perform(offset: Float, details: InitialViewDetails, view: View) } fun normalize( oldValue: Float, newMin: Float, newMax: Float, oldMin: Float = 0f, oldMax: Float = 1f ): Float = newMin + ((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)
基本的なルールでは、値の範囲(最小、最大)および補間器が決定されます。 これは、ほとんどすべての動作を説明するのに十分です。
ビューのアルファを0.5〜0.9の範囲で設定するとします。 また、最初にスクロールビューをすばやく透明にしてから、変更率を下げます。
ルールは次のようになります。
BRuleAlpha(min = 0.5f, max = 0.9f, interpolator = DecelerateInterpolator())
そして、これがBRuleAlphaの実装です。
BRuleAlpha.kt 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 } }
そして最後に、BehaviorByRulesコード。 振る舞いを書いた人にとっては、すべてが明らかであるはずです(onMeasureChildの中にあるものを除き、これについては以下で説明します)。
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 ) } 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 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) } } }
それでは、onMeasureChildはどうなっていますか?
これは、上で書いた問題を解決するために必要です。ツールバーの一部が消えると、NestedScrollはより高く移動するはずです。 高く乗せるには、CollapsingToolbarLayoutの高さを低くする必要があります。
もう1つの非自明なメソッド、canUpdateHeightがあります。 高さを変更できないときに相続人がルールを設定できるようにするために必要です。 たとえば、高さが依存するビューが現在非表示になっている場合。 これですべてのケースがカバーされるかどうかはわかりませんが、それを改善するためのアイデアがあれば、コメントまたはPMでお書きください。
CollapsingToolbarLayoutで作業するときにステップできるレーキ
- ビューを変更するときは、onLayoutを避ける必要があります。 たとえば、BehaviorRule内でlayoutParamsまたはtextSizeを変更しないでください。変更すると、パフォーマンスが大幅に低下します。
- OnOffsetChangedListenerを使用してツールバーを操作する場合、onLayoutはさらに危険です。onOffsetChangedメソッドは無期限にトリガーされます。
- CoordinatorLayout.Behaviorは、ビュー(layoutDependsOn)に依存しないようにする必要があります。ビューは、表示されなくなる可能性があります。 このビューがView.VISIBLEに戻ると、動作は反応しません。
- ツールバーがAppBarLayoutの外側にある場合、ツールバーがブロックしないようにするには、android:translationZ = "5dp"属性をツールバーの親ViewGroupに追加する必要があります。
結論として
比較的簡単に読み取りおよび変更できるロジックを使用して、CollapsingToolbarLayoutをすばやくスケッチできるソリューションがあります。 すべてのルールと依存関係は、1つのクラス-CoordinatorLayout.Behaviorのフレームワーク内で形成されます。 コードは
githubで表示できます。