pythonnative 0.35.0__py3-none-any.whl → 0.37.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonnative/__init__.py +6 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +34 -88
- pythonnative/bootstrap.py +90 -0
- pythonnative/bridge/__init__.py +306 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +164 -0
- pythonnative/bridge/fake.py +236 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/cli/pn.py +24 -6
- pythonnative/components/layout.py +65 -3
- pythonnative/gestures.py +13 -10
- pythonnative/hooks.py +41 -26
- pythonnative/hosts/__init__.py +13 -36
- pythonnative/hosts/native.py +348 -0
- pythonnative/images.py +6 -5
- pythonnative/mutations.py +21 -0
- pythonnative/native_modules/__init__.py +30 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +20 -84
- pythonnative/native_modules/biometrics.py +11 -124
- pythonnative/native_modules/camera.py +15 -291
- pythonnative/native_modules/clipboard.py +14 -109
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +15 -21
- pythonnative/native_modules/haptics.py +19 -129
- pythonnative/native_modules/linking.py +24 -137
- pythonnative/native_modules/location.py +28 -180
- pythonnative/native_modules/net_info.py +25 -135
- pythonnative/native_modules/notifications.py +36 -257
- pythonnative/native_modules/permissions.py +20 -184
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +18 -155
- pythonnative/native_modules/share.py +8 -113
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +0 -3
- pythonnative/project/builder.py +38 -16
- pythonnative/project/config.py +12 -2
- pythonnative/project/plugins.py +366 -0
- pythonnative/py.typed +0 -0
- pythonnative/runtime.py +35 -203
- pythonnative/sdk/__init__.py +48 -33
- pythonnative/sdk/_components.py +57 -76
- pythonnative/storage.py +18 -243
- pythonnative/templates/android_template/app/build.gradle +6 -9
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
- pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
- pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
- pythonnative/templates/android_template/settings.gradle +1 -0
- pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
- pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
- pythonnative/utils.py +4 -82
- pythonnative/virtual_rows.py +3 -3
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
- pythonnative-0.37.0.dist-info/RECORD +263 -0
- pythonnative/hosts/android.py +0 -269
- pythonnative/hosts/ios.py +0 -404
- pythonnative/native_views/android.py +0 -3702
- pythonnative/native_views/ios.py +0 -5253
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
- pythonnative-0.35.0.dist-info/RECORD +0 -155
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.graphics.Rect
|
|
4
|
+
import android.graphics.drawable.GradientDrawable
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.view.TouchDelegate
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.view.ViewGroup
|
|
9
|
+
import com.pythonnative.runtime.PNBridge
|
|
10
|
+
import com.pythonnative.runtime.bridge.JsonUtil
|
|
11
|
+
import com.pythonnative.runtime.bridge.PNLog
|
|
12
|
+
import com.pythonnative.runtime.bridge.ViewRecord
|
|
13
|
+
import com.pythonnative.runtime.bridge.num
|
|
14
|
+
import com.pythonnative.runtime.bridge.obj
|
|
15
|
+
import com.pythonnative.runtime.bridge.str
|
|
16
|
+
import com.pythonnative.runtime.bridge.value
|
|
17
|
+
import com.pythonnative.runtime.views.PNAccessibilityDelegate
|
|
18
|
+
import com.pythonnative.runtime.views.PNBorderDrawable
|
|
19
|
+
import com.pythonnative.runtime.views.PNFrameLayout
|
|
20
|
+
import org.json.JSONArray
|
|
21
|
+
import org.json.JSONObject
|
|
22
|
+
import kotlin.math.roundToInt
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Applies the visual props every element supports: background and
|
|
26
|
+
* borders, opacity, overflow, display, z-order, pointer events, hit
|
|
27
|
+
* slop, shadows, transforms, and accessibility.
|
|
28
|
+
*
|
|
29
|
+
* Border-related props are baked into a single background drawable,
|
|
30
|
+
* so the styler keeps the merged "visual" subset per view and re-bakes
|
|
31
|
+
* it whenever one of those keys changes.
|
|
32
|
+
*/
|
|
33
|
+
object ViewStyler {
|
|
34
|
+
private val sideWidthKeys = listOf("border_left_width", "border_top_width", "border_right_width", "border_bottom_width")
|
|
35
|
+
private val sideColorKeys = listOf("border_left_color", "border_top_color", "border_right_color", "border_bottom_color")
|
|
36
|
+
// Order matches GradientDrawable.setCornerRadii: TL, TR, BR, BL.
|
|
37
|
+
private val cornerKeys = listOf(
|
|
38
|
+
"border_top_left_radius", "border_top_right_radius", "border_bottom_right_radius", "border_bottom_left_radius",
|
|
39
|
+
)
|
|
40
|
+
private val drawableKeys: List<String> =
|
|
41
|
+
listOf("background_color", "border_radius", "border_width", "border_color") + cornerKeys + sideWidthKeys + sideColorKeys
|
|
42
|
+
|
|
43
|
+
private fun density(): Float = PNBridge.density()
|
|
44
|
+
private fun px(dp: Double): Float = (dp * density()).toFloat()
|
|
45
|
+
private fun pxI(dp: Double): Int = (dp * density()).roundToInt()
|
|
46
|
+
|
|
47
|
+
private fun record(view: View): ViewRecord? = PNBridge.registry.recordFor(view)
|
|
48
|
+
|
|
49
|
+
/** Apply the common visual props present in `props` to `view`. */
|
|
50
|
+
fun apply(view: View, props: JSONObject) {
|
|
51
|
+
val record = record(view)
|
|
52
|
+
if (drawableKeys.any { props.has(it) }) {
|
|
53
|
+
val visual = visualProps(record)
|
|
54
|
+
for (key in drawableKeys) {
|
|
55
|
+
if (props.has(key)) visual.put(key, props.opt(key) ?: JSONObject.NULL)
|
|
56
|
+
}
|
|
57
|
+
applyBorder(view, visual)
|
|
58
|
+
}
|
|
59
|
+
if (props.has("overflow")) {
|
|
60
|
+
val clip = props.str("overflow") == "hidden"
|
|
61
|
+
if (view is ViewGroup) {
|
|
62
|
+
view.clipChildren = clip
|
|
63
|
+
view.clipToPadding = clip
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (props.has("display")) {
|
|
67
|
+
view.visibility = if (props.str("display") == "none") View.GONE else View.VISIBLE
|
|
68
|
+
}
|
|
69
|
+
props.num("opacity")?.let { view.alpha = it.toFloat() }
|
|
70
|
+
if (props.has("z_index")) {
|
|
71
|
+
val z = props.num("z_index")
|
|
72
|
+
view.z = if (z != null) px(z) else 0f
|
|
73
|
+
}
|
|
74
|
+
if (props.has("pointer_events")) applyPointerEvents(view, props.str("pointer_events"))
|
|
75
|
+
if (props.has("hit_slop")) updateHitSlop(view)
|
|
76
|
+
applyShadow(view, props)
|
|
77
|
+
applyTransform(view, props)
|
|
78
|
+
applyAccessibility(view, props)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun visualProps(record: ViewRecord?): JSONObject {
|
|
82
|
+
val existing = record?.state?.get("visual") as? JSONObject
|
|
83
|
+
if (existing != null) return existing
|
|
84
|
+
val fresh = JSONObject()
|
|
85
|
+
record?.state?.put("visual", fresh)
|
|
86
|
+
return fresh
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ------------------------------------------------------------------
|
|
90
|
+
// Background and borders
|
|
91
|
+
// ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/** Re-bake the background drawable from the merged visual props. */
|
|
94
|
+
fun applyBorder(view: View, visual: JSONObject) {
|
|
95
|
+
if (applySideBorder(view, visual)) return
|
|
96
|
+
val hasBorder = listOf("border_radius", "border_width", "border_color").any { visual.has(it) } ||
|
|
97
|
+
cornerKeys.any { visual.has(it) }
|
|
98
|
+
val hasBg = visual.value("background_color") != null
|
|
99
|
+
if (!hasBorder && !hasBg) {
|
|
100
|
+
if (view.background is GradientDrawable || view.background is PNBorderDrawable) view.background = null
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
val drawable = GradientDrawable()
|
|
104
|
+
PNColor.parse(visual.value("background_color"))?.let { drawable.setColor(it) }
|
|
105
|
+
if (cornerKeys.any { visual.value(it) != null }) {
|
|
106
|
+
val base = visual.num("border_radius") ?: 0.0
|
|
107
|
+
val radii = FloatArray(8)
|
|
108
|
+
for ((i, key) in cornerKeys.withIndex()) {
|
|
109
|
+
val r = px(visual.num(key) ?: base)
|
|
110
|
+
radii[i * 2] = r
|
|
111
|
+
radii[i * 2 + 1] = r
|
|
112
|
+
}
|
|
113
|
+
drawable.cornerRadii = radii
|
|
114
|
+
} else {
|
|
115
|
+
visual.num("border_radius")?.let { drawable.cornerRadius = px(it) }
|
|
116
|
+
}
|
|
117
|
+
if (visual.value("border_width") != null || visual.value("border_color") != null) {
|
|
118
|
+
val width = visual.num("border_width") ?: 1.0
|
|
119
|
+
val color = PNColor.parseOr(visual.value("border_color"), 0xFF000000.toInt())
|
|
120
|
+
drawable.setStroke(pxI(width), color)
|
|
121
|
+
}
|
|
122
|
+
view.background = drawable
|
|
123
|
+
view.invalidate()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun applySideBorder(view: View, visual: JSONObject): Boolean {
|
|
127
|
+
if (sideWidthKeys.none { visual.value(it) != null }) return false
|
|
128
|
+
val baseWidth = visual.num("border_width") ?: 0.0
|
|
129
|
+
val baseColor = visual.value("border_color") ?: "#000000"
|
|
130
|
+
val widths = FloatArray(4) { i -> px(visual.num(sideWidthKeys[i]) ?: baseWidth) }
|
|
131
|
+
val colors = IntArray(4) { i -> PNColor.parseOr(visual.value(sideColorKeys[i]) ?: baseColor, 0xFF000000.toInt()) }
|
|
132
|
+
val bg = PNColor.parse(visual.value("background_color"))
|
|
133
|
+
val radius = px(visual.num("border_radius") ?: 0.0)
|
|
134
|
+
view.background = PNBorderDrawable(bg != null, bg ?: 0, radius, widths, colors)
|
|
135
|
+
view.invalidate()
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply one animated `background_color` frame without losing borders:
|
|
141
|
+
* mutate the existing drawable's fill in place when possible.
|
|
142
|
+
*/
|
|
143
|
+
fun setAnimatedBackground(view: View, value: Any?) {
|
|
144
|
+
val color = PNColor.parse(value) ?: return
|
|
145
|
+
val record = record(view)
|
|
146
|
+
val visual = record?.state?.get("visual") as? JSONObject
|
|
147
|
+
val hasBorder = visual != null && drawableKeys.any { it != "background_color" && visual.value(it) != null }
|
|
148
|
+
if (hasBorder && visual != null) {
|
|
149
|
+
visual.put("background_color", String.format("#%08X", color))
|
|
150
|
+
when (val bg = view.background) {
|
|
151
|
+
is GradientDrawable -> bg.setColor(color)
|
|
152
|
+
is PNBorderDrawable -> bg.setFillColor(color)
|
|
153
|
+
else -> applyBorder(view, visual)
|
|
154
|
+
}
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
view.setBackgroundColor(color)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ------------------------------------------------------------------
|
|
161
|
+
// Shadow / elevation
|
|
162
|
+
// ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
private fun applyShadow(view: View, props: JSONObject) {
|
|
165
|
+
var elevation = props.num("elevation")
|
|
166
|
+
if (elevation == null && props.has("shadow_radius")) elevation = props.num("shadow_radius")
|
|
167
|
+
if (elevation == null && (props.value("shadow_color") != null || props.value("shadow_opacity") != null)) {
|
|
168
|
+
// Shadow requested without an explicit size: Material card-like default.
|
|
169
|
+
elevation = 4.0
|
|
170
|
+
}
|
|
171
|
+
if (elevation == null) return
|
|
172
|
+
view.elevation = px(elevation)
|
|
173
|
+
val color = props.value("shadow_color") ?: return
|
|
174
|
+
if (Build.VERSION.SDK_INT < 28) return
|
|
175
|
+
var argb = PNColor.parse(color) ?: return
|
|
176
|
+
props.num("shadow_opacity")?.let { argb = PNColor.withAlpha(argb, it) }
|
|
177
|
+
view.outlineAmbientShadowColor = argb
|
|
178
|
+
view.outlineSpotShadowColor = argb
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ------------------------------------------------------------------
|
|
182
|
+
// Transform
|
|
183
|
+
// ------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
private fun applyTransform(view: View, props: JSONObject) {
|
|
186
|
+
if (!props.has("transform")) return
|
|
187
|
+
val spec = props.value("transform")
|
|
188
|
+
if (spec == null) {
|
|
189
|
+
view.rotation = 0f
|
|
190
|
+
view.rotationX = 0f
|
|
191
|
+
view.rotationY = 0f
|
|
192
|
+
view.scaleX = 1f
|
|
193
|
+
view.scaleY = 1f
|
|
194
|
+
view.translationX = 0f
|
|
195
|
+
view.translationY = 0f
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
val entries: List<Any?> = when (spec) {
|
|
199
|
+
is JSONArray -> JsonUtil.toList(spec)
|
|
200
|
+
else -> listOf(spec)
|
|
201
|
+
}
|
|
202
|
+
for (entry in entries) {
|
|
203
|
+
val e = entry as? JSONObject ?: continue
|
|
204
|
+
try {
|
|
205
|
+
if (e.has("rotate")) view.rotation = angleDegrees(e.value("rotate"))
|
|
206
|
+
if (e.has("rotate_x")) view.rotationX = angleDegrees(e.value("rotate_x"))
|
|
207
|
+
if (e.has("rotate_y")) view.rotationY = angleDegrees(e.value("rotate_y"))
|
|
208
|
+
if (e.has("rotate_z")) view.rotation = angleDegrees(e.value("rotate_z"))
|
|
209
|
+
e.num("scale")?.let { view.scaleX = it.toFloat(); view.scaleY = it.toFloat() }
|
|
210
|
+
e.num("scale_x")?.let { view.scaleX = it.toFloat() }
|
|
211
|
+
e.num("scale_y")?.let { view.scaleY = it.toFloat() }
|
|
212
|
+
e.num("translate_x")?.let { view.translationX = px(it) }
|
|
213
|
+
e.num("translate_y")?.let { view.translationY = px(it) }
|
|
214
|
+
if (e.has("skew_x") || e.has("skew_y")) {
|
|
215
|
+
// Android views have no skew transform; approximate with rotation around the axis.
|
|
216
|
+
PNLog.once("skew", "transform skew_x/skew_y is not supported on Android and is ignored")
|
|
217
|
+
}
|
|
218
|
+
} catch (ex: Exception) {
|
|
219
|
+
PNLog.swallowed("ViewStyler.applyTransform", ex)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Parse `"45deg"`, `"0.5rad"`, or a bare number (degrees) into degrees. */
|
|
225
|
+
fun angleDegrees(value: Any?): Float {
|
|
226
|
+
return when (value) {
|
|
227
|
+
is Number -> value.toFloat()
|
|
228
|
+
is String -> {
|
|
229
|
+
val s = value.trim()
|
|
230
|
+
when {
|
|
231
|
+
s.endsWith("deg") -> s.dropLast(3).toFloatOrNull() ?: 0f
|
|
232
|
+
s.endsWith("rad") -> Math.toDegrees((s.dropLast(3).toDoubleOrNull() ?: 0.0)).toFloat()
|
|
233
|
+
else -> s.toFloatOrNull() ?: 0f
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else -> 0f
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ------------------------------------------------------------------
|
|
241
|
+
// Accessibility
|
|
242
|
+
// ------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/** Apply accessibility props (label / accessible / state / live region / role / test_id). */
|
|
245
|
+
fun applyAccessibility(view: View, props: JSONObject) {
|
|
246
|
+
if (props.has("accessible")) {
|
|
247
|
+
view.importantForAccessibility = if (JsonUtil.truthy(props.value("accessible"))) {
|
|
248
|
+
View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
|
249
|
+
} else {
|
|
250
|
+
View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (props.has("accessibility_label")) {
|
|
254
|
+
view.contentDescription = props.str("accessibility_label")
|
|
255
|
+
}
|
|
256
|
+
if (props.has("accessibility_hint") && Build.VERSION.SDK_INT >= 28) {
|
|
257
|
+
view.tooltipText = props.str("accessibility_hint")
|
|
258
|
+
}
|
|
259
|
+
if (props.has("accessibility_live_region")) {
|
|
260
|
+
view.accessibilityLiveRegion = when (props.str("accessibility_live_region")?.lowercase()) {
|
|
261
|
+
"polite" -> View.ACCESSIBILITY_LIVE_REGION_POLITE
|
|
262
|
+
"assertive" -> View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
|
|
263
|
+
else -> View.ACCESSIBILITY_LIVE_REGION_NONE
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
val state = props.obj("accessibility_state")
|
|
267
|
+
if (state != null && state.has("selected")) {
|
|
268
|
+
view.isSelected = JsonUtil.truthy(state.value("selected"))
|
|
269
|
+
}
|
|
270
|
+
val testId = props.str("test_id")
|
|
271
|
+
val role = props.str("accessibility_role")
|
|
272
|
+
if (testId == null && state == null && role == null) return
|
|
273
|
+
val record = record(view)
|
|
274
|
+
var delegate = record?.state?.get("a11y_delegate") as? PNAccessibilityDelegate
|
|
275
|
+
if (delegate == null) {
|
|
276
|
+
delegate = PNAccessibilityDelegate()
|
|
277
|
+
view.accessibilityDelegate = delegate
|
|
278
|
+
record?.state?.put("a11y_delegate", delegate)
|
|
279
|
+
}
|
|
280
|
+
if (testId != null) {
|
|
281
|
+
delegate.testId = testId
|
|
282
|
+
view.tag = testId
|
|
283
|
+
}
|
|
284
|
+
if (role != null) delegate.role = role
|
|
285
|
+
if (state != null) {
|
|
286
|
+
delegate.stateDisabled = optBool(state, "disabled")
|
|
287
|
+
delegate.stateSelected = optBool(state, "selected")
|
|
288
|
+
delegate.stateChecked = optBool(state, "checked")
|
|
289
|
+
delegate.stateBusy = optBool(state, "busy")
|
|
290
|
+
delegate.stateExpanded = optBool(state, "expanded")
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private fun optBool(obj: JSONObject, key: String): Boolean? {
|
|
295
|
+
val v = obj.value(key) ?: return null
|
|
296
|
+
return JsonUtil.truthy(v)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ------------------------------------------------------------------
|
|
300
|
+
// Pointer events and hit slop
|
|
301
|
+
// ------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/** Whether `pointer_events` disables the view's own touch handling. */
|
|
304
|
+
fun pointerEventsBlocked(view: View): Boolean {
|
|
305
|
+
val mode = record(view)?.props?.str("pointer_events") ?: return false
|
|
306
|
+
return mode == "none" || mode == "box_none"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private fun applyPointerEvents(view: View, value: String?) {
|
|
310
|
+
val mode = value ?: "auto"
|
|
311
|
+
val state = record(view)?.state
|
|
312
|
+
(view as? PNFrameLayout)?.setPointerEventsMode(mode)
|
|
313
|
+
when (mode) {
|
|
314
|
+
"none", "box_none" -> {
|
|
315
|
+
state?.let { if (!it.containsKey("pe_was_clickable")) it["pe_was_clickable"] = view.isClickable }
|
|
316
|
+
view.isClickable = false
|
|
317
|
+
}
|
|
318
|
+
"box_only" -> {
|
|
319
|
+
state?.let { if (!it.containsKey("pe_was_clickable")) it["pe_was_clickable"] = view.isClickable }
|
|
320
|
+
view.isClickable = true
|
|
321
|
+
}
|
|
322
|
+
else -> {
|
|
323
|
+
val was = state?.remove("pe_was_clickable") as? Boolean
|
|
324
|
+
if (was != null) view.isClickable = was
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private fun hitSlopInsets(value: Any?): DoubleArray? {
|
|
330
|
+
return when (value) {
|
|
331
|
+
null, JSONObject.NULL -> null
|
|
332
|
+
is JSONObject -> doubleArrayOf(
|
|
333
|
+
value.num("top") ?: 0.0, value.num("left") ?: 0.0, value.num("bottom") ?: 0.0, value.num("right") ?: 0.0,
|
|
334
|
+
)
|
|
335
|
+
else -> JsonUtil.toDoubleOrNull(value)?.let { doubleArrayOf(it, it, it, it) }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Extend the view's touch target via a `TouchDelegate` on its parent (needs a frame). */
|
|
340
|
+
fun updateHitSlop(view: View) {
|
|
341
|
+
val record = record(view) ?: return
|
|
342
|
+
val frame = record.frame ?: return
|
|
343
|
+
val insets = hitSlopInsets(record.props.value("hit_slop"))
|
|
344
|
+
val parent = view.parent as? ViewGroup ?: return
|
|
345
|
+
if (insets == null) {
|
|
346
|
+
if (record.state.remove("hit_slop_active") != null) parent.touchDelegate = null
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
val (top, left, bottom, right) = insets
|
|
350
|
+
val (x, y, w, h) = frame
|
|
351
|
+
val rect = Rect(pxI(x - left), pxI(y - top), pxI(x + w + right), pxI(y + h + bottom))
|
|
352
|
+
parent.touchDelegate = TouchDelegate(rect, view)
|
|
353
|
+
record.state["hit_slop_active"] = true
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import android.widget.FrameLayout
|
|
7
|
+
import com.pythonnative.runtime.PNBridge
|
|
8
|
+
import com.pythonnative.runtime.bridge.JsonUtil
|
|
9
|
+
import com.pythonnative.runtime.bridge.PNLog
|
|
10
|
+
import com.pythonnative.runtime.bridge.arr
|
|
11
|
+
import com.pythonnative.runtime.bridge.num
|
|
12
|
+
import com.pythonnative.runtime.bridge.value
|
|
13
|
+
import com.pythonnative.runtime.views.PNVirtualListView
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
import kotlin.math.max
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `VirtualList` element: a `RecyclerView` (via [PNVirtualListView])
|
|
20
|
+
* whose rows are rendered lazily by Python.
|
|
21
|
+
*
|
|
22
|
+
* Binding a row is a synchronous round trip: `on_bind_row` is sent with
|
|
23
|
+
* `{container, index, width, height}` (the container key is its
|
|
24
|
+
* identity hash) and Python answers `{"root": tag}`; the tag's view is
|
|
25
|
+
* detached from any previous parent and added to the row container.
|
|
26
|
+
* Recycling emits `on_unbind_row` with the same key.
|
|
27
|
+
*/
|
|
28
|
+
class VirtualListManager : ComponentManager() {
|
|
29
|
+
private class Info {
|
|
30
|
+
var count = 0
|
|
31
|
+
var rowHeight = 44.0
|
|
32
|
+
var rowHeights: JSONArray? = null
|
|
33
|
+
var list: PNVirtualListView? = null
|
|
34
|
+
val mounted = HashMap<Int, View>()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
38
|
+
val info = Info()
|
|
39
|
+
info.count = JsonUtil.toInt(props.value("count"))
|
|
40
|
+
info.rowHeight = props.num("row_height") ?: 44.0
|
|
41
|
+
info.rowHeights = props.arr("row_heights")
|
|
42
|
+
val delegate = object : PNVirtualListView.Delegate {
|
|
43
|
+
override fun getCount(): Int = info.count
|
|
44
|
+
|
|
45
|
+
override fun getRowHeightDp(position: Int): Float {
|
|
46
|
+
val heights = info.rowHeights
|
|
47
|
+
if (heights != null && position in 0 until heights.length()) {
|
|
48
|
+
return JsonUtil.toDouble(heights.opt(position), info.rowHeight).toFloat()
|
|
49
|
+
}
|
|
50
|
+
return info.rowHeight.toFloat()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun bindRow(position: Int, container: FrameLayout, widthDp: Float, heightDp: Float) {
|
|
54
|
+
val list = info.list ?: return
|
|
55
|
+
val key = System.identityHashCode(container)
|
|
56
|
+
val payload = JSONObject()
|
|
57
|
+
.put("container", key)
|
|
58
|
+
.put("index", position)
|
|
59
|
+
.put("width", widthDp.toDouble())
|
|
60
|
+
.put("height", heightDp.toDouble())
|
|
61
|
+
val reply = fireForResult(list, "on_bind_row", payload) ?: return
|
|
62
|
+
val rootTag = try {
|
|
63
|
+
JSONObject(reply).optLong("root", -1L)
|
|
64
|
+
} catch (e: Exception) {
|
|
65
|
+
PNLog.rateLimited("vlist-bind", "on_bind_row returned invalid JSON: $reply", e)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if (rootTag < 0) return
|
|
69
|
+
val root = PNBridge.registry.get(rootTag)?.view ?: run {
|
|
70
|
+
PNLog.rateLimited("vlist-root", "on_bind_row root tag $rootTag is unknown")
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
(root.parent as? ViewGroup)?.let { if (it !== container) it.removeView(root) }
|
|
74
|
+
if (root.parent !== container) {
|
|
75
|
+
container.addView(root, 0)
|
|
76
|
+
}
|
|
77
|
+
// The layout engine frames only the descendants of a subtree
|
|
78
|
+
// root, so the root itself must fill the cell.
|
|
79
|
+
root.layoutParams = FrameLayout.LayoutParams(
|
|
80
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
81
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
82
|
+
)
|
|
83
|
+
info.mounted[key] = root
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun onRowPress(position: Int) {
|
|
87
|
+
info.list?.let { fire(it, "on_row_press", position) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override fun onRowRecycled(container: FrameLayout) {
|
|
91
|
+
val list = info.list ?: return
|
|
92
|
+
val key = System.identityHashCode(container)
|
|
93
|
+
info.mounted.remove(key)
|
|
94
|
+
fire(list, "on_unbind_row", JSONObject().put("container", key))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun onScrolled(offsetDp: Float, extentDp: Float, rangeDp: Float) {
|
|
98
|
+
val list = info.list ?: return
|
|
99
|
+
if (!hasEvent(list, "on_scroll")) return
|
|
100
|
+
fire(
|
|
101
|
+
list,
|
|
102
|
+
"on_scroll",
|
|
103
|
+
mapOf(
|
|
104
|
+
"x" to 0.0,
|
|
105
|
+
"y" to offsetDp.toDouble(),
|
|
106
|
+
"extent" to extentDp.toDouble(),
|
|
107
|
+
"range" to rangeDp.toDouble(),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
val list = PNVirtualListView(context, delegate)
|
|
113
|
+
info.list = list
|
|
114
|
+
infos[list] = info
|
|
115
|
+
return list
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private val infos = java.util.IdentityHashMap<View, Info>()
|
|
119
|
+
|
|
120
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
121
|
+
ViewStyler.apply(view, props)
|
|
122
|
+
val list = view as PNVirtualListView
|
|
123
|
+
val info = infos[list] ?: return
|
|
124
|
+
var dataChanged = false
|
|
125
|
+
if (props.has("count")) {
|
|
126
|
+
info.count = JsonUtil.toInt(props.value("count"))
|
|
127
|
+
dataChanged = true
|
|
128
|
+
}
|
|
129
|
+
props.num("row_height")?.let {
|
|
130
|
+
info.rowHeight = it
|
|
131
|
+
dataChanged = true
|
|
132
|
+
}
|
|
133
|
+
if (props.has("row_heights")) {
|
|
134
|
+
info.rowHeights = props.arr("row_heights")
|
|
135
|
+
dataChanged = true
|
|
136
|
+
}
|
|
137
|
+
if (props.has("shows_scroll_indicator")) {
|
|
138
|
+
list.isVerticalScrollBarEnabled = props.value("shows_scroll_indicator") != false
|
|
139
|
+
}
|
|
140
|
+
if (dataChanged && !initial) list.notifyDataChanged()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun insertChild(parent: View, child: View, index: Int) {
|
|
144
|
+
// Row roots are attached through the bind protocol, not the tree ops.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override fun removeChild(parent: View, child: View) {
|
|
148
|
+
(child.parent as? ViewGroup)?.removeView(child)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override fun teardown(view: View) {
|
|
152
|
+
val info = infos.remove(view) ?: return
|
|
153
|
+
info.mounted.clear()
|
|
154
|
+
info.list = null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override fun measure(view: View, maxWidth: Double, maxHeight: Double): FloatArray {
|
|
158
|
+
// Fill the available space; collapse to 0 on an unbounded axis
|
|
159
|
+
// (nested inside another scroll view), like React Native.
|
|
160
|
+
val w = if (maxWidth.isFinite() && maxWidth < 1e6) maxWidth else 0.0
|
|
161
|
+
val h = if (maxHeight.isFinite() && maxHeight < 1e6) maxHeight else 0.0
|
|
162
|
+
return floatArrayOf(max(0.0, w).toFloat(), max(0.0, h).toFloat())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
override fun command(view: View, name: String, args: JSONObject): Any? {
|
|
166
|
+
val list = view as PNVirtualListView
|
|
167
|
+
val animated = args.value("animated") != false
|
|
168
|
+
when (name) {
|
|
169
|
+
"scroll_to_offset" -> list.scrollToOffsetDp(JsonUtil.toDouble(args.opt("y")).toFloat(), animated)
|
|
170
|
+
"scroll_to_index" -> list.scrollToIndex(JsonUtil.toInt(args.opt("index")), animated)
|
|
171
|
+
"scroll_to_end" -> {
|
|
172
|
+
val count = infos[list]?.count ?: 0
|
|
173
|
+
list.scrollToIndex(max(0, count - 1), animated)
|
|
174
|
+
}
|
|
175
|
+
"get_scroll_offset" -> return mapOf("x" to 0.0, "y" to dp(list.computeVerticalScrollOffset()))
|
|
176
|
+
}
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Bitmap
|
|
6
|
+
import android.view.MotionEvent
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.webkit.WebResourceError
|
|
9
|
+
import android.webkit.WebResourceRequest
|
|
10
|
+
import android.webkit.WebView
|
|
11
|
+
import android.webkit.WebViewClient
|
|
12
|
+
import com.pythonnative.runtime.bridge.JsonUtil
|
|
13
|
+
import com.pythonnative.runtime.bridge.str
|
|
14
|
+
import com.pythonnative.runtime.bridge.value
|
|
15
|
+
import org.json.JSONArray
|
|
16
|
+
import org.json.JSONObject
|
|
17
|
+
import java.net.URLDecoder
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `WebView` element. Fires `on_navigation_state_change`, `on_load`,
|
|
21
|
+
* `on_error`, and `on_message` (via a `window.pythonnative.postMessage`
|
|
22
|
+
* shim that navigates to a `pythonnative://message/` URL). Commands:
|
|
23
|
+
* `load_url`, `reload`, `go_back`, `go_forward`, `stop_loading`,
|
|
24
|
+
* `inject_javascript`, `post_message`.
|
|
25
|
+
*/
|
|
26
|
+
class WebViewManager : ComponentManager() {
|
|
27
|
+
private val scheme = "pythonnative://message/"
|
|
28
|
+
|
|
29
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
30
|
+
val wv = WebView(context)
|
|
31
|
+
wv.webViewClient = client(wv)
|
|
32
|
+
return wv
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@SuppressLint("SetJavaScriptEnabled")
|
|
36
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
37
|
+
val wv = view as WebView
|
|
38
|
+
val merged = propsOf(wv)
|
|
39
|
+
val events = merged.value("_pn_events") as? JSONArray
|
|
40
|
+
val needsJs = merged.value("inject_javascript") != null ||
|
|
41
|
+
(events != null && events.length() > 0) ||
|
|
42
|
+
merged.value("javascript_enabled") != false
|
|
43
|
+
if (needsJs) wv.settings.javaScriptEnabled = true
|
|
44
|
+
if (props.has("javascript_enabled")) wv.settings.javaScriptEnabled = JsonUtil.truthy(props.value("javascript_enabled"))
|
|
45
|
+
if (props.has("dom_storage_enabled")) wv.settings.domStorageEnabled = JsonUtil.truthy(props.value("dom_storage_enabled"))
|
|
46
|
+
if (props.has("user_agent")) wv.settings.userAgentString = props.str("user_agent")
|
|
47
|
+
|
|
48
|
+
val html = props.str("html")
|
|
49
|
+
val url = props.str("url") ?: sourceUrl(props.value("source"))
|
|
50
|
+
// `html` takes precedence over `url` when both are present.
|
|
51
|
+
if (!html.isNullOrEmpty()) {
|
|
52
|
+
wv.loadDataWithBaseURL(props.str("base_url"), html, "text/html", "utf-8", null)
|
|
53
|
+
} else if (!url.isNullOrEmpty()) {
|
|
54
|
+
wv.loadUrl(url)
|
|
55
|
+
}
|
|
56
|
+
if (props.has("scroll_enabled")) applyScrollEnabled(wv, props.value("scroll_enabled"))
|
|
57
|
+
ViewStyler.apply(wv, props)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun sourceUrl(source: Any?): String? = when (source) {
|
|
61
|
+
is JSONObject -> source.str("uri") ?: source.str("url")
|
|
62
|
+
is String -> source
|
|
63
|
+
else -> null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
67
|
+
private fun applyScrollEnabled(wv: WebView, enabled: Any?) {
|
|
68
|
+
if (enabled == false) {
|
|
69
|
+
wv.setOnTouchListener { _, event -> event.action == MotionEvent.ACTION_MOVE }
|
|
70
|
+
} else {
|
|
71
|
+
wv.setOnTouchListener(null)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun client(wv: WebView): WebViewClient = object : WebViewClient() {
|
|
76
|
+
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
|
77
|
+
fire(wv, "on_navigation_state_change", url ?: "")
|
|
78
|
+
fire(wv, "on_load_start", url ?: "")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override fun onPageFinished(view: WebView, url: String?) {
|
|
82
|
+
fire(wv, "on_load", url ?: "")
|
|
83
|
+
if (hasEvent(wv, "on_message")) {
|
|
84
|
+
val shim = "(function(){window.pythonnative=window.pythonnative||{};" +
|
|
85
|
+
"window.pythonnative.postMessage=function(m){" +
|
|
86
|
+
"window.location.href='$scheme'+encodeURIComponent(m);};})();"
|
|
87
|
+
view.evaluateJavascript(shim, null)
|
|
88
|
+
}
|
|
89
|
+
propsOf(wv).str("inject_javascript")?.takeIf { it.isNotEmpty() }?.let { view.evaluateJavascript(it, null) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
|
|
93
|
+
if (request.isForMainFrame) {
|
|
94
|
+
fire(wv, "on_error", error.description?.toString() ?: "load error")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
|
99
|
+
val url = request.url?.toString() ?: return false
|
|
100
|
+
return handleUrl(url)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Deprecated("Deprecated in Java")
|
|
104
|
+
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = handleUrl(url)
|
|
105
|
+
|
|
106
|
+
private fun handleUrl(url: String): Boolean {
|
|
107
|
+
if (url.startsWith(scheme)) {
|
|
108
|
+
val message = try {
|
|
109
|
+
URLDecoder.decode(url.substring(scheme.length), "UTF-8")
|
|
110
|
+
} catch (e: Exception) {
|
|
111
|
+
url.substring(scheme.length)
|
|
112
|
+
}
|
|
113
|
+
fire(wv, "on_message", message)
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override fun command(view: View, name: String, args: JSONObject): Any? {
|
|
121
|
+
val wv = view as? WebView ?: return null
|
|
122
|
+
when (name) {
|
|
123
|
+
"load_url" -> args.str("url")?.let { wv.loadUrl(it) }
|
|
124
|
+
"reload" -> wv.reload()
|
|
125
|
+
"go_back" -> if (wv.canGoBack()) wv.goBack()
|
|
126
|
+
"go_forward" -> if (wv.canGoForward()) wv.goForward()
|
|
127
|
+
"stop_loading" -> wv.stopLoading()
|
|
128
|
+
"inject_javascript", "evaluate_javascript" -> {
|
|
129
|
+
val script = args.str("script") ?: args.str("javascript") ?: return null
|
|
130
|
+
wv.evaluateJavascript(script, null)
|
|
131
|
+
}
|
|
132
|
+
"post_message" -> {
|
|
133
|
+
val message = JSONObject.quote(args.str("message") ?: "")
|
|
134
|
+
wv.evaluateJavascript("window.dispatchEvent(new MessageEvent('message',{data:$message}));", null)
|
|
135
|
+
}
|
|
136
|
+
"can_go_back" -> return wv.canGoBack()
|
|
137
|
+
"can_go_forward" -> return wv.canGoForward()
|
|
138
|
+
"get_url" -> return wv.url
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun teardown(view: View) {
|
|
144
|
+
(view as? WebView)?.let {
|
|
145
|
+
it.stopLoading()
|
|
146
|
+
it.destroy()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|