accompanist で使用する WebView の指定サイズには気をつけよう
ポート株式会社 サービス開発部 Advent Calendar 2024 の9日目です。
初めに
ポート株式会社でAndroid開発をしている@shxun6934 です。
弊社のAndroidアプリでは、WebViewをComposeで表現する際、accompanist.webview
を使用しています。
accompanist.webview
を使用していたそんなある日の出来事です。
ユーザーからのお問い合わせから
ユーザーさんのお問い合わせから意見をいただきました。
ある画面のボタンを押しても、何も表示されない画面になり、操作できませんでした
その画面は、WebViewになっていて、accompanist
のwebview
を使用していました。
なぜ問い合わせ内容の挙動になっていたのか、その理由を見つけるべく、accompanist.webview
の挙動を調べることにしました。
accompanist.webview
とは
話に入る前に、まず、accompanist.webview
について紹介します。
accompanist.webview
は、WebView をラップしたCompose を提供してくれるライブラリーです。
使い方
基本的な使い方は、至ってシンプル。
rememberWebViewState
にWebViewで表示したいURLを渡して、WebView
に渡すだけです。
val state = rememberWebViewState(
url = "https://example.com"
)
WebView(
state = state
)
(アプリがインターネット通信できるようしておかないといけません。)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- インターネット通信できるようにする -->
<uses-permission android:name="android.permission.INTERNET"/>
<application>
<!-- 中略 -->
</application>
</manifest>
簡単そうに見えて...
では、例としてGitHubを開いてみましょう。
val state = rememberWebViewState(
url = "https://github.com"
)
WebView(
state = state,
onCreated = {
it.settings.apply {
// JSを許可
javaScriptEnabled = true
}
}
)
(画質荒くて申し訳ないです。。。)
一見よさそうだけど...
アプリの画面全体にWebViewが表示されていて、スクロールもできている、ハンバーガメニューをタップすると、メニューも開けているように見えます。
ですが、、、
「Sign Up」や「Sign In」が画面全体ではなかったり、ハンバーガメニューの検索欄がちょっと違和感があります。
なぜこうなっているのか
普段、UIをCompose で書いている人からすれば、当たり前のことなのですが、Modifierでサイズを指定してないとこうなってしまいます。
内部のコードを見てみる
accompanist.webview
の内部コードを見てみましょう。
BoxWithConstraints(modifier) {
// WebView changes it's layout strategy based on
// it's layoutParams. We convert from Compose Modifier to
// layout params here.
val width =
if (constraints.hasFixedWidth)
LayoutParams.MATCH_PARENT
else
LayoutParams.WRAP_CONTENT
val height =
if (constraints.hasFixedHeight)
LayoutParams.MATCH_PARENT
else
LayoutParams.WRAP_CONTENT
val layoutParams = FrameLayout.LayoutParams(
width,
height
)
/* 中略 */
}
BoxWithConstraints
内では、親のレイアウトの制約情報(== Constraints
)を取得できます。
minWidth
・maxWidth
・minHeight
・maxHeight
といったような情報ですね。
accompanist.webview
は、親のレイアウトの制約情報を取得し、Width
およびHeight
の制約がFixed
、つまりレスポンシブであるかどうかでレイアウトのパラメータを決めています。
レスポンシブである場合は親のレイアウトの制約に合わせるMATCH_PARENT
、そうでない場合は自身のレイアウトのサイズに合わせるWRAP_CONTENT
になります。
原因
例に挙げたGitHubのような、スクロールできるぐらいのHeight
が大きいWebページだと、Androidの端末によっては、WebViewのレイアウトサイズが画面全体になります。
そのため、内部のコード的にはWRAP_CONTENT
の制約になっていても、MATCH_PARENT
の制約がかかっているように見えてしまいます。
画面遷移
Webページ内で画面遷移をした際に、WebページのHeight
が画面以内に収まっているとWRAP_CONTENT
本来の制御になり、WebページのHeight
分しか表示されなくなります。
ダイアログ・モーダル
Webページ内のコンテンツをタップした際にダイアログやモーダルを表示する挙動になっている場合は、ダイアログやモーダルが画面内に正常に表示されなくなります。
詳しいレンダリングは自分も深く理解できていないのですが、おそらく、WRAP_CONTENT
の制約だと、ダイアログやモーダルを表示するための領域をあらかじめ確保していないと思われます。
(ご存じの方いたらコメントやX等で教えてくださるとうれしいです!)
サイズ指定を行うと
こうしないためには、MATCH_PARENT
の制約をWebViewにかけるようにしてあげます。
ということで、Modifier.fillMaxHeight()
を指定するとちゃんと動くようになります。
val state = rememberWebViewState(
url = "https://github.com"
)
WebView(
state = state,
+ modifier = Modifier.fillMaxHeight(),
onCreated = {
it.settings.apply {
// JSを許可
javaScriptEnabled = true
}
}
)
最後に
一見実装できているように見えて、実際に動作確認してみるとできていないことがあると思います。
そういう時は、やはり、コードを見て仕組みがどうなっているかを理解するのが一番だと改めて感じました。
時間がある時に、ライブラリーを使用する際は内部のコード見ておくのもいいかもしれないですね。
10日目は、ogom さんの記事です!
余談
accompanist.webview
はDeprecatedなので、今後使うことはないと思います。
ただし、公式から、移行する場合はForkすることをお勧めされているので、しっかりと内部コードを理解しておくと特に不具合なく移行できると思います。
Discussion