androidandroid-layoutscrollviewandroid-linearlayout

Two scrollviews, one below the other, should each use half the space, but shrink in favour of the other if they are not completely filled


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)


Solution

  • 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.