Skip to content

Commit

Permalink
Merge pull request #1397 from google/ben/webview_layoutparams
Browse files Browse the repository at this point in the history
[Web] Fix WebView always filling max available space
  • Loading branch information
bentrengrove committed Nov 14, 2022
2 parents 9b2a451 + 0554af0 commit 7a293b3
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 35 deletions.
13 changes: 13 additions & 0 deletions sample/src/main/AndroidManifest.xml
Expand Up @@ -16,6 +16,7 @@
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.accompanist.sample">

<!-- Used for loading images from the network -->
Expand All @@ -35,6 +36,9 @@
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">

<profileable android:shell="true"
tools:targetApi="q" />

<activity
android:name="com.google.accompanist.sample.MainActivity"
android:label="@string/app_name"
Expand Down Expand Up @@ -412,6 +416,15 @@
<category android:name="com.google.accompanist.sample.SAMPLE_CODE" />
</intent-filter>
</activity>
<activity
android:name=".webview.WrappedContentWebViewSample"
android:label="@string/webview_title_wrapped"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.google.accompanist.sample.SAMPLE_CODE" />
</intent-filter>
</activity>

<activity
android:name=".adaptive.BasicTwoPaneSample"
Expand Down
@@ -0,0 +1,96 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.accompanist.sample.webview

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Button
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewStateWithHTMLData
import kotlinx.coroutines.launch

class WrappedContentWebViewSample : ComponentActivity() {
@OptIn(ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface {
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
)
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
WebContent()
}
) {
val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize()) {
Button(onClick = {
scope.launch { sheetState.show() }
}, Modifier.align(Alignment.Center)) {
Text("Open Sheet")
}
}
}
}
}
}
}
}

/***
* A sample WebView that is wrapping it's content inside of a bottom sheet.
* The sheet should be the size of the rendered content and not unbounded.
*/
@Composable
fun WebContent() {
val webViewState = rememberWebViewStateWithHTMLData(
data = "<html><head>\n" +
"<style>\n" +
"body {\n" +
" background-color: #f00;\n" +
"}\n" +
"</style>\n" +
"</head><body><p>Hello</p></body></html>"
)
WebView(
state = webViewState,
modifier = Modifier.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 1.dp) // A bottom sheet can't support content with 0 height.
)
}
1 change: 1 addition & 0 deletions sample/src/main/res/values/strings.xml
Expand Up @@ -63,6 +63,7 @@
<string name="placeholder_title_shimmer">Placeholder: Shimmer</string>

<string name="webview_title_basic">WebView: Basic</string>
<string name="webview_title_wrapped">WebView: Wrapped Content</string>

<string name="adaptive_two_pane_basic">Adaptive: TwoPane Basic</string>
<string name="adaptive_two_pane_horizontal">Adaptive: TwoPane Horizontal</string>
Expand Down
38 changes: 37 additions & 1 deletion web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt
Expand Up @@ -19,6 +19,12 @@ package com.google.accompanist.web
import android.content.Context
import android.graphics.Bitmap
import android.webkit.WebView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand All @@ -27,11 +33,14 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.dp
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.model.Atoms.getCurrentUrl
import androidx.test.espresso.web.model.Atoms.getTitle
Expand Down Expand Up @@ -681,6 +690,32 @@ class WebTest {

assertThat(constructedWebView).isInstanceOf(CustomWebView::class.java)
}

@Test
fun testWebViewCanWrapHeight() {
rule.setContent {
Column(Modifier.fillMaxSize()) {
val webViewState = rememberWebViewStateWithHTMLData(data = TEST_DATA)
WebTestContent(
webViewState = webViewState,
idlingResource = idleResource,
modifier = Modifier.wrapContentHeight()
)
Box(
Modifier
.weight(1f)
.fillMaxWidth()
.background(Color.Red)
.testTag("box")
)
}
}

rule.waitForIdle()

// If the WebView is wrapping it's content successfully, the box will have some height.
rule.onNodeWithTag("box").assertHeightIsAtLeast(1.dp)
}
}

private const val LINK_ID = "link"
Expand All @@ -699,6 +734,7 @@ private const val WebViewTag = "webview_tag"
private fun WebTestContent(
webViewState: WebViewState,
idlingResource: WebViewIdlingResource,
modifier: Modifier = Modifier,
navigator: WebViewNavigator = rememberWebViewNavigator(),
onCreated: (WebView) -> Unit = { it.settings.javaScriptEnabled = true },
onDispose: (WebView) -> Unit = {},
Expand All @@ -711,7 +747,7 @@ private fun WebTestContent(
MaterialTheme {
WebView(
state = webViewState,
modifier = Modifier.testTag(WebViewTag),
modifier = modifier.testTag(WebViewTag),
navigator = navigator,
onCreated = onCreated,
onDispose = onDispose,
Expand Down
88 changes: 54 additions & 34 deletions web/src/main/java/com/google/accompanist/web/WebView.kt
Expand Up @@ -18,13 +18,14 @@ package com.google.accompanist.web

import android.content.Context
import android.graphics.Bitmap
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
Expand Down Expand Up @@ -105,40 +106,56 @@ fun WebView(

val runningInPreview = LocalInspectionMode.current

AndroidView(
factory = { context ->
(factory?.invoke(context) ?: WebView(context)).apply {
onCreated(this)

layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)

webChromeClient = chromeClient
webViewClient = client
}.also { webView = it }
},
modifier = modifier
) { view ->
// AndroidViews are not supported by preview, bail early
if (runningInPreview) return@AndroidView

when (val content = state.content) {
is WebContent.Url -> {
val url = content.url

if (url.isNotEmpty() && url != view.url) {
view.loadUrl(url, content.additionalHttpHeaders.toMutableMap())
}
BoxWithConstraints(modifier) {
AndroidView(
factory = { context ->
(factory?.invoke(context) ?: WebView(context)).apply {
onCreated(this)

// 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

layoutParams = LayoutParams(
width,
height
)

webChromeClient = chromeClient
webViewClient = client
}.also { webView = it }
}
is WebContent.Data -> {
view.loadDataWithBaseURL(content.baseUrl, content.data, null, "utf-8", null)
) { view ->
// AndroidViews are not supported by preview, bail early
if (runningInPreview) return@AndroidView

when (val content = state.content) {
is WebContent.Url -> {
val url = content.url

if (url.isNotEmpty() && url != view.url) {
view.loadUrl(url, content.additionalHttpHeaders.toMutableMap())
}
}

is WebContent.Data -> {
view.loadDataWithBaseURL(content.baseUrl, content.data, null, "utf-8", null)
}
}
}

navigator.canGoBack = view.canGoBack()
navigator.canGoForward = view.canGoForward()
navigator.canGoBack = view.canGoBack()
navigator.canGoForward = view.canGoForward()
}
}
}

Expand Down Expand Up @@ -436,8 +453,11 @@ data class WebViewError(
* Note that these headers are used for all subsequent requests of the WebView.
*/
@Composable
fun rememberWebViewState(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()): WebViewState =
// Rather than using .apply {} here we will recreate the state, this prevents
fun rememberWebViewState(
url: String,
additionalHttpHeaders: Map<String, String> = emptyMap()
): WebViewState =
// Rather than using .apply {} here we will recreate the state, this prevents
// a recomposition loop when the webview updates the url itself.
remember(url, additionalHttpHeaders) {
WebViewState(
Expand Down

0 comments on commit 7a293b3

Please sign in to comment.