
Ich möchte eine Lösung für die Beschreibung der CollapsingToolbar vorstellen, wobei der Schwerpunkt auf der Lesbarkeit des Codes liegt. Der Artikel erklärt nicht, was ist und wie Sie Ihr CoordinatorLayout.Behavior schreiben. Wenn der Leser daran interessiert ist, dies zu verstehen, gibt es viele Artikel, einschließlich der
über den Habr . Wenn Sie nicht verstehen möchten, ist es in Ordnung: Ich habe versucht, die Schreibweise von CollapsingToolbar zu ermitteln, damit ich von CoordinatorLayout.Behavior und OnOffsetChangedListener abstrahieren kann.
Bedingungen
- Symbolleiste - Eine Reihe von Ansichten, die oben auf dem Bildschirm angezeigt werden sollen (nicht android.widget.Toolbar).
- NestedScroll - jede scrollbare Ansicht, die AppBarLayout zugeordnet werden kann (RecyclerView, NestedScrollView).
Warum mussten Sie Ihre Entscheidung schreiben?
Ich habe mir verschiedene Ansätze im „Internet“ angesehen und fast alle wurden wie folgt aufgebaut:
- Legt eine feste Höhe für AppBarLayout fest.
- CoordinatorLayout.Behavior wird geschrieben, bei dem mit einigen Berechnungen (die zwischengespeicherte Ansichtshöhe wird am unteren Rand einer anderen Ansicht hinzugefügt und abzüglich des Randes mit der hier berechneten Schriftrolle multipliziert) eine Ansicht geändert wird.
- Andere Ansichten ändern sich im OnOffsetChangedListener des AppBarLayout.
Hier ist
ein Beispiel für Verhalten mit dem beschriebenen Ansatz, 2,5k Sterne auf Github.
Realität: Setzen Sie Ihren OnePlus auf Sie können das Layout für diese Lösung korrigieren, aber etwas anderes verwirrt mich. Einige Ansichten werden über OnOffsetChangedListener verwaltet, andere über Behaviour. Um das ganze Bild zu verstehen, muss der Entwickler viele Klassen durchgehen. Wenn für eine neue Ansicht ein Verhalten hinzugefügt werden muss, das von einem anderen Verhalten und den Ansichten abhängt, die sich in OnOffsetChangedListener ändern, können Krücken und Fehler aus heiterem Himmel wachsen
Darüber hinaus zeigt dieses Beispiel nicht, was zu tun ist, wenn der Symbolleiste zusätzliche Elemente hinzugefügt werden, die sich auf die Höhe dieser Symbolleiste auswirken.
Im GIF am Anfang des Artikels können Sie sehen, wie TextView ausgeblendet wird, indem Sie auf die Schaltfläche klicken - und NestedScroll wird höher gezogen, damit kein Leerzeichen vorhanden ist.
Wie kann man das machen? Die Lösungen, die zuerst in den Sinn kommen, bestehen darin, ein weiteres CoordinatorLayout.Behavior für NestedScroll zu schreiben (wobei die Logik des zugrunde liegenden AppBarLayout.Behavior beibehalten wird) oder die Symbolleiste in AppBarLayout einzufügen und in OnOffsetChangedListener zu ändern. Ich habe beide Lösungen ausprobiert, und es stellte sich heraus, dass ein Code an Implementierungsdetails gebunden war, der für andere schwer zu verstehen war und nicht wiederverwendet werden konnte.
Ich würde mich freuen, wenn jemand ein Beispiel teilt, in dem eine solche Logik „sauber“ implementiert ist, aber jetzt werde ich meine Lösung zeigen. Die Idee ist,
an einem Ort deklarativ beschreiben zu können, welche Ansichten und wie sie sich verhalten sollen.
Wie sieht API aus?
Um CoordinatorLayout.Behavior zu erstellen, benötigen Sie:
- erbe BehaviorByRules;
- Überschreiben Sie Methoden, die AppBarLayout, CollapsingToolbarLayout und die Bildlauflänge (AppBarLayout-Höhe) zurückgeben.
- überschreiben Sie die setUpViews-Methode - beschreiben Sie die Regeln für das Verhalten der Ansicht, wenn sich die Bildlaufleiste der AppBar ändert.
TopInfoBehavior für die Symbolleiste aus dem GIF am Anfang des Artikels sieht folgendermaßen aus (später im Artikel werde ich erklären, wie es funktioniert):
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-Layout (offensichtliche Attribute zur besseren Lesbarkeit entfernt) <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>
Wie funktioniert es?
Die Aufgabe besteht darin, die Regeln zu schreiben:
interface BehaviorRule { fun manage(ratio: Float, details: InitialViewDetails, view: View) }
Hier ist alles klar - ein Gleitkommawert von 0 bis 1 wird eingegeben, der den Prozentsatz des ActionBar-Bildlaufs, eine Ansicht und ihren Anfangszustand widerspiegelt. Es sieht interessanter aus BaseBehaviorRule - eine Regel, von der andere Grundregeln geerbt werden.
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)
Für die Grundregeln wird der Wertebereich (min, max) und der Interpolator festgelegt. Dies reicht aus, um fast jedes Verhalten zu beschreiben.
Angenommen, wir möchten das Alpha für unsere Ansicht im Bereich von 0,5 bis 0,9 festlegen. Wir möchten auch, dass die Bildlaufansicht zuerst schnell transparent wird und dann die Änderungsrate sinkt.
Die Regel sieht folgendermaßen aus:
BRuleAlpha(min = 0.5f, max = 0.9f, interpolator = DecelerateInterpolator())
Und hier ist die Implementierung von 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 } }
Und schließlich der BehaviorByRules-Code. Für diejenigen, die ihr Verhalten geschrieben haben, sollte alles offensichtlich sein (außer was in onMeasureChild enthalten ist, werde ich unten darüber sprechen):
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) } } }
Was ist mit onMeasureChild los?
Dies ist notwendig, um das Problem zu lösen, über das ich oben geschrieben habe: Wenn ein Teil der Symbolleiste verschwindet, sollte NestedScroll höher verschoben werden. Damit es höher fährt, müssen Sie die Höhe des CollapsingToolbarLayout verringern.
Es gibt eine andere nicht offensichtliche Methode - canUpdateHeight. Es wird benötigt, damit Sie dem Erben erlauben können, eine Regel festzulegen, wenn Sie die Höhe nicht ändern können. Zum Beispiel, wenn die Ansicht, von der die Höhe abhängt, derzeit ausgeblendet ist. Ich bin nicht sicher, ob dies alle Fälle abdeckt, aber wenn jemand Ideen hat, wie man es besser machen kann, schreibe bitte in einen Kommentar oder in eine persönliche Nachricht.
Rechen, auf den Sie bei der Arbeit mit CollapsingToolbarLayout treten können
- Wenn Sie Ansichten ändern, sollten Sie onLayout vermeiden. Beispielsweise sollten Sie layoutParams oder textSize in einer BehaviorRule nicht ändern, da sonst die Leistung drastisch beeinträchtigt wird.
- Wenn Sie über OnOffsetChangedListener mit der Symbolleiste arbeiten möchten, ist onLayout noch gefährlicher - die onOffsetChanged-Methode wird auf unbestimmte Zeit ausgelöst.
- CoordinatorLayout.Behavior sollte nicht von der Ansicht (layoutDependsOn) abhängen, die in die Sichtbarkeit GONE übergehen kann. Wenn diese Ansicht zu View.VISIBLE zurückkehrt, reagiert Behavior nicht.
- Wenn sich die Symbolleiste außerhalb des AppBarLayout befindet, müssen Sie der übergeordneten ViewGroup der Symbolleiste das android-Attribut hinzufügen, damit die Symbolleiste sie nicht blockiert: translationZ = "5dp".
Abschließend
Wir haben eine Lösung, mit der Sie Ihr CollapsingToolbarLayout schnell mit einer Logik skizzieren können, die relativ einfach zu lesen und zu ändern ist. Alle Regeln und Abhängigkeiten werden im Rahmen einer Klasse gebildet - CoordinatorLayout.Behavior. Der Code kann auf dem
Github angezeigt werden .