Android

[Android] NestedScrollView 활용해서 가변 높이 ScrollView 밑에 버튼 넣기 (feat. chainStyle, minHeight)

욘두로이드 2023. 1. 4. 22:07

세줄요약 (요구사항 / 해결방법)

1. 스크롤뷰(WebView and ScrollView)가 이중으로 들어가야한다. / NestedScrollView 사용

2. 높이가 가변인 뷰의 최소 높이를 정하고 싶다. / MinHeight 사용

3. Constraint Chain 에서 뷰들 사이 간격을 최대로 설정 해야한다. / ConstraintLayout ChainStyle CHAIN_SPREAD_INSIDE 사용

 

배경

WebView와 그 하단에 버튼 하나로 구성된 화면을 개발해야했다.

WebView와 버튼을 포함하는 스크롤 영역을 끝까지 내렸을때 최하단에 버튼이 나오게 구성하면서

스크롤이 안생겼을 때도 버튼이 화면 하단에 딱맞게 들어가도록 의도된 디자인이 나왔다.

 

필자의 경우 WebView를 사용하는 케이스였으나 이는 사실 ScrollView 와 다른 View 를 스크롤 뷰로 감싸고 싶을때 모두 적용 할 수 있는 방법이다.

 

이해를 돕기 위해 간단한 테스트앱을 빌드 해보았다.

 

스크롤 있을 때

스크롤 없을 때

 

해결방법

Step 1. NestedScrollView를 사용해 UI를 구성한다.

 

NestedScrollView를 사용하지 않고 ScrollView 안에 ScrollView를 사용하게되면 사용자의 제스쳐가 어떤 ScrollView에 focus될지 명확하지 않기 때문에 의도한 것처럼 동작 하지 않을 수 있다.

 

Android Developer의 공식 문서에서 NestedScrollView 의 설명을 보면 다음과 같다.

NestedScrollView is just like ScrollView, but it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.

요약하자면 NestedScrollView는 ScrollView와 비슷하지만 중첩된 스크롤 부모 및 자식 역할을 모두 지원한다는 이야기 이다.

 

Step 2. 자식 스크롤 뷰와 형제 뷰들을 체인으로 연결하고 첫번째 뷰에 chainStyle을 spread_inside로 설정해둔다.

 

여기까지만 하면 스크롤이 생겼을때는 화면이 의도한것처럼 보일 수 있다.

 

하지만 만약 높이가 가변인 자식 스크롤 영역의 내용이 짧아서 스크롤이 생기지 않았다면?

 

버튼이 화면 아래에 붙어있지 않고 위에 둥둥 떠있을 것이다 왜냐하면 자식 스크롤뷰와 버튼을 감싸는 부모 스크롤뷰의 바로 하위 컨테이너의 높이가 wrap_content로 가변이기 때문이다.

최소높이 적용 전

Step 3. 위 문제를 해결하는 방법은 간단하다.

부모 스크롤뷰의 높이 만큼 컨테이너의 최소 높이를 설정하는것이다.

// 부모 스크롤뷰
val nestedScrollView = findViewById<NestedScrollView>(R.id.nestedScrollView)

// 자식 스크롤뷰와 버튼을 감싸는 컨테이너
val contentAreaWrapper = findViewById<ConstraintLayout>(R.id.contentAreaWrapper)

contentAreaWrapper.post {
    // 최소 높이를 부모 스크롤뷰의 높이 만큼 설정
    contentAreaWrapper.minHeight = nestedScrollView.height
}

이렇게 하면 항상 꽉차있는 화면이 될 것이고 위에서 chainStyle을 spread_inside로 지정했기 때문에 버튼이 화면 하단 정렬한것과 같은 결과를 얻을 수 있다.

최소높이 적용 후

 

끝.

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/nestedScrollView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/purple_200"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/contentAreaWrapper"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/titleTextView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:text="부모 스크롤 뷰 영역"
                android:textColor="@color/black"
                android:textSize="20sp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <ScrollView
                android:id="@+id/dynamicHeightArea"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="20dp"
                android:layout_marginTop="80dp"
                android:layout_marginEnd="20dp"
                android:background="@android:color/darker_gray"
                android:padding="20dp"
                app:layout_constraintBottom_toTopOf="@id/button"
                app:layout_constraintHorizontal_bias="0.6"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_bias="0"
                app:layout_constraintVertical_chainStyle="spread_inside">

                <androidx.constraintlayout.widget.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <!--                        <TextView-->
                    <!--                            android:layout_width="50dp"-->
                    <!--                            android:layout_height="wrap_content"-->
                    <!--                            android:gravity="center"-->
                    <!--                            android:text="000000000000000000000000 자식 스크롤뷰 영역 000000000000000000000000"-->
                    <!--                            android:textColor="@color/black"-->
                    <!--                            android:textSize="30sp"-->
                    <!--                            app:layout_constraintBottom_toBottomOf="parent"-->
                    <!--                            app:layout_constraintLeft_toLeftOf="parent"-->
                    <!--                            app:layout_constraintRight_toRightOf="parent"-->
                    <!--                            app:layout_constraintTop_toTopOf="parent" />-->

                    <TextView
                        android:layout_width="50dp"
                        android:layout_height="wrap_content"
                        android:gravity="center"
                        android:text="자식 스크롤뷰"
                        android:textColor="@color/black"
                        android:textSize="30sp"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintLeft_toLeftOf="parent"
                        app:layout_constraintRight_toRightOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                </androidx.constraintlayout.widget.ConstraintLayout>

            </ScrollView>

            <Button
                android:id="@+id/button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:layout_marginBottom="20dp"
                android:text="가변영역 하단 버튼"
                android:textSize="25sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/dynamicHeightArea" />

        </androidx.constraintlayout.widget.ConstraintLayout>


    </androidx.core.widget.NestedScrollView>


</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity.kt

package com.dev_yangkj.tistory.nestedscrollviewtest

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.NestedScrollView

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 부모 스크롤뷰
        val nestedScrollView = findViewById<NestedScrollView>(R.id.nestedScrollView)

        // 자식 스크롤뷰와 버튼을 감싸는 컨테이너
        val contentAreaWrapper = findViewById<ConstraintLayout>(R.id.contentAreaWrapper)

        contentAreaWrapper.post {
            // 최소 높이를 부모 스크롤뷰의 높이 만큼 설정
            contentAreaWrapper.minHeight = nestedScrollView.height
        }

    }
}