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

Feature/WebView Snapshot Capture #204

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package com.kevinnzou.sample

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
@@ -24,13 +28,18 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.LightGray
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger
import com.multiplatform.webview.cookie.Cookie
@@ -39,8 +48,10 @@ import com.multiplatform.webview.web.LoadingState
import com.multiplatform.webview.web.WebView
import com.multiplatform.webview.web.WebViewState
import com.multiplatform.webview.web.rememberWebViewNavigator
import com.multiplatform.webview.web.rememberWebViewSnapshot
import com.multiplatform.webview.web.rememberWebViewState
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch

/**
* Created By Kevin Zou On 2023/9/8
@@ -65,6 +76,10 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) {
onDispose { }
}
val navigator = rememberWebViewNavigator()
val webViewSnapshot = rememberWebViewSnapshot()
val coroutineScope = rememberCoroutineScope()
var webViewSnapshotResult: ImageBitmap? by remember { mutableStateOf(null) }

var textFieldValue by remember(state.lastLoadedUrl) {
mutableStateOf(state.lastLoadedUrl)
}
@@ -86,6 +101,22 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) {
)
}
},
actions = {
TextButton(onClick = {
coroutineScope.launch {
webViewSnapshotResult = webViewSnapshot.takeSnapshot(
Rect(
top = 50f,
left = 50f,
right = 1000f,
bottom = 2000f
)
)
}
}) {
Text("Snapshot", color = Color.White)
}
}
)

Row {
@@ -96,9 +127,9 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) {
contentDescription = "Error",
colorFilter = ColorFilter.tint(Color.Red),
modifier =
Modifier
.align(Alignment.CenterEnd)
.padding(8.dp),
Modifier
.align(Alignment.CenterEnd)
.padding(8.dp),
)
}

@@ -128,14 +159,45 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) {
modifier = Modifier.fillMaxWidth(),
)
}

WebView(
state = state,
modifier =
Box {
WebView(
state = state,
modifier =
Modifier
.fillMaxSize(),
navigator = navigator,
)
navigator = navigator,
webViewSnapshot = webViewSnapshot
)

// When WebView's Bitmap image is captured, show snapshot in dialog
webViewSnapshotResult?.let { bitmap ->
Dialog(onDismissRequest = { }) {
Box {
Column(
modifier = Modifier
.background(LightGray)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Preview of WebView snapshot")
Spacer(Modifier.size(16.dp))
Image(
bitmap = bitmap,
contentDescription = "Preview of WebViewSnapshot"
)
Spacer(Modifier.size(4.dp))
}

Button(
onClick = { webViewSnapshotResult = null },
modifier = Modifier.align(Alignment.BottomCenter)
) {
Text("Close Snapshot Preview")
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.kevinnzou.sample

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -16,10 +21,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color.Companion.LightGray
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger
import com.kevinnzou.sample.eventbus.FlowEventBus
@@ -32,8 +42,10 @@ import com.multiplatform.webview.util.KLogSeverity
import com.multiplatform.webview.web.WebView
import com.multiplatform.webview.web.WebViewState
import com.multiplatform.webview.web.rememberWebViewNavigator
import com.multiplatform.webview.web.rememberWebViewSnapshot
import com.multiplatform.webview.web.rememberWebViewStateWithHTMLFile
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch

/**
* Created By Kevin Zou On 2023/9/8
@@ -55,6 +67,11 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? =
val webViewNavigator = rememberWebViewNavigator()
val jsBridge = rememberWebViewJsBridge(webViewNavigator)
var jsRes by mutableStateOf("Evaluate JavaScript")
val webViewSnapshot = rememberWebViewSnapshot()

val coroutineScope = rememberCoroutineScope()
var webViewSnapshotResult: ImageBitmap? by remember { mutableStateOf(null) }

LaunchedEffect(Unit) {
initWebView(webViewState)
initJsBridge(jsBridge)
@@ -82,11 +99,16 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? =
captureBackPresses = false,
navigator = webViewNavigator,
webViewJsBridge = jsBridge,
webViewSnapshot = webViewSnapshot
)
Button(
onClick = {
webViewNavigator.evaluateJavaScript(
"""
Column(
Modifier.align(Alignment.BottomCenter).padding(bottom = 30.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = {
webViewNavigator.evaluateJavaScript(
"""
document.getElementById("subtitle").innerText = "Hello from KMM!";
window.kmpJsBridge.callNative("Greet",JSON.stringify({message: "Hello"}),
function (data) {
@@ -96,13 +118,50 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? =
);
callJS();
""".trimIndent(),
) {
jsRes = it
}
},
modifier = Modifier,
) {
Text(jsRes)
}

Spacer(Modifier.height(12.dp))

Button(
onClick = {
coroutineScope.launch {
webViewSnapshotResult = webViewSnapshot.takeSnapshot(null)
}
},
modifier = Modifier,
) {
Text("Take Snapshot")
}
}

// When WebView's Bitmap image is captured, show snapshot in dialog
webViewSnapshotResult?.let { bitmap ->
Dialog(onDismissRequest = { }) {
Column(
modifier = Modifier
.background(LightGray)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
jsRes = it
Text("Preview of WebView snapshot")
Spacer(Modifier.size(16.dp))
Image(
bitmap = bitmap,
contentDescription = "Preview of WebViewSnapshot"
)
Spacer(Modifier.size(4.dp))
Button(onClick = { webViewSnapshotResult = null }) {
Text("Close Snapshot Preview")
}
}
},
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 50.dp),
) {
Text(jsRes)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.multiplatform.webview.util

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.webkit.WebView
import androidx.compose.ui.geometry.Rect

internal fun WebView.takeSnapshot(rect: Rect?): Bitmap? {
// Create a Rect object representing the area to capture.
// If the provided rect is null, default to the full dimensions of the WebView.
val snapshotRect = Rect(
left = rect?.left ?: 0f,
top = rect?.top ?: 0f,
right = rect?.right ?: width.toFloat(),
bottom = rect?.bottom ?: height.toFloat()
)

// Calculate the scale factor and dimensions
val (scaledWidth, scaledHeight, scaleFactor) = WebViewSnapshotUtil.calculateScaleFactorAndDimensions(
snapshotRect.width.toInt(),
snapshotRect.height.toInt()
)

// Create a bitmap with the target dimensions
val bm = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888)

// Create a canvas to draw the WebView content
val scaledCanvas = Canvas(bm)

// Apply the scaling transformation to the canvas and adjust for scroll position
val matrix = Matrix().apply {
setScale(scaleFactor, scaleFactor)
postTranslate(
-snapshotRect.left * scaleFactor - scrollX * scaleFactor,
-snapshotRect.top * scaleFactor - scrollY * scaleFactor
)
}
scaledCanvas.setMatrix(matrix)

// Draw the WebView onto the scaled canvas
draw(scaledCanvas)
return bm
}

object WebViewSnapshotUtil {

fun calculateScaleFactorAndDimensions(
width: Int,
height: Int
): Triple<Int, Int, Float> {
val targetWidth = minOf(MAX_SNAPSHOT_WIDTH, width)
val targetHeight = minOf(MAX_SNAPSHOT_HEIGHT, height)

// Determine the aspect ratio preserving scale factor
val scaleFactor = minOf(
targetWidth.toFloat() / width,
targetHeight.toFloat() / height
)

// Calculate the scaled dimensions
val scaledWidth = (width * scaleFactor).toInt()
val scaledHeight = (height * scaleFactor).toInt()

return Triple(scaledWidth, scaledHeight, scaleFactor)
}

private const val MAX_SNAPSHOT_WIDTH = 2500
private const val MAX_SNAPSHOT_HEIGHT = 2500
}
Original file line number Diff line number Diff line change
@@ -62,6 +62,7 @@ fun AccompanistWebView(
captureBackPresses: Boolean = true,
navigator: WebViewNavigator = rememberWebViewNavigator(),
webViewJsBridge: WebViewJsBridge? = null,
webViewSnapshot: WebViewSnapshot? = null,
onCreated: (WebView) -> Unit = {},
onDispose: (WebView) -> Unit = {},
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
@@ -98,6 +99,7 @@ fun AccompanistWebView(
captureBackPresses,
navigator,
webViewJsBridge,
webViewSnapshot,
onCreated,
onDispose,
client,
@@ -140,6 +142,7 @@ fun AccompanistWebView(
captureBackPresses: Boolean = true,
navigator: WebViewNavigator = rememberWebViewNavigator(),
webViewJsBridge: WebViewJsBridge? = null,
webViewSnapshot: WebViewSnapshot? = null,
onCreated: (WebView) -> Unit = {},
onDispose: (WebView) -> Unit = {},
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
@@ -230,6 +233,7 @@ fun AccompanistWebView(
val androidWebView = AndroidWebView(it, scope, webViewJsBridge)
state.webView = androidWebView
webViewJsBridge?.webView = androidWebView
webViewSnapshot?.webView = androidWebView
}
},
modifier = modifier,
Loading