Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Web] Fix WebView always filling max available space #1397

Merged
merged 2 commits into from Nov 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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