AlertDialogのレイアウトをHackする

# はじめに
AlertDialogの見た目はAlertDialog.Builderの下記のAPIで調整できます。しかし、どうしても謎のpaddingやSpaceに邪魔をされることがあったので、その解決方法を記載します。

* [Builder()](https://developer.android.com/reference/android/app/AlertDialog.Builder.html#AlertDialog.Builder(android.content.Context,%20int)) : ダイアログ全体のstyleが指定できる。
* [setCustomTitle()](https://developer.android.com/reference/android/app/AlertDialog.Builder.html#setCustomTitle(android.view.View)) : タイトルのlayoutを指定できる。backgroundを変更したい時などに使える。
* [setView()](https://developer.android.com/reference/android/app/AlertDialog.Builder.html#setView(android.view.View)) : コンテンツ領域のlayoutを指定できる。

# 実例
Android 6 で下記のようにAlertDialogを作成すると、下線付きタイトルと縦長のメッセージが表示されますが、画像の赤丸のように謎のpaddingが入ってしまっています。

```kotlin:CustomDialogFragment.kt
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(context)
            .setCustomTitle(activity!!.layoutInflater.inflate(R.layout.layout_dialog_title, null))
            .setMessage((0..100).joinToString("\n") { i -> "hello ${i}" })            
            .setPositiveButton("OK", null)
            .create()
    }
```
![screen_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132020/4b767e60-5ff2-e685-28ac-da9d11b8f291.png)

# 原因の調査
まず、AlertDialogのレイアウトの構造を調べるために先ほどのコードを以下のように書き換え実行し、Logcatを確認します。

```kotlin:CustomDialogFragment.kt
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(context)
            .setCustomTitle(activity!!.layoutInflater.inflate(R.layout.layout_dialog_title, null))
            .setMessage((0..100).joinToString("\n") { i -> "hello ${i}" })
            .setPositiveButton("OK", null)
            .create()
            .also { dialog ->
                dialog.show() // show() しておかないとViewが取得できない
                showViewInfoRecursively(dialog.window.decorView)
            }
    }

    private fun showViewInfoRecursively(
        view: View?,
        hierarchy: Int = 0
    ) {
        if (view == null) return
        val indent = (0..hierarchy).joinToString("  ") { "" }
        val resourceName =
            if (view.id != View.NO_ID) view.resources.getResourceName(view.id)
            else "none"
        Log.d("CustomDialog", "${indent}class=${view.javaClass.simpleName}, resource=${resourceName}, id=${view.id}")

        if (view is ViewGroup)
            for (i in 0..view.childCount)
                showViewInfoRecursively(view.getChildAt(i), hierarchy + 1)
    }
```
```text:Logcat
D/CustomDialog: class=DecorView, resource=none, id=-1
D/CustomDialog:   class=FrameLayout, resource=none, id=-1
D/CustomDialog:     class=FrameLayout, resource=android:id/content, id=16908290
D/CustomDialog:       class=LinearLayout, resource=android:id/parentPanel, id=16909076
D/CustomDialog:         class=LinearLayout, resource=android:id/topPanel, id=16909077
D/CustomDialog:           class=ConstraintLayout, resource=none, id=-1
D/CustomDialog:             class=AppCompatTextView, resource=none, id=-1
D/CustomDialog:           class=LinearLayout, resource=android:id/title_template, id=16909078
D/CustomDialog:             class=AppCompatImageView, resource=android:id/icon, id=16908294
D/CustomDialog:             class=DialogTitle, resource=android:id/alertTitle, id=16909079
D/CustomDialog:         class=FrameLayout, resource=android:id/contentPanel, id=16909084
D/CustomDialog:           class=ScrollView, resource=android:id/scrollView, id=16909085
D/CustomDialog:             class=LinearLayout, resource=none, id=-1
D/CustomDialog:               class=AppCompatTextView, resource=android:id/message, id=16908299
D/CustomDialog:               class=Space, resource=android:id/textSpacerNoButtons, id=16909090
D/CustomDialog:         class=FrameLayout, resource=android:id/customPanel, id=16909086
D/CustomDialog:           class=FrameLayout, resource=android:id/custom, id=16908331
D/CustomDialog:         class=ButtonBarLayout, resource=android:id/buttonPanel, id=16909081
D/CustomDialog:           class=AppCompatButton, resource=android:id/button3, id=16908315
D/CustomDialog:           class=Space, resource=android:id/spacer, id=16909087
D/CustomDialog:           class=AppCompatButton, resource=android:id/button2, id=16908314
D/CustomDialog:           class=AppCompatButton, resource=android:id/button1, id=16908313
D/CustomDialog:     class=ViewStub, resource=android:id/action_mode_bar_stub, id=16909303
```
Logcatに表示されたレイアウト構造から怪しげなViewを探します。ここでは、Spaceクラスが見当たらないので、いづれかのViewのmarginかpaddingが原因だと思われます。ここではリソース名がscrollViewのScroolViewとその子のLinearLayoutに当たりをつけます。

# 原因の確認
さらに以下のようにコードを変更し実行後、ScrollViewの境界(赤)とLinearLayoutの境界(青)を表示してみます。画像を見ると、問題の空白が赤線と青線で挟まれているので、原因はScrollViewのpaddingTopかLinearLayoutのlayoutMarginTopです。

```kotlin:CustomDialogFragment.kt
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(context)
            .setCustomTitle(activity!!.layoutInflater.inflate(R.layout.layout_dialog_title, null))
            .setMessage((0..100).joinToString("\n") { i -> "hello ${i}" })
            .setPositiveButton("OK", null)
            .create()
            .also { dialog ->
                dialog.show() // show() しておかないとViewが取得できない
//                showViewInfoRecursively(dialog.window.decorView)
                // 公開されていないリソース名でViewを探す
                dialog.findViewById<ScrollView?>(getResources().getIdentifier(
                    "scrollView", "id", "android"
                ))?.also { scrollView ->
                    // ScrollView に赤いフレームを付与
                    scrollView.setBackgroundResource(R.drawable.shape_frame_red)
                    // ScrollView の子のLinearLayoutに青いフレームを付与
                    scrollView.getChildAt(0).setBackgroundResource(R.drawable.shape_frame_blue)
                }
            }
    }

```
![aaa_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132020/6d0ffefd-3858-1ae9-2fd7-f13c7d5dcda3.png)

# 修正
最終的な修正コードは以下のようになります。実行すると画像のように空白がなくなっています。

```kotlin:CustomDialogFragment.kt
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(context)
            .setCustomTitle(activity!!.layoutInflater.inflate(R.layout.layout_dialog_title, null))
            .setMessage((0..100).joinToString("\n") { i -> "hello ${i}" })
            .setPositiveButton("OK", null)
            .create()
            .also { dialog ->
                dialog.show() // show() しておかないとViewが取得できない
//                showViewInfoRecursively(dialog.window.decorView)
                // 公開されていないリソース名でViewを探す
                dialog.findViewById<ScrollView?>(getResources().getIdentifier(
                    "scrollView", "id", "android"
                ))?.also { scrollView ->
                    // 今回の原因. paddingTopに24が設定されていた.
                    Log.d("CustomDialog", "paddingTop=${scrollView.paddingTop}")
                    scrollView.setPadding(0, 0, 0, 0)
                    // 今回の原因ではないが修正方法を記載
                    // val child = scrollView.getChildAt(0)
                    // child.layoutParams = FrameLayout.LayoutParams(child.layoutParams)
                    //     .also { it.setMargins(0, 0, 0, 0) }
                }
            }
    }
```
![bbb_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132020/609e1fba-0a2e-c79d-f4a7-26abffde80c9.png)

# おわりに
SDKが意図していない方法でのレイアウト編集なので、SDKのバージョンが変われば動かなくなるかもしれません。あくまでも最終手段となります。