Dans un ScrollView, je passe dynamiquement entre deux fragments de hauteurs différentes. Malheureusement, cela conduit à sauter. On peut le voir dans l'animation suivante:
Je veux que les deux boutons restent à la même position lors du passage au fragment jaune. Comment cela peut-il être fait?
Code source disponible sur https://github.com/wondering639/stack-dynamiccontent respectivement https://github.com/wondering639/stack-dynamiccontent.git
Extraits de code pertinents:
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" />
[~ # ~] indice [~ # ~]
Veuillez noter qu'il s'agit bien sûr d'un exemple de travail minimum pour montrer mon problème. Dans mon vrai projet, j'ai également des vues sous le @+id/fragment_container
. Donc, donner une taille fixe à @+id/fragment_container
n'est pas une option pour moi - cela entraînerait une grande zone vide lors du passage au fragment jaune bas.
UPDATE: Aperçu des solutions proposées
J'ai mis en œuvre les solutions proposées à des fins de test et ajouté mes expériences personnelles avec elles.
réponse de Cheticamp, https://stackoverflow.com/a/60323255
-> disponible en https://github.com/wondering639/stack-dynamiccontent/tree/60323255
-> FrameLayout enveloppe le contenu, le code court
réponse de Pavneet_Singh, https://stackoverflow.com/a/60310807
-> disponible en https://github.com/wondering639/stack-dynamiccontent/tree/60310807
-> FrameLayout obtient la taille du fragment bleu. Donc pas d'emballage de contenu. Lors du passage au fragment jaune, il y a un espace entre celui-ci et le contenu qui le suit (si un contenu le suit). Pas de rendu supplémentaire cependant! ** mise à jour ** une deuxième version a été fournie montrant comment le faire sans lacunes. Vérifiez les commentaires sur la réponse.
réponse de Ben P., https://stackoverflow.com/a/60251036
-> disponible en https://github.com/wondering639/stack-dynamiccontent/tree/60251036
-> FrameLayout enveloppe le contenu. Plus de code que la solution de Chéticamp. Toucher deux fois le bouton "Afficher jaune" conduit à un "bug" (les boutons sautent vers le bas, en fait mon problème d'origine). On pourrait discuter de la désactivation du bouton "Afficher jaune" après y être passé, donc je ne considérerais pas cela comme un vrai problème.
Mise à jour : Pour garder les autres vues juste en dessous de framelayout
et pour gérer le scénario automatiquement, nous devons utiliser onMeasure
pour implémenter la gestion automatique, procédez comme suit
• Créez un ConstraintLayout
personnalisé comme (ou pouvez utiliser MaxHeightFrameConstraintLayout lib ):
import Android.content.Context
import Android.os.Build
import Android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import kotlin.math.max
/**
* Created by Pavneet_Singh on 2020-02-23.
*/
class MaxHeightConstraintLayout @kotlin.jvm.JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr){
private var _maxHeight: Int = 0
// required to support the minHeight attribute
private var _minHeight = attrs?.getAttributeValue(
"http://schemas.Android.com/apk/res/Android",
"minHeight"
)?.substringBefore(".")?.toInt() ?: 0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
_minHeight = minHeight
}
var maxValue = max(_maxHeight, max(height, _minHeight))
if (maxValue != 0 && && maxValue > minHeight) {
minHeight = maxValue
}
_maxHeight = maxValue
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
et utilisez-le dans votre mise en page à la place de ConstraintLayout
<?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">
<com.example.pavneet_singh.temp.MaxHeightConstraintLayout
Android:id="@+id/constraint"
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="Some 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.3"
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_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toEndOf="@+id/button_fragment1"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<Button
Android:id="@+id/button_fragment3"
Android:layout_width="0dp"
Android:layout_height="wrap_content"
Android:layout_marginEnd="16dp"
Android:layout_marginRight="16dp"
Android:text="show green"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toEndOf="@+id/button_fragment2"
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_fragment3" />
<TextView
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:text="additional text\nMore data"
Android:textSize="24dp"
app:layout_constraintTop_toBottomOf="@+id/fragment_container" />
</com.example.pavneet_singh.temp.MaxHeightConstraintLayout>
</androidx.core.widget.NestedScrollView>
Cela gardera une trace de la hauteur et l'appliquera à chaque changement de fragment.
Sortie:
Remarque: Comme mentionné dans commentaires précédemment, définir minHeight entraînera une passe de rendu supplémentaire et cela ne peut pas être évité dans la version actuelle de ConstraintLayout
.
Ancienne approche avec FrameLayout personnalisé
C'est une exigence intéressante et mon approche est de la résoudre en créant une vue personnalisée.
Idée :
Mon idée de solution est d'ajuster la hauteur du conteneur en gardant la trace du plus grand enfant ou de la hauteur totale des enfants dans le conteneur.
Tentatives :
Mes premières tentatives étaient basées sur la modification du comportement existant de NestedScrollView
en l'étendant mais cela ne donne pas accès à toutes les données ou méthodes nécessaires. La personnalisation a entraîné une prise en charge médiocre de tous les scénarios et cas Edge.
Plus tard, j'ai réalisé la solution en créant un Framelayout
personnalisé avec une approche différente.
Implémentation de la solution
Lors de l'implémentation du comportement personnalisé des phases de mesure de hauteur, j'ai creusé plus profondément et manipulé la hauteur avec getSuggestedMinimumHeight
tout en suivant la hauteur des enfants pour implémenter la solution la plus optimisée car elle ne provoquera pas de rendu supplémentaire ou explicite car elle gérera la hauteur pendant le cycle de rendu interne, créez donc une classe FrameLayout
personnalisée pour implémenter la solution et remplacez le getSuggestedMinimumHeight
comme:
class MaxChildHeightFrameLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
// to keep track of max height
private var maxHeight: Int = 0
// required to get support the minHeight attribute
private val minHeight = attrs?.getAttributeValue(
"http://schemas.Android.com/apk/res/Android",
"minHeight"
)?.substringBefore(".")?.toInt() ?: 0
override fun getSuggestedMinimumHeight(): Int {
var maxChildHeight = 0
for (i in 0 until childCount) {
maxChildHeight = max(maxChildHeight, getChildAt(i).measuredHeight)
}
if (maxHeight != 0 && layoutParams.height < (maxHeight - maxChildHeight) && maxHeight > maxChildHeight) {
return maxHeight
} else if (maxHeight == 0 || maxHeight < maxChildHeight) {
maxHeight = maxChildHeight
}
return if (background == null) minHeight else max(
minHeight,
background.minimumHeight
)
}
}
Maintenant, remplacez FrameLayout
par MaxChildHeightFrameLayout
dans activity_main.xml
Comme:
<?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="Some 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" />
<com.example.pavneet_singh.temp.MaxChildHeightFrameLayout
Android:id="@+id/fragment_container"
Android:layout_width="match_parent"
Android:minHeight="2dp"
Android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/button_fragment2"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
getSuggestedMinimumHeight()
sera utilisé pour calculer la hauteur de la vue pendant le cycle de vie du rendu de la vue.
Sortie:
Avec plus de vues, fragment et hauteur différente. (400dp, 20dp, 500dp respectivement)
Une solution simple consiste à ajuster la hauteur minimale du ConstraintLayout dans le NestedScrollView avant de changer de fragment. Pour éviter de sauter, la hauteur du ConstraintLayout doit être supérieure ou égale à
plus
Le code suivant encapsule ce concept:
private fun adjustMinHeight(nsv: NestedScrollView, layout: ConstraintLayout) {
layout.minHeight = nsv.scrollY + nsv.height
}
Veuillez noter que layout.minimumHeight
ne fonctionnera pas pour ConstraintLayout . Tu dois utiliser layout.minHeight
.
Pour appeler cette fonction, procédez comme suit:
private fun insertYellowFragment() {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, YellowFragment())
transaction.commit()
val nsv = findViewById<NestedScrollView>(R.id.myScrollView)
val layout = findViewById<ConstraintLayout>(R.id.constraintLayout)
adjustMinHeight(nsv, layout)
}
Il en va de même pour insertBlueFragment () . Vous pouvez, bien sûr, simplifier cela en faisant findViewById () une fois.
Voici une vidéo rapide des résultats.
Dans la vidéo, j'ai ajouté une vue de texte en bas pour représenter des éléments supplémentaires qui peuvent exister dans votre mise en page sous le fragment. Si vous supprimez cette vue de texte, le code fonctionnera toujours, mais vous verrez un espace vide en bas. Voici à quoi cela ressemble:
Et si les vues sous le fragment ne remplissent pas la vue de défilement, vous verrez les vues supplémentaires plus un espace blanc en bas.
Votre FrameLayout
à l'intérieur de activity_main.xml
a un attribut de hauteur de wrap_content
.
Les dispositions de vos fragments enfants déterminent les différences de hauteur que vous voyez.
Devrait publier votre XML pour les fragments enfants
Essayez de définir une hauteur spécifique sur le FrameLayout
dans votre activity_main.xml
J'ai résolu ce problème en créant un écouteur de mise en page qui garde la trace de la hauteur "précédente" et ajoute un remplissage au ScrollView si la nouvelle hauteur est inférieure à la hauteur précédente.
class HeightLayoutListener(
private val activity: MainActivity,
private val root: View,
private val previousHeight: Int,
private val targetScroll: Int
) : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
root.viewTreeObserver.removeOnGlobalLayoutListener(this)
val padding = max((previousHeight - root.height), 0)
activity.setPaddingBottom(padding)
activity.setScrollPosition(targetScroll)
}
companion object {
fun create(fragment: Fragment): HeightLayoutListener {
val activity = fragment.activity as MainActivity
val root = fragment.view!!
val previousHeight = fragment.requireArguments().getInt("height")
val targetScroll = fragment.requireArguments().getInt("scroll")
return HeightLayoutListener(activity, root, previousHeight, targetScroll)
}
}
}
Pour activer cet écouteur, ajoutez cette méthode à vos deux fragments:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val listener = HeightLayoutListener.create(this)
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
}
Ce sont les méthodes que l'écouteur appelle afin de mettre à jour le ScrollView. Ajoutez-les à votre activité:
fun setPaddingBottom(padding: Int) {
val wrapper = findViewById<View>(R.id.wrapper) // add this ID to your ConstraintLayout
wrapper.setPadding(0, 0, 0, padding)
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(wrapper.width, View.MeasureSpec.EXACTLY)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
wrapper.measure(widthMeasureSpec, heightMeasureSpec)
wrapper.layout(0, 0, wrapper.measuredWidth, wrapper.measuredHeight)
}
fun setScrollPosition(scrollY: Int) {
val scroll = findViewById<NestedScrollView>(R.id.myScrollView)
scroll.scrollY = scrollY
}
Et vous devez définir des arguments pour vos fragments pour que l'auditeur sache quelle était la hauteur précédente et la position de défilement précédente. Assurez-vous donc de les ajouter à vos transactions fragmentées:
private fun insertYellowFragment() {
val fragment = YellowFragment().apply {
this.arguments = createArgs()
}
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.commit()
}
private fun insertBlueFragment() {
val fragment = BlueFragment().apply {
this.arguments = createArgs()
}
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.commit()
}
private fun createArgs(): Bundle {
val scroll = findViewById<NestedScrollView>(R.id.myScrollView)
val container = findViewById<View>(R.id.fragment_container)
return Bundle().apply {
putInt("scroll", scroll.scrollY)
putInt("height", container.height)
}
}
Et cela devrait le faire!