<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>chen-studio.log</title>
        <link>https://velog.io/</link>
        <description>Android Engineer</description>
        <lastBuildDate>Sat, 07 Dec 2024 04:33:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. chen-studio.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chen-studio" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Modal BottomSheetDialogFragment 크기 (feat 2 step, full screen)]]></title>
            <link>https://velog.io/@chen-studio/Modal-BottomSheetDialogFragment-%ED%81%AC%EA%B8%B0-%EC%A1%B0%EC%A0%88%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chen-studio/Modal-BottomSheetDialogFragment-%ED%81%AC%EA%B8%B0-%EC%A1%B0%EC%A0%88%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 07 Dec 2024 04:33:41 GMT</pubDate>
            <description><![CDATA[<h1 id="bottomsheetdialogfragment">BottomSheetDialogFragment</h1>
<p>Android Material에서 제공하는 스와이프 가능한 <strong>BottomSheetDialogFragment</strong>를 구현하는 방법은 크게 2가지이다. Compose에서 제공하는 <strong>ModalBottomSheetLayout</strong>은 이 글에서 다루지 않는다.</p>
<h2 id="standard-bottom-sheet">Standard Bottom Sheet</h2>
<ul>
<li>Standard Bottom Sheet는 현재 주 UI영역에 함께 포함되어 현재 UI와 상호작용이 가능한 BottomSheet이다. 예를들어, BottomSheet의 확장된 상태에 따라 내가 보고있는 UI를 동적으로 변경할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chen-studio/post/26572701-e02c-4267-b94c-abb3b49164ee/image.png" alt=""></p>
<ul>
<li><strong>Standard Bottom Sheet</strong>는 <strong>CoordinatorLayout</strong>과 함께 사용해야 하며 기존에 사용하던 UI에 이 방식의 Bottom Sheet를 도입하려고 하면 기존 UI구조의 변경이 불가피하다. </li>
</ul>
<pre><code class="language-xml">&lt;androidx.coordinatorlayout.widget.CoordinatorLayout
  ...&gt;

  &lt;FrameLayout
    ...
    android:id=&quot;@+id/standard_bottom_sheet&quot;
    app:layout_behavior=&quot;com.google.android.material.bottomsheet.BottomSheetBehavior&quot;&gt;

    &lt;!-- Bottom sheet contents. --&gt;

  &lt;/FrameLayout&gt;

&lt;/androidx.coordinatorlayout.widget.CoordinatorLayout&gt;</code></pre>
<ul>
<li>좀더 유연한 UI를 만들고 싶다면 이 방식을 선택하는것이 맞다. 하지만 BottomSheet없이 구현되어 있는 기존의 복잡한 UI에서 이 방식을 도입하려고 하면 구조의 변경이 불가피하여 어떤 SideEffect가 발생할 지 예측하기 어렵다. </li>
<li>따라서 필자는 아래에서 설명할 <strong>Modal Bottom Sheet</strong>로 구현하는 방법을 택했다.</li>
</ul>
<h2 id="modal-bottom-sheet">Modal Bottom Sheet</h2>
<ul>
<li>Modal Bottom Sheet는 현재 UI에 종속되지 않고 새로운 Window를 가지는 별도의 Fragment이다. <strong>BottomSheetDialogFragment</strong>를 상속받은 Fragment를 구현하고 show하는 것으로 BottomSheet를 사용할 수 있다.</li>
</ul>
<pre><code class="language-kotlin">class ModalBottomSheet : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.modal_bottom_sheet_content, container, false)

    companion object {
        const val TAG = &quot;ModalBottomSheet&quot;
    }
}

class MainActivity : AppCompatActivity() {
    ...
    val modalBottomSheet = ModalBottomSheet()
    modalBottomSheet.show(supportFragmentManager, ModalBottomSheet.TAG)
    ...
}</code></pre>
<h3 id="modal-bottom-sheet의-문제">Modal Bottom Sheet의 문제</h3>
<ul>
<li>필자가 이 방식을 도입해본 결과 크게 2가지 문제가 있었다.</li>
</ul>
<ol>
<li><p><strong>Modal Bottom Sheet</strong> 방식으로 구현된 BottomSheet는 아무리 높이를 설정해도 <strong>9:16(가로:세로)</strong> 비율 이상으로 크기를 조절할 수 없다.</p>
</li>
<li><p>요구사항은 이 BottomSheet를 접거나 펼칠 수 있는 Step이 3단계(완전히 접힌 상태, 중간, 펼쳐진 상태) 로 3가지 상태를 만들어야 했다.</p>
</li>
</ol>
<ul>
<li><p>위 문제를 해결한 방법을 소개한다.</p>
</li>
<li><p>Material에서 제공하는 BottomSheetDialogFragment는 여러가지 상태를 조절할 수 있도록 <strong>behavior</strong> 라는것을 제공한다.</p>
</li>
<li><p>behavior에 대한 속성은 워낙 많으므로 모두 설명하지 않고 현재 문제를 해결하는데 필요한 것들만 소개한다.</p>
</li>
<li><p>먼저, <strong>behavior</strong>에는 크게 6가지 상태가 존재한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chen-studio/post/8985e437-06a3-429a-be1b-32e5a4c73505/image.png" alt=""></p>
<ul>
<li><p>여러가지 상태가 존재하지만 펼친상태를 2단계로 사용하기 위해 <strong>STATE_COLLAPSED</strong>(접힌상태)와 <strong>STATE_EXPANDED</strong>(펼쳐진상태)를 사용할 것이다.</p>
</li>
<li><p>그리고 <strong>behavior</strong>의 <strong>peekHeight</strong>라는 변수를 사용할 것이다.</p>
</li>
<li><p>peekHeight는 STATE_COLLAPSED상태의 높이를 지정하는 변수이다.
<img src="https://velog.velcdn.com/images/chen-studio/post/4a15e69c-7b86-4a71-be67-3648ce4f1f36/image.png" alt=""></p>
</li>
<li><p>STATE_COLLAPSED 상태의 높이를 적용하는 방법은 아래와 같다</p>
</li>
<li><p>먼저 아래와 같은 방법으로 현재 디바이스의 height pixel을 구한다</p>
</li>
</ul>
<pre><code class="language-kotlin">@Px
fun Fragment.getHeightPixels(): Int = resources.displayMetrics.heightPixels</code></pre>
<ul>
<li>위에서 설명한 <strong>peekHeight</strong>는 BottomSheetDialogFragment가 아닌 BottomSheetDialog의 속성이다</li>
<li>따라서 BottomSheetDialog로부터 behavior를 구하는 확장함수를 정의했다</li>
</ul>
<pre><code class="language-kotlin">fun BottomSheetDialogFragment.getBehavior(): BottomSheetBehavior&lt;*&gt;? =
    (dialog as? BottomSheetDialog)?.behavior</code></pre>
<ul>
<li>그럼 behavior의 peekHeight를 원하는 pixel값으로 설정하여 원하는 높이를 설정할 수 있다 </li>
<li>예를들어, 전체 화면의 50%만큼의 높이를 접힌상태의 높이로 정의하고 싶은 경우 아래와 같이 설정할 수 있다</li>
</ul>
<pre><code class="language-kotlin">behavior?.peekHeight = (heightPixels * 0.5).toInt()</code></pre>
<ul>
<li>이제 STATE_EXPANDED 상태의높이를 정의하려면 내가 사용하고자 하는 View의 높이를 STATE_COLLAPSED를 적용했던 것과 같은 방법으로 적용한다</li>
<li>예를들어, 펼쳐진 상태에서 화면을 가득 채우고 싶은 경우 아래와 같이 설정할 수 있다</li>
</ul>
<pre><code class="language-kotlin">// root는 BottomSheetDialogFragment의 root view, viewBinding 사용중이라면 binding.root
root.layoutParams.height = (heightPixels * expandedRatio).toInt()</code></pre>
<h2 id="결과">결과</h2>
<ul>
<li><p>위와같은 내용을 바탕으로 펼쳐진 상태를 2step으로 나눌 수 있으며 처음 bottom sheet가 보일때 상태를 정의하는 parameter까지 추가하여 아래와 같은 확장함수를 정의할 수 있다</p>
</li>
<li><p>또한 <strong>collapsedRatio와 expandedRatio를 같은 값 (예를들어 1.0F)</strong>로 설정한 경우, 기존에 <strong>9:16 이상으로 확장되지 않던 높이 문제도 해결</strong>할 수 있으며 같은 값을 가지는 경우 2step이 아닌 <strong>1step</strong>으로 동작하게 된다</p>
</li>
</ul>
<pre><code class="language-kotlin">@Px
fun Fragment.getHeightPixels(): Int = resources.displayMetrics.heightPixels

fun BottomSheetDialogFragment.getBehavior(): BottomSheetBehavior&lt;*&gt;? =
    (dialog as? BottomSheetDialog)?.behavior

/**
 * Set BottomSheetDialogFragment&#39;s height into 2 step
 *
 * If the values of collapsedRatio and expandedRatio are the same, it works only 1 step regardless of initialState
 *
 * @param initialState STATE_COLLAPSED or STATE_EXPANDED
 * @param collapsedRatio height ratio when bottom sheet is collapsed
 * @param expandedRatio height ratio when bottom sheet is expanded
 */
fun BottomSheetDialogFragment.setHeightRatio(
    root: View,
    initialState: Int = STATE_COLLAPSED,
    @FloatRange(from = 0.1, to = 1.0) collapsedRatio: Float = 0.7F,
    @FloatRange(from = 0.1, to = 1.0) expandedRatio: Float = 1.0F
) {
    validateParams(initialState, collapsedRatio, expandedRatio)

    root.layoutParams.height = (getHeightPixels() * expandedRatio).toInt()
    getBehavior()?.peekHeight = (getHeightPixels() * collapsedRatio).toInt()
    getBehavior()?.state = initialState
}

private fun validateParams(initialState: Int, collapsedRatio: Float, expandedRatio: Float) =
    when {
        initialState != STATE_COLLAPSED &amp;&amp; initialState != STATE_EXPANDED -&gt;
            throw IllegalArgumentException(
                &quot;initialState should be STATE_COLLAPSED or STATE_EXPANDED&quot;
            )

        collapsedRatio.toBigDecimal() &lt; 0.1.toBigDecimal() -&gt;
            throw IllegalArgumentException(&quot;collapsedRatio should be more than 0.1&quot;)

        expandedRatio.toBigDecimal() &lt; collapsedRatio.toBigDecimal() -&gt;
            throw IllegalArgumentException(&quot;expandedRatio should be more than collapsedRatio&quot;)

        else -&gt; Unit
    }</code></pre>
<ul>
<li><p>parameter가 <strong>올바른 값을 가지는지 검증하기 위한 validateParams</strong> 를 구현하였으며 위 메소드를 BottomSheetDialogFragment의 onViewCreated() 이후에 설정하면 정상적으로 동작한다</p>
</li>
<li><p>샘플 예제가 궁금하다면 아래 github를 참고하세요
<a href="https://github.com/chen-studio/android-modal-bottomsheet">https://github.com/chen-studio/android-modal-bottomsheet</a></p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>