Pulando a rolagem ao alternar fragmentos

8

Dentro de um ScrollView, alterno dinamicamente entre dois fragmentos com alturas diferentes. Infelizmente isso leva ao salto. Pode-se vê-lo na seguinte animação:

  1. Estou rolando para baixo até chegar ao botão "mostrar amarelo".
  2. Pressionar "mostrar amarelo" substitui um enorme fragmento azul por um pequeno fragmento amarelo. Quando isso acontece, os dois botões saltam para o final da tela.

Eu quero que os dois botões fiquem na mesma posição ao mudar para o fragmento amarelo. Como isso pode ser feito?

lista

Código-fonte disponível em https://github.com/wondering639/stack-dynamiccontent, respectivamente https://github.com/wondering639/stack-dynamiccontent.git

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

DICA

Observe que este é, obviamente, um exemplo de trabalho mínimo para mostrar meu problema. No meu projeto real, também tenho visualizações abaixo do @+id/fragment_container. Portanto, atribuir um tamanho fixo a @+id/fragment_containernão é uma opção para mim - isso causaria uma grande área em branco ao mudar para o fragmento baixo e amarelo.

ATUALIZAÇÃO: Visão geral das soluções propostas

Implementei as soluções propostas para fins de teste e adicionei minhas experiências pessoais a elas.

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

-> disponível em https://github.com/wondering639/stack-dynamiccontent/tree/60323255

-> FrameLayout agrupa conteúdo, código curto

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

-> disponível em https://github.com/wondering639/stack-dynamiccontent/tree/60310807

-> FrameLayout obtém o tamanho do fragmento azul. Portanto, sem quebra de conteúdo. Ao alternar para o fragmento amarelo, existe um espaço entre ele e o conteúdo a seguir (se houver algum conteúdo). Nenhuma renderização adicional! ** atualização ** Uma segunda versão foi fornecida mostrando como fazê-lo sem falhas. Verifique os comentários para a resposta.

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

-> disponível em https://github.com/wondering639/stack-dynamiccontent/tree/60251036

-> FrameLayout agrupa o conteúdo. Mais código que a solução da Cheticamp. Tocar no botão "mostrar amarelo" duas vezes leva a um "bug" (os botões saltam para baixo, na verdade, é o meu problema original). Alguém poderia argumentar sobre apenas desativar o botão "mostrar amarelo" depois de mudar para ele, para que eu não considerasse isso um problema real.

stefan.at.wpf
fonte
Quando tento reproduzir isso, o "salto" ocorre apenas se a exibição for rolada para cima o suficiente para que o espaço abaixo dos botões seja maior que o tamanho total do fragmento amarelo. Não há nada abaixo disso, então não há como manter os botões em posição. Tem certeza de que isso é realmente um bug?
Ben P.
Depois de pensar mais sobre o seu comentário, acho que você está certo de que o comportamento atual é tecnicamente correto. O Scrollview tenta preencher a tela completa e, após alternar para o fragmento amarelo, não há conteúdo suficiente abaixo dele; portanto, ele rola para obter mais conteúdo de cima. Para que eu possa imaginar duas soluções: 1) diga ao scrollview para não forçar o preenchimento da tela 2) ao alternar para o fragmento amarelo, adicione a diferença de altura entre o fragmento azul e amarelo como preenchimento inferior ao layout da restrição externa ou ao scrollview, se suporta diretamente.
stefan.at.wpf 16/02
@BenP. você tem alguma idéia de como fazer isso da melhor maneira? por exemplo, para 1) se possível e para 2) como fazê-lo de uma maneira que evite a renderização desnecessária, tanto quanto possível.
stefan.at.wpf 16/02

Respostas:

1

Atualização : para manter as outras visualizações logo abaixo do framelayoute para manipular o cenário automaticamente, precisamos usar onMeasurepara implementar o tratamento automático, assim como as seguintes etapas

• Crie um personalizado ConstraintLayoutcomo (ou pode 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)
    }

}

e use-o em seu layout no 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>

Isso acompanhará a altura e a aplicará durante cada alteração de fragmento.

Resultado:

Nota: Conforme mencionado nos comentários anteriores, a configuração de minHeight resultará em um passo de renderização adicional e não poderá ser evitado na versão atual do ConstraintLayout.


Abordagem antiga com FrameLayout personalizado

Esse é um requisito interessante e minha abordagem é resolvê-lo criando uma exibição personalizada.

Idéia :

Minha idéia para a solução é ajustar a altura do contêiner, mantendo o controle do maior filho ou da altura total de filhos no contêiner.

Tentativas :

Minhas primeiras tentativas foram baseadas na modificação do comportamento existente NestedScrollView, estendendo-o, mas ele não fornece acesso a todos os dados ou métodos necessários. A personalização resultou em suporte insuficiente para todos os cenários e casos extremos.

Mais tarde, consegui a solução criando um costume Framelayoutcom uma abordagem diferente.

Implementação da solução

Ao implementar o comportamento personalizado das fases de medição de altura, eu me aprofundava e manipulava a altura getSuggestedMinimumHeightenquanto rastreia a altura dos filhos para implementar a solução mais otimizada, pois não causará nenhuma renderização adicional ou explícita, pois gerenciará a altura durante a renderização interna ciclo para criar uma FrameLayoutclasse personalizada para implementar a solução e substituir o 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
        )
    }

}

Agora substitua FrameLayoutpor MaxChildHeightFrameLayoutpor 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() será usado para calcular a altura da vista durante o ciclo de vida da renderização da vista.

Resultado:

Com mais visualizações, fragmentos e diferentes alturas. (400dp, 20dp, 500dp respectivamente)

Pavneet_Singh
fonte
Antes de tudo, muito obrigado! Devido à complexidade deste tópico, verificarei sua resposta e as demais no sábado. Você tem um link ou pode adicionar mais informações sobre o ciclo de vida da renderização? Ainda não estou muito interessado em todos os métodos, como onMeasure, etc, e como getSuggestedMinimumHeight()está relacionado a eles ou em qual ordem eles são executados.
stefan.at.wpf 20/02
@ stefan.at.wpf onMeasureé o primeiro passo para medir o processo de renderização e getSuggestedMinimumHeight(e apenas um getter para não causar renderização adicional) está sendo usado dentro onMeasurepara definir a altura. setMinHeightchamará requestLayout () enquanto resultará na medição, layout e desenho da árvore de exibição de layout
Pavneet_Singh
O @ stefan.at.wpf adicionou mais informações e casos de uso tediosos ou que não podem ser manipulados manualmente, especialmente quando o número de fragmentos e visualizações é desconhecido ou cresce dinamicamente. esta solução é uma solução dinâmica para todos os cenários e não requer manipulação manual.
Pavneet_Singh 21/02
acabou de verificar e testar sua resposta. Na verdade, quero que o FrameLayout envolva o conteúdo. Caso contrário, há uma lacuna quando o conteúdo segue após o FrameLayout. Você adicionaria uma segunda variante para completude, onde o FrameLayout agrupa o conteúdo? Como a tentativa de qualquer maneira, porque eu aprendi sobre o ciclo de renderização :-) Verifique a atualização da minha postagem original, implementei sua versão incluindo o conteúdo seguindo o FrameLayout. Talvez possa ser usado como base para um FrameLayout de embrulho :-)
stefan.at.wpf
@ stefan.at.wpf Sim, então eu posso criar um layout de restrição personalizado (ou qualquer layout de contêiner) para evitar configurações manuais com altura mínima ou outra abordagem é adicionar visão adicional (como você disse, talvez possa ser usado como base para uma embalagem) FrameLayout), então aqui está a demonstração com saída, que pode tentar adicionar o terceiro maior fragmento, que funcionará com essa abordagem, mas não com a configuração manual de altura.
Pavneet_Singh
1

Uma solução direta é ajustar a altura mínima do ConstraintLayout no NestedScrollView antes de alternar fragmentos. Para evitar pulos, a altura do ConstraintLayout deve ser maior ou igual a

  1. a quantidade pela qual o NestedScrollView rolou na direção "y"

mais

  1. a altura do NestedScrollView .

O código a seguir encapsula esse conceito:

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

Observe que layout.minimumHeightnão funcionará no ConstraintLayout . Você deve usar layout.minHeight.

Para invocar esta função, faça o seguinte:

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

É semelhante para insertBlueFragment () . Obviamente, você pode simplificar isso fazendo findViewById () uma vez.

Aqui está um rápido vídeo dos resultados.

insira a descrição da imagem aqui

No vídeo, adicionei uma exibição de texto na parte inferior para representar itens adicionais que podem existir no seu layout abaixo do fragmento. Se você excluir essa exibição de texto, o código ainda funcionará, mas você verá um espaço em branco na parte inferior. Aqui está o que parece:

insira a descrição da imagem aqui

E se as vistas abaixo do fragmento não preencherem a vista de rolagem, você verá as vistas adicionais mais o espaço em branco na parte inferior.

insira a descrição da imagem aqui

Cheticamp
fonte
bem, essa solução simples é a implementação manual (tediosa) da minha solução para ajustar o minHeight, aplicado em diferentes contêineres :). de qualquer maneira, também pensei nessa abordagem, mas ela não funcionará em todos os cenários, como se o primeiro fragmento fosse menor que o segundo fragmento, essa lógica falhará, pois você não terá a altura desejada. outro exemplo, e se houver mais de dois fragmentos (bastante comuns) e o terceiro for maior que os anteriores.
Pavneet_Singh 20/02
@Pavneet_Singh Não examinei seu código, portanto não posso comentar sobre semelhanças. Embora, pelo seu comentário, suponho que os dois não sejam os mesmos. Eu acredito que o que apresentei é geral. Os botões pulam porque o filho do NestedScrollView (o ConstraintLayout) não tem altura suficiente para cobrir a parte da exibição de rolagem rolada para fora da tela e o que está na tela. Desde que esse requisito de altura seja atendido (por meio do cálculo da altura mínima), não importa realmente o que mais acontece abaixo dos botões. Você pode estar vendo minha primeira postagem antes das edições.
Cheticamp
Não é o código, apenas a idéia central da solução, mas não há problema algum. Entendo muito bem o conceito e, como disse antes e agora, seus requisitos de altura não corresponderão se o primeiro fragmento for pequeno, portanto não funcionará. Além disso, definir e manter a altura explicitamente é entediante e também causará renderização adicional.
Pavneet_Singh 20/02
@Pavneet_Singh Gostaria de saber agora se você está comentando a resposta que pensa. Eu não defino nenhuma altura explicitamente e o primeiro fragmento pequeno não é um problema AFAIK.
Cheticamp
Cheticamp, só queria dizer muito obrigado e que você saiba que examinarei mais de perto essa e as outras respostas no sábado. Agora não estou muito interessado no ciclo de vida da renderização, mas como o FragmentManager commité chamado antes adjustMinHeight, presumo que haja um ciclo de renderização adicional envolvido?
stefan.at.wpf 20/02
0

Seu FrameLayoutinterior activity_main.xmltem um atributo de altura de wrap_content.

Os layouts de fragmentos filhos estão determinando as diferenças de altura que você vê.

Deve postar seu xml para os fragmentos filhos

Tente definir uma altura específica para o FrameLayoutseuactivity_main.xml

DevJZ
fonte
Obrigado pela sua resposta, sua explicação faz sentido. Atualizei minha postagem e adicionei o XML para os dois fragmentos. Neste exemplo, ambos têm alturas fixas (mas é claro que diferentes). No meu aplicativo "real", a altura deles é, obviamente, wrap_content. Não consigo definir a altura do FrameLayout para uma altura fixa, pois no meu aplicativo real eu tenho algum conteúdo após o FrameLayout. Não o incluí no exemplo, desculpe \: Alguma idéia de como eu poderia lidar com o meu caso?
stefan.at.wpf
0

Resolvi isso criando um ouvinte de layout que monitora a altura "anterior" e adiciona preenchimento ao ScrollView se a nova altura for menor do 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 esse ouvinte, adicione este método aos dois fragmentos:

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

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

Estes são os métodos que o ouvinte chama para realmente atualizar o ScrollView. Adicione-os à sua atividade:

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
}

E você precisa definir argumentos para seus fragmentos para que o ouvinte saiba qual era a altura anterior e a posição de rolagem anterior. Portanto, adicione-os às suas transações de fragmento:

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

E isso deveria bastar!

Ben P.
fonte
obrigado por essa resposta extensa! dê-me algum tempo para experimentá-lo e aguarde talvez outras respostas sem tremer. Caso contrário, é claro que vou aceitá-lo como uma boa base de partida e recompensar a recompensa :-)
stefan.at.wpf
@ stefan.at.wpf Atualizei minha resposta com uma solução alternativa para a cintilação
Ben P.
Mais uma vez, muito obrigado! Vou dar uma olhada mais de perto no sábado :-)
stefan.at.wpf 20/02
Eu verifiquei sua versão atualizada e funciona bem. Quando alguém toca em amarelo novamente, ele salta como na descrição do meu problema original, mas eu não consideraria isso um problema prático. Sua versão causa um ciclo de renderização adicional? Eu tenho dificuldade em selecionar UMA resposta de todas as ótimas respostas aqui \:
stefan.at.wpf
Ah, sim, o preenchimento é atualizado apenas para ser pelo menos tão grande quanto o anterior, portanto, se o fragmento "anterior" for amarelo e você adicionar amarelo novamente, o preenchimento se tornará 0. Você pode resolver isso mantendo um único "máximo" "valor em vez de apenas o anterior. E, ei, vote já o que achar útil e premie quem quiser!
Ben P.