web-dev-qa-db-fra.com

Défilement sautant lors du changement de fragments

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:

  1. Je fais défiler vers le bas jusqu'à ce que j'atteigne le bouton "afficher jaune".
  2. Appuyer sur "show yellow" remplace un énorme fragment bleu par un minuscule fragment jaune. Lorsque cela se produit, les deux boutons sautent à la fin de l'écran.

Je veux que les deux boutons restent à la même position lors du passage au fragment jaune. Comment cela peut-il être fait?

roll

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.

8
stefan.at.wpf

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) 

1
Pavneet_Singh

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 à

  1. la quantité de défilement de NestedScrollView dans la direction "y"

plus

  1. la hauteur de NestedScrollView .

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.

enter image description here

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:

enter image description here

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.

enter image description here

1
Cheticamp

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

0
DevJZ

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.

  • HeightLayoutListener.kt
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!

0
Ben P.