From 0efb1b6f0fafae12d4d410aa81f686726402c686 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Mon, 8 Jun 2026 11:20:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor(webview):=20=E5=B0=86=20WebView=20?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E9=87=8D=E6=9E=84=E4=B8=BA=20Compose=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除传统 View 系统实现,采用 Jetpack Compose 架构 - 创建 XWebViewScreen 组件替代原有的 Activity 布局 - 集成 Material Design 3 主题支持 - 保留 WebView 的核心功能和配置选项 - 添加导航状态回调接口支持 - 简化窗口边距和系统栏适配逻辑 - 优化组件生命周期管理和资源释放 --- .../com/xuqm/sdk/webview/XWebViewActivity.kt | 231 +----------------- .../java/com/xuqm/sdk/webview/XWebViewView.kt | 11 + 2 files changed, 20 insertions(+), 222 deletions(-) diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewActivity.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewActivity.kt index 8e04f40..830cd32 100644 --- a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewActivity.kt +++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewActivity.kt @@ -1,235 +1,22 @@ 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.activity.compose.setContent +import androidx.compose.material3.MaterialTheme 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>? = 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(GetContentWithMimeTypes()) { 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 - } - } - - private fun launchCamera() { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - 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 { - shouldLaunchCamera = true - fileCameraPermissionLauncher.launch(Manifest.permission.CAMERA) - } - } - - @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) - - val config = getXWebViewConfig() - - WebView.setWebContentsDebuggingEnabled(config.debugEnabled) - 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 }, { null }), - config.jsBridgeName, - ) - - 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>, - fileChooserParams: FileChooserParams, - ): Boolean { - pendingFileCallback?.onReceiveValue(null) - pendingFileCallback = filePathCallback - - val acceptMimes = resolvePickerMimeTypes(fileChooserParams.acceptTypes) - val isCameraCapture = fileChooserParams.isCaptureEnabled && - fileChooserParams.acceptTypes.any { it.contains("image") } - val isImageOnly = acceptMimes.all { it.startsWith("image/") || it == "image/*" } - - when { - isCameraCapture -> launchCamera() - isImageOnly -> android.app.AlertDialog.Builder(this@XWebViewActivity) - .setTitle("选择图片") - .setItems(arrayOf("从相册选择", "拍照")) { _, which -> - if (which == 0) pickContentLauncher.launch(arrayOf("image/*")) - else launchCamera() - } - .setOnCancelListener { - pendingFileCallback?.onReceiveValue(null) - pendingFileCallback = null - } - .show() - else -> pickContentLauncher.launch(acceptMimes) - } - return true - } + setContent { + MaterialTheme { + XWebViewScreen( + config = getXWebViewConfig(), + onBack = { finish() }, + ) } } - 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() - } -} \ No newline at end of file +} diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt index c0b61f7..fa8156e 100644 --- a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt +++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt @@ -203,6 +203,7 @@ internal fun String.escapeJs(): String = replace("\\", "\\\\") fun XWebViewView( modifier: Modifier = Modifier, config: XWebViewConfig = getXWebViewConfig(), + onNavigationChanged: ((canGoBack: Boolean, canGoForward: Boolean, pageTitle: String) -> Unit)? = null, ) { val context = LocalContext.current var webView by remember { mutableStateOf(null) } @@ -287,6 +288,9 @@ fun XWebViewView( val xwvMessageRef = remember { mutableStateOf(xwvMessageHandler) } SideEffect { xwvMessageRef.value = xwvMessageHandler } + val onNavigationChangedRef = remember { mutableStateOf(onNavigationChanged) } + SideEffect { onNavigationChangedRef.value = onNavigationChanged } + // WebRTC getUserMedia() camera permission val pendingWebRtcRequest = remember { mutableStateOf(null) } val webRtcPermissionLauncher = rememberLauncherForActivityResult( @@ -389,6 +393,13 @@ fun XWebViewView( super.onPageFinished(view, url) // Inject DIALOG_OVERRIDE_JS + user script after every page load. view?.evaluateJavascript(buildInjectedJs(config), null) + mainHandler.post { + onNavigationChangedRef.value?.invoke( + view?.canGoBack() == true, + view?.canGoForward() == true, + view?.title.orEmpty(), + ) + } } override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {