Skip to content

Commit a5869b0

Browse files
committed
feat(android): implement DiminaNativeComponentBridge for enhanced touch handling and integrate with WebView for improved native component interaction
1 parent edcc94e commit a5869b0

6 files changed

Lines changed: 219 additions & 55 deletions

File tree

android/dimina/src/main/kotlin/com/didi/dimina/core/Bridge.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.didi.dimina.common.PathUtils
88
import com.didi.dimina.common.Utils
99
import com.didi.dimina.common.VersionUtils
1010
import com.didi.dimina.engine.qjs.JSValue
11+
import com.didi.dimina.ui.view.DiminaNativeComponentBridge
1112
import com.didi.dimina.ui.container.DiminaActivity
1213
import com.didi.dimina.ui.view.DiminaRenderBridge
1314
import com.didi.dimina.ui.view.postMessage
@@ -48,6 +49,10 @@ class Bridge(
4849
invokeHandler = { msg -> messageInvoke("render", msg) },
4950
publishHandler = { msg -> messagePublish(msg) }
5051
), DiminaRenderBridge.TAG)
52+
options.webview.addJavascriptInterface(
53+
DiminaNativeComponentBridge(
54+
touchHandler = { msg -> parent.dispatchNativeComponentTouch(msg) }
55+
), DiminaNativeComponentBridge.TAG)
5156
}
5257
// 加载模版页面
5358
options.webview.loadUrl(

android/dimina/src/main/kotlin/com/didi/dimina/ui/container/DiminaActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,10 @@ class DiminaActivity : ComponentActivity() {
618618
return nativeComponentHost?.handle(apiName, params) ?: false
619619
}
620620

621+
fun dispatchNativeComponentTouch(params: JSONObject): Boolean {
622+
return nativeComponentHost?.dispatchTouchFromWeb(params) ?: false
623+
}
624+
621625
fun clearNativeComponents() {
622626
nativeComponentHost?.clear()
623627
}

android/dimina/src/main/kotlin/com/didi/dimina/ui/view/DiminaWebView.kt

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,6 @@ fun DiminaWebView(
5050
}
5151

5252
Box(modifier = modifier.fillMaxSize()) {
53-
AndroidView(
54-
modifier = Modifier.fillMaxSize(),
55-
factory = { context ->
56-
val webView = if (enableCache) {
57-
// 使用缓存管理器获取WebView实例
58-
WebViewCacheManager.getWebView(context, onPageCompleted, webViewIdentifier)
59-
} else {
60-
// 传统方式创建WebView(使用WebViewCacheManager中的统一配置)
61-
createWebView(context, onPageCompleted)
62-
}
63-
64-
webView.apply {
65-
onInitReady(this)
66-
LogUtils.d(TAG, "WebView initialized with identifier: $webViewIdentifier")
67-
LogUtils.d(TAG, "Cache info: ${getWebViewCacheInfo()}")
68-
}
69-
}
70-
)
71-
7253
AndroidView(
7354
modifier = Modifier.fillMaxSize(),
7455
factory = { context ->
@@ -83,6 +64,23 @@ fun DiminaWebView(
8364
}
8465
}
8566
)
67+
68+
AndroidView(
69+
modifier = Modifier.fillMaxSize(),
70+
factory = { context ->
71+
if (enableCache) {
72+
// 使用缓存管理器获取WebView实例
73+
WebViewCacheManager.getWebView(context, onPageCompleted, webViewIdentifier)
74+
} else {
75+
// 传统方式创建WebView(使用WebViewCacheManager中的统一配置)
76+
createWebView(context, onPageCompleted)
77+
}.apply {
78+
onInitReady(this)
79+
LogUtils.d(TAG, "WebView initialized with identifier: $webViewIdentifier")
80+
LogUtils.d(TAG, "Cache info: ${getWebViewCacheInfo()}")
81+
}
82+
}
83+
)
8684
}
8785
}
8886

@@ -152,3 +150,20 @@ class DiminaRenderBridge(
152150
}
153151
}
154152

153+
class DiminaNativeComponentBridge(
154+
private val touchHandler: (JSONObject) -> Unit,
155+
) {
156+
@Suppress("unused")
157+
@JavascriptInterface
158+
fun dispatchTouch(message: String) {
159+
try {
160+
touchHandler(JSONObject(message))
161+
} catch (e: Exception) {
162+
LogUtils.e(TAG, "DiminaNativeComponentBridge.dispatchTouch failed: ${e.message}")
163+
}
164+
}
165+
166+
companion object {
167+
const val TAG = "DiminaNativeComponentBridge"
168+
}
169+
}

android/dimina/src/main/kotlin/com/didi/dimina/ui/view/NativeComponentHost.kt

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package com.didi.dimina.ui.view
33
import android.content.res.ColorStateList
44
import android.graphics.Color
55
import android.graphics.Typeface
6+
import android.graphics.drawable.Drawable
67
import android.graphics.drawable.GradientDrawable
78
import android.media.MediaPlayer
89
import android.net.Uri
910
import android.os.Handler
1011
import android.os.Looper
12+
import android.os.SystemClock
1113
import android.text.TextUtils
1214
import android.view.Gravity
15+
import android.view.InputDevice
16+
import android.view.MotionEvent
1317
import android.view.View
1418
import android.webkit.WebView
1519
import android.widget.FrameLayout
@@ -30,11 +34,12 @@ import kotlinx.coroutines.SupervisorJob
3034
import kotlinx.coroutines.cancel
3135
import kotlinx.coroutines.launch
3236
import kotlinx.coroutines.withContext
37+
import org.json.JSONArray
3338
import org.json.JSONObject
3439
import kotlin.math.roundToInt
3540

3641
/**
37-
* Hosts native components as siblings of WebView and keeps their bounds aligned
42+
* Hosts native components behind the WebView and keeps their bounds aligned
3843
* with DOM placeholders reported from the render layer.
3944
*/
4045
class NativeComponentHost(
@@ -43,7 +48,10 @@ class NativeComponentHost(
4348
private val overlay: FrameLayout,
4449
) {
4550
private val components = mutableMapOf<String, NativeComponent>()
51+
private val touchDownTimes = mutableMapOf<String, Long>()
52+
private val originalWebViewBackground: Drawable? = webView.background
4653
private val imageScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
54+
private var webViewTransparent = false
4755

4856
init {
4957
webView.setOnScrollChangeListener { _, _, _, _, _ ->
@@ -75,16 +83,140 @@ class NativeComponentHost(
7583
return true
7684
}
7785

86+
fun dispatchTouchFromWeb(params: JSONObject): Boolean {
87+
val targetId = params.optString("targetId")
88+
if (targetId.isEmpty()) {
89+
return false
90+
}
91+
activity.runOnUiThread {
92+
dispatchNativeTouch(params)
93+
}
94+
return true
95+
}
96+
7897
fun clear() {
7998
activity.runOnUiThread {
8099
webView.setOnScrollChangeListener(null)
81100
components.values.forEach { it.release() }
82101
components.clear()
102+
touchDownTimes.clear()
103+
restoreWebViewBackground()
83104
imageScope.cancel()
84105
overlay.removeAllViews()
85106
}
86107
}
87108

109+
private fun dispatchNativeTouch(params: JSONObject) {
110+
val targetId = params.optString("targetId")
111+
val component = components[targetId] ?: return
112+
val targetView = component.view
113+
if (targetView.visibility != View.VISIBLE) {
114+
return
115+
}
116+
117+
val actionName = params.optString("action")
118+
val now = SystemClock.uptimeMillis()
119+
if (actionName == TOUCH_ACTION_DOWN) {
120+
touchDownTimes[targetId] = now
121+
}
122+
val downTime = touchDownTimes[targetId] ?: now.also {
123+
touchDownTimes[targetId] = it
124+
}
125+
126+
val event = createMotionEvent(params, targetView, downTime, now) ?: return
127+
try {
128+
targetView.dispatchTouchEvent(event)
129+
} finally {
130+
event.recycle()
131+
}
132+
133+
if (actionName == TOUCH_ACTION_UP || actionName == TOUCH_ACTION_CANCEL) {
134+
touchDownTimes.remove(targetId)
135+
}
136+
}
137+
138+
private fun createMotionEvent(
139+
params: JSONObject,
140+
targetView: View,
141+
downTime: Long,
142+
eventTime: Long,
143+
): MotionEvent? {
144+
val pointers = params.optJSONArray("pointers") ?: return null
145+
if (pointers.length() == 0) {
146+
return null
147+
}
148+
149+
val actionPointerId = params.optInt("actionPointerId", -1)
150+
val actionPointerIndex = findPointerIndex(pointers, actionPointerId)
151+
val action = when (params.optString("action")) {
152+
TOUCH_ACTION_DOWN -> MotionEvent.ACTION_DOWN
153+
TOUCH_ACTION_POINTER_DOWN -> MotionEvent.ACTION_POINTER_DOWN or
154+
(actionPointerIndex.coerceAtLeast(0) shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
155+
TOUCH_ACTION_MOVE -> MotionEvent.ACTION_MOVE
156+
TOUCH_ACTION_POINTER_UP -> MotionEvent.ACTION_POINTER_UP or
157+
(actionPointerIndex.coerceAtLeast(0) shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
158+
TOUCH_ACTION_UP -> MotionEvent.ACTION_UP
159+
TOUCH_ACTION_CANCEL -> MotionEvent.ACTION_CANCEL
160+
else -> return null
161+
}
162+
163+
val viewportWidth = params.optDouble("viewportWidth", 0.0)
164+
val viewportHeight = params.optDouble("viewportHeight", 0.0)
165+
val scaleX = if (viewportWidth > 0.0 && webView.width > 0) {
166+
webView.width / viewportWidth
167+
} else {
168+
1.0
169+
}
170+
val scaleY = if (viewportHeight > 0.0 && webView.height > 0) {
171+
webView.height / viewportHeight
172+
} else {
173+
scaleX
174+
}
175+
176+
val pointerProperties = Array(pointers.length()) { index ->
177+
val pointer = pointers.getJSONObject(index)
178+
MotionEvent.PointerProperties().apply {
179+
id = pointer.optInt("id", index)
180+
toolType = MotionEvent.TOOL_TYPE_FINGER
181+
}
182+
}
183+
val pointerCoords = Array(pointers.length()) { index ->
184+
val pointer = pointers.getJSONObject(index)
185+
MotionEvent.PointerCoords().apply {
186+
x = (pointer.optDouble("clientX") * scaleX - targetView.left).toFloat()
187+
y = (pointer.optDouble("clientY") * scaleY - targetView.top).toFloat()
188+
pressure = 1f
189+
size = 1f
190+
}
191+
}
192+
193+
return MotionEvent.obtain(
194+
downTime,
195+
eventTime,
196+
action,
197+
pointers.length(),
198+
pointerProperties,
199+
pointerCoords,
200+
0,
201+
0,
202+
1f,
203+
1f,
204+
0,
205+
0,
206+
InputDevice.SOURCE_TOUCHSCREEN,
207+
0,
208+
)
209+
}
210+
211+
private fun findPointerIndex(pointers: JSONArray, pointerId: Int): Int {
212+
for (index in 0 until pointers.length()) {
213+
if (pointers.getJSONObject(index).optInt("id", -1) == pointerId) {
214+
return index
215+
}
216+
}
217+
return 0
218+
}
219+
88220
private fun mountComponent(type: String, id: String, params: JSONObject) {
89221
val existing = components[id]
90222
if (existing != null && existing.type != type) {
@@ -94,17 +226,47 @@ class NativeComponentHost(
94226
createComponent(type, id).also { overlay.addView(it.view) }
95227
}
96228
component.update(params)
229+
updateWebViewBackgroundForNativeComponents()
97230
}
98231

99232
private fun updateComponent(type: String, id: String, params: JSONObject) {
100-
components[id]?.update(params) ?: mountComponent(type, id, params)
233+
components[id]?.let { component ->
234+
component.update(params)
235+
updateWebViewBackgroundForNativeComponents()
236+
} ?: mountComponent(type, id, params)
101237
}
102238

103239
private fun unmountComponent(id: String) {
104240
components.remove(id)?.let { component ->
241+
touchDownTimes.remove(id)
105242
component.release()
106243
overlay.removeView(component.view)
244+
updateWebViewBackgroundForNativeComponents()
245+
}
246+
}
247+
248+
private fun updateWebViewBackgroundForNativeComponents() {
249+
val hasVisibleNativeComponent = components.values.any { component ->
250+
component.view.visibility == View.VISIBLE
251+
}
252+
if (hasVisibleNativeComponent && !webViewTransparent) {
253+
webView.setBackgroundColor(Color.TRANSPARENT)
254+
webViewTransparent = true
255+
} else if (!hasVisibleNativeComponent && webViewTransparent) {
256+
restoreWebViewBackground()
257+
}
258+
}
259+
260+
private fun restoreWebViewBackground() {
261+
if (!webViewTransparent) {
262+
return
263+
}
264+
if (originalWebViewBackground != null) {
265+
webView.background = originalWebViewBackground
266+
} else {
267+
webView.setBackgroundColor(Color.WHITE)
107268
}
269+
webViewTransparent = false
108270
}
109271

110272
private fun createComponent(type: String, id: String): NativeComponent {
@@ -748,6 +910,12 @@ class NativeComponentHost(
748910
private const val COVER_IMAGE_TYPE = "native/cover-image"
749911
private const val TIME_UPDATE_INTERVAL_MS = 250L
750912
private const val FIRST_FRAME_SEEK_MS = 1
913+
private const val TOUCH_ACTION_DOWN = "down"
914+
private const val TOUCH_ACTION_POINTER_DOWN = "pointerDown"
915+
private const val TOUCH_ACTION_MOVE = "move"
916+
private const val TOUCH_ACTION_POINTER_UP = "pointerUp"
917+
private const val TOUCH_ACTION_UP = "up"
918+
private const val TOUCH_ACTION_CANCEL = "cancel"
751919
private val SUPPORTED_TYPES = setOf(VIDEO_TYPE, COVER_VIEW_TYPE, COVER_IMAGE_TYPE)
752920
}
753921

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,13 @@
11
package com.didi.dimina.ui.view
22

33
import android.content.Context
4-
import android.view.MotionEvent
5-
import android.view.View
64
import android.widget.FrameLayout
75

86
/**
9-
* Transparent native component layer above the WebView.
7+
* Transparent native component layer below the WebView.
108
*
11-
* It only consumes touches that hit visible native children, so normal WebView
12-
* interactions still work in empty overlay areas.
9+
* The WebView stays on top so normal HTML can cover native components. Touches
10+
* for native placeholders are forwarded explicitly from the render layer after
11+
* DOM hit testing.
1312
*/
14-
class NativeComponentOverlay(context: Context) : FrameLayout(context) {
15-
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
16-
if (!hasTouchableChildAt(ev.x, ev.y)) {
17-
return false
18-
}
19-
return super.dispatchTouchEvent(ev)
20-
}
21-
22-
private fun hasTouchableChildAt(x: Float, y: Float): Boolean {
23-
val children = (0 until childCount)
24-
.map { getChildAt(it) }
25-
.sortedWith(compareBy<View> { it.z }.thenBy { indexOfChild(it) })
26-
.asReversed()
27-
for (child in children) {
28-
if (child.visibility != View.VISIBLE || !child.isClickable) {
29-
continue
30-
}
31-
if (
32-
x >= child.left &&
33-
x <= child.right &&
34-
y >= child.top &&
35-
y <= child.bottom
36-
) {
37-
return true
38-
}
39-
}
40-
return false
41-
}
42-
}
13+
class NativeComponentOverlay(context: Context) : FrameLayout(context)

0 commit comments

Comments
 (0)