Desplazamiento de salto al cambiar fragmentos

8

Dentro de un ScrollView, estoy cambiando dinámicamente entre dos fragmentos con diferentes alturas. Lamentablemente eso lleva a saltar. Se puede ver en la siguiente animación:

  1. Me desplazo hacia abajo hasta llegar al botón "mostrar amarillo".
  2. Al presionar "mostrar amarillo" se reemplaza un enorme fragmento azul con un pequeño fragmento amarillo. Cuando esto sucede, ambos botones saltan al final de la pantalla.

Quiero que ambos botones permanezcan en la misma posición al cambiar al fragmento amarillo. ¿Cómo se puede hacer eso?

rodar

Código fuente disponible en https://github.com/wondering639/stack-dynamiccontent respectivamente https://github.com/wondering639/stack-dynamiccontent.git

Fragmentos de código relevantes:

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" />

INSINUACIÓN

Tenga en cuenta que este es, por supuesto, un ejemplo mínimo de trabajo para mostrar mi problema. En mi proyecto real, también tengo vistas debajo de @+id/fragment_container. Entonces dando un tamaño fijo a@+id/fragment_container no es una opción para mí: causaría un área en blanco grande al cambiar al fragmento amarillo bajo.

ACTUALIZACIÓN: Descripción general de las soluciones propuestas

Implementé las soluciones propuestas para propósitos de prueba y agregué mis experiencias personales con ellas.

respuesta por Cheticamp, https://stackoverflow.com/a/60323255

-> disponible en https://github.com/wondering639/stack-dynamiccontent/tree/60323255

-> FrameLayout envuelve contenido, código corto

respuesta por Pavneet_Singh, https://stackoverflow.com/a/60310807

-> disponible en https://github.com/wondering639/stack-dynamiccontent/tree/60310807

-> FrameLayout obtiene el tamaño del fragmento azul. Así que no hay envoltura de contenido. Al cambiar al fragmento amarillo, hay una brecha entre este y el contenido que lo sigue (si algún contenido lo sigue). Sin embargo, no hay renderizado adicional. ** actualización ** se proporcionó una segunda versión que muestra cómo hacerlo sin vacíos. Revisa los comentarios a la respuesta.

respuesta por Ben P., https://stackoverflow.com/a/60251036

-> disponible en https://github.com/wondering639/stack-dynamiccontent/tree/60251036

-> FrameLayout ajusta el contenido. Más código que la solución de Cheticamp. Tocar el botón "mostrar amarillo" dos veces conduce a un "error" (los botones saltan hacia abajo, en realidad mi problema original). Uno podría discutir sobre simplemente deshabilitar el botón "mostrar amarillo" después de cambiar a él, por lo que no consideraría esto como un problema real.

stefan.at.wpf
fuente
Cuando intento reproducir esto, el "salto" solo ocurre si la vista se desplaza lo suficiente como para que el espacio debajo de los botones sea mayor que el tamaño total del fragmento amarillo. No hay nada debajo de eso, por lo que no hay forma de mantener los botones en posición. ¿Estás seguro de que esto es realmente un error?
Ben P.
Después de pensar más en su comentario, creo que tiene razón en que el comportamiento actual es técnicamente correcto. Scrollview intenta llenar la pantalla completa y después de cambiar al fragmento amarillo no hay suficiente contenido debajo, por lo que se desplaza para obtener más contenido desde arriba. Así que puedo imaginar dos soluciones: 1) decirle a scrollview que no fuerce a llenar la pantalla 2) cuando cambie al fragmento amarillo, agregue la diferencia de altura entre el fragmento azul y amarillo como relleno inferior a la disposición de restricción externa o a la vista de desplazamiento, si directamente lo apoya.
stefan.at.wpf
@BenP. ¿Tienes alguna idea de cómo hacer esto mejor? por ejemplo, para 1) si es posible y para 2) cómo hacerlo de una manera que evite la representación innecesaria tanto como sea posible.
stefan.at.wpf

Respuestas:

1

Actualización : para mantener las otras vistas justo debajo del framelayouty para manejar el escenario automáticamente, necesitamos usarlo onMeasurepara implementar el manejo automático, así que siga los siguientes pasos

• Cree una costumbre ConstraintLayoutcomo (o puede usar 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)
    }

}

y úselo en su diseño en lugar 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>

Esto hará un seguimiento de la altura y lo aplicará durante cada cambio de fragmento.

Salida:

Nota: Como se mencionó en los comentarios anteriores, establecer minHeight dará como resultado un paso de representación adicional y no se puede evitar en la versión actual de ConstraintLayout.


Enfoque antiguo con FrameLayout personalizado

Este es un requisito interesante y mi enfoque es resolverlo creando una vista personalizada.

Idea :

Mi idea para la solución es ajustar la altura del contenedor manteniendo el seguimiento del niño más grande o la altura total de los niños en el contenedor.

Intentos :

Mis primeros intentos se basaron en modificar el comportamiento existente NestedScrollViewal extenderlo, pero no proporciona acceso a todos los datos o métodos necesarios. La personalización resultó en un pobre soporte para todos los escenarios y casos extremos.

Más tarde, logré la solución creando una costumbre Framelayoutcon un enfoque diferente.

Implementación de soluciones

Mientras implementaba el comportamiento personalizado de las fases de medición de altura, profundicé y manipulé la altura getSuggestedMinimumHeightmientras rastreaba la altura de los niños para implementar la solución más optimizada, ya que no causará ninguna representación adicional o explícita porque administrará la altura durante la representación interna ciclo, así que cree una FrameLayoutclase personalizada para implementar la solución y anule el getSuggestedMinimumHeightcomo:

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
        )
    }

}

Ahora reemplace FrameLayoutcon MaxChildHeightFrameLayoutin activity_main.xmlcomo:

<?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() se usará para calcular la altura de la vista durante el ciclo de vida de representación de la vista.

Salida:

Con más vistas, fragmento y altura diferente. (400dp, 20dp, 500dp respectivamente)

Pavneet_Singh
fuente
en primer lugar, muchas gracias! Debido a la complejidad de este tema, comprobaré su respuesta y las demás el sábado. ¿Tiene un enlace o puede agregar más información sobre el ciclo de vida de renderizado? Todavía no estoy muy interesado en todos los métodos como onMeasure, etc. y cómo getSuggestedMinimumHeight()se relaciona con ellos o en qué orden se ejecutan.
stefan.at.wpf
@ stefan.at.wpf onMeasurees el primer paso para medir el proceso de renderizado y getSuggestedMinimumHeight(y solo un captador para no generar renderizado adicional) se está utilizando en el interior onMeasurepara definir la altura. setMinHeightllamará a requestLayout () mientras dará como resultado la medición, el diseño y el dibujo del árbol de la vista de diseño
Pavneet_Singh
@ stefan.at.wpf agregó más información y casos de uso que son tediosos o que no se pueden manejar manualmente, especialmente cuando el número de fragmentos, vistas son desconocidos o crecen dinámicamente. Esta solución es una solución dinámica para todos los escenarios y no requiere manejo manual.
Pavneet_Singh
solo revisé y probé tu respuesta. En realidad, quiero que FrameLayout ajuste el contenido. De lo contrario, hay una brecha cuando el contenido sigue después de FrameLayout. ¿Agregaría una segunda variante para completar, donde FrameLayout envuelve el contenido? Como el intento de todos modos, porque aprendí sobre el ciclo de renderizado :-) Verifique la actualización de mi publicación original, implementé su versión, incluido el contenido, siguiendo el FrameLayout. Tal vez podría usarse como base para un FrameLayout de ajuste :-)
stefan.at.wpf
@ stefan.at.wpf sí, entonces puedo crear un diseño de restricción personalizado (o cualquier diseño de contenedor) para evitar configuraciones manuales con altura mínima u otro enfoque es agregar una vista adicional (como dijiste, tal vez podría usarse como base para un ajuste FrameLayout), así que aquí está la demostración con salida, aunque puede intentar agregar el tercer fragmento más grande, que funcionará con este enfoque pero no con la configuración manual de altura.
Pavneet_Singh
1

Una solución sencilla es ajustar la altura mínima de ConstraintLayout dentro de NestedScrollView antes de cambiar los fragmentos. Para evitar saltos, la altura del diseño de restricción debe ser mayor o igual que

  1. la cantidad por la cual NestedScrollView se ha desplazado en la dirección "y"

más

  1. la altura de NestedScrollView .

El siguiente código encapsula este concepto:

private fun adjustMinHeight(nsv: NestedScrollView, layout: ConstraintLayout) {
    layout.minHeight = nsv.scrollY + nsv.height
}

Tenga en cuenta que layout.minimumHeightno funcionará para RestraintLayout . Debes usar layout.minHeight.

Para invocar esta función, haga lo siguiente:

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)
}

Es similar para insertBlueFragment () . Puede, por supuesto, simplificar esto haciendo findViewById () una vez.

Aquí hay un video rápido de los resultados.

ingrese la descripción de la imagen aquí

En el video, agregué una vista de texto en la parte inferior para representar elementos adicionales que pueden existir en su diseño debajo del fragmento. Si elimina esa vista de texto, el código seguirá funcionando, pero verá un espacio en blanco en la parte inferior. Así es como se ve:

ingrese la descripción de la imagen aquí

Y si las vistas debajo del fragmento no llenan la vista de desplazamiento, verá las vistas adicionales más el espacio en blanco en la parte inferior.

ingrese la descripción de la imagen aquí

Cheticamp
fuente
bueno, esta solución directa es la implementación manual (tediosa) de mi solución para ajustar el minHeight, aplicado en un contenedor diferente :). de todos modos, también pensé en este enfoque, pero no funcionará en todos los escenarios, como si el primer fragmento es más pequeño que el segundo fragmento, entonces esta lógica fallará ya que no tendrá la altura deseada. otro ejemplo, qué pasa si hay más de dos fragmentos (bastante comunes) y el tercero es más grande que los anteriores.
Pavneet_Singh
@Pavneet_Singh No he examinado tu código, así que no puedo comentar sobre similitudes. Aunque, por su comentario, supongo que los dos no son lo mismo. Creo que lo que presenté es general. Los botones saltan porque el elemento secundario de NestedScrollView (ConstraintLayout) no tiene suficiente altura para cubrir la parte de la vista de desplazamiento que se desplaza fuera de la pantalla más lo que está en la pantalla. Mientras se cumpla ese requisito de altura (a través del cálculo de altura mínima), en realidad no importa qué más suceda debajo de los botones. Puede que estés mirando mi primera publicación antes de las ediciones.
Cheticamp
No es el código, solo la idea central de la solución, aunque no hay ningún problema. Entiendo el concepto bastante bien y, como dije antes y ahora, sus requisitos de altura no coincidirán si el primer fragmento es pequeño, por lo que no funcionará. Además, establecer y mantener la altura de mantenimiento de manera explícita es tedioso y también causará renderizado adicional.
Pavneet_Singh
@Pavneet_Singh Me pregunto ahora si estás comentando la respuesta que crees que eres. No establezco ninguna altura explícitamente y el primer fragmento pequeño no es un problema AFAIK.
Cheticamp
Cheticamp, solo quería decir muchas gracias y hacerle saber que examinaré más de cerca esta y las otras respuestas el sábado. Ahora no estoy tan interesado en el ciclo de vida de la representación, pero como commitse llama antes al FragmentManager adjustMinHeight, ¿supongo que hay un ciclo de representación adicional involucrado?
stefan.at.wpf
0

Su FrameLayoutinterior activity_main.xmltiene un atributo de altura de wrap_content.

Los diseños de fragmentos de su hijo determinan las diferencias de altura que está viendo.

Debería publicar su xml para los fragmentos secundarios

Intente establecer una altura específica FrameLayoutenactivity_main.xml

DevJZ
fuente
Gracias por su respuesta, su explicación tiene sentido. Actualicé mi publicación y agregué el XML para ambos fragmentos. En este ejemplo, ambos tienen alturas fijas (pero, por supuesto, diferentes). En mi aplicación "real" su altura es, por supuesto, wrap_content. No puedo establecer la altura de FrameLayout en una altura fija, ya que en mi aplicación real tengo algo de contenido después de FrameLayout. No lo incluí en el ejemplo, lo siento \: ¿Alguna idea de cómo podría manejar mi caso?
stefan.at.wpf
0

Resolví esto creando un oyente de diseño que realiza un seguimiento de la altura "anterior" y agrega relleno al ScrollView si la nueva altura es menor que antes.

  • 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)
        }
    }
}

Para habilitar esta escucha, agregue este método a ambos fragmentos:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val listener = HeightLayoutListener.create(this)
    view.viewTreeObserver.addOnGlobalLayoutListener(listener)
}

Estos son los métodos que el oyente llama para actualizar realmente ScrollView. Agréguelos a su actividad:

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
}

Y debe establecer argumentos en sus fragmentos para que el oyente sepa cuál era la altura anterior y la posición de desplazamiento anterior. Así que asegúrese de agregarlos a sus transacciones fragmentarias:

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)
    }
}

¡Y eso debería hacerlo!

Ben P.
fuente
gracias por esa extensa respuesta! dame algo de tiempo para experimentarlo y esperar quizás otras respuestas sin parpadear. De lo contrario, por supuesto, lo aceptaré como un buen comienzo y recompensaré la recompensa :-)
stefan.at.wpf
@ stefan.at.wpf He actualizado mi respuesta con una solución alternativa para el parpadeo
Ben P.
¡Una vez más, muchas gracias!
Miraré
Revisé tu versión actualizada y funciona bien. Cuando uno toca el amarillo nuevamente, entonces salta como en la descripción de mi problema original, pero no lo consideraría un problema práctico. ¿Su versión causa un ciclo de renderizado adicional? Me
resulta
Ah, sí, el relleno solo se actualiza para ser al menos tan grande como lo anterior, por lo que si el fragmento "anterior" es amarillo y agrega amarillo nuevamente, el relleno se convertirá en 0. Puede resolver eso manteniendo un solo "máximo" "valor en lugar de solo el anterior. Y oye, ¡vota lo que encuentres útil y premia a quien quieras!
Ben P.