Прыжки с прокруткой при переключении фрагментов - PullRequest
8 голосов
/ 14 февраля 2020

Внутри ScrollView Я динамически переключаюсь между двумя фрагментами с разной высотой. К сожалению, это приводит к прыжкам. Это можно увидеть в следующей анимации:

  1. Я прокручиваю вниз, пока не достигну кнопки «показать желтый».
  2. Нажатие «показать желтый» заменяет огромный синий фрагмент на крошечный желтый фрагмент. Когда это происходит, обе кнопки переходят в конец экрана.

Я хочу, чтобы обе кнопки оставались в одном и том же положении при переключении на желтый фрагмент. Как это можно сделать?

roll

Исходный код доступен по адресу https://github.com/wondering639/stack-dynamiccontent соответственно https://github.com/wondering639/stack-dynamiccontent.git

Соответствующие фрагменты кода:

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


}

фрагмент_ синий. 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" />

фрагмент_желтый. 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" />

СОВЕТ

Обратите внимание, что это, конечно, минимальный рабочий пример, чтобы показать мою проблему. В моем реальном проекте у меня также есть представления ниже @+id/fragment_container. Поэтому предоставление фиксированного размера @+id/fragment_container для меня не вариант - это приведет к большой пустой области при переключении на низкий желтый фрагмент.

ОБНОВЛЕНИЕ: Обзор предлагаемых решений

Я реализовал предложенные решения для целей тестирования и добавил свой личный опыт работы с ними.

ответ от Cheticamp, { ссылка }

-> доступно в https://github.com/wondering639/stack-dynamiccontent/tree/60323255

-> FrameLayout оборачивает содержимое, короткий код

ответ от Pavneet_Singh, { ссылка }

-> доступно в https://github.com/wondering639/stack-dynamiccontent/tree/60310807

-> FrameLayout получает размер синего фрагмента. Так что без упаковки контента. При переключении на желтый фрагмент, между ним и контентом, следующим за ним, есть разрыв (если какой-либо контент следует за ним). Никакого дополнительного рендеринга, хотя! ** обновление ** была предоставлена ​​вторая версия, показывающая, как это сделать без пробелов. Проверьте комментарии к ответу.

ответ Бен П., { ссылка }

-> доступно в https://github.com/wondering639/stack-dynamiccontent/tree/60251036

-> FrameLayout переносит содержимое. Больше кода, чем решение от Cheticamp. Двойное прикосновение к кнопке «показать желтый» приводит к «ошибке» (кнопки прыгают вниз, на самом деле это моя первоначальная проблема). Можно было бы поспорить о том, чтобы просто отключить кнопку «показать желтую» после переключения на нее, поэтому я не считаю это реальной проблемой.

Ответы [ 4 ]

1 голос
/ 20 февраля 2020

Простым решением является настройка минимальной высоты ConstraintLayout в пределах NestedScrollView перед переключением фрагментов. Чтобы предотвратить прыжок, высота ConstraintLayout должна быть больше или равна

  1. величине, на которую NestedScrollView прокручивается в «y» направление

плюс

высота NestedScrollView .

Следующий код инкапсулирует эту концепцию:

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

Обратите внимание, что layout.minimumHeight не будет работать для ConstraintLayout . Вы должны использовать layout.minHeight.

Чтобы вызвать эту функцию, выполните следующие действия:

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

Это похоже на insertBlueFragment () . Конечно, вы можете упростить это, выполнив findViewById () один раз.

Вот краткое видео результатов.

enter image description here

В видео я добавил текстовое представление внизу, чтобы представить дополнительные элементы, которые могут существовать в вашем макете ниже фрагмента. Если вы удалите это текстовое представление, код все равно будет работать, но вы увидите пустое пространство внизу. Вот как это выглядит:

enter image description here

И если виды под фрагментом не заполняют вид прокрутки, вы увидите дополнительные виды плюс пустое пространство внизу.

enter image description here

1 голос
/ 20 февраля 2020

Обновление : чтобы другие виды оставались прямо под framelayout и обрабатывать сценарий автоматически, нам нужно использовать onMeasure для реализации автоматической обработки, поэтому выполните следующие шаги

• Создайте пользовательский ConstraintLayout как (или можете использовать 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)
    }

}

и используйте его в макете вместо 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>

Это будет отслеживать высоту и применять ее при каждом изменении фрагмента.

Вывод:

комментарии до, настройка minHeight приведет к дополнительному проходу рендеринга, и этого нельзя избежать в текущей версии ConstraintLayout.


Старый подход с пользовательским FrameLayout

Это интересное требование, и мой подход состоит в том, чтобы решить его путем создания собственного представления.

Идея :

Моя идея для решения состоит в том, чтобы отрегулировать высоту контейнера, сохраняя след самого большого дочернего элемента или общую высоту дочерних элементов в контейнере.

Попытки :

Мои первые несколько попыток были основаны на изменении существующего поведения NestedScrollView путем его расширения, но оно не обеспечивает доступа ко всем необходимым данным. или методы. Настройка привела к плохой поддержке всех сценариев ios и крайних случаев.

Позже я достиг решения, создав собственный Framelayout с другим подходом.

Реализация решения

При реализации пользовательского поведения высоты На этапах измерения я копал глубже и манипулировал высотой с помощью getSuggestedMinimumHeight, отслеживая высоту дочерних элементов, чтобы реализовать наиболее оптимизированное решение, поскольку оно не вызовет никакого дополнительного или явного рендеринга, поскольку оно будет управлять высотой во время внутреннего цикла рендеринга, поэтому создайте Пользовательский класс FrameLayout для реализации решения и переопределения getSuggestedMinimumHeight как:

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

}

Теперь замените FrameLayout на MaxChildHeightFrameLayout в 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="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() будет использоваться для расчета высоты вида в течение жизненного цикла рендеринга вида.

Вывод:

С большим количеством видов, фрагментов и разной высоты. (400dp, 20dp, 500dp соответственно)

0 голосов
/ 16 февраля 2020

Я решил эту проблему, создав прослушиватель макета, который отслеживает «предыдущую» высоту и добавляет отступ в ScrollView, если новая высота меньше, чем раньше.

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

Чтобы включить этот слушатель, добавьте этот метод к обоим фрагментам:

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

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

Это методы, которые слушатель вызывает для фактического обновления ScrollView. Добавьте их к своей деятельности:

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
}

И вам необходимо установить аргументы для ваших фрагментов, чтобы слушатель знал, какова была предыдущая высота и предыдущая позиция прокрутки. Поэтому обязательно добавьте их в свои транзакции фрагментов:

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

И это должно быть сделано!

0 голосов
/ 14 февраля 2020

Ваш FrameLayout внутри activity_main.xml имеет атрибут высоты wrap_content.

Схемы дочерних фрагментов определяют разницу в высоте, которую вы видите.

Должна опубликовать ваш xml для дочерних фрагментов

Попробуйте установить конкретную c высоту для FrameLayout в вашем activity_main.xml

...