pythonnative 0.5.0__py3-none-any.whl → 0.7.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 +53 -15
- pythonnative/cli/pn.py +150 -30
- pythonnative/components.py +217 -107
- pythonnative/element.py +14 -8
- pythonnative/hooks.py +334 -0
- pythonnative/hot_reload.py +143 -0
- pythonnative/native_modules/__init__.py +19 -0
- pythonnative/native_modules/camera.py +105 -0
- pythonnative/native_modules/file_system.py +131 -0
- pythonnative/native_modules/location.py +61 -0
- pythonnative/native_modules/notifications.py +151 -0
- pythonnative/native_views.py +638 -34
- pythonnative/page.py +138 -171
- pythonnative/reconciler.py +153 -20
- pythonnative/style.py +135 -0
- pythonnative/templates/android_template/app/build.gradle +2 -7
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
- pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
- pythonnative/collection_view.py +0 -0
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_toolbar.py +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/top_level.txt +0 -0
pythonnative/native_views.py
CHANGED
|
@@ -123,6 +123,21 @@ def _resolve_padding(
|
|
|
123
123
|
return (0, 0, 0, 0)
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
_LAYOUT_KEYS = frozenset(
|
|
127
|
+
{
|
|
128
|
+
"width",
|
|
129
|
+
"height",
|
|
130
|
+
"flex",
|
|
131
|
+
"margin",
|
|
132
|
+
"min_width",
|
|
133
|
+
"max_width",
|
|
134
|
+
"min_height",
|
|
135
|
+
"max_height",
|
|
136
|
+
"align_self",
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
126
141
|
# ======================================================================
|
|
127
142
|
# Platform handler registration (lazy imports inside functions)
|
|
128
143
|
# ======================================================================
|
|
@@ -142,15 +157,52 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
142
157
|
def _dp(value: float) -> int:
|
|
143
158
|
return int(value * _density())
|
|
144
159
|
|
|
160
|
+
def _apply_layout(view: Any, props: Dict[str, Any]) -> None:
|
|
161
|
+
"""Apply common layout properties to an Android view."""
|
|
162
|
+
lp = view.getLayoutParams()
|
|
163
|
+
LayoutParams = jclass("android.widget.LinearLayout$LayoutParams")
|
|
164
|
+
ViewGroupLP = jclass("android.view.ViewGroup$LayoutParams")
|
|
165
|
+
needs_set = False
|
|
166
|
+
|
|
167
|
+
if lp is None:
|
|
168
|
+
lp = LayoutParams(ViewGroupLP.WRAP_CONTENT, ViewGroupLP.WRAP_CONTENT)
|
|
169
|
+
needs_set = True
|
|
170
|
+
|
|
171
|
+
if "width" in props and props["width"] is not None:
|
|
172
|
+
lp.width = _dp(float(props["width"]))
|
|
173
|
+
needs_set = True
|
|
174
|
+
if "height" in props and props["height"] is not None:
|
|
175
|
+
lp.height = _dp(float(props["height"]))
|
|
176
|
+
needs_set = True
|
|
177
|
+
if "flex" in props and props["flex"] is not None:
|
|
178
|
+
try:
|
|
179
|
+
lp.weight = float(props["flex"])
|
|
180
|
+
needs_set = True
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
if "margin" in props and props["margin"] is not None:
|
|
184
|
+
left, top, right, bottom = _resolve_padding(props["margin"])
|
|
185
|
+
try:
|
|
186
|
+
lp.setMargins(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
187
|
+
needs_set = True
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
if needs_set:
|
|
192
|
+
view.setLayoutParams(lp)
|
|
193
|
+
|
|
145
194
|
# ---- Text -----------------------------------------------------------
|
|
146
195
|
class AndroidTextHandler(ViewHandler):
|
|
147
196
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
148
197
|
tv = jclass("android.widget.TextView")(_ctx())
|
|
149
198
|
self._apply(tv, props)
|
|
199
|
+
_apply_layout(tv, props)
|
|
150
200
|
return tv
|
|
151
201
|
|
|
152
202
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
153
203
|
self._apply(native_view, changed)
|
|
204
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
205
|
+
_apply_layout(native_view, changed)
|
|
154
206
|
|
|
155
207
|
def _apply(self, tv: Any, props: Dict[str, Any]) -> None:
|
|
156
208
|
if "text" in props:
|
|
@@ -175,10 +227,13 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
175
227
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
176
228
|
btn = jclass("android.widget.Button")(_ctx())
|
|
177
229
|
self._apply(btn, props)
|
|
230
|
+
_apply_layout(btn, props)
|
|
178
231
|
return btn
|
|
179
232
|
|
|
180
233
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
181
234
|
self._apply(native_view, changed)
|
|
235
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
236
|
+
_apply_layout(native_view, changed)
|
|
182
237
|
|
|
183
238
|
def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
|
|
184
239
|
if "title" in props:
|
|
@@ -213,12 +268,16 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
213
268
|
ll = jclass("android.widget.LinearLayout")(_ctx())
|
|
214
269
|
ll.setOrientation(jclass("android.widget.LinearLayout").VERTICAL)
|
|
215
270
|
self._apply(ll, props)
|
|
271
|
+
_apply_layout(ll, props)
|
|
216
272
|
return ll
|
|
217
273
|
|
|
218
274
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
219
275
|
self._apply(native_view, changed)
|
|
276
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
277
|
+
_apply_layout(native_view, changed)
|
|
220
278
|
|
|
221
279
|
def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
|
|
280
|
+
Gravity = jclass("android.view.Gravity")
|
|
222
281
|
if "spacing" in props and props["spacing"]:
|
|
223
282
|
px = _dp(float(props["spacing"]))
|
|
224
283
|
GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
|
|
@@ -230,17 +289,31 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
230
289
|
if "padding" in props:
|
|
231
290
|
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
232
291
|
ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
292
|
+
gravity = 0
|
|
293
|
+
ai = props.get("align_items") or props.get("alignment")
|
|
294
|
+
if ai:
|
|
295
|
+
cross_map = {
|
|
296
|
+
"stretch": Gravity.FILL_HORIZONTAL,
|
|
236
297
|
"fill": Gravity.FILL_HORIZONTAL,
|
|
237
|
-
"
|
|
298
|
+
"flex_start": Gravity.START,
|
|
238
299
|
"leading": Gravity.START,
|
|
239
300
|
"start": Gravity.START,
|
|
301
|
+
"center": Gravity.CENTER_HORIZONTAL,
|
|
302
|
+
"flex_end": Gravity.END,
|
|
240
303
|
"trailing": Gravity.END,
|
|
241
304
|
"end": Gravity.END,
|
|
242
305
|
}
|
|
243
|
-
|
|
306
|
+
gravity |= cross_map.get(ai, 0)
|
|
307
|
+
jc = props.get("justify_content")
|
|
308
|
+
if jc:
|
|
309
|
+
main_map = {
|
|
310
|
+
"flex_start": Gravity.TOP,
|
|
311
|
+
"center": Gravity.CENTER_VERTICAL,
|
|
312
|
+
"flex_end": Gravity.BOTTOM,
|
|
313
|
+
}
|
|
314
|
+
gravity |= main_map.get(jc, 0)
|
|
315
|
+
if gravity:
|
|
316
|
+
ll.setGravity(gravity)
|
|
244
317
|
if "background_color" in props and props["background_color"] is not None:
|
|
245
318
|
ll.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
246
319
|
|
|
@@ -259,12 +332,16 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
259
332
|
ll = jclass("android.widget.LinearLayout")(_ctx())
|
|
260
333
|
ll.setOrientation(jclass("android.widget.LinearLayout").HORIZONTAL)
|
|
261
334
|
self._apply(ll, props)
|
|
335
|
+
_apply_layout(ll, props)
|
|
262
336
|
return ll
|
|
263
337
|
|
|
264
338
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
265
339
|
self._apply(native_view, changed)
|
|
340
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
341
|
+
_apply_layout(native_view, changed)
|
|
266
342
|
|
|
267
343
|
def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
|
|
344
|
+
Gravity = jclass("android.view.Gravity")
|
|
268
345
|
if "spacing" in props and props["spacing"]:
|
|
269
346
|
px = _dp(float(props["spacing"]))
|
|
270
347
|
GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
|
|
@@ -276,15 +353,29 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
276
353
|
if "padding" in props:
|
|
277
354
|
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
278
355
|
ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
356
|
+
gravity = 0
|
|
357
|
+
ai = props.get("align_items") or props.get("alignment")
|
|
358
|
+
if ai:
|
|
359
|
+
cross_map = {
|
|
360
|
+
"stretch": Gravity.FILL_VERTICAL,
|
|
282
361
|
"fill": Gravity.FILL_VERTICAL,
|
|
283
|
-
"
|
|
362
|
+
"flex_start": Gravity.TOP,
|
|
284
363
|
"top": Gravity.TOP,
|
|
364
|
+
"center": Gravity.CENTER_VERTICAL,
|
|
365
|
+
"flex_end": Gravity.BOTTOM,
|
|
285
366
|
"bottom": Gravity.BOTTOM,
|
|
286
367
|
}
|
|
287
|
-
|
|
368
|
+
gravity |= cross_map.get(ai, 0)
|
|
369
|
+
jc = props.get("justify_content")
|
|
370
|
+
if jc:
|
|
371
|
+
main_map = {
|
|
372
|
+
"flex_start": Gravity.START,
|
|
373
|
+
"center": Gravity.CENTER_HORIZONTAL,
|
|
374
|
+
"flex_end": Gravity.END,
|
|
375
|
+
}
|
|
376
|
+
gravity |= main_map.get(jc, 0)
|
|
377
|
+
if gravity:
|
|
378
|
+
ll.setGravity(gravity)
|
|
288
379
|
if "background_color" in props and props["background_color"] is not None:
|
|
289
380
|
ll.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
290
381
|
|
|
@@ -303,11 +394,14 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
303
394
|
sv = jclass("android.widget.ScrollView")(_ctx())
|
|
304
395
|
if "background_color" in props and props["background_color"] is not None:
|
|
305
396
|
sv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
397
|
+
_apply_layout(sv, props)
|
|
306
398
|
return sv
|
|
307
399
|
|
|
308
400
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
309
401
|
if "background_color" in changed and changed["background_color"] is not None:
|
|
310
402
|
native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
|
|
403
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
404
|
+
_apply_layout(native_view, changed)
|
|
311
405
|
|
|
312
406
|
def add_child(self, parent: Any, child: Any) -> None:
|
|
313
407
|
parent.addView(child)
|
|
@@ -315,15 +409,18 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
315
409
|
def remove_child(self, parent: Any, child: Any) -> None:
|
|
316
410
|
parent.removeView(child)
|
|
317
411
|
|
|
318
|
-
# ---- TextInput (EditText)
|
|
412
|
+
# ---- TextInput (EditText) with on_change ----------------------------
|
|
319
413
|
class AndroidTextInputHandler(ViewHandler):
|
|
320
414
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
321
415
|
et = jclass("android.widget.EditText")(_ctx())
|
|
322
416
|
self._apply(et, props)
|
|
417
|
+
_apply_layout(et, props)
|
|
323
418
|
return et
|
|
324
419
|
|
|
325
420
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
326
421
|
self._apply(native_view, changed)
|
|
422
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
423
|
+
_apply_layout(native_view, changed)
|
|
327
424
|
|
|
328
425
|
def _apply(self, et: Any, props: Dict[str, Any]) -> None:
|
|
329
426
|
if "value" in props:
|
|
@@ -339,26 +436,113 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
339
436
|
if "secure" in props and props["secure"]:
|
|
340
437
|
InputType = jclass("android.text.InputType")
|
|
341
438
|
et.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
|
439
|
+
if "on_change" in props:
|
|
440
|
+
cb = props["on_change"]
|
|
441
|
+
if cb is not None:
|
|
442
|
+
TextWatcher = jclass("android.text.TextWatcher")
|
|
443
|
+
|
|
444
|
+
class ChangeProxy(dynamic_proxy(TextWatcher)):
|
|
445
|
+
def __init__(self, callback: Callable[[str], None]) -> None:
|
|
446
|
+
super().__init__()
|
|
447
|
+
self.callback = callback
|
|
342
448
|
|
|
343
|
-
|
|
449
|
+
def afterTextChanged(self, s: Any) -> None:
|
|
450
|
+
self.callback(str(s))
|
|
451
|
+
|
|
452
|
+
def beforeTextChanged(self, s: Any, start: int, count: int, after: int) -> None:
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
def onTextChanged(self, s: Any, start: int, before: int, count: int) -> None:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
et.addTextChangedListener(ChangeProxy(cb))
|
|
459
|
+
|
|
460
|
+
# ---- Image (with URL loading) ---------------------------------------
|
|
344
461
|
class AndroidImageHandler(ViewHandler):
|
|
345
462
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
346
463
|
iv = jclass("android.widget.ImageView")(_ctx())
|
|
347
464
|
self._apply(iv, props)
|
|
465
|
+
_apply_layout(iv, props)
|
|
348
466
|
return iv
|
|
349
467
|
|
|
350
468
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
351
469
|
self._apply(native_view, changed)
|
|
470
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
471
|
+
_apply_layout(native_view, changed)
|
|
352
472
|
|
|
353
473
|
def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
|
|
354
474
|
if "background_color" in props and props["background_color"] is not None:
|
|
355
475
|
iv.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
356
|
-
|
|
357
|
-
|
|
476
|
+
if "source" in props and props["source"]:
|
|
477
|
+
self._load_source(iv, props["source"])
|
|
478
|
+
if "scale_type" in props and props["scale_type"]:
|
|
479
|
+
ScaleType = jclass("android.widget.ImageView$ScaleType")
|
|
480
|
+
mapping = {
|
|
481
|
+
"cover": ScaleType.CENTER_CROP,
|
|
482
|
+
"contain": ScaleType.FIT_CENTER,
|
|
483
|
+
"stretch": ScaleType.FIT_XY,
|
|
484
|
+
"center": ScaleType.CENTER,
|
|
485
|
+
}
|
|
486
|
+
st = mapping.get(props["scale_type"])
|
|
487
|
+
if st:
|
|
488
|
+
iv.setScaleType(st)
|
|
489
|
+
|
|
490
|
+
def _load_source(self, iv: Any, source: str) -> None:
|
|
491
|
+
try:
|
|
492
|
+
if source.startswith(("http://", "https://")):
|
|
493
|
+
Thread = jclass("java.lang.Thread")
|
|
494
|
+
Runnable = jclass("java.lang.Runnable")
|
|
495
|
+
URL = jclass("java.net.URL")
|
|
496
|
+
BitmapFactory = jclass("android.graphics.BitmapFactory")
|
|
497
|
+
Handler = jclass("android.os.Handler")
|
|
498
|
+
Looper = jclass("android.os.Looper")
|
|
499
|
+
handler = Handler(Looper.getMainLooper())
|
|
500
|
+
|
|
501
|
+
class LoadTask(dynamic_proxy(Runnable)):
|
|
502
|
+
def __init__(self, image_view: Any, url_str: str, main_handler: Any) -> None:
|
|
503
|
+
super().__init__()
|
|
504
|
+
self.image_view = image_view
|
|
505
|
+
self.url_str = url_str
|
|
506
|
+
self.main_handler = main_handler
|
|
507
|
+
|
|
508
|
+
def run(self) -> None:
|
|
509
|
+
try:
|
|
510
|
+
url = URL(self.url_str)
|
|
511
|
+
stream = url.openStream()
|
|
512
|
+
bitmap = BitmapFactory.decodeStream(stream)
|
|
513
|
+
stream.close()
|
|
514
|
+
|
|
515
|
+
class SetImage(dynamic_proxy(Runnable)):
|
|
516
|
+
def __init__(self, view: Any, bmp: Any) -> None:
|
|
517
|
+
super().__init__()
|
|
518
|
+
self.view = view
|
|
519
|
+
self.bmp = bmp
|
|
520
|
+
|
|
521
|
+
def run(self) -> None:
|
|
522
|
+
self.view.setImageBitmap(self.bmp)
|
|
523
|
+
|
|
524
|
+
self.main_handler.post(SetImage(self.image_view, bitmap))
|
|
525
|
+
except Exception:
|
|
526
|
+
pass
|
|
527
|
+
|
|
528
|
+
Thread(LoadTask(iv, source, handler)).start()
|
|
529
|
+
else:
|
|
530
|
+
ctx = _ctx()
|
|
531
|
+
res = ctx.getResources()
|
|
532
|
+
pkg = ctx.getPackageName()
|
|
533
|
+
res_name = source.rsplit(".", 1)[0] if "." in source else source
|
|
534
|
+
res_id = res.getIdentifier(res_name, "drawable", pkg)
|
|
535
|
+
if res_id != 0:
|
|
536
|
+
iv.setImageResource(res_id)
|
|
537
|
+
except Exception:
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
# ---- Switch (with on_change) ----------------------------------------
|
|
358
541
|
class AndroidSwitchHandler(ViewHandler):
|
|
359
542
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
360
543
|
sw = jclass("android.widget.Switch")(_ctx())
|
|
361
544
|
self._apply(sw, props)
|
|
545
|
+
_apply_layout(sw, props)
|
|
362
546
|
return sw
|
|
363
547
|
|
|
364
548
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -387,6 +571,7 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
387
571
|
pb = jclass("android.widget.ProgressBar")(_ctx(), None, 0, style)
|
|
388
572
|
pb.setMax(1000)
|
|
389
573
|
self._apply(pb, props)
|
|
574
|
+
_apply_layout(pb, props)
|
|
390
575
|
return pb
|
|
391
576
|
|
|
392
577
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -402,6 +587,7 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
402
587
|
pb = jclass("android.widget.ProgressBar")(_ctx())
|
|
403
588
|
if not props.get("animating", True):
|
|
404
589
|
pb.setVisibility(jclass("android.view.View").GONE)
|
|
590
|
+
_apply_layout(pb, props)
|
|
405
591
|
return pb
|
|
406
592
|
|
|
407
593
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -415,6 +601,7 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
415
601
|
wv = jclass("android.webkit.WebView")(_ctx())
|
|
416
602
|
if "url" in props and props["url"]:
|
|
417
603
|
wv.loadUrl(str(props["url"]))
|
|
604
|
+
_apply_layout(wv, props)
|
|
418
605
|
return wv
|
|
419
606
|
|
|
420
607
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -429,6 +616,12 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
429
616
|
px = _dp(float(props["size"]))
|
|
430
617
|
lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
|
|
431
618
|
v.setLayoutParams(lp)
|
|
619
|
+
if "flex" in props and props["flex"] is not None:
|
|
620
|
+
lp = v.getLayoutParams()
|
|
621
|
+
if lp is None:
|
|
622
|
+
lp = jclass("android.widget.LinearLayout$LayoutParams")(0, 0)
|
|
623
|
+
lp.weight = float(props["flex"])
|
|
624
|
+
v.setLayoutParams(lp)
|
|
432
625
|
return v
|
|
433
626
|
|
|
434
627
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -437,6 +630,156 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
437
630
|
lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
|
|
438
631
|
native_view.setLayoutParams(lp)
|
|
439
632
|
|
|
633
|
+
# ---- View (generic container FrameLayout) ---------------------------
|
|
634
|
+
class AndroidViewHandler(ViewHandler):
|
|
635
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
636
|
+
fl = jclass("android.widget.FrameLayout")(_ctx())
|
|
637
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
638
|
+
fl.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
639
|
+
if "padding" in props:
|
|
640
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
641
|
+
fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
642
|
+
_apply_layout(fl, props)
|
|
643
|
+
return fl
|
|
644
|
+
|
|
645
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
646
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
647
|
+
native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
|
|
648
|
+
if "padding" in changed:
|
|
649
|
+
left, top, right, bottom = _resolve_padding(changed["padding"])
|
|
650
|
+
native_view.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
651
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
652
|
+
_apply_layout(native_view, changed)
|
|
653
|
+
|
|
654
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
655
|
+
parent.addView(child)
|
|
656
|
+
|
|
657
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
658
|
+
parent.removeView(child)
|
|
659
|
+
|
|
660
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
661
|
+
parent.addView(child, index)
|
|
662
|
+
|
|
663
|
+
# ---- SafeAreaView (FrameLayout with fitsSystemWindows) ---------------
|
|
664
|
+
class AndroidSafeAreaViewHandler(ViewHandler):
|
|
665
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
666
|
+
fl = jclass("android.widget.FrameLayout")(_ctx())
|
|
667
|
+
fl.setFitsSystemWindows(True)
|
|
668
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
669
|
+
fl.setBackgroundColor(parse_color_int(props["background_color"]))
|
|
670
|
+
if "padding" in props:
|
|
671
|
+
left, top, right, bottom = _resolve_padding(props["padding"])
|
|
672
|
+
fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
|
|
673
|
+
return fl
|
|
674
|
+
|
|
675
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
676
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
677
|
+
native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
|
|
678
|
+
|
|
679
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
680
|
+
parent.addView(child)
|
|
681
|
+
|
|
682
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
683
|
+
parent.removeView(child)
|
|
684
|
+
|
|
685
|
+
# ---- Modal (AlertDialog) -------------------------------------------
|
|
686
|
+
class AndroidModalHandler(ViewHandler):
|
|
687
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
688
|
+
placeholder = jclass("android.view.View")(_ctx())
|
|
689
|
+
placeholder.setVisibility(jclass("android.view.View").GONE)
|
|
690
|
+
return placeholder
|
|
691
|
+
|
|
692
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
693
|
+
pass
|
|
694
|
+
|
|
695
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
696
|
+
pass
|
|
697
|
+
|
|
698
|
+
# ---- Slider (SeekBar) -----------------------------------------------
|
|
699
|
+
class AndroidSliderHandler(ViewHandler):
|
|
700
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
701
|
+
sb = jclass("android.widget.SeekBar")(_ctx())
|
|
702
|
+
sb.setMax(1000)
|
|
703
|
+
self._apply(sb, props)
|
|
704
|
+
_apply_layout(sb, props)
|
|
705
|
+
return sb
|
|
706
|
+
|
|
707
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
708
|
+
self._apply(native_view, changed)
|
|
709
|
+
|
|
710
|
+
def _apply(self, sb: Any, props: Dict[str, Any]) -> None:
|
|
711
|
+
min_val = float(props.get("min_value", 0))
|
|
712
|
+
max_val = float(props.get("max_value", 1))
|
|
713
|
+
rng = max_val - min_val if max_val != min_val else 1
|
|
714
|
+
if "value" in props:
|
|
715
|
+
normalized = (float(props["value"]) - min_val) / rng
|
|
716
|
+
sb.setProgress(int(normalized * 1000))
|
|
717
|
+
if "on_change" in props and props["on_change"] is not None:
|
|
718
|
+
cb = props["on_change"]
|
|
719
|
+
|
|
720
|
+
class SeekProxy(dynamic_proxy(jclass("android.widget.SeekBar").OnSeekBarChangeListener)):
|
|
721
|
+
def __init__(self, callback: Callable[[float], None], mn: float, rn: float) -> None:
|
|
722
|
+
super().__init__()
|
|
723
|
+
self.callback = callback
|
|
724
|
+
self.mn = mn
|
|
725
|
+
self.rn = rn
|
|
726
|
+
|
|
727
|
+
def onProgressChanged(self, seekBar: Any, progress: int, fromUser: bool) -> None:
|
|
728
|
+
if fromUser:
|
|
729
|
+
self.callback(self.mn + (progress / 1000.0) * self.rn)
|
|
730
|
+
|
|
731
|
+
def onStartTrackingTouch(self, seekBar: Any) -> None:
|
|
732
|
+
pass
|
|
733
|
+
|
|
734
|
+
def onStopTrackingTouch(self, seekBar: Any) -> None:
|
|
735
|
+
pass
|
|
736
|
+
|
|
737
|
+
sb.setOnSeekBarChangeListener(SeekProxy(cb, min_val, rng))
|
|
738
|
+
|
|
739
|
+
# ---- Pressable (FrameLayout with click listener) --------------------
|
|
740
|
+
class AndroidPressableHandler(ViewHandler):
|
|
741
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
742
|
+
fl = jclass("android.widget.FrameLayout")(_ctx())
|
|
743
|
+
fl.setClickable(True)
|
|
744
|
+
self._apply(fl, props)
|
|
745
|
+
return fl
|
|
746
|
+
|
|
747
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
748
|
+
self._apply(native_view, changed)
|
|
749
|
+
|
|
750
|
+
def _apply(self, fl: Any, props: Dict[str, Any]) -> None:
|
|
751
|
+
if "on_press" in props and props["on_press"] is not None:
|
|
752
|
+
cb = props["on_press"]
|
|
753
|
+
|
|
754
|
+
class PressProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
|
|
755
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
|
756
|
+
super().__init__()
|
|
757
|
+
self.callback = callback
|
|
758
|
+
|
|
759
|
+
def onClick(self, view: Any) -> None:
|
|
760
|
+
self.callback()
|
|
761
|
+
|
|
762
|
+
fl.setOnClickListener(PressProxy(cb))
|
|
763
|
+
if "on_long_press" in props and props["on_long_press"] is not None:
|
|
764
|
+
cb = props["on_long_press"]
|
|
765
|
+
|
|
766
|
+
class LongPressProxy(dynamic_proxy(jclass("android.view.View").OnLongClickListener)):
|
|
767
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
|
768
|
+
super().__init__()
|
|
769
|
+
self.callback = callback
|
|
770
|
+
|
|
771
|
+
def onLongClick(self, view: Any) -> bool:
|
|
772
|
+
self.callback()
|
|
773
|
+
return True
|
|
774
|
+
|
|
775
|
+
fl.setOnLongClickListener(LongPressProxy(cb))
|
|
776
|
+
|
|
777
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
778
|
+
parent.addView(child)
|
|
779
|
+
|
|
780
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
781
|
+
parent.removeView(child)
|
|
782
|
+
|
|
440
783
|
registry.register("Text", AndroidTextHandler())
|
|
441
784
|
registry.register("Button", AndroidButtonHandler())
|
|
442
785
|
registry.register("Column", AndroidColumnHandler())
|
|
@@ -449,6 +792,11 @@ def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C
|
|
|
449
792
|
registry.register("ActivityIndicator", AndroidActivityIndicatorHandler())
|
|
450
793
|
registry.register("WebView", AndroidWebViewHandler())
|
|
451
794
|
registry.register("Spacer", AndroidSpacerHandler())
|
|
795
|
+
registry.register("View", AndroidViewHandler())
|
|
796
|
+
registry.register("SafeAreaView", AndroidSafeAreaViewHandler())
|
|
797
|
+
registry.register("Modal", AndroidModalHandler())
|
|
798
|
+
registry.register("Slider", AndroidSliderHandler())
|
|
799
|
+
registry.register("Pressable", AndroidPressableHandler())
|
|
452
800
|
|
|
453
801
|
|
|
454
802
|
def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
@@ -468,15 +816,37 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
468
816
|
b = (argb & 0xFF) / 255.0
|
|
469
817
|
return UIColor.colorWithRed_green_blue_alpha_(r, g, b, a)
|
|
470
818
|
|
|
819
|
+
def _apply_ios_layout(view: Any, props: Dict[str, Any]) -> None:
|
|
820
|
+
"""Apply common layout constraints to an iOS view."""
|
|
821
|
+
if "width" in props and props["width"] is not None:
|
|
822
|
+
try:
|
|
823
|
+
for c in list(view.constraints or []):
|
|
824
|
+
if c.firstAttribute == 7: # NSLayoutAttributeWidth
|
|
825
|
+
c.setActive_(False)
|
|
826
|
+
view.widthAnchor.constraintEqualToConstant_(float(props["width"])).setActive_(True)
|
|
827
|
+
except Exception:
|
|
828
|
+
pass
|
|
829
|
+
if "height" in props and props["height"] is not None:
|
|
830
|
+
try:
|
|
831
|
+
for c in list(view.constraints or []):
|
|
832
|
+
if c.firstAttribute == 8: # NSLayoutAttributeHeight
|
|
833
|
+
c.setActive_(False)
|
|
834
|
+
view.heightAnchor.constraintEqualToConstant_(float(props["height"])).setActive_(True)
|
|
835
|
+
except Exception:
|
|
836
|
+
pass
|
|
837
|
+
|
|
471
838
|
# ---- Text -----------------------------------------------------------
|
|
472
839
|
class IOSTextHandler(ViewHandler):
|
|
473
840
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
474
841
|
label = ObjCClass("UILabel").alloc().init()
|
|
475
842
|
self._apply(label, props)
|
|
843
|
+
_apply_ios_layout(label, props)
|
|
476
844
|
return label
|
|
477
845
|
|
|
478
846
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
479
847
|
self._apply(native_view, changed)
|
|
848
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
849
|
+
_apply_ios_layout(native_view, changed)
|
|
480
850
|
|
|
481
851
|
def _apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
482
852
|
if "text" in props:
|
|
@@ -501,10 +871,6 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
501
871
|
|
|
502
872
|
# ---- Button ---------------------------------------------------------
|
|
503
873
|
|
|
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
874
|
_pn_btn_handler_map: dict = {}
|
|
509
875
|
|
|
510
876
|
class _PNButtonTarget(NSObject): # type: ignore[valid-type]
|
|
@@ -515,9 +881,6 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
515
881
|
if self._callback is not None:
|
|
516
882
|
self._callback()
|
|
517
883
|
|
|
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
884
|
_pn_retained_views: list = []
|
|
522
885
|
|
|
523
886
|
class IOSButtonHandler(ViewHandler):
|
|
@@ -528,10 +891,13 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
528
891
|
_ios_blue = UIColor.colorWithRed_green_blue_alpha_(0.0, 0.478, 1.0, 1.0)
|
|
529
892
|
btn.setTitleColor_forState_(_ios_blue, 0)
|
|
530
893
|
self._apply(btn, props)
|
|
894
|
+
_apply_ios_layout(btn, props)
|
|
531
895
|
return btn
|
|
532
896
|
|
|
533
897
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
534
898
|
self._apply(native_view, changed)
|
|
899
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
900
|
+
_apply_ios_layout(native_view, changed)
|
|
535
901
|
|
|
536
902
|
def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
|
|
537
903
|
if "title" in props:
|
|
@@ -563,17 +929,40 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
563
929
|
sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
|
|
564
930
|
sv.setAxis_(1) # vertical
|
|
565
931
|
self._apply(sv, props)
|
|
932
|
+
_apply_ios_layout(sv, props)
|
|
566
933
|
return sv
|
|
567
934
|
|
|
568
935
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
569
936
|
self._apply(native_view, changed)
|
|
937
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
938
|
+
_apply_ios_layout(native_view, changed)
|
|
570
939
|
|
|
571
940
|
def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
|
|
572
941
|
if "spacing" in props and props["spacing"]:
|
|
573
942
|
sv.setSpacing_(float(props["spacing"]))
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
943
|
+
ai = props.get("align_items") or props.get("alignment")
|
|
944
|
+
if ai:
|
|
945
|
+
alignment_map = {
|
|
946
|
+
"stretch": 0,
|
|
947
|
+
"fill": 0,
|
|
948
|
+
"flex_start": 1,
|
|
949
|
+
"leading": 1,
|
|
950
|
+
"center": 3,
|
|
951
|
+
"flex_end": 4,
|
|
952
|
+
"trailing": 4,
|
|
953
|
+
}
|
|
954
|
+
sv.setAlignment_(alignment_map.get(ai, 0))
|
|
955
|
+
jc = props.get("justify_content")
|
|
956
|
+
if jc:
|
|
957
|
+
distribution_map = {
|
|
958
|
+
"flex_start": 0,
|
|
959
|
+
"center": 0,
|
|
960
|
+
"flex_end": 0,
|
|
961
|
+
"space_between": 3,
|
|
962
|
+
"space_around": 4,
|
|
963
|
+
"space_evenly": 4,
|
|
964
|
+
}
|
|
965
|
+
sv.setDistribution_(distribution_map.get(jc, 0))
|
|
577
966
|
if "background_color" in props and props["background_color"] is not None:
|
|
578
967
|
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
579
968
|
if "padding" in props:
|
|
@@ -600,17 +989,40 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
600
989
|
sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
|
|
601
990
|
sv.setAxis_(0) # horizontal
|
|
602
991
|
self._apply(sv, props)
|
|
992
|
+
_apply_ios_layout(sv, props)
|
|
603
993
|
return sv
|
|
604
994
|
|
|
605
995
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
606
996
|
self._apply(native_view, changed)
|
|
997
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
998
|
+
_apply_ios_layout(native_view, changed)
|
|
607
999
|
|
|
608
1000
|
def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
|
|
609
1001
|
if "spacing" in props and props["spacing"]:
|
|
610
1002
|
sv.setSpacing_(float(props["spacing"]))
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1003
|
+
ai = props.get("align_items") or props.get("alignment")
|
|
1004
|
+
if ai:
|
|
1005
|
+
alignment_map = {
|
|
1006
|
+
"stretch": 0,
|
|
1007
|
+
"fill": 0,
|
|
1008
|
+
"flex_start": 1,
|
|
1009
|
+
"top": 1,
|
|
1010
|
+
"center": 3,
|
|
1011
|
+
"flex_end": 4,
|
|
1012
|
+
"bottom": 4,
|
|
1013
|
+
}
|
|
1014
|
+
sv.setAlignment_(alignment_map.get(ai, 0))
|
|
1015
|
+
jc = props.get("justify_content")
|
|
1016
|
+
if jc:
|
|
1017
|
+
distribution_map = {
|
|
1018
|
+
"flex_start": 0,
|
|
1019
|
+
"center": 0,
|
|
1020
|
+
"flex_end": 0,
|
|
1021
|
+
"space_between": 3,
|
|
1022
|
+
"space_around": 4,
|
|
1023
|
+
"space_evenly": 4,
|
|
1024
|
+
}
|
|
1025
|
+
sv.setDistribution_(distribution_map.get(jc, 0))
|
|
614
1026
|
if "background_color" in props and props["background_color"] is not None:
|
|
615
1027
|
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
616
1028
|
|
|
@@ -630,6 +1042,7 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
630
1042
|
sv = ObjCClass("UIScrollView").alloc().init()
|
|
631
1043
|
if "background_color" in props and props["background_color"] is not None:
|
|
632
1044
|
sv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1045
|
+
_apply_ios_layout(sv, props)
|
|
633
1046
|
return sv
|
|
634
1047
|
|
|
635
1048
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -650,16 +1063,33 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
650
1063
|
def remove_child(self, parent: Any, child: Any) -> None:
|
|
651
1064
|
child.removeFromSuperview()
|
|
652
1065
|
|
|
653
|
-
# ---- TextInput (UITextField)
|
|
1066
|
+
# ---- TextInput (UITextField with on_change) -------------------------
|
|
1067
|
+
_pn_tf_handler_map: dict = {}
|
|
1068
|
+
|
|
1069
|
+
class _PNTextFieldTarget(NSObject): # type: ignore[valid-type]
|
|
1070
|
+
_callback: Optional[Callable[[str], None]] = None
|
|
1071
|
+
|
|
1072
|
+
@objc_method
|
|
1073
|
+
def onEdit_(self, sender: object) -> None:
|
|
1074
|
+
if self._callback is not None:
|
|
1075
|
+
try:
|
|
1076
|
+
text = str(sender.text) if sender and hasattr(sender, "text") else ""
|
|
1077
|
+
self._callback(text)
|
|
1078
|
+
except Exception:
|
|
1079
|
+
pass
|
|
1080
|
+
|
|
654
1081
|
class IOSTextInputHandler(ViewHandler):
|
|
655
1082
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
656
1083
|
tf = ObjCClass("UITextField").alloc().init()
|
|
657
1084
|
tf.setBorderStyle_(2) # RoundedRect
|
|
658
1085
|
self._apply(tf, props)
|
|
1086
|
+
_apply_ios_layout(tf, props)
|
|
659
1087
|
return tf
|
|
660
1088
|
|
|
661
1089
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
662
1090
|
self._apply(native_view, changed)
|
|
1091
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
1092
|
+
_apply_ios_layout(native_view, changed)
|
|
663
1093
|
|
|
664
1094
|
def _apply(self, tf: Any, props: Dict[str, Any]) -> None:
|
|
665
1095
|
if "value" in props:
|
|
@@ -674,20 +1104,72 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
674
1104
|
tf.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
675
1105
|
if "secure" in props and props["secure"]:
|
|
676
1106
|
tf.setSecureTextEntry_(True)
|
|
1107
|
+
if "on_change" in props:
|
|
1108
|
+
existing = _pn_tf_handler_map.get(id(tf))
|
|
1109
|
+
if existing is not None:
|
|
1110
|
+
existing._callback = props["on_change"]
|
|
1111
|
+
else:
|
|
1112
|
+
handler = _PNTextFieldTarget.new()
|
|
1113
|
+
handler._callback = props["on_change"]
|
|
1114
|
+
_pn_tf_handler_map[id(tf)] = handler
|
|
1115
|
+
tf.addTarget_action_forControlEvents_(handler, SEL("onEdit:"), 1 << 17)
|
|
677
1116
|
|
|
678
|
-
# ---- Image
|
|
1117
|
+
# ---- Image (with URL loading) ---------------------------------------
|
|
679
1118
|
class IOSImageHandler(ViewHandler):
|
|
680
1119
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
681
1120
|
iv = ObjCClass("UIImageView").alloc().init()
|
|
682
|
-
|
|
683
|
-
|
|
1121
|
+
self._apply(iv, props)
|
|
1122
|
+
_apply_ios_layout(iv, props)
|
|
684
1123
|
return iv
|
|
685
1124
|
|
|
686
1125
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
687
|
-
|
|
688
|
-
|
|
1126
|
+
self._apply(native_view, changed)
|
|
1127
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
1128
|
+
_apply_ios_layout(native_view, changed)
|
|
1129
|
+
|
|
1130
|
+
def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
|
|
1131
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
1132
|
+
iv.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1133
|
+
if "source" in props and props["source"]:
|
|
1134
|
+
self._load_source(iv, props["source"])
|
|
1135
|
+
if "scale_type" in props and props["scale_type"]:
|
|
1136
|
+
mapping = {"cover": 2, "contain": 1, "stretch": 0, "center": 4}
|
|
1137
|
+
iv.setContentMode_(mapping.get(props["scale_type"], 1))
|
|
1138
|
+
|
|
1139
|
+
def _load_source(self, iv: Any, source: str) -> None:
|
|
1140
|
+
try:
|
|
1141
|
+
if source.startswith(("http://", "https://")):
|
|
1142
|
+
NSURL = ObjCClass("NSURL")
|
|
1143
|
+
NSData = ObjCClass("NSData")
|
|
1144
|
+
UIImage = ObjCClass("UIImage")
|
|
1145
|
+
url = NSURL.URLWithString_(source)
|
|
1146
|
+
data = NSData.dataWithContentsOfURL_(url)
|
|
1147
|
+
if data:
|
|
1148
|
+
image = UIImage.imageWithData_(data)
|
|
1149
|
+
if image:
|
|
1150
|
+
iv.setImage_(image)
|
|
1151
|
+
else:
|
|
1152
|
+
UIImage = ObjCClass("UIImage")
|
|
1153
|
+
image = UIImage.imageNamed_(source)
|
|
1154
|
+
if image:
|
|
1155
|
+
iv.setImage_(image)
|
|
1156
|
+
except Exception:
|
|
1157
|
+
pass
|
|
1158
|
+
|
|
1159
|
+
# ---- Switch (with on_change) ----------------------------------------
|
|
1160
|
+
_pn_switch_handler_map: dict = {}
|
|
1161
|
+
|
|
1162
|
+
class _PNSwitchTarget(NSObject): # type: ignore[valid-type]
|
|
1163
|
+
_callback: Optional[Callable[[bool], None]] = None
|
|
1164
|
+
|
|
1165
|
+
@objc_method
|
|
1166
|
+
def onToggle_(self, sender: object) -> None:
|
|
1167
|
+
if self._callback is not None:
|
|
1168
|
+
try:
|
|
1169
|
+
self._callback(bool(sender.isOn()))
|
|
1170
|
+
except Exception:
|
|
1171
|
+
pass
|
|
689
1172
|
|
|
690
|
-
# ---- Switch ---------------------------------------------------------
|
|
691
1173
|
class IOSSwitchHandler(ViewHandler):
|
|
692
1174
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
693
1175
|
sw = ObjCClass("UISwitch").alloc().init()
|
|
@@ -700,6 +1182,15 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
700
1182
|
def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
|
|
701
1183
|
if "value" in props:
|
|
702
1184
|
sw.setOn_animated_(bool(props["value"]), False)
|
|
1185
|
+
if "on_change" in props:
|
|
1186
|
+
existing = _pn_switch_handler_map.get(id(sw))
|
|
1187
|
+
if existing is not None:
|
|
1188
|
+
existing._callback = props["on_change"]
|
|
1189
|
+
else:
|
|
1190
|
+
handler = _PNSwitchTarget.new()
|
|
1191
|
+
handler._callback = props["on_change"]
|
|
1192
|
+
_pn_switch_handler_map[id(sw)] = handler
|
|
1193
|
+
sw.addTarget_action_forControlEvents_(handler, SEL("onToggle:"), 1 << 12)
|
|
703
1194
|
|
|
704
1195
|
# ---- ProgressBar (UIProgressView) -----------------------------------
|
|
705
1196
|
class IOSProgressBarHandler(ViewHandler):
|
|
@@ -707,6 +1198,7 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
707
1198
|
pv = ObjCClass("UIProgressView").alloc().init()
|
|
708
1199
|
if "value" in props:
|
|
709
1200
|
pv.setProgress_(float(props["value"]))
|
|
1201
|
+
_apply_ios_layout(pv, props)
|
|
710
1202
|
return pv
|
|
711
1203
|
|
|
712
1204
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -737,6 +1229,7 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
737
1229
|
NSURLRequest = ObjCClass("NSURLRequest")
|
|
738
1230
|
url_obj = NSURL.URLWithString_(str(props["url"]))
|
|
739
1231
|
wv.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
|
|
1232
|
+
_apply_ios_layout(wv, props)
|
|
740
1233
|
return wv
|
|
741
1234
|
|
|
742
1235
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -760,6 +1253,112 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
760
1253
|
size = float(changed["size"])
|
|
761
1254
|
native_view.setFrame_(((0, 0), (size, size)))
|
|
762
1255
|
|
|
1256
|
+
# ---- View (generic UIView) -----------------------------------------
|
|
1257
|
+
class IOSViewHandler(ViewHandler):
|
|
1258
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1259
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1260
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
1261
|
+
v.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1262
|
+
_apply_ios_layout(v, props)
|
|
1263
|
+
return v
|
|
1264
|
+
|
|
1265
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1266
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
1267
|
+
native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
|
|
1268
|
+
if changed.keys() & _LAYOUT_KEYS:
|
|
1269
|
+
_apply_ios_layout(native_view, changed)
|
|
1270
|
+
|
|
1271
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
1272
|
+
parent.addSubview_(child)
|
|
1273
|
+
|
|
1274
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1275
|
+
child.removeFromSuperview()
|
|
1276
|
+
|
|
1277
|
+
# ---- SafeAreaView ---------------------------------------------------
|
|
1278
|
+
class IOSSafeAreaViewHandler(ViewHandler):
|
|
1279
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1280
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1281
|
+
if "background_color" in props and props["background_color"] is not None:
|
|
1282
|
+
v.setBackgroundColor_(_uicolor(props["background_color"]))
|
|
1283
|
+
return v
|
|
1284
|
+
|
|
1285
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1286
|
+
if "background_color" in changed and changed["background_color"] is not None:
|
|
1287
|
+
native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
|
|
1288
|
+
|
|
1289
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
1290
|
+
parent.addSubview_(child)
|
|
1291
|
+
|
|
1292
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1293
|
+
child.removeFromSuperview()
|
|
1294
|
+
|
|
1295
|
+
# ---- Modal ----------------------------------------------------------
|
|
1296
|
+
class IOSModalHandler(ViewHandler):
|
|
1297
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1298
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1299
|
+
v.setHidden_(True)
|
|
1300
|
+
return v
|
|
1301
|
+
|
|
1302
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1303
|
+
pass
|
|
1304
|
+
|
|
1305
|
+
# ---- Slider (UISlider) ----------------------------------------------
|
|
1306
|
+
_pn_slider_handler_map: dict = {}
|
|
1307
|
+
|
|
1308
|
+
class _PNSliderTarget(NSObject): # type: ignore[valid-type]
|
|
1309
|
+
_callback: Optional[Callable[[float], None]] = None
|
|
1310
|
+
|
|
1311
|
+
@objc_method
|
|
1312
|
+
def onSlide_(self, sender: object) -> None:
|
|
1313
|
+
if self._callback is not None:
|
|
1314
|
+
try:
|
|
1315
|
+
self._callback(float(sender.value))
|
|
1316
|
+
except Exception:
|
|
1317
|
+
pass
|
|
1318
|
+
|
|
1319
|
+
class IOSSliderHandler(ViewHandler):
|
|
1320
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1321
|
+
sl = ObjCClass("UISlider").alloc().init()
|
|
1322
|
+
self._apply(sl, props)
|
|
1323
|
+
_apply_ios_layout(sl, props)
|
|
1324
|
+
return sl
|
|
1325
|
+
|
|
1326
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1327
|
+
self._apply(native_view, changed)
|
|
1328
|
+
|
|
1329
|
+
def _apply(self, sl: Any, props: Dict[str, Any]) -> None:
|
|
1330
|
+
if "min_value" in props:
|
|
1331
|
+
sl.setMinimumValue_(float(props["min_value"]))
|
|
1332
|
+
if "max_value" in props:
|
|
1333
|
+
sl.setMaximumValue_(float(props["max_value"]))
|
|
1334
|
+
if "value" in props:
|
|
1335
|
+
sl.setValue_(float(props["value"]))
|
|
1336
|
+
if "on_change" in props:
|
|
1337
|
+
existing = _pn_slider_handler_map.get(id(sl))
|
|
1338
|
+
if existing is not None:
|
|
1339
|
+
existing._callback = props["on_change"]
|
|
1340
|
+
else:
|
|
1341
|
+
handler = _PNSliderTarget.new()
|
|
1342
|
+
handler._callback = props["on_change"]
|
|
1343
|
+
_pn_slider_handler_map[id(sl)] = handler
|
|
1344
|
+
sl.addTarget_action_forControlEvents_(handler, SEL("onSlide:"), 1 << 12)
|
|
1345
|
+
|
|
1346
|
+
# ---- Pressable (UIView with tap gesture) ----------------------------
|
|
1347
|
+
class IOSPressableHandler(ViewHandler):
|
|
1348
|
+
def create(self, props: Dict[str, Any]) -> Any:
|
|
1349
|
+
v = ObjCClass("UIView").alloc().init()
|
|
1350
|
+
v.setUserInteractionEnabled_(True)
|
|
1351
|
+
return v
|
|
1352
|
+
|
|
1353
|
+
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
1354
|
+
pass
|
|
1355
|
+
|
|
1356
|
+
def add_child(self, parent: Any, child: Any) -> None:
|
|
1357
|
+
parent.addSubview_(child)
|
|
1358
|
+
|
|
1359
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1360
|
+
child.removeFromSuperview()
|
|
1361
|
+
|
|
763
1362
|
registry.register("Text", IOSTextHandler())
|
|
764
1363
|
registry.register("Button", IOSButtonHandler())
|
|
765
1364
|
registry.register("Column", IOSColumnHandler())
|
|
@@ -772,6 +1371,11 @@ def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
|
|
|
772
1371
|
registry.register("ActivityIndicator", IOSActivityIndicatorHandler())
|
|
773
1372
|
registry.register("WebView", IOSWebViewHandler())
|
|
774
1373
|
registry.register("Spacer", IOSSpacerHandler())
|
|
1374
|
+
registry.register("View", IOSViewHandler())
|
|
1375
|
+
registry.register("SafeAreaView", IOSSafeAreaViewHandler())
|
|
1376
|
+
registry.register("Modal", IOSModalHandler())
|
|
1377
|
+
registry.register("Slider", IOSSliderHandler())
|
|
1378
|
+
registry.register("Pressable", IOSPressableHandler())
|
|
775
1379
|
|
|
776
1380
|
|
|
777
1381
|
# ======================================================================
|