web-dev-qa-db-ja.com

フラグメントを切り替えるときのジャンプスクロール

ScrollViewの内部では、高さが異なる2つのフラグメントを動的に切り替えています。残念ながらそれはジャンプにつながります。次のアニメーションで確認できます。

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


}

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

[〜#〜]ヒント[〜#〜]

これはもちろん、私の問題を自慢するための最低限の実例です。実際のプロジェクトでは、@+id/fragment_containerの下にもビューがあります。したがって、@+id/fragment_containerに固定サイズを指定することは私にとってオプションではありません-低い黄色のフラグメントに切り替えると、大きな空白領域が発生します。

更新:提案されたソリューションの概要

私はテスト目的で提案されたソリューションを実装し、それらの個人的な経験を追加しました。

Cheticampによる回答 https://stackoverflow.com/a/60323255

->利用可能 https://github.com/wondering639/stack-dynamiccontent/tree/60323255

-> FrameLayoutはコンテンツ、短いコードをラップします

Pavneet_Singhによる回答 https://stackoverflow.com/a/60310807

->利用可能 https://github.com/wondering639/stack-dynamiccontent/tree/60310807

-> FrameLayoutは青いフラグメントのサイズを取得します。したがって、コンテンツの折り返しはありません。黄色のフラグメントに切り替えると、それとそれに続くコンテンツの間にギャップがあります(コンテンツがそれに続く場合)。追加のレンダリングはありません! **更新**隙間なく実行する方法を示す2つ目のバージョンが提供されました。回答へのコメントを確認してください。

Ben P.による回答 https://stackoverflow.com/a/60251036

->利用可能 https://github.com/wondering639/stack-dynamiccontent/tree/60251036

-> FrameLayoutはコンテンツをラップします。 Cheticampによるソリューションよりも多くのコード。 「黄色を表示」ボタンを2回タッチすると、「バグ」が表示されます(ボタンは一番下にジャンプしますが、実際には元の問題です)。 「黄色を表示」ボタンに切り替えた後、それを無効にすることだけを主張することができるので、これを本当の問題とは考えません。

8
stefan.at.wpf

Update:他のビューを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>
_

これにより、高さが追跡され、フラグメントが変更されるたびに適用されます。

出力:

注:前に comments で述べたように、 minHeight を設定すると追加のレンダリングパスが発生し、現在のバージョンのConstraintLayoutでは回避できません。


カスタムFrameLayoutを使用した古いアプローチ

これは興味深い要件であり、私のアプローチはカスタムビューを作成して解決することです。

アイデア

解決策についての私の考えは、コンテナ内の最大の子または子の合計の高さを追跡することによって、コンテナの高さを調整することです。

試行回数

最初の数回の試みは、NestedScrollViewの既存の動作を拡張して変更することに基づいていましたが、必要なすべてのデータまたはメソッドへのアクセスを提供していません。カスタマイズにより、すべてのシナリオとEdgeケースのサポートが不十分になりました。

後で、異なるアプローチでカスタムの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
        )
    }

}
_

次に、_activity_main.xml_のFrameLayoutMaxChildHeightFrameLayoutに置き換えます。

_<?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()は、ビューのレンダリングライフサイクル中にビューの高さを計算するために使用されます。

出力:

より多くのビュー、フラグメント、異なる高さ。 (それぞれ400 dp、20 dp、500 dp) 

1
Pavneet_Singh

簡単な解決策は、フラグメントを切り替える前に、NestedScrollView内のConstraintLayoutの最小の高さを調整することです。ジャンプしないようにするには、ConstraintLayoutの高さが以上でなければなりません

  1. NestedScrollViewが「y」方向にスクロールした量

プラス

  1. NestedScrollViewの高さ.

次のコードは、この概念をカプセル化します。

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

layout.minimumHeightConstraintLayoutでは機能しないことに注意してください。 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()を1回実行することでこれを簡略化できます。

これは結果の簡単なビデオです。

enter image description here

ビデオでは、フラグメントの下のレイアウトに存在する可能性のある追加のアイテムを表すテキストビューを下部に追加しました。そのテキストビューを削除しても、コードは機能しますが、下部に空白スペースが表示されます。これは次のようになります。

enter image description here

また、フラグメントの下のビューがスクロールビューを満たさない場合は、追加のビューと下部の空白が表示されます。

enter image description here

1
Cheticamp

activity_main.xml内のFrameLayoutの高さ属性はwrap_contentです。

子フラグメントのレイアウトによって、表示される高さの違いが決まります。

子フラグメントのXMLをポストする必要があります

特定の高さをactivity_main.xmlFrameLayoutに設定してみてください

0
DevJZ

これを解決するには、「以前の」高さを追跡し、新しい高さが以前よりも低い場合は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
Ben P.