ScrollViewの内部では、高さが異なる2つのフラグメントを動的に切り替えています。残念ながらそれはジャンプにつながります。次のアニメーションで確認できます。
黄色のフラグメントに切り替えるときに、両方のボタンを同じ位置に留めておきたい。どうすればできますか?
ソースコードは 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回タッチすると、「バグ」が表示されます(ボタンは一番下にジャンプしますが、実際には元の問題です)。 「黄色を表示」ボタンに切り替えた後、それを無効にすることだけを主張することができるので、これを本当の問題とは考えません。
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
_のFrameLayout
をMaxChildHeightFrameLayout
に置き換えます。
_<?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)
簡単な解決策は、フラグメントを切り替える前に、NestedScrollView内のConstraintLayoutの最小の高さを調整することです。ジャンプしないようにするには、ConstraintLayoutの高さが以上でなければなりません
プラス
次のコードは、この概念をカプセル化します。
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()を1回実行することでこれを簡略化できます。
これは結果の簡単なビデオです。
ビデオでは、フラグメントの下のレイアウトに存在する可能性のある追加のアイテムを表すテキストビューを下部に追加しました。そのテキストビューを削除しても、コードは機能しますが、下部に空白スペースが表示されます。これは次のようになります。
また、フラグメントの下のビューがスクロールビューを満たさない場合は、追加のビューと下部の空白が表示されます。
activity_main.xml
内のFrameLayout
の高さ属性はwrap_content
です。
子フラグメントのレイアウトによって、表示される高さの違いが決まります。
子フラグメントのXMLをポストする必要があります
特定の高さをactivity_main.xml
のFrameLayout
に設定してみてください
これを解決するには、「以前の」高さを追跡し、新しい高さが以前よりも低い場合はScrollViewにパディングを追加するレイアウトリスナーを作成します。
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)
}
}
そして、それでうまくいくはずです!