In einer ScrollView wechsle ich dynamisch zwischen zwei Fragmenten mit unterschiedlichen Höhen. Das führt leider zum Springen. Man kann es in der folgenden Animation sehen:
- Ich scrolle nach unten, bis ich die Schaltfläche "gelb anzeigen" erreiche.
- Durch Drücken von "gelb anzeigen" wird ein riesiges blaues Fragment durch ein winziges gelbes Fragment ersetzt. In diesem Fall springen beide Tasten zum Ende des Bildschirms.
Ich möchte, dass beide Tasten an derselben Position bleiben, wenn Sie zum gelben Fragment wechseln. Wie kann das gemacht werden?
Quellcode verfügbar unter https://github.com/wondering639/stack-dynamiccontent bzw. https://github.com/wondering639/stack-dynamiccontent.git
Relevante Codefragmente:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/myScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="800dp"
android:background="@color/colorAccent"
android:text="@string/long_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_fragment1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="show blue"
app:layout_constraintEnd_toStartOf="@+id/button_fragment2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<Button
android:id="@+id/button_fragment2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="show yellow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/button_fragment1"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/button_fragment2">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.example.dynamiccontent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// onClick handlers
findViewById<Button>(R.id.button_fragment1).setOnClickListener {
insertBlueFragment()
}
findViewById<Button>(R.id.button_fragment2).setOnClickListener {
insertYellowFragment()
}
// by default show the blue fragment
insertBlueFragment()
}
private fun insertYellowFragment() {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, YellowFragment())
transaction.commit()
}
private fun insertBlueFragment() {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, BlueFragment())
transaction.commit()
}
}
fragment_blue.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#0000ff"
tools:context=".BlueFragment" />
fragment_yellow.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="20dp"
android:background="#ffff00"
tools:context=".YellowFragment" />
HINWEIS
Bitte beachten Sie, dass dies natürlich ein Mindestarbeitsbeispiel ist, um mein Problem aufzuzeigen. In meinem realen Projekt habe ich auch Ansichten unter dem @+id/fragment_container
. Daher @+id/fragment_container
ist es für mich keine Option , eine feste Größe anzugeben - dies würde beim Umschalten auf das niedrige gelbe Fragment einen großen leeren Bereich verursachen.
UPDATE: Überblick über die vorgeschlagenen Lösungen
Ich habe die vorgeschlagenen Lösungen zu Testzwecken implementiert und meine persönlichen Erfahrungen mit ihnen hinzugefügt.
Antwort von Cheticamp, https://stackoverflow.com/a/60323255
-> verfügbar unter https://github.com/wondering639/stack-dynamiccontent/tree/60323255
-> FrameLayout umschließt Inhalt und Funktionscode
Antwort von Pavneet_Singh, https://stackoverflow.com/a/60310807
-> verfügbar unter https://github.com/wondering639/stack-dynamiccontent/tree/60310807
-> FrameLayout erhält die Größe des blauen Fragments. Also kein Content Wrapping. Wenn Sie zum gelben Fragment wechseln, besteht eine Lücke zwischen diesem und dem darauf folgenden Inhalt (falls ein Inhalt darauf folgt). Kein zusätzliches Rendering! ** Update ** Es wurde eine zweite Version bereitgestellt, die zeigt, wie es ohne Lücken geht. Überprüfen Sie die Kommentare zur Antwort.
Antwort von Ben P., https://stackoverflow.com/a/60251036
-> verfügbar unter https://github.com/wondering639/stack-dynamiccontent/tree/60251036
-> FrameLayout umschließt den Inhalt. Mehr Code als die Lösung von Cheticamp. Das zweimalige Berühren der Schaltfläche "Gelb anzeigen" führt zu einem "Fehler" (Schaltflächen springen nach unten, eigentlich meine ursprüngliche Ausgabe). Man könnte darüber streiten, nur die Schaltfläche "Gelb anzeigen" nach dem Wechsel zu deaktivieren, daher würde ich dies nicht als echtes Problem betrachten.
Antworten:
Update : Um die anderen Ansichten direkt unter dem zu behalten
framelayout
und das Szenario automatisch zu behandeln, müssen SieonMeasure
die automatische Behandlung implementieren. Führen Sie daher die folgenden Schritte aus• Erstellen Sie eine benutzerdefinierte
ConstraintLayout
als (oder verwenden Sie die MaxHeightFrameConstraintLayout-Bibliothek ):und verwenden Sie es in Ihrem Layout anstelle von
ConstraintLayout
Dadurch wird die Höhe verfolgt und bei jedem Fragmentwechsel angewendet.
Ausgabe:
Hinweis: Wie bereits in den Kommentaren erwähnt , führt das Festlegen von minHeight zu einem zusätzlichen Rendering-Durchgang und kann in der aktuellen Version von nicht vermieden werden
ConstraintLayout
.Alter Ansatz mit benutzerdefiniertem FrameLayout
Dies ist eine interessante Anforderung, und mein Ansatz besteht darin, sie durch Erstellen einer benutzerdefinierten Ansicht zu lösen.
Idee :
Meine Idee für die Lösung besteht darin, die Höhe des Containers anzupassen, indem die Spur des größten Kindes oder der Gesamthöhe der Kinder im Container beibehalten wird.
Versuche :
Meine ersten Versuche basierten darauf, das vorhandene Verhalten zu
NestedScrollView
ändern, indem es erweitert wurde, aber es bietet keinen Zugriff auf alle erforderlichen Daten oder Methoden. Die Anpassung führte zu einer schlechten Unterstützung für alle Szenarien und Randfälle.Später erreichte ich die Lösung, indem ich einen benutzerdefinierten
Framelayout
Ansatz mit einem anderen Ansatz erstellte.Lösungsimplementierung
Während ich das benutzerdefinierte Verhalten von Phasen der Höhenmessung implementierte, habe ich die Höhe vertieft und manipuliert,
getSuggestedMinimumHeight
während ich die Größe von Kindern verfolgte, um die optimierte Lösung zu implementieren, da dies kein zusätzliches oder explizites Rendern verursacht, da die Höhe während des internen Renderns verwaltet wird Erstellen Sie eine benutzerdefinierteFrameLayout
Klasse, um die Lösung zu implementieren, und überschreiben Sie FolgendesgetSuggestedMinimumHeight
:Ersetzen Sie nun das
FrameLayout
durchMaxChildHeightFrameLayout
inactivity_main.xml
als:getSuggestedMinimumHeight()
wird verwendet, um die Höhe der Ansicht während des Rendering-Lebenszyklus der Ansicht zu berechnen.Ausgabe:
Mit mehr Ansichten, Fragment und unterschiedlicher Höhe. (400 dp, 20 dp bzw. 500 dp)
quelle
getSuggestedMinimumHeight()
sie damit zusammenhängen oder in welcher Reihenfolge sie alle ausgeführt werden.onMeasure
ist der erste Schritt zur Messung des Renderprozesses und wirdgetSuggestedMinimumHeight
(und nur ein Getter, der kein zusätzliches Rendern verursacht) im Inneren verwendetonMeasure
, um die Höhe zu definieren.setMinHeight
ruft requestLayout () auf, während der Layoutansichtsbaum gemessen, angelegt und gezeichnet wirdEine einfache Lösung besteht darin, die Mindesthöhe des ConstraintLayout in NestedScrollView anzupassen, bevor Fragmente gewechselt werden . Um ein Springen zu verhindern, muss die Höhe des ConstraintLayout größer oder gleich sein
Plus
Der folgende Code kapselt dieses Konzept:
Bitte beachten Sie, dass
layout.minimumHeight
dies für ConstraintLayout nicht funktioniert . Sie müssen verwendenlayout.minHeight
.Gehen Sie wie folgt vor, um diese Funktion aufzurufen:
Ähnlich verhält es sich mit insertBlueFragment () . Sie können dies natürlich vereinfachen, indem Sie findViewById () einmal ausführen .
Hier ist ein kurzes Video der Ergebnisse.
Im Video habe ich unten eine Textansicht hinzugefügt, um zusätzliche Elemente darzustellen, die möglicherweise in Ihrem Layout unterhalb des Fragments vorhanden sind. Wenn Sie diese Textansicht löschen, funktioniert der Code weiterhin, aber unten wird ein Leerzeichen angezeigt. So sieht das aus:
Wenn die Ansichten unter dem Fragment die Bildlaufansicht nicht ausfüllen, werden unten die zusätzlichen Ansichten sowie der Leerraum angezeigt.
quelle
commit
zuvor aufgerufen wurdeadjustMinHeight
, gehe ich davon aus, dass ein zusätzlicher Rendering-Zyklus erforderlich ist.Ihr
FrameLayout
Inneresactivity_main.xml
hat ein Höhenattribut vonwrap_content
.Ihre untergeordneten Fragmentlayouts bestimmen die Höhenunterschiede, die Sie sehen.
Sollte Ihre XML für die untergeordneten Fragmente veröffentlichen
Versuchen Sie, eine bestimmte Höhe auf die
FrameLayout
in Ihrem einzustellenactivity_main.xml
quelle
Ich habe dieses Problem gelöst, indem ich einen Layout-Listener erstellt habe, der die "vorherige" Höhe verfolgt und der ScrollView eine Auffüllung hinzufügt, wenn die neue Höhe geringer als zuvor ist.
Fügen Sie diese Methode beiden Fragmenten hinzu, um diesen Listener zu aktivieren:
Dies sind die Methoden, die der Listener aufruft, um die ScrollView tatsächlich zu aktualisieren. Fügen Sie sie Ihrer Aktivität hinzu:
Und Sie müssen Argumente für Ihre Fragmente festlegen, damit der Hörer die vorherige Höhe und die vorherige Bildlaufposition kennt. Stellen Sie also sicher, dass Sie sie zu Ihren Fragmenttransaktionen hinzufügen:
Und das sollte es tun!
quelle