
Quiero presentar una solución a cómo se puede describir CollapsingToolbar, con énfasis en la legibilidad del código. El artículo no explicará qué es y cómo escribir su CoordinatorLayout.Behavior. Si el lector está interesado en comprender esto, hay muchos artículos, incluidos los
del Habr . Si no quieres entender, está bien: intenté deletrear CollapsingToolbar para poder abstraer de CoordinatorLayout.Behavior y OnOffsetChangedListener.
Términos
- Barra de herramientas: un conjunto de vistas que queremos mostrar en la parte superior de la pantalla (no android.widget.Toolbar).
- NestedScroll: cualquier vista desplazable que se puede asociar con AppBarLayout (RecyclerView, NestedScrollView).
¿Por qué necesitabas escribir tu decisión?
Miré varios enfoques en "Internet", y casi todos se construyeron de la siguiente manera:
- Establece una altura fija para AppBarLayout.
- CoordinatorLayout.Behavior se escribe, en el que, con algunos cálculos (la altura de la vista en caché se agrega al final de otra vista y menos el margen se multiplica por el desplazamiento, calculado aquí) cambian algún tipo de vista.
- Otras vistas cambian en OnOffsetChangedListener de AppBarLayout.
Aquí hay
un ejemplo de Comportamiento con el enfoque descrito, 2.5k estrellas en Github.
Realidad: ponte tu OnePlus Puede corregir el diseño de esta solución, pero algo más me confunde. Algunas vistas se administran a través de OnOffsetChangedListener, algunas a través de Comportamiento, algo funciona de forma inmediata. Para comprender la imagen completa, el desarrollador tendrá que repasar muchas clases, y si para una nueva vista es necesario agregar un comportamiento que depende de otro Comportamiento y de las vistas que cambian en OnOffsetChangedListener, las muletas y los errores pueden crecer de la nada
Además, este ejemplo no muestra qué hacer si se agregan elementos adicionales a la barra de herramientas que afectan la altura de esta barra de herramientas.
En el gif al comienzo del artículo, puede ver cómo TextView está oculto haciendo clic en el botón, y NestedScroll se tira hacia arriba para que no haya espacio vacío).
Como hacerlo Las soluciones que vienen a la mente primero son escribir otro CoordinatorLayout.Behavior para NestedScroll (manteniendo la lógica del AppBarLayout.Behavior) o pegar la barra de herramientas en AppBarLayout y cambiarlo a OnOffsetChangedListener. Probé ambas soluciones, y resultó que el código estaba vinculado a los detalles de implementación, lo que sería bastante difícil de entender para otra persona y no podría reutilizarse.
Me alegraría si alguien comparte un ejemplo donde dicha lógica se implemente "limpiamente", pero por ahora mostraré mi solución. La idea es poder
describir declarativamente en un lugar qué puntos de vista y cómo deben comportarse.
¿Cómo se ve la API?
Entonces, para crear CoordinatorLayout.Behavior necesitas:
- heredar BehaviorByRules;
- anular métodos que devuelven AppBarLayout, CollapsingToolbarLayout y longitud de desplazamiento (altura de AppBarLayout).
- anule el método setUpViews: describa las reglas sobre cómo se comportará la vista cuando cambie la barra de desplazamiento de la barra de aplicaciones.
TopInfoBehavior para la barra de herramientas del gif al comienzo del artículo se verá así (más adelante en el artículo explicaré cómo funciona):
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 } }
Diseño XML (atributos obvios eliminados para facilitar la lectura) <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>
Como funciona
La tarea es escribir las reglas:
interface BehaviorRule { fun manage(ratio: Float, details: InitialViewDetails, view: View) }
Aquí todo está claro: aparece un valor flotante de 0 a 1, que refleja el porcentaje del desplazamiento de ActionBar, aparece una vista y su estado inicial. Parece más interesante BaseBehaviorRule, una regla de la que se heredan otras reglas básicas.
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)
Para reglas básicas, se determina el rango de valores (min, max) y el interpolador. Esto es suficiente para describir casi cualquier comportamiento.
Supongamos que queremos establecer el alfa para nuestra vista en el rango de 0.5 a 0.9. También queremos que la vista de desplazamiento se vuelva rápidamente transparente primero, y luego la tasa de cambio disminuirá.
La regla se verá así:
BRuleAlpha(min = 0.5f, max = 0.9f, interpolator = DecelerateInterpolator())
Y aquí está la implementación 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 } }
Y finalmente, el código BehaviorByRules. Para aquellos que escribieron su comportamiento, todo debería ser obvio (a excepción de lo que hay dentro de MeasureChild, hablaré sobre esto a continuación):
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) } } }
Entonces, ¿qué pasa con onMeasureChild?
Esto es necesario para resolver el problema que escribí anteriormente: si alguna parte de la barra de herramientas desaparece, NestedScroll debería moverse más arriba. Para hacerlo subir más alto, debe reducir la altura de CollapsingToolbarLayout.
Hay otro método no obvio: canUpdateHeight. Es necesario para que pueda permitir que el heredero establezca una regla cuando no puede cambiar la altura. Por ejemplo, si la vista de la que depende la altura está actualmente oculta. No estoy seguro de que esto abarque todos los casos, pero si alguien tiene ideas sobre cómo hacerlo mejor, escriba un comentario o un mensaje personal.
Rastrillo que puedes pisar cuando trabajas con CollapsingToolbarLayout
- Al cambiar las vistas, debe evitar onLayout. Por ejemplo, no debe cambiar layoutParams o textSize dentro de una BehaviorRule, de lo contrario el rendimiento se reducirá drásticamente.
- Si desea trabajar con la barra de herramientas a través de OnOffsetChangedListener, onLayout es aún más peligroso: el método onOffsetChanged se activará indefinidamente.
- CoordinatorLayout.Behavior no debe depender de la vista (layoutDependsOn), que puede entrar en visibilidad GONE. Cuando esta vista vuelve a View.VISIBLE, Behavior no reaccionará.
- Si la barra de herramientas estará fuera de AppBarLayout, para evitar que la barra de herramientas la bloquee, agregue el atributo de Android: translationZ = "5dp" al ViewGroup padre de la barra de herramientas.
En conclusión
Tenemos una solución que le permite esbozar rápidamente su CollapsingToolbarLayout con una lógica que es relativamente fácil de leer y modificar. Todas las reglas y dependencias se forman dentro de una clase: CoordinatorLayout.Behavior. El código se puede ver en el
github .