feat(webview): 添加独立的WebView活动支持
- 引入XWebViewActivity以提供完整的WebView界面 - 在AndroidManifest.xml中注册新的WebView活动 - 更新桥接功能以支持上下文启动活动 - 将内部函数和类可见性调整为internal以便组件间访问 - 增加发布版本号从0.4.3到0.4.9
这个提交包含在:
父节点
4fe7678e07
当前提交
9e9b41fedb
@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
android.nonTransitiveRClass=true
|
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
|
package com.xuqm.sdk.webview
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
||||||
private val currentConfig = mutableStateOf(XWebViewConfig())
|
private val currentConfig = mutableStateOf(XWebViewConfig())
|
||||||
@ -9,6 +11,11 @@ fun openXWebView(config: XWebViewConfig) {
|
|||||||
currentConfig.value = config
|
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 getXWebViewConfig(): XWebViewConfig = currentConfig.value
|
||||||
|
|
||||||
fun setXWebViewController(controller: XWebViewController?) {
|
fun setXWebViewController(controller: XWebViewController?) {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import java.io.File
|
|||||||
|
|
||||||
// JS injected into every page to bridge dialog APIs and download interception.
|
// JS injected into every page to bridge dialog APIs and download interception.
|
||||||
// Uses addJavascriptInterface("ReactNativeWebView") for the JS→Native channel.
|
// Uses addJavascriptInterface("ReactNativeWebView") for the JS→Native channel.
|
||||||
private val DIALOG_OVERRIDE_JS = """
|
internal val DIALOG_OVERRIDE_JS = """
|
||||||
(function() {
|
(function() {
|
||||||
function post(obj) {
|
function post(obj) {
|
||||||
window.ReactNativeWebView.postMessage(JSON.stringify(obj));
|
window.ReactNativeWebView.postMessage(JSON.stringify(obj));
|
||||||
@ -103,12 +103,12 @@ private val DIALOG_OVERRIDE_JS = """
|
|||||||
true;
|
true;
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
private fun buildInjectedJs(config: XWebViewConfig): String =
|
internal fun buildInjectedJs(config: XWebViewConfig): String =
|
||||||
DIALOG_OVERRIDE_JS + "\n" + (config.injectedJavaScript ?: "") + "\ntrue;"
|
DIALOG_OVERRIDE_JS + "\n" + (config.injectedJavaScript ?: "") + "\ntrue;"
|
||||||
|
|
||||||
// Routes window.ReactNativeWebView.postMessage() calls to [onMessage].
|
// Routes window.ReactNativeWebView.postMessage() calls to [onMessage].
|
||||||
// @JavascriptInterface methods are called on a background thread; we post to main.
|
// @JavascriptInterface methods are called on a background thread; we post to main.
|
||||||
private class XWebViewJsBridge(
|
internal class XWebViewJsBridge(
|
||||||
private val mainHandler: Handler,
|
private val mainHandler: Handler,
|
||||||
private val onMessage: () -> ((String) -> Unit)?,
|
private val onMessage: () -> ((String) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户