
Je veux présenter une solution à la façon dont CollapsingToolbar peut être décrite, en mettant l'accent sur la lisibilité du code. L'article n'expliquera pas ce qu'est et comment écrire votre CoordinatorLayout.Behavior. Si le lecteur est intéressé à comprendre cela, il existe de nombreux articles, y compris ceux
sur le Habr . Si vous ne voulez pas comprendre, ça va: j'ai essayé d'obtenir l'orthographe de CollapsingToolbar afin de pouvoir résumer de CoordinatorLayout.Behavior et OnOffsetChangedListener.
Termes
- Barre d'outils - un ensemble de vues que nous voulons afficher en haut de l'écran (pas android.widget.Toolbar).
- NestedScroll - toute vue déroulante qui peut être associée à AppBarLayout (RecyclerView, NestedScrollView).
Pourquoi aviez-vous besoin d'écrire votre décision
J'ai regardé plusieurs approches sur «Internet», et presque toutes ont été construites comme suit:
- Définit une hauteur fixe pour AppBarLayout.
- CoordinatorLayout.Behavior est écrit, dans lequel, avec certains calculs (la hauteur de la vue mise en cache est ajoutée au bas d'une autre vue et moins la marge est multipliée par le défilement, calculée ici), ils changent une sorte de vue.
- D'autres vues changent dans le OnOffsetChangedListener de AppBarLayout.
Voici
un exemple de comportement avec l'approche décrite, 2,5k étoiles sur Github.
Réalité: enfilez votre OnePlus Vous pouvez corriger la mise en page de cette solution, mais quelque chose d'autre m'embrouille. Certaines vues sont gérées via OnOffsetChangedListener, d'autres via Behavior, quelque chose fonctionne par défaut. Pour comprendre l'image dans son ensemble, le développeur devra passer par de nombreuses classes, et si pour une nouvelle vue, il est nécessaire d'ajouter un comportement qui dépend des autres comportements et des vues qui changent dans OnOffsetChangedListener, les béquilles et les bogues peuvent sortir du bleu
En outre, cet exemple ne montre pas quoi faire si des éléments supplémentaires sont ajoutés à la barre d'outils qui affectent la hauteur de cette barre d'outils.
Dans le gif au début de l'article, vous pouvez voir comment TextView est masqué en cliquant sur le bouton - et NestedScroll est tiré plus haut pour qu'aucun espace vide n'apparaisse).
Comment faire Les solutions qui vous viennent à l'esprit en premier sont d'écrire un autre CoordinatorLayout.Behavior pour NestedScroll (en gardant la logique de l'AppBarLayout.Behavior sous-jacent) ou de coller la barre d'outils dans AppBarLayout et de la changer en OnOffsetChangedListener. J'ai essayé les deux solutions, et il s'est avéré que le code était lié aux détails d'implémentation, ce qui serait assez difficile à comprendre pour quelqu'un d'autre et ne pourrait pas être réutilisé.
Je serais heureux si quelqu'un partage un exemple où une telle logique est implémentée «proprement», mais pour l'instant je vais montrer ma solution. L'idée est de pouvoir
décrire de manière déclarative en un seul endroit quelles vues et comment elles doivent se comporter.
À quoi ressemble l'api?
Donc, pour créer CoordinatorLayout.Behavior, vous avez besoin de:
- hériter BehaviorByRules;
- remplacer les méthodes qui renvoient AppBarLayout, CollapsingToolbarLayout et la longueur de défilement (hauteur AppBarLayout).
- remplacer la méthode setUpViews - décrivez les règles de comportement de la vue lorsque la barre de défilement de la barre d'applications change.
TopInfoBehavior pour la barre d'outils du gif au début de l'article ressemblera à ceci (plus loin dans l'article, je vais expliquer comment cela fonctionne):
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 } }
Disposition XML (suppression des attributs évidents pour la lisibilité) <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>
Comment ça marche
La tâche consiste à écrire les règles:
interface BehaviorRule { fun manage(ratio: Float, details: InitialViewDetails, view: View) }
Tout est clair ici - une valeur flottante de 0 à 1 entre, reflétant le pourcentage du défilement ActionBar, une vue arrive et son état initial. Il semble plus intéressant BaseBehaviorRule - une règle dont les autres règles de base sont héritées.
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)
Pour les règles de base, la plage de valeurs (min, max) et l'interpolateur sont déterminés. Cela suffit pour décrire presque tous les comportements.
Supposons que nous voulons définir l'alpha de notre vue dans une plage de 0,5 à 0,9. Nous voulons également que la vue de défilement devienne rapidement transparente d'abord, puis le taux de changement diminuera.
La règle ressemblera à ceci:
BRuleAlpha(min = 0.5f, max = 0.9f, interpolator = DecelerateInterpolator())
Et voici l'implémentation de 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 } }
Et enfin, le code BehaviorByRules. Pour ceux qui ont écrit leur comportement, tout devrait être évident (sauf pour ce qui est à l'intérieur de onMeasureChild, je vais en parler ci-dessous):
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) } } }
Alors quoi de neuf avec onMeasureChild?
Cela est nécessaire pour résoudre le problème que j'ai décrit ci-dessus: si une partie de la barre d'outils disparaît, NestedScroll devrait se déplacer plus haut. Pour le faire monter plus haut, vous devez réduire la hauteur de CollapsingToolbarLayout.
Il existe une autre méthode non évidente - canUpdateHeight. Il est nécessaire pour permettre à l'héritier de définir une règle lorsque vous ne pouvez pas modifier la hauteur. Par exemple, si la vue dont dépend la hauteur est actuellement masquée. Je ne suis pas sûr que cela couvrira tous les cas, mais si quelqu'un a des idées sur la façon de faire mieux, veuillez écrire dans un commentaire ou dans un message personnel.
Râteau sur lequel vous pouvez marcher lorsque vous travaillez avec CollapsingToolbarLayout
- Lorsque vous changez de vue, vous devez éviter onLayout. Par exemple, vous ne devez pas modifier layoutParams ou textSize dans un BehaviorRule, sinon les performances seront considérablement réduites.
- Si vous souhaitez travailler avec la barre d'outils via OnOffsetChangedListener, onLayout est encore plus dangereux - la méthode onOffsetChanged se déclenchera indéfiniment.
- CoordinatorLayout.Behavior ne doit pas dépendre de la vue (layoutDependsOn), qui peut entrer en visibilité GONE. Lorsque cette vue revient à View.VISIBLE, Behavior ne réagit pas.
- Si la barre d'outils se trouve en dehors de AppBarLayout, pour empêcher la barre d'outils de la bloquer, vous devez ajouter l'attribut android: translationZ = "5dp" au ViewGroup parent de la barre d'outils.
En conclusion
Nous avons une solution qui vous permet d'esquisser rapidement votre CollapsingToolbarLayout avec une logique relativement facile à lire et à modifier. Toutes les règles et dépendances sont formées dans le cadre d'une classe - CoordinatorLayout.Behavior. Le code peut être consulté sur le
github .