pythonnative 0.3.0__py3-none-any.whl → 0.5.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 +45 -65
- pythonnative/cli/pn.py +16 -10
- pythonnative/components.py +241 -0
- pythonnative/element.py +47 -0
- pythonnative/native_views.py +800 -0
- pythonnative/page.py +321 -249
- pythonnative/reconciler.py +129 -0
- pythonnative/templates/android_template/app/build.gradle +2 -2
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/build.gradle +3 -3
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
- pythonnative/utils.py +21 -29
- pythonnative-0.5.0.dist-info/METADATA +161 -0
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/RECORD +19 -39
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/licenses/LICENSE +1 -1
- pythonnative/activity_indicator_view.py +0 -71
- pythonnative/button.py +0 -109
- pythonnative/date_picker.py +0 -72
- pythonnative/image_view.py +0 -76
- pythonnative/label.py +0 -66
- pythonnative/list_view.py +0 -73
- pythonnative/material_activity_indicator_view.py +0 -69
- pythonnative/material_button.py +0 -65
- pythonnative/material_date_picker.py +0 -85
- pythonnative/material_progress_view.py +0 -66
- pythonnative/material_search_bar.py +0 -65
- pythonnative/material_switch.py +0 -65
- pythonnative/material_time_picker.py +0 -72
- pythonnative/picker_view.py +0 -65
- pythonnative/progress_view.py +0 -68
- pythonnative/scroll_view.py +0 -63
- pythonnative/search_bar.py +0 -65
- pythonnative/stack_view.py +0 -60
- pythonnative/switch.py +0 -66
- pythonnative/text_field.py +0 -67
- pythonnative/text_view.py +0 -70
- pythonnative/time_picker.py +0 -73
- pythonnative/view.py +0 -25
- pythonnative/web_view.py +0 -58
- pythonnative-0.3.0.dist-info/METADATA +0 -137
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.3.0.dist-info → pythonnative-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
"""Platform-specific native view creation and update logic.
|
|
2
|
+
|
|
3
|
+
This module replaces the old per-widget files. All platform-branching
|
|
4
|
+
lives here, guarded behind lazy imports so the module can be imported
|
|
5
|
+
on desktop for testing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
9
|
+
|
|
10
|
+
from .utils import IS_ANDROID
|
|
11
|
+
|
|
12
|
+
# ======================================================================
|
|
13
|
+
# Abstract handler protocol
|
|
14
|
+
# ======================================================================
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ViewHandler:
|
|
18
|
+
"""Protocol for creating, updating, and managing children of a native view type."""
|
|
19
|
+
|
|
20
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def update(self, native_view: Any, changed_props: Dict[str, Any]) -> None:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
33
|
+
self.add_child(parent, child)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ======================================================================
|
|
37
|
+
# Registry
|
|
38
|
+
# ======================================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NativeViewRegistry:
|
|
42
|
+
"""Maps element type names to platform-specific :class:`ViewHandler` instances."""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._handlers: Dict[str, ViewHandler] = {}
|
|
46
|
+
|
|
47
|
+
def register(self, type_name: str, handler: ViewHandler) -> None:
|
|
48
|
+
self._handlers[type_name] = handler
|
|
49
|
+
|
|
50
|
+
def create_view(self, type_name: str, props: Dict[str, Any]) -> Any:
|
|
51
|
+
handler = self._handlers.get(type_name)
|
|
52
|
+
if handler is None:
|
|
53
|
+
raise ValueError(f"Unknown element type: {type_name!r}")
|
|
54
|
+
return handler.create(props)
|
|
55
|
+
|
|
56
|
+
def update_view(self, native_view: Any, type_name: str, changed_props: Dict[str, Any]) -> None:
|
|
57
|
+
handler = self._handlers.get(type_name)
|
|
58
|
+
if handler is not None:
|
|
59
|
+
handler.update(native_view, changed_props)
|
|
60
|
+
|
|
61
|
+
def add_child(self, parent: Any, child: Any, parent_type: str) -> None:
|
|
62
|
+
handler = self._handlers.get(parent_type)
|
|
63
|
+
if handler is not None:
|
|
64
|
+
handler.add_child(parent, child)
|
|
65
|
+
|
|
66
|
+
def remove_child(self, parent: Any, child: Any, parent_type: str) -> None:
|
|
67
|
+
handler = self._handlers.get(parent_type)
|
|
68
|
+
if handler is not None:
|
|
69
|
+
handler.remove_child(parent, child)
|
|
70
|
+
|
|
71
|
+
def insert_child(self, parent: Any, child: Any, parent_type: str, index: int) -> None:
|
|
72
|
+
handler = self._handlers.get(parent_type)
|
|
73
|
+
if handler is not None:
|
|
74
|
+
handler.insert_child(parent, child, index)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ======================================================================
|
|
78
|
+
# Shared helpers
|
|
79
|
+
# ======================================================================
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_color_int(color: Union[str, int]) -> int:
|
|
83
|
+
"""Parse ``#RRGGBB`` / ``#AARRGGBB`` hex string or raw int to a *signed* ARGB int.
|
|
84
|
+
|
|
85
|
+
Java's ``setBackgroundColor`` et al. expect a signed 32-bit int, so values
|
|
86
|
+
with a high alpha byte (e.g. 0xFF…) must be converted to negative ints.
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(color, int):
|
|
89
|
+
val = color
|
|
90
|
+
else:
|
|
91
|
+
c = color.strip().lstrip("#")
|
|
92
|
+
if len(c) == 6:
|
|
93
|
+
c = "FF" + c
|
|
94
|
+
val = int(c, 16)
|
|
95
|
+
if val > 0x7FFFFFFF:
|
|
96
|
+
val -= 0x100000000
|
|
97
|
+
return val
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_padding(
|
|
101
|
+
padding: Any,
|
|
102
|
+
) -> tuple:
|
|
103
|
+
"""Normalise various padding representations to ``(left, top, right, bottom)``."""
|
|
104
|
+
if padding is None:
|
|
105
|
+
return (0, 0, 0, 0)
|
|
106
|
+
if isinstance(padding, (int, float)):
|
|
107
|
+
v = int(padding)
|
|
108
|
+
return (v, v, v, v)
|
|
109
|
+
if isinstance(padding, dict):
|
|
110
|
+
h = int(padding.get("horizontal", 0))
|
|
111
|
+
v = int(padding.get("vertical", 0))
|
|
112
|
+
left = int(padding.get("left", h))
|
|
113
|
+
right = int(padding.get("right", h))
|
|
114
|
+
top = int(padding.get("top", v))
|
|
115
|
+
bottom = int(padding.get("bottom", v))
|
|
116
|
+
a = int(padding.get("all", 0))
|
|
117
|
+
if a:
|
|
118
|
+
left = left or a
|
|
119
|
+
right = right or a
|
|
120
|
+
top = top or a
|
|
121
|
+
bottom = bottom or a
|
|
122
|
+
return (left, top, right, bottom)
|
|
123
|
+
return (0, 0, 0, 0)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ======================================================================
|
|
127
|
+
# Platform handler registration (lazy imports inside functions)
|
|
128
|
+
# ======================================================================
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
132
|
+
from java import dynamic_proxy, jclass
|
|
133
|
+
|
|
134
|
+
from .utils import get_android_context
|
|
135
|
+
|
|
136
|
+
def _ctx() -> Any:
|
|
137
|
+
return get_android_context()
|
|
138
|
+
|
|
139
|
+
def _density() -> float:
|
|
140
|
+
return float(_ctx().getResources().getDisplayMetrics().density)
|
|
141
|
+
|
|
142
|
+
def _dp(value: float) -> int:
|
|
143
|
+
return int(value * _density())
|
|
144
|
+
|
|
145
|
+
# ---- Text -----------------------------------------------------------
|
|
146
|
+
class AndroidTextHandler(ViewHandler):
|
|
147
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
148
|
+
tv = jclass("android.widget.TextView")(_ctx())
|
|
149
|
+
self._apply(tv, props)
|
|
150
|
+
return tv
|
|
151
|
+
|
|
152
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
153
|
+
self._apply(native_view, changed)
|
|
154
|
+
|
|
155
|
+
def _apply(self, tv: Any, props: Dict[str, Any]) -> None:
|
|
156
|
+
if "text" in props:
|
|
157
|
+
tv.setText(str(props["text"]))
|
|
158
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
159
|
+
tv.setTextSize(float(props["font_size"]))
|
|
160
|
+
if "color" in props and props["color"] is not None:
|
|
161
|
+
tv.setTextColor(parse_color_int(props["color"]))
|
|
162
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
163
|
+
tv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
164
|
+
if "bold" in props and props["bold"]:
|
|
165
|
+
tv.setTypeface(tv.getTypeface(), 1) # Typeface.BOLD = 1
|
|
166
|
+
if "max_lines" in props and props["max_lines"] is not None:
|
|
167
|
+
tv.setMaxLines(int(props["max_lines"]))
|
|
168
|
+
if "text_align" in props:
|
|
169
|
+
Gravity = jclass("android.view.Gravity")
|
|
170
|
+
mapping = {"left": Gravity.START, "center": Gravity.CENTER, "right": Gravity.END}
|
|
171
|
+
tv.setGravity(mapping.get(props["text_align"], Gravity.START))
|
|
172
|
+
|
|
173
|
+
# ---- Button ---------------------------------------------------------
|
|
174
|
+
class AndroidButtonHandler(ViewHandler):
|
|
175
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
176
|
+
btn = jclass("android.widget.Button")(_ctx())
|
|
177
|
+
self._apply(btn, props)
|
|
178
|
+
return btn
|
|
179
|
+
|
|
180
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
181
|
+
self._apply(native_view, changed)
|
|
182
|
+
|
|
183
|
+
def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
|
|
184
|
+
if "title" in props:
|
|
185
|
+
btn.setText(str(props["title"]))
|
|
186
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
187
|
+
btn.setTextSize(float(props["font_size"]))
|
|
188
|
+
if "color" in props and props["color"] is not None:
|
|
189
|
+
btn.setTextColor(parse_color_int(props["color"]))
|
|
190
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
191
|
+
btn.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
192
|
+
if "enabled" in props:
|
|
193
|
+
btn.setEnabled(bool(props["enabled"]))
|
|
194
|
+
if "on_click" in props:
|
|
195
|
+
cb = props["on_click"]
|
|
196
|
+
if cb is not None:
|
|
197
|
+
|
|
198
|
+
class ClickProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
|
|
199
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
|
200
|
+
super().__init__()
|
|
201
|
+
self.callback = callback
|
|
202
|
+
|
|
203
|
+
def onClick(self, view: Any) -> None:
|
|
204
|
+
self.callback()
|
|
205
|
+
|
|
206
|
+
btn.setOnClickListener(ClickProxy(cb))
|
|
207
|
+
else:
|
|
208
|
+
btn.setOnClickListener(None)
|
|
209
|
+
|
|
210
|
+
# ---- Column (vertical LinearLayout) ---------------------------------
|
|
211
|
+
class AndroidColumnHandler(ViewHandler):
|
|
212
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
213
|
+
ll = jclass("android.widget.LinearLayout")(_ctx())
|
|
214
|
+
ll.setOrientation(jclass("android.widget.LinearLayout").VERTICAL)
|
|
215
|
+
self._apply(ll, props)
|
|
216
|
+
return ll
|
|
217
|
+
|
|
218
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
219
|
+
self._apply(native_view, changed)
|
|
220
|
+
|
|
221
|
+
def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
|
|
222
|
+
if "spacing" in props and props["spacing"]:
|
|
223
|
+
px = _dp(float(props["spacing"]))
|
|
224
|
+
GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
|
|
225
|
+
d = GradientDrawable()
|
|
226
|
+
d.setColor(0x00000000)
|
|
227
|
+
d.setSize(1, px)
|
|
228
|
+
ll.setShowDividers(jclass("android.widget.LinearLayout").SHOW_DIVIDER_MIDDLE)
|
|
229
|
+
ll.setDividerDrawable(d)
|
|
230
|
+
if "padding" in props:
|
|
231
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
232
|
+
ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
233
|
+
if "alignment" in props and props["alignment"]:
|
|
234
|
+
Gravity = jclass("android.view.Gravity")
|
|
235
|
+
mapping = {
|
|
236
|
+
"fill": Gravity.FILL_HORIZONTAL,
|
|
237
|
+
"center": Gravity.CENTER_HORIZONTAL,
|
|
238
|
+
"leading": Gravity.START,
|
|
239
|
+
"start": Gravity.START,
|
|
240
|
+
"trailing": Gravity.END,
|
|
241
|
+
"end": Gravity.END,
|
|
242
|
+
}
|
|
243
|
+
ll.setGravity(mapping.get(props["alignment"], Gravity.FILL_HORIZONTAL))
|
|
244
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
245
|
+
ll.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
246
|
+
|
|
247
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
248
|
+
parent.addView(child)
|
|
249
|
+
|
|
250
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
251
|
+
parent.removeView(child)
|
|
252
|
+
|
|
253
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
254
|
+
parent.addView(child, index)
|
|
255
|
+
|
|
256
|
+
# ---- Row (horizontal LinearLayout) ----------------------------------
|
|
257
|
+
class AndroidRowHandler(ViewHandler):
|
|
258
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
259
|
+
ll = jclass("android.widget.LinearLayout")(_ctx())
|
|
260
|
+
ll.setOrientation(jclass("android.widget.LinearLayout").HORIZONTAL)
|
|
261
|
+
self._apply(ll, props)
|
|
262
|
+
return ll
|
|
263
|
+
|
|
264
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
265
|
+
self._apply(native_view, changed)
|
|
266
|
+
|
|
267
|
+
def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
|
|
268
|
+
if "spacing" in props and props["spacing"]:
|
|
269
|
+
px = _dp(float(props["spacing"]))
|
|
270
|
+
GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
|
|
271
|
+
d = GradientDrawable()
|
|
272
|
+
d.setColor(0x00000000)
|
|
273
|
+
d.setSize(px, 1)
|
|
274
|
+
ll.setShowDividers(jclass("android.widget.LinearLayout").SHOW_DIVIDER_MIDDLE)
|
|
275
|
+
ll.setDividerDrawable(d)
|
|
276
|
+
if "padding" in props:
|
|
277
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
278
|
+
ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
279
|
+
if "alignment" in props and props["alignment"]:
|
|
280
|
+
Gravity = jclass("android.view.Gravity")
|
|
281
|
+
mapping = {
|
|
282
|
+
"fill": Gravity.FILL_VERTICAL,
|
|
283
|
+
"center": Gravity.CENTER_VERTICAL,
|
|
284
|
+
"top": Gravity.TOP,
|
|
285
|
+
"bottom": Gravity.BOTTOM,
|
|
286
|
+
}
|
|
287
|
+
ll.setGravity(mapping.get(props["alignment"], Gravity.FILL_VERTICAL))
|
|
288
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
289
|
+
ll.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
290
|
+
|
|
291
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
292
|
+
parent.addView(child)
|
|
293
|
+
|
|
294
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
295
|
+
parent.removeView(child)
|
|
296
|
+
|
|
297
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
298
|
+
parent.addView(child, index)
|
|
299
|
+
|
|
300
|
+
# ---- ScrollView -----------------------------------------------------
|
|
301
|
+
class AndroidScrollViewHandler(ViewHandler):
|
|
302
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
303
|
+
sv = jclass("android.widget.ScrollView")(_ctx())
|
|
304
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
305
|
+
sv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
306
|
+
return sv
|
|
307
|
+
|
|
308
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
309
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
310
|
+
native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
|
|
311
|
+
|
|
312
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
313
|
+
parent.addView(child)
|
|
314
|
+
|
|
315
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
316
|
+
parent.removeView(child)
|
|
317
|
+
|
|
318
|
+
# ---- TextInput (EditText) -------------------------------------------
|
|
319
|
+
class AndroidTextInputHandler(ViewHandler):
|
|
320
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
321
|
+
et = jclass("android.widget.EditText")(_ctx())
|
|
322
|
+
self._apply(et, props)
|
|
323
|
+
return et
|
|
324
|
+
|
|
325
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
326
|
+
self._apply(native_view, changed)
|
|
327
|
+
|
|
328
|
+
def _apply(self, et: Any, props: Dict[str, Any]) -> None:
|
|
329
|
+
if "value" in props:
|
|
330
|
+
et.setText(str(props["value"]))
|
|
331
|
+
if "placeholder" in props:
|
|
332
|
+
et.setHint(str(props["placeholder"]))
|
|
333
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
334
|
+
et.setTextSize(float(props["font_size"]))
|
|
335
|
+
if "color" in props and props["color"] is not None:
|
|
336
|
+
et.setTextColor(parse_color_int(props["color"]))
|
|
337
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
338
|
+
et.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
339
|
+
if "secure" in props and props["secure"]:
|
|
340
|
+
InputType = jclass("android.text.InputType")
|
|
341
|
+
et.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
|
342
|
+
|
|
343
|
+
# ---- Image ----------------------------------------------------------
|
|
344
|
+
class AndroidImageHandler(ViewHandler):
|
|
345
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
346
|
+
iv = jclass("android.widget.ImageView")(_ctx())
|
|
347
|
+
self._apply(iv, props)
|
|
348
|
+
return iv
|
|
349
|
+
|
|
350
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
351
|
+
self._apply(native_view, changed)
|
|
352
|
+
|
|
353
|
+
def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
|
|
354
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
355
|
+
iv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
356
|
+
|
|
357
|
+
# ---- Switch ---------------------------------------------------------
|
|
358
|
+
class AndroidSwitchHandler(ViewHandler):
|
|
359
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
360
|
+
sw = jclass("android.widget.Switch")(_ctx())
|
|
361
|
+
self._apply(sw, props)
|
|
362
|
+
return sw
|
|
363
|
+
|
|
364
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
365
|
+
self._apply(native_view, changed)
|
|
366
|
+
|
|
367
|
+
def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
|
|
368
|
+
if "value" in props:
|
|
369
|
+
sw.setChecked(bool(props["value"]))
|
|
370
|
+
if "on_change" in props and props["on_change"] is not None:
|
|
371
|
+
cb = props["on_change"]
|
|
372
|
+
|
|
373
|
+
class CheckedProxy(dynamic_proxy(jclass("android.widget.CompoundButton").OnCheckedChangeListener)):
|
|
374
|
+
def __init__(self, callback: Callable[[bool], None]) -> None:
|
|
375
|
+
super().__init__()
|
|
376
|
+
self.callback = callback
|
|
377
|
+
|
|
378
|
+
def onCheckedChanged(self, button: Any, checked: bool) -> None:
|
|
379
|
+
self.callback(checked)
|
|
380
|
+
|
|
381
|
+
sw.setOnCheckedChangeListener(CheckedProxy(cb))
|
|
382
|
+
|
|
383
|
+
# ---- ProgressBar ----------------------------------------------------
|
|
384
|
+
class AndroidProgressBarHandler(ViewHandler):
|
|
385
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
386
|
+
style = jclass("android.R$attr").progressBarStyleHorizontal
|
|
387
|
+
pb = jclass("android.widget.ProgressBar")(_ctx(), None, 0, style)
|
|
388
|
+
pb.setMax(1000)
|
|
389
|
+
self._apply(pb, props)
|
|
390
|
+
return pb
|
|
391
|
+
|
|
392
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
393
|
+
self._apply(native_view, changed)
|
|
394
|
+
|
|
395
|
+
def _apply(self, pb: Any, props: Dict[str, Any]) -> None:
|
|
396
|
+
if "value" in props:
|
|
397
|
+
pb.setProgress(int(float(props["value"]) * 1000))
|
|
398
|
+
|
|
399
|
+
# ---- ActivityIndicator (circular ProgressBar) -----------------------
|
|
400
|
+
class AndroidActivityIndicatorHandler(ViewHandler):
|
|
401
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
402
|
+
pb = jclass("android.widget.ProgressBar")(_ctx())
|
|
403
|
+
if not props.get("animating", True):
|
|
404
|
+
pb.setVisibility(jclass("android.view.View").GONE)
|
|
405
|
+
return pb
|
|
406
|
+
|
|
407
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
408
|
+
View = jclass("android.view.View")
|
|
409
|
+
if "animating" in changed:
|
|
410
|
+
native_view.setVisibility(View.VISIBLE if changed["animating"] else View.GONE)
|
|
411
|
+
|
|
412
|
+
# ---- WebView --------------------------------------------------------
|
|
413
|
+
class AndroidWebViewHandler(ViewHandler):
|
|
414
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
415
|
+
wv = jclass("android.webkit.WebView")(_ctx())
|
|
416
|
+
if "url" in props and props["url"]:
|
|
417
|
+
wv.loadUrl(str(props["url"]))
|
|
418
|
+
return wv
|
|
419
|
+
|
|
420
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
421
|
+
if "url" in changed and changed["url"]:
|
|
422
|
+
native_view.loadUrl(str(changed["url"]))
|
|
423
|
+
|
|
424
|
+
# ---- Spacer ---------------------------------------------------------
|
|
425
|
+
class AndroidSpacerHandler(ViewHandler):
|
|
426
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
427
|
+
v = jclass("android.view.View")(_ctx())
|
|
428
|
+
if "size" in props and props["size"] is not None:
|
|
429
|
+
px = _dp(float(props["size"]))
|
|
430
|
+
lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
|
|
431
|
+
v.setLayoutParams(lp)
|
|
432
|
+
return v
|
|
433
|
+
|
|
434
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
435
|
+
if "size" in changed and changed["size"] is not None:
|
|
436
|
+
px = _dp(float(changed["size"]))
|
|
437
|
+
lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
|
|
438
|
+
native_view.setLayoutParams(lp)
|
|
439
|
+
|
|
440
|
+
registry.register("Text", AndroidTextHandler())
|
|
441
|
+
registry.register("Button", AndroidButtonHandler())
|
|
442
|
+
registry.register("Column", AndroidColumnHandler())
|
|
443
|
+
registry.register("Row", AndroidRowHandler())
|
|
444
|
+
registry.register("ScrollView", AndroidScrollViewHandler())
|
|
445
|
+
registry.register("TextInput", AndroidTextInputHandler())
|
|
446
|
+
registry.register("Image", AndroidImageHandler())
|
|
447
|
+
registry.register("Switch", AndroidSwitchHandler())
|
|
448
|
+
registry.register("ProgressBar", AndroidProgressBarHandler())
|
|
449
|
+
registry.register("ActivityIndicator", AndroidActivityIndicatorHandler())
|
|
450
|
+
registry.register("WebView", AndroidWebViewHandler())
|
|
451
|
+
registry.register("Spacer", AndroidSpacerHandler())
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
455
|
+
from rubicon.objc import SEL, ObjCClass, objc_method
|
|
456
|
+
|
|
457
|
+
NSObject = ObjCClass("NSObject")
|
|
458
|
+
UIColor = ObjCClass("UIColor")
|
|
459
|
+
UIFont = ObjCClass("UIFont")
|
|
460
|
+
|
|
461
|
+
def _uicolor(color: Any) -> Any:
|
|
462
|
+
argb = parse_color_int(color)
|
|
463
|
+
if argb < 0:
|
|
464
|
+
argb += 0x100000000
|
|
465
|
+
a = ((argb >> 24) & 0xFF) / 255.0
|
|
466
|
+
r = ((argb >> 16) & 0xFF) / 255.0
|
|
467
|
+
g = ((argb >> 8) & 0xFF) / 255.0
|
|
468
|
+
b = (argb & 0xFF) / 255.0
|
|
469
|
+
return UIColor.colorWithRed_green_blue_alpha_(r, g, b, a)
|
|
470
|
+
|
|
471
|
+
# ---- Text -----------------------------------------------------------
|
|
472
|
+
class IOSTextHandler(ViewHandler):
|
|
473
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
474
|
+
label = ObjCClass("UILabel").alloc().init()
|
|
475
|
+
self._apply(label, props)
|
|
476
|
+
return label
|
|
477
|
+
|
|
478
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
479
|
+
self._apply(native_view, changed)
|
|
480
|
+
|
|
481
|
+
def _apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
482
|
+
if "text" in props:
|
|
483
|
+
label.setText_(str(props["text"]))
|
|
484
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
485
|
+
if props.get("bold"):
|
|
486
|
+
label.setFont_(UIFont.boldSystemFontOfSize_(float(props["font_size"])))
|
|
487
|
+
else:
|
|
488
|
+
label.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
|
|
489
|
+
elif "bold" in props and props["bold"]:
|
|
490
|
+
size = label.font().pointSize() if label.font() else 17.0
|
|
491
|
+
label.setFont_(UIFont.boldSystemFontOfSize_(size))
|
|
492
|
+
if "color" in props and props["color"] is not None:
|
|
493
|
+
label.setTextColor_(_uicolor(props["color"]))
|
|
494
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
495
|
+
label.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
496
|
+
if "max_lines" in props and props["max_lines"] is not None:
|
|
497
|
+
label.setNumberOfLines_(int(props["max_lines"]))
|
|
498
|
+
if "text_align" in props:
|
|
499
|
+
mapping = {"left": 0, "center": 1, "right": 2}
|
|
500
|
+
label.setTextAlignment_(mapping.get(props["text_align"], 0))
|
|
501
|
+
|
|
502
|
+
# ---- Button ---------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
# btn id(ObjCInstance) -> _PNButtonTarget. Keeps a strong ref to
|
|
505
|
+
# each handler (preventing GC) and lets us swap the callback on
|
|
506
|
+
# re-render without calling removeTarget/addTarget (which crashes
|
|
507
|
+
# due to rubicon-objc wrapper lifecycle issues).
|
|
508
|
+
_pn_btn_handler_map: dict = {}
|
|
509
|
+
|
|
510
|
+
class _PNButtonTarget(NSObject): # type: ignore[valid-type]
|
|
511
|
+
_callback: Optional[Callable[[], None]] = None
|
|
512
|
+
|
|
513
|
+
@objc_method
|
|
514
|
+
def onTap_(self, sender: object) -> None:
|
|
515
|
+
if self._callback is not None:
|
|
516
|
+
self._callback()
|
|
517
|
+
|
|
518
|
+
# Strong refs to retained UIButton wrappers so the ObjCInstance
|
|
519
|
+
# (and its prevent-deallocation retain) stays alive for the
|
|
520
|
+
# lifetime of the app.
|
|
521
|
+
_pn_retained_views: list = []
|
|
522
|
+
|
|
523
|
+
class IOSButtonHandler(ViewHandler):
|
|
524
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
525
|
+
btn = ObjCClass("UIButton").alloc().init()
|
|
526
|
+
btn.retain()
|
|
527
|
+
_pn_retained_views.append(btn)
|
|
528
|
+
_ios_blue = UIColor.colorWithRed_green_blue_alpha_(0.0, 0.478, 1.0, 1.0)
|
|
529
|
+
btn.setTitleColor_forState_(_ios_blue, 0)
|
|
530
|
+
self._apply(btn, props)
|
|
531
|
+
return btn
|
|
532
|
+
|
|
533
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
534
|
+
self._apply(native_view, changed)
|
|
535
|
+
|
|
536
|
+
def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
|
|
537
|
+
if "title" in props:
|
|
538
|
+
btn.setTitle_forState_(str(props["title"]), 0)
|
|
539
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
540
|
+
btn.titleLabel().setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
|
|
541
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
542
|
+
btn.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
543
|
+
if "color" not in props:
|
|
544
|
+
_white = UIColor.colorWithRed_green_blue_alpha_(1.0, 1.0, 1.0, 1.0)
|
|
545
|
+
btn.setTitleColor_forState_(_white, 0)
|
|
546
|
+
if "color" in props and props["color"] is not None:
|
|
547
|
+
btn.setTitleColor_forState_(_uicolor(props["color"]), 0)
|
|
548
|
+
if "enabled" in props:
|
|
549
|
+
btn.setEnabled_(bool(props["enabled"]))
|
|
550
|
+
if "on_click" in props:
|
|
551
|
+
existing = _pn_btn_handler_map.get(id(btn))
|
|
552
|
+
if existing is not None:
|
|
553
|
+
existing._callback = props["on_click"]
|
|
554
|
+
else:
|
|
555
|
+
handler = _PNButtonTarget.new()
|
|
556
|
+
handler._callback = props["on_click"]
|
|
557
|
+
_pn_btn_handler_map[id(btn)] = handler
|
|
558
|
+
btn.addTarget_action_forControlEvents_(handler, SEL("onTap:"), 1 << 6)
|
|
559
|
+
|
|
560
|
+
# ---- Column (vertical UIStackView) ----------------------------------
|
|
561
|
+
class IOSColumnHandler(ViewHandler):
|
|
562
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
563
|
+
sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
|
|
564
|
+
sv.setAxis_(1) # vertical
|
|
565
|
+
self._apply(sv, props)
|
|
566
|
+
return sv
|
|
567
|
+
|
|
568
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
569
|
+
self._apply(native_view, changed)
|
|
570
|
+
|
|
571
|
+
def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
|
|
572
|
+
if "spacing" in props and props["spacing"]:
|
|
573
|
+
sv.setSpacing_(float(props["spacing"]))
|
|
574
|
+
if "alignment" in props and props["alignment"]:
|
|
575
|
+
mapping = {"fill": 0, "leading": 1, "top": 1, "center": 3, "trailing": 4, "bottom": 4}
|
|
576
|
+
sv.setAlignment_(mapping.get(props["alignment"], 0))
|
|
577
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
578
|
+
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
579
|
+
if "padding" in props:
|
|
580
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
581
|
+
sv.setLayoutMarginsRelativeArrangement_(True)
|
|
582
|
+
try:
|
|
583
|
+
sv.setDirectionalLayoutMargins_((top, left, bottom, right))
|
|
584
|
+
except Exception:
|
|
585
|
+
sv.setLayoutMargins_((top, left, bottom, right))
|
|
586
|
+
|
|
587
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
588
|
+
parent.addArrangedSubview_(child)
|
|
589
|
+
|
|
590
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
591
|
+
parent.removeArrangedSubview_(child)
|
|
592
|
+
child.removeFromSuperview()
|
|
593
|
+
|
|
594
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
595
|
+
parent.insertArrangedSubview_atIndex_(child, index)
|
|
596
|
+
|
|
597
|
+
# ---- Row (horizontal UIStackView) -----------------------------------
|
|
598
|
+
class IOSRowHandler(ViewHandler):
|
|
599
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
600
|
+
sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
|
|
601
|
+
sv.setAxis_(0) # horizontal
|
|
602
|
+
self._apply(sv, props)
|
|
603
|
+
return sv
|
|
604
|
+
|
|
605
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
606
|
+
self._apply(native_view, changed)
|
|
607
|
+
|
|
608
|
+
def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
|
|
609
|
+
if "spacing" in props and props["spacing"]:
|
|
610
|
+
sv.setSpacing_(float(props["spacing"]))
|
|
611
|
+
if "alignment" in props and props["alignment"]:
|
|
612
|
+
mapping = {"fill": 0, "leading": 1, "top": 1, "center": 3, "trailing": 4, "bottom": 4}
|
|
613
|
+
sv.setAlignment_(mapping.get(props["alignment"], 0))
|
|
614
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
615
|
+
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
616
|
+
|
|
617
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
618
|
+
parent.addArrangedSubview_(child)
|
|
619
|
+
|
|
620
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
621
|
+
parent.removeArrangedSubview_(child)
|
|
622
|
+
child.removeFromSuperview()
|
|
623
|
+
|
|
624
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
625
|
+
parent.insertArrangedSubview_atIndex_(child, index)
|
|
626
|
+
|
|
627
|
+
# ---- ScrollView -----------------------------------------------------
|
|
628
|
+
class IOSScrollViewHandler(ViewHandler):
|
|
629
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
630
|
+
sv = ObjCClass("UIScrollView").alloc().init()
|
|
631
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
632
|
+
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
633
|
+
return sv
|
|
634
|
+
|
|
635
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
636
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
637
|
+
native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
|
|
638
|
+
|
|
639
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
640
|
+
child.setTranslatesAutoresizingMaskIntoConstraints_(False)
|
|
641
|
+
parent.addSubview_(child)
|
|
642
|
+
content_guide = parent.contentLayoutGuide
|
|
643
|
+
frame_guide = parent.frameLayoutGuide
|
|
644
|
+
child.topAnchor.constraintEqualToAnchor_(content_guide.topAnchor).setActive_(True)
|
|
645
|
+
child.leadingAnchor.constraintEqualToAnchor_(content_guide.leadingAnchor).setActive_(True)
|
|
646
|
+
child.trailingAnchor.constraintEqualToAnchor_(content_guide.trailingAnchor).setActive_(True)
|
|
647
|
+
child.bottomAnchor.constraintEqualToAnchor_(content_guide.bottomAnchor).setActive_(True)
|
|
648
|
+
child.widthAnchor.constraintEqualToAnchor_(frame_guide.widthAnchor).setActive_(True)
|
|
649
|
+
|
|
650
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
651
|
+
child.removeFromSuperview()
|
|
652
|
+
|
|
653
|
+
# ---- TextInput (UITextField) ----------------------------------------
|
|
654
|
+
class IOSTextInputHandler(ViewHandler):
|
|
655
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
656
|
+
tf = ObjCClass("UITextField").alloc().init()
|
|
657
|
+
tf.setBorderStyle_(2) # RoundedRect
|
|
658
|
+
self._apply(tf, props)
|
|
659
|
+
return tf
|
|
660
|
+
|
|
661
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
662
|
+
self._apply(native_view, changed)
|
|
663
|
+
|
|
664
|
+
def _apply(self, tf: Any, props: Dict[str, Any]) -> None:
|
|
665
|
+
if "value" in props:
|
|
666
|
+
tf.setText_(str(props["value"]))
|
|
667
|
+
if "placeholder" in props:
|
|
668
|
+
tf.setPlaceholder_(str(props["placeholder"]))
|
|
669
|
+
if "font_size" in props and props["font_size"] is not None:
|
|
670
|
+
tf.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
|
|
671
|
+
if "color" in props and props["color"] is not None:
|
|
672
|
+
tf.setTextColor_(_uicolor(props["color"]))
|
|
673
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
674
|
+
tf.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
675
|
+
if "secure" in props and props["secure"]:
|
|
676
|
+
tf.setSecureTextEntry_(True)
|
|
677
|
+
|
|
678
|
+
# ---- Image ----------------------------------------------------------
|
|
679
|
+
class IOSImageHandler(ViewHandler):
|
|
680
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
681
|
+
iv = ObjCClass("UIImageView").alloc().init()
|
|
682
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
683
|
+
iv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
684
|
+
return iv
|
|
685
|
+
|
|
686
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
687
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
688
|
+
native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
|
|
689
|
+
|
|
690
|
+
# ---- Switch ---------------------------------------------------------
|
|
691
|
+
class IOSSwitchHandler(ViewHandler):
|
|
692
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
693
|
+
sw = ObjCClass("UISwitch").alloc().init()
|
|
694
|
+
self._apply(sw, props)
|
|
695
|
+
return sw
|
|
696
|
+
|
|
697
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
698
|
+
self._apply(native_view, changed)
|
|
699
|
+
|
|
700
|
+
def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
|
|
701
|
+
if "value" in props:
|
|
702
|
+
sw.setOn_animated_(bool(props["value"]), False)
|
|
703
|
+
|
|
704
|
+
# ---- ProgressBar (UIProgressView) -----------------------------------
|
|
705
|
+
class IOSProgressBarHandler(ViewHandler):
|
|
706
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
707
|
+
pv = ObjCClass("UIProgressView").alloc().init()
|
|
708
|
+
if "value" in props:
|
|
709
|
+
pv.setProgress_(float(props["value"]))
|
|
710
|
+
return pv
|
|
711
|
+
|
|
712
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
713
|
+
if "value" in changed:
|
|
714
|
+
native_view.setProgress_(float(changed["value"]))
|
|
715
|
+
|
|
716
|
+
# ---- ActivityIndicator ----------------------------------------------
|
|
717
|
+
class IOSActivityIndicatorHandler(ViewHandler):
|
|
718
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
719
|
+
ai = ObjCClass("UIActivityIndicatorView").alloc().init()
|
|
720
|
+
if props.get("animating", True):
|
|
721
|
+
ai.startAnimating()
|
|
722
|
+
return ai
|
|
723
|
+
|
|
724
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
725
|
+
if "animating" in changed:
|
|
726
|
+
if changed["animating"]:
|
|
727
|
+
native_view.startAnimating()
|
|
728
|
+
else:
|
|
729
|
+
native_view.stopAnimating()
|
|
730
|
+
|
|
731
|
+
# ---- WebView (WKWebView) --------------------------------------------
|
|
732
|
+
class IOSWebViewHandler(ViewHandler):
|
|
733
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
734
|
+
wv = ObjCClass("WKWebView").alloc().init()
|
|
735
|
+
if "url" in props and props["url"]:
|
|
736
|
+
NSURL = ObjCClass("NSURL")
|
|
737
|
+
NSURLRequest = ObjCClass("NSURLRequest")
|
|
738
|
+
url_obj = NSURL.URLWithString_(str(props["url"]))
|
|
739
|
+
wv.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
|
|
740
|
+
return wv
|
|
741
|
+
|
|
742
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
743
|
+
if "url" in changed and changed["url"]:
|
|
744
|
+
NSURL = ObjCClass("NSURL")
|
|
745
|
+
NSURLRequest = ObjCClass("NSURLRequest")
|
|
746
|
+
url_obj = NSURL.URLWithString_(str(changed["url"]))
|
|
747
|
+
native_view.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
|
|
748
|
+
|
|
749
|
+
# ---- Spacer ---------------------------------------------------------
|
|
750
|
+
class IOSSpacerHandler(ViewHandler):
|
|
751
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
752
|
+
v = ObjCClass("UIView").alloc().init()
|
|
753
|
+
if "size" in props and props["size"] is not None:
|
|
754
|
+
size = float(props["size"])
|
|
755
|
+
v.setFrame_(((0, 0), (size, size)))
|
|
756
|
+
return v
|
|
757
|
+
|
|
758
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
759
|
+
if "size" in changed and changed["size"] is not None:
|
|
760
|
+
size = float(changed["size"])
|
|
761
|
+
native_view.setFrame_(((0, 0), (size, size)))
|
|
762
|
+
|
|
763
|
+
registry.register("Text", IOSTextHandler())
|
|
764
|
+
registry.register("Button", IOSButtonHandler())
|
|
765
|
+
registry.register("Column", IOSColumnHandler())
|
|
766
|
+
registry.register("Row", IOSRowHandler())
|
|
767
|
+
registry.register("ScrollView", IOSScrollViewHandler())
|
|
768
|
+
registry.register("TextInput", IOSTextInputHandler())
|
|
769
|
+
registry.register("Image", IOSImageHandler())
|
|
770
|
+
registry.register("Switch", IOSSwitchHandler())
|
|
771
|
+
registry.register("ProgressBar", IOSProgressBarHandler())
|
|
772
|
+
registry.register("ActivityIndicator", IOSActivityIndicatorHandler())
|
|
773
|
+
registry.register("WebView", IOSWebViewHandler())
|
|
774
|
+
registry.register("Spacer", IOSSpacerHandler())
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# ======================================================================
|
|
778
|
+
# Factory
|
|
779
|
+
# ======================================================================
|
|
780
|
+
|
|
781
|
+
_registry: Optional[NativeViewRegistry] = None
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def get_registry() -> NativeViewRegistry:
|
|
785
|
+
"""Return the singleton registry, lazily creating platform handlers."""
|
|
786
|
+
global _registry
|
|
787
|
+
if _registry is not None:
|
|
788
|
+
return _registry
|
|
789
|
+
_registry = NativeViewRegistry()
|
|
790
|
+
if IS_ANDROID:
|
|
791
|
+
_register_android_handlers(_registry)
|
|
792
|
+
else:
|
|
793
|
+
_register_ios_handlers(_registry)
|
|
794
|
+
return _registry
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def set_registry(registry: NativeViewRegistry) -> None:
|
|
798
|
+
"""Inject a custom or mock registry (primarily for testing)."""
|
|
799
|
+
global _registry
|
|
800
|
+
_registry = registry
|