I have two vertical ScrollViews in a vertical LinearLayout. If I set the layout_weight for both to 1, they both use half of the full height. However, this is also the case if one of them only needs e.g. a quarter of the height due to its content. In such a case, I would like to shrink the first ScrollView and use the superfluous space for the second ScrollView (and vice versa).
Any idea how I can achieve this? The surrounding layout does not necessarily have to be a LinearLayout. (I have seen this, but it does not answer my question)
Ok, as there seems to be no way to use LinearLayout directly, I created a custom layout. It must have exactly two ScrollViews as children, but may easily be adapted if one needs additional views inside.
/**
* A vertical LinearLayout containing 2 ScrollViews.
* The height of each ScrollView is growing with its content, as long as both
* ScrollViews fit into the parent.
* If one ScrollView needs to scroll its content, and the other is still smaller than half
* of the parent, the smaller one takes only as much space as needed.
* Only if both views need more than half of the parent space,
* each of them receives exactly half of it.
*/
class DoubleScrollView : LinearLayout {
private lateinit var scrollView1: ScrollView
private lateinit var scrollView2: ScrollView
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle)
init {
orientation = VERTICAL
}
override fun onFinishInflate() {
super.onFinishInflate()
//In this simple example, we assume to have exactly 2 ScrollViews as children
scrollView1 = getChildAt(0) as ScrollView
scrollView2 = getChildAt(1) as ScrollView
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//Allow the scroll views to set the full content height at first
scrollView1.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, 0f)
scrollView2.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, 0f)
val widthSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY)
val heightSpec = MeasureSpec.makeMeasureSpec(height, AT_MOST)
scrollView1.measure(widthSpec, heightSpec)
scrollView2.measure(widthSpec, heightSpec)
// Based on the height the full content would need, adapt the LayoutParams
// of the ScrollViews.
val height = View.MeasureSpec.getSize(heightMeasureSpec)
val height1 = scrollView1.measuredHeight
val height2 = scrollView2.measuredHeight
if (height1 + height2 >= height) {
if (height1 < height / 2) {
//First wrap content, second fills the rest
scrollView2.setLayout(0, 1f)
} else if (height2 < height / 2) {
//Second wrap content, first fills the rest
scrollView1.setLayout(0, 1f)
} else {
//First and second both with same weight
scrollView1.setLayout(0, 1f)
scrollView2.setLayout(0, 1f)
}
} //else: both views can just wrap content
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
private fun View.setLayout(height: Int, weight: Float) {
this.layoutParams.height = height
(this.layoutParams as LinearLayout.LayoutParams).weight = weight
}
}
For testing and demo I created the following minimal app.
Activity:
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
private lateinit var vb: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vb = ActivityMainBinding.inflate(layoutInflater)
setContentView(vb.root)
mainViewModel.text1.observe(this) { text -> updateText(vb.text1, text) }
mainViewModel.text2.observe(this) { text -> updateText(vb.text2, text) }
listOf(vb.plus1, vb.minus1, vb.plus2, vb.minus2).forEachIndexed() { i, button ->
button.setOnClickListener { mainViewModel.changeText(i <= 1, i % 2 == 0) }
}
}
private fun updateText(textView: TextView, text: String?) {
textView.text = text
}
}
ViewModel:
class MainViewModel : ViewModel() {
val text1: LiveData<String> = MutableLiveData("")
val text2: LiveData<String> = MutableLiveData("")
fun changeText(first: Boolean, plus: Boolean) {
val liveData = if (first) text1 else text2
val text = liveData.value!!
val size = text.count { it == '#' } + if (plus) 1 else -1
val lines = Array(size.coerceAtLeast(0)) { "This is line #$it" }
(liveData as MutableLiveData).value = lines.joinToString("\n")
}
}
Layout:
<?xml version="1.0" encoding="utf-8"?>
<yourPackage.DoubleScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/plus1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"></Button>
<Button
android:id="@+id/minus1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-"></Button>
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/plus2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"></Button>
<Button
android:id="@+id/minus2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-"></Button>
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
</yourPackage.DoubleScrollView>
By pressing the + and - Buttons you can fill the scroll views and see how the layout reacts.