feat(webview): 添加独立的WebView活动支持

- 引入XWebViewActivity以提供完整的WebView界面
- 在AndroidManifest.xml中注册新的WebView活动
- 更新桥接功能以支持上下文启动活动
- 将内部函数和类可见性调整为internal以便组件间访问
- 增加发布版本号从0.4.3到0.4.9
这个提交包含在:
XuqmGroup 2026-05-11 18:12:59 +08:00
父节点 4fe7678e07
当前提交 9e9b41fedb
共有 5 个文件被更改,包括 244 次插入5 次删除

查看文件

@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
PUBLISH_VERSION=0.4.3
PUBLISH_VERSION=0.4.9

查看文件

@ -1 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".XWebViewActivity"
android:exported="false"
android:screenOrientation="unspecified"
android:windowSoftInputMode="adjustResize" />
</application>
</manifest>

查看文件

@ -0,0 +1,222 @@
package com.xuqm.sdk.webview
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import java.io.File
class XWebViewActivity : ComponentActivity() {
private lateinit var webView: WebView
private val mainHandler = Handler(Looper.getMainLooper())
private var pendingWebRtcRequest: PermissionRequest? = null
private var pendingFileCallback: ValueCallback<Array<Uri>>? = null
private var pendingCameraUri: Uri? = null
private var shouldLaunchCamera = false
private var currentUrl: String? = null
private val webRtcPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
val req = pendingWebRtcRequest
pendingWebRtcRequest = null
if (granted) req?.grant(req.resources) else req?.deny()
}
private val takePictureLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
val cb = pendingFileCallback
val uri = pendingCameraUri
pendingFileCallback = null
pendingCameraUri = null
cb?.onReceiveValue(if (success && uri != null) arrayOf(uri) else null)
}
private val pickContentLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
val cb = pendingFileCallback
pendingFileCallback = null
cb?.onReceiveValue(if (uri != null) arrayOf(uri) else null)
}
private val fileCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
val launch = shouldLaunchCamera
shouldLaunchCamera = false
if (granted && launch) {
runCatching {
val imageFile = File.createTempFile("cam_", ".jpg", cacheDir)
val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", imageFile)
pendingCameraUri = uri
takePictureLauncher.launch(uri)
}.onFailure {
pendingFileCallback?.onReceiveValue(null)
pendingFileCallback = null
}
} else {
pendingFileCallback?.onReceiveValue(null)
pendingFileCallback = null
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val config = getXWebViewConfig()
val container = FrameLayout(this)
webView = WebView(this).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.useWideViewPort = true
settings.loadWithOverviewMode = true
config.userAgent?.let { settings.userAgentString = it }
addJavascriptInterface(
XWebViewJsBridge(mainHandler) { config.onMessage },
"ReactNativeWebView",
)
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
currentUrl = url ?: view?.url
super.onPageFinished(view, url)
view?.evaluateJavascript(buildInjectedJs(config), null)
}
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false
}
webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
if (PermissionRequest.RESOURCE_VIDEO_CAPTURE in request.resources) {
if (ContextCompat.checkSelfPermission(this@XWebViewActivity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
request.grant(request.resources)
} else {
pendingWebRtcRequest = request
webRtcPermissionLauncher.launch(Manifest.permission.CAMERA)
}
} else {
request.deny()
}
}
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams,
): Boolean {
pendingFileCallback?.onReceiveValue(null)
pendingFileCallback = filePathCallback
val isCameraCapture = fileChooserParams.isCaptureEnabled &&
fileChooserParams.acceptTypes.any { it.contains("image") }
if (isCameraCapture) {
if (ContextCompat.checkSelfPermission(this@XWebViewActivity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
runCatching {
val imageFile = File.createTempFile("cam_", ".jpg", cacheDir)
val uri = FileProvider.getUriForFile(this@XWebViewActivity, "${packageName}.fileprovider", imageFile)
pendingCameraUri = uri
takePictureLauncher.launch(uri)
}.onFailure {
pendingFileCallback?.onReceiveValue(null)
pendingFileCallback = null
}
} else {
shouldLaunchCamera = true
fileCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
} else {
val mimeType = fileChooserParams.acceptTypes
.firstOrNull { it.isNotBlank() } ?: "image/*"
pickContentLauncher.launch(mimeType)
}
return true
}
}
}
container.addView(webView, FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
))
setContentView(container)
ViewCompat.setOnApplyWindowInsetsListener(container) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
)
v.setPadding(0, bars.top, 0, bars.bottom)
insets
}
container.viewTreeObserver.addOnGlobalLayoutListener {
recalculateBottomPadding(container)
}
setXWebViewController(object : XWebViewController {
override fun canGoBack(): Boolean = webView.canGoBack()
override fun canGoForward(): Boolean = webView.canGoForward()
override fun currentUrl(): String? = currentUrl ?: webView.url
override fun goBack() { if (webView.canGoBack()) webView.goBack() }
override fun goForward() { if (webView.canGoForward()) webView.goForward() }
override fun reload() { webView.reload() }
override fun loadUrl(url: String) { webView.loadUrl(url) }
override fun postMessageToWeb(js: String) { webView.evaluateJavascript(js, null) }
})
if (config.url.isNotBlank()) {
webView.loadUrl(config.url)
}
}
private fun recalculateBottomPadding(container: View) {
val visibleFrame = Rect()
container.getWindowVisibleDisplayFrame(visibleFrame)
val screenHeight = resources.displayMetrics.heightPixels
val bottomHidden = screenHeight - visibleFrame.bottom
val currentTop = container.paddingTop
val currentBottom = container.paddingBottom
val neededBottom = maxOf(currentBottom, bottomHidden)
if (neededBottom != currentBottom) {
container.setPadding(0, currentTop, 0, neededBottom)
}
}
@Suppress("OVERRIDE_DEPRECATION")
override fun onBackPressed() {
if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
}
override fun onDestroy() {
setXWebViewController(null)
webView.destroy()
super.onDestroy()
}
}

查看文件

@ -1,5 +1,7 @@
package com.xuqm.sdk.webview
import android.content.Context
import android.content.Intent
import androidx.compose.runtime.mutableStateOf
private val currentConfig = mutableStateOf(XWebViewConfig())
@ -9,6 +11,11 @@ fun openXWebView(config: XWebViewConfig) {
currentConfig.value = config
}
fun openXWebView(context: Context, config: XWebViewConfig) {
currentConfig.value = config
context.startActivity(Intent(context, XWebViewActivity::class.java))
}
fun getXWebViewConfig(): XWebViewConfig = currentConfig.value
fun setXWebViewController(controller: XWebViewController?) {

查看文件

@ -32,7 +32,7 @@ import java.io.File
// JS injected into every page to bridge dialog APIs and download interception.
// Uses addJavascriptInterface("ReactNativeWebView") for the JS→Native channel.
private val DIALOG_OVERRIDE_JS = """
internal val DIALOG_OVERRIDE_JS = """
(function() {
function post(obj) {
window.ReactNativeWebView.postMessage(JSON.stringify(obj));
@ -103,12 +103,12 @@ private val DIALOG_OVERRIDE_JS = """
true;
""".trimIndent()
private fun buildInjectedJs(config: XWebViewConfig): String =
internal fun buildInjectedJs(config: XWebViewConfig): String =
DIALOG_OVERRIDE_JS + "\n" + (config.injectedJavaScript ?: "") + "\ntrue;"
// Routes window.ReactNativeWebView.postMessage() calls to [onMessage].
// @JavascriptInterface methods are called on a background thread; we post to main.
private class XWebViewJsBridge(
internal class XWebViewJsBridge(
private val mainHandler: Handler,
private val onMessage: () -> ((String) -> Unit)?,
) {