WebViewの高さがガタガタ変わらないようにしたい

WebViewの使い方として正しいかは分かりませんが、WebViewをRecyclerViewやScrollViewに`android:layout_height="wrap_content"`で配置して、Viewの高さをコンテンツの高さ分持たせるような使い方をしたくなることがあります。
こういったことをするとき、WebViewがコンテンツを読み込んでレンダリングが完了するまで高さが確定しなくて、他のViewの位置も連動して変わってしまう、ってのが悩みです。一方で、WebViewのようなものを配置すると言うことはアプリの更新を行わずコンテンツを柔軟に変更したい等の要求があるはずなので安易に最低限の高さみたいなものを設定する訳にも・・・という悩ましい問題があります。

まずは、最低限の高さを設定しつつ、それ以上になることを許容するという方法を考えてみましょう。
方法は他にもいろいろあるかとは思いますが、View自体の大きさはonMeasureの中でmeasuredWidth/measuredHeightが設定されることによって決まります。
ですので、基準値をstableHeightという変数に格納するとして、onMeasureをoverrideし、measuredHeightがstableHeight以下ならsetMeasuredDimensionで再設定、そうでなければstableHeightを更新する。
とすれば、最低限の高さを設定しつつ。それ以上になることを許容する動作をさせることができます。

```kotlin
private var stableHeight: Int = XXXX

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val pendingHeight = measuredHeight
    if (pendingHeight < stableHeight) {
        setMeasuredDimension(measuredWidth, stableHeight)
    } else {
        stableHeight = pendingHeight
    }
}
```

次に、コンテンツを全部読み込んでみたら、stableHeightよりも小さかったという場合を考慮する必要がありますね。
この場合は、高さが確定した段階で、上記のpendingHeightを保持しておいて、その値でstableHeightをコールすれば良いですね。任意のWebページとなると様々な要因を考慮する必要がありますが、こういう埋め込み用の特定コンテンツならonPageFinishedのタイミングを拾えば十分でしょう。

# 高さがガタガタかわらないWebView

ということで、コードの全体像が以下になります。

まずは、レイアウトXMLからstableHeightを指定できるようにattrs.xmlでdeclare-styleableを定義しましょう。

```xml:attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyWebView">
        <attr name="stableHeight" format="dimension"/>
    </declare-styleable>
</resources>
```

WebViewを継承したクラスで以下のようにします。
setBackgroundColor(0)としているのは、そのままだと読み込みが行われるまで白い背景が描画されてしまうのでそれを防ぐためです。

```kotlin
class MyWebView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : WebView(context, attrs, defStyle) {
    private var stableHeight: Int = 0
    private var pendingHeight: Int = 0
    private var loading: Boolean = true
    private val updateStableHeight = Runnable { 
        updateStableHeight()
    }

    init {
        context.obtainStyledAttributes(attrs, R.styleable.MyWebView).use {
            stableHeight = it.getDimensionPixelSize(R.styleable.MyWebView_stableHeight, 0)
        }
        setBackgroundColor(Color.TRANSPARENT)
    }

    fun updateStableHeight() {
        if (pendingHeight == stableHeight) return
        stableHeight = pendingHeight
        requestLayout()
    }

    fun onPageStarted() {
        loading = true
        removeCallbacks(updateStableHeight)
    }

    fun onPageFinished() {
        loading = false
        postUpdateStableHeight()
    }

    private fun postUpdateStableHeight() {
        removeCallbacks(updateStableHeight)
        if (loading || pendingHeight == stableHeight) return
        postDelayed(updateStableHeight, 500)
    }

    private fun updateStableHeight() {
        if (pendingHeight == stableHeight) return
        stableHeight = pendingHeight
        requestLayout()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        pendingHeight = measuredHeight
        if (pendingHeight < stableHeight) {
            setMeasuredDimension(measuredWidth, stableHeight)
        } else {
            stableHeight = pendingHeight
        }
        postUpdateStableHeight()
    }
}
```

onPageStartedとonPageFinishedはWebViewClientからコールする必要がありますね。また、JSなどで動的に高さが変わる場合で他に適切なトリガがあるならそのタイミングで更新すれば良いかなと思います。

```kotlin
webView.webViewClient = object : WebViewClient() {
    override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
        (view as? MyWebView)?.onPageStarted()
    }
    override fun onPageFinished(view: WebView, url: String?) {
        (view as? MyWebView)?.onPageFinished()
    }
}
```

汎用的なViewとして作るのはちょっと難しいですが、特定目的のカスタムViewとしてなら、この方法で十分ではないかと思います。stableHeightは永続化データとして持っておくというのもありかもしれません。

以上です。