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
Show file tree
Hide file tree
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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),
)
}

Expand Down Expand Up @@ -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")
}
}
}
}
}
}
}
}
Expand Down
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
}
}
}
Expand Down
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
Expand Up @@ -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() },
Expand Down Expand Up @@ -98,6 +99,7 @@ fun AccompanistWebView(
captureBackPresses,
navigator,
webViewJsBridge,
webViewSnapshot,
onCreated,
onDispose,
client,
Expand Down Expand Up @@ -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() },
Expand Down Expand Up @@ -230,6 +233,7 @@ fun AccompanistWebView(
val androidWebView = AndroidWebView(it, scope, webViewJsBridge)
state.webView = androidWebView
webViewJsBridge?.webView = androidWebView
webViewSnapshot?.webView = androidWebView
}
},
modifier = modifier,
Expand Down
Loading
Loading