pythonnative 0.26.0__py3-none-any.whl → 0.28.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 +1 -1
- pythonnative/animated.py +697 -62
- pythonnative/cli/pn.py +7 -0
- pythonnative/components.py +61 -7
- pythonnative/gestures.py +446 -34
- pythonnative/native_modules/net_info.py +73 -4
- pythonnative/native_modules/notifications.py +37 -9
- pythonnative/native_views/android.py +320 -16
- pythonnative/native_views/desktop.py +766 -47
- pythonnative/native_views/ios.py +522 -33
- pythonnative/reconciler.py +28 -0
- pythonnative/style.py +19 -0
- pythonnative/templates/android_template/app/build.gradle +4 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +4 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +60 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +5 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/METADATA +3 -3
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/RECORD +23 -22
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.28.0.dist-info}/top_level.txt +0 -0
|
@@ -21,12 +21,28 @@ applies those frames via per-child `MarginLayoutParams`.
|
|
|
21
21
|
|
|
22
22
|
**Gestures** feed raw ``MotionEvent`` streams into the shared
|
|
23
23
|
pure-Python [`GestureArbiter`][pythonnative.gestures.GestureArbiter],
|
|
24
|
-
so gesture semantics match the desktop preview exactly.
|
|
24
|
+
so gesture semantics match the desktop preview exactly. Serialized
|
|
25
|
+
specs (including ``simultaneous`` / ``wait_for`` composition metadata
|
|
26
|
+
and ``fling`` configs) pass through untouched; this module only
|
|
27
|
+
schedules main-looper polls at the arbiter's ``next_deadline()`` so
|
|
28
|
+
long-press activation and multi-tap windows fire even when no
|
|
29
|
+
further touch events arrive.
|
|
25
30
|
|
|
26
31
|
**Animations**: ``timing`` animations on transform/opacity/color props
|
|
27
32
|
are driven natively by ``ObjectAnimator`` (Choreographer-paced, no
|
|
28
33
|
Python per-frame work); springs and decay fall back to the Python
|
|
29
|
-
ticker
|
|
34
|
+
ticker, whose per-frame values arrive through
|
|
35
|
+
``set_animated_property`` (floats for transforms and opacity,
|
|
36
|
+
interpolated "#AARRGGBB" strings for background and text colors that
|
|
37
|
+
re-bake through the border-drawable machinery instead of clobbering
|
|
38
|
+
it).
|
|
39
|
+
|
|
40
|
+
**Interaction props**: ``pointer_events`` maps onto the template's
|
|
41
|
+
``PNFrameLayout`` touch interception when available (with a
|
|
42
|
+
Python-side fallback for older projects), ``hit_slop`` expands touch
|
|
43
|
+
targets via a parent ``TouchDelegate`` sized after layout,
|
|
44
|
+
``z_index`` maps to ``View.setZ``, and per-corner radii extend the
|
|
45
|
+
``GradientDrawable`` border path.
|
|
30
46
|
|
|
31
47
|
This module is only imported on Android at runtime. Desktop tests
|
|
32
48
|
inject a mock registry via
|
|
@@ -58,11 +74,20 @@ _SIDE_BORDER_COLOR_KEYS = (
|
|
|
58
74
|
"border_right_color",
|
|
59
75
|
"border_bottom_color",
|
|
60
76
|
)
|
|
77
|
+
# Order matches GradientDrawable.setCornerRadii: top-left, top-right,
|
|
78
|
+
# bottom-right, bottom-left (each corner takes an x and a y radius).
|
|
79
|
+
_CORNER_RADIUS_KEYS = (
|
|
80
|
+
"border_top_left_radius",
|
|
81
|
+
"border_top_right_radius",
|
|
82
|
+
"border_bottom_right_radius",
|
|
83
|
+
"border_bottom_left_radius",
|
|
84
|
+
)
|
|
61
85
|
_DRAWABLE_STYLE_KEYS = (
|
|
62
86
|
"background_color",
|
|
63
87
|
"border_radius",
|
|
64
88
|
"border_width",
|
|
65
89
|
"border_color",
|
|
90
|
+
*_CORNER_RADIUS_KEYS,
|
|
66
91
|
*_SIDE_BORDER_WIDTH_KEYS,
|
|
67
92
|
*_SIDE_BORDER_COLOR_KEYS,
|
|
68
93
|
)
|
|
@@ -98,6 +123,38 @@ def _pn_runtime_class(class_name: str) -> Any:
|
|
|
98
123
|
return jclass(f"{package}.{class_name}")
|
|
99
124
|
|
|
100
125
|
|
|
126
|
+
# Cached lookup for the template's PNFrameLayout container class.
|
|
127
|
+
# ``None`` means "not resolved yet"; ``False`` means "this generated
|
|
128
|
+
# project predates the class", so creation falls back to FrameLayout
|
|
129
|
+
# without retrying the (exception-throwing) lookup per view.
|
|
130
|
+
_pn_container_cls: Any = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _new_container() -> Any:
|
|
134
|
+
"""Create a container view, preferring the template's ``PNFrameLayout``.
|
|
135
|
+
|
|
136
|
+
``PNFrameLayout`` adds native ``pointer_events`` interception
|
|
137
|
+
(``onInterceptTouchEvent`` / ``dispatchTouchEvent``), which
|
|
138
|
+
Chaquopy can't express from Python. Older generated projects
|
|
139
|
+
without the class get a plain ``FrameLayout``; the
|
|
140
|
+
``pointer_events`` prop then degrades to the Python-side
|
|
141
|
+
approximation in
|
|
142
|
+
[`_apply_pointer_events`][pythonnative.native_views.android._apply_pointer_events].
|
|
143
|
+
"""
|
|
144
|
+
global _pn_container_cls
|
|
145
|
+
if _pn_container_cls is None:
|
|
146
|
+
try:
|
|
147
|
+
_pn_container_cls = _pn_runtime_class("PNFrameLayout")
|
|
148
|
+
except Exception:
|
|
149
|
+
_pn_container_cls = False
|
|
150
|
+
if _pn_container_cls is not False:
|
|
151
|
+
try:
|
|
152
|
+
return _pn_container_cls(_ctx())
|
|
153
|
+
except Exception:
|
|
154
|
+
diagnostics.swallowed("android._new_container")
|
|
155
|
+
return jclass("android.widget.FrameLayout")(_ctx())
|
|
156
|
+
|
|
157
|
+
|
|
101
158
|
def _signed_color(value: int) -> int:
|
|
102
159
|
"""Clamp a Python color int into Java's signed 32-bit range."""
|
|
103
160
|
value &= 0xFFFFFFFF
|
|
@@ -227,7 +284,7 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
|
|
|
227
284
|
"""
|
|
228
285
|
if _apply_side_border(view, props):
|
|
229
286
|
return
|
|
230
|
-
has_border = any(k in props for k in ("border_radius", "border_width", "border_color"))
|
|
287
|
+
has_border = any(k in props for k in ("border_radius", "border_width", "border_color", *_CORNER_RADIUS_KEYS))
|
|
231
288
|
has_bg = "background_color" in props and props["background_color"] is not None
|
|
232
289
|
if not has_border and not has_bg:
|
|
233
290
|
return
|
|
@@ -239,7 +296,21 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
|
|
|
239
296
|
drawable.setColor(parse_color_int(props["background_color"]))
|
|
240
297
|
except Exception:
|
|
241
298
|
diagnostics.swallowed("android._apply_border")
|
|
242
|
-
if
|
|
299
|
+
if any(props.get(k) is not None for k in _CORNER_RADIUS_KEYS):
|
|
300
|
+
# Per-corner radii: unspecified corners inherit the uniform
|
|
301
|
+
# ``border_radius`` (else 0). ``setCornerRadii`` takes eight
|
|
302
|
+
# floats, an (x, y) pair per corner in _CORNER_RADIUS_KEYS
|
|
303
|
+
# order, each in pixels.
|
|
304
|
+
try:
|
|
305
|
+
base_radius = float(props.get("border_radius") or 0.0)
|
|
306
|
+
radii = []
|
|
307
|
+
for key in _CORNER_RADIUS_KEYS:
|
|
308
|
+
r = float(_dp(float(props[key] if props.get(key) is not None else base_radius)))
|
|
309
|
+
radii.extend([r, r])
|
|
310
|
+
drawable.setCornerRadii(radii)
|
|
311
|
+
except Exception:
|
|
312
|
+
diagnostics.swallowed("android._apply_border")
|
|
313
|
+
elif "border_radius" in props and props["border_radius"] is not None:
|
|
243
314
|
try:
|
|
244
315
|
drawable.setCornerRadius(float(_dp(float(props["border_radius"]))))
|
|
245
316
|
except Exception:
|
|
@@ -269,6 +340,44 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
|
|
|
269
340
|
diagnostics.swallowed("android._apply_border")
|
|
270
341
|
|
|
271
342
|
|
|
343
|
+
def _set_animated_background(view: Any, value: Any) -> None:
|
|
344
|
+
"""Apply one animated ``background_color`` frame without losing borders.
|
|
345
|
+
|
|
346
|
+
Interpolations hand this module raw color values ("#AARRGGBB" or
|
|
347
|
+
"#RRGGBB" strings, or ints). Views whose background is a
|
|
348
|
+
border-rendering drawable (the uniform ``GradientDrawable`` or the
|
|
349
|
+
per-side ``PNBorderDrawable``) must keep their corner radii and
|
|
350
|
+
strokes while the fill animates, so the color routes through the
|
|
351
|
+
same [`_apply_border`][pythonnative.native_views.android._apply_border]
|
|
352
|
+
machinery ``update()`` uses. When the current background already is
|
|
353
|
+
a ``GradientDrawable``, its fill is mutated in place, so no
|
|
354
|
+
drawable is allocated per animation frame. Plain views take the
|
|
355
|
+
direct ``setBackgroundColor`` path.
|
|
356
|
+
"""
|
|
357
|
+
state = _state_of(view)
|
|
358
|
+
visual = state.get("visual")
|
|
359
|
+
has_border = isinstance(visual, dict) and any(
|
|
360
|
+
visual.get(k) is not None for k in _DRAWABLE_STYLE_KEYS if k != "background_color"
|
|
361
|
+
)
|
|
362
|
+
if has_border:
|
|
363
|
+
visual = dict(visual)
|
|
364
|
+
visual["background_color"] = value
|
|
365
|
+
state["visual"] = visual
|
|
366
|
+
try:
|
|
367
|
+
GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
|
|
368
|
+
bg = view.getBackground()
|
|
369
|
+
if bg is not None and isinstance(bg, GradientDrawable):
|
|
370
|
+
bg.setColor(parse_color_int(value))
|
|
371
|
+
return
|
|
372
|
+
except Exception:
|
|
373
|
+
diagnostics.swallowed("android._set_animated_background")
|
|
374
|
+
# Per-side ``PNBorderDrawable`` backgrounds (immutable fill) and
|
|
375
|
+
# first frames before any drawable exists re-bake the full set.
|
|
376
|
+
_apply_border(view, visual)
|
|
377
|
+
return
|
|
378
|
+
view.setBackgroundColor(parse_color_int(value))
|
|
379
|
+
|
|
380
|
+
|
|
272
381
|
def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
|
|
273
382
|
"""Apply shadow props via elevation plus tinted outline shadows.
|
|
274
383
|
|
|
@@ -417,6 +526,138 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
|
|
|
417
526
|
diagnostics.swallowed("android._apply_accessibility")
|
|
418
527
|
|
|
419
528
|
|
|
529
|
+
def _pointer_events_blocked(view: Any) -> bool:
|
|
530
|
+
"""Whether ``pointer_events`` disables the view's own touch handling.
|
|
531
|
+
|
|
532
|
+
Consulted at the top of every touch-stream proxy (gesture, press)
|
|
533
|
+
so listeners installed at create time honor the prop without being
|
|
534
|
+
re-wired: ``"none"`` and ``"box_none"`` both mean the view itself
|
|
535
|
+
must not observe or consume the stream.
|
|
536
|
+
"""
|
|
537
|
+
merged = _state_of(view).get("props") or {}
|
|
538
|
+
return merged.get("pointer_events") in ("none", "box_none")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _apply_pointer_events(view: Any, value: Any) -> None:
|
|
542
|
+
"""Apply the ``pointer_events`` prop (``None`` restores ``"auto"``).
|
|
543
|
+
|
|
544
|
+
Two cooperating layers implement the modes:
|
|
545
|
+
|
|
546
|
+
- **Native interception** (preferred): containers built by
|
|
547
|
+
[`_new_container`][pythonnative.native_views.android._new_container]
|
|
548
|
+
are the template's ``PNFrameLayout``, whose
|
|
549
|
+
``setPointerEventsMode(String)`` drives real
|
|
550
|
+
``dispatchTouchEvent`` / ``onInterceptTouchEvent`` overrides.
|
|
551
|
+
``"none"`` declines the whole dispatch so touches pass through
|
|
552
|
+
to views underneath, ``"box_only"`` intercepts events away from
|
|
553
|
+
children, and ``"box_none"`` never intercepts. The call is
|
|
554
|
+
guarded so older generated projects without the class (and
|
|
555
|
+
non-container views) fall back cleanly.
|
|
556
|
+
- **Python-side muting**: ``setClickable`` toggles whether the
|
|
557
|
+
view claims events, and the touch-stream proxies consult
|
|
558
|
+
[`_pointer_events_blocked`][pythonnative.native_views.android._pointer_events_blocked]
|
|
559
|
+
so gesture/press wiring goes quiet without being torn down.
|
|
560
|
+
``"auto"`` / removal restores the original clickability
|
|
561
|
+
captured before the first override.
|
|
562
|
+
|
|
563
|
+
On a plain ``FrameLayout`` (old template) the fallback keeps the
|
|
564
|
+
previous approximation: ``"none"`` silences the view itself but
|
|
565
|
+
interactive children keep receiving touches, and ``"box_only"``
|
|
566
|
+
only claims events no interactive child consumed. ``"box_none"``
|
|
567
|
+
is exact either way, since children need no help.
|
|
568
|
+
"""
|
|
569
|
+
mode = value if value is not None else "auto"
|
|
570
|
+
state = _state_of(view)
|
|
571
|
+
try:
|
|
572
|
+
view.setPointerEventsMode(str(mode))
|
|
573
|
+
except Exception:
|
|
574
|
+
# Plain FrameLayout, a leaf widget, or an older template
|
|
575
|
+
# without PNFrameLayout; the Python-side fallback still runs.
|
|
576
|
+
pass
|
|
577
|
+
try:
|
|
578
|
+
if mode in ("none", "box_none"):
|
|
579
|
+
state.setdefault("pe_was_clickable", bool(view.isClickable()))
|
|
580
|
+
view.setClickable(False)
|
|
581
|
+
elif mode == "box_only":
|
|
582
|
+
state.setdefault("pe_was_clickable", bool(view.isClickable()))
|
|
583
|
+
view.setClickable(True)
|
|
584
|
+
elif "pe_was_clickable" in state:
|
|
585
|
+
view.setClickable(bool(state.pop("pe_was_clickable")))
|
|
586
|
+
except Exception:
|
|
587
|
+
diagnostics.swallowed("android._apply_pointer_events")
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _hit_slop_insets(value: Any) -> Optional[Tuple[float, float, float, float]]:
|
|
591
|
+
"""Normalize a ``hit_slop`` prop into ``(top, left, bottom, right)`` points.
|
|
592
|
+
|
|
593
|
+
Accepts a uniform number or a dict with any of ``top`` / ``left``
|
|
594
|
+
/ ``bottom`` / ``right``. Returns ``None`` for absent or
|
|
595
|
+
unparseable values.
|
|
596
|
+
"""
|
|
597
|
+
if value is None:
|
|
598
|
+
return None
|
|
599
|
+
if isinstance(value, dict):
|
|
600
|
+
try:
|
|
601
|
+
return (
|
|
602
|
+
float(value.get("top") or 0.0),
|
|
603
|
+
float(value.get("left") or 0.0),
|
|
604
|
+
float(value.get("bottom") or 0.0),
|
|
605
|
+
float(value.get("right") or 0.0),
|
|
606
|
+
)
|
|
607
|
+
except Exception:
|
|
608
|
+
return None
|
|
609
|
+
try:
|
|
610
|
+
uniform = float(value)
|
|
611
|
+
except Exception:
|
|
612
|
+
return None
|
|
613
|
+
return (uniform, uniform, uniform, uniform)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _update_hit_slop(view: Any) -> None:
|
|
617
|
+
"""Extend the view's touch target via a ``TouchDelegate`` on its parent.
|
|
618
|
+
|
|
619
|
+
The standard Android mechanism: the parent forwards touches
|
|
620
|
+
landing inside an expanded ``Rect`` (the view's frame plus the
|
|
621
|
+
slop insets, in parent-relative pixels) to the view. Parents are
|
|
622
|
+
PythonNative-managed ``FrameLayout``s, so installing the delegate
|
|
623
|
+
is safe; a parent only holds one delegate, so among siblings the
|
|
624
|
+
last ``hit_slop`` applied wins (matching the platform limitation).
|
|
625
|
+
|
|
626
|
+
Requires a known frame, so
|
|
627
|
+
[`set_frame`][pythonnative.native_views.android.AndroidViewHandler.set_frame]
|
|
628
|
+
records the frame and re-invokes this; prop-only updates before
|
|
629
|
+
the first frame defer until layout runs.
|
|
630
|
+
"""
|
|
631
|
+
state = _state_of(view)
|
|
632
|
+
frame = state.get("frame")
|
|
633
|
+
if frame is None:
|
|
634
|
+
return
|
|
635
|
+
merged = state.get("props") or {}
|
|
636
|
+
insets = _hit_slop_insets(merged.get("hit_slop"))
|
|
637
|
+
try:
|
|
638
|
+
parent = view.getParent()
|
|
639
|
+
if parent is None:
|
|
640
|
+
return
|
|
641
|
+
if insets is None:
|
|
642
|
+
if state.pop("hit_slop_active", None):
|
|
643
|
+
parent.setTouchDelegate(None)
|
|
644
|
+
return
|
|
645
|
+
top, left, bottom, right = insets
|
|
646
|
+
x, y, width, height = frame
|
|
647
|
+
Rect = jclass("android.graphics.Rect")
|
|
648
|
+
TouchDelegate = jclass("android.view.TouchDelegate")
|
|
649
|
+
rect = Rect(
|
|
650
|
+
_dp(x - left),
|
|
651
|
+
_dp(y - top),
|
|
652
|
+
_dp(x + width + right),
|
|
653
|
+
_dp(y + height + bottom),
|
|
654
|
+
)
|
|
655
|
+
parent.setTouchDelegate(TouchDelegate(rect, view))
|
|
656
|
+
state["hit_slop_active"] = True
|
|
657
|
+
except Exception:
|
|
658
|
+
diagnostics.swallowed("android._update_hit_slop")
|
|
659
|
+
|
|
660
|
+
|
|
420
661
|
def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
421
662
|
"""Apply visual properties shared across many handlers."""
|
|
422
663
|
has_drawable_keys = any(k in props for k in _DRAWABLE_STYLE_KEYS)
|
|
@@ -440,6 +681,18 @@ def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
|
440
681
|
view.setAlpha(float(props["opacity"]))
|
|
441
682
|
except Exception:
|
|
442
683
|
diagnostics.swallowed("android._apply_common_visual")
|
|
684
|
+
if "z_index" in props:
|
|
685
|
+
# ``setZ`` takes pixels, so scale like other coordinates; a
|
|
686
|
+
# removal (``None``) resets the view to the default plane.
|
|
687
|
+
try:
|
|
688
|
+
z = props["z_index"]
|
|
689
|
+
view.setZ(float(_dp(float(z))) if z is not None else 0.0)
|
|
690
|
+
except Exception:
|
|
691
|
+
diagnostics.swallowed("android._apply_common_visual")
|
|
692
|
+
if "pointer_events" in props:
|
|
693
|
+
_apply_pointer_events(view, props["pointer_events"])
|
|
694
|
+
if "hit_slop" in props:
|
|
695
|
+
_update_hit_slop(view)
|
|
443
696
|
_apply_shadow(view, props)
|
|
444
697
|
_apply_transform(view, props)
|
|
445
698
|
_apply_accessibility(view, props)
|
|
@@ -454,10 +707,14 @@ def _wire_gestures(view: Any, specs: Any) -> None:
|
|
|
454
707
|
"""Feed ``MotionEvent`` streams on ``view`` into a `GestureArbiter`.
|
|
455
708
|
|
|
456
709
|
The arbiter emits ``(gesture_index, payload)`` pairs which are
|
|
457
|
-
forwarded as ``gesture:<i>`` events for this view's tag.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
710
|
+
forwarded as ``gesture:<i>`` events for this view's tag. Specs
|
|
711
|
+
(including ``simultaneous`` / ``wait_for`` metadata and fling
|
|
712
|
+
configs) pass through to the arbiter untouched; all arbitration
|
|
713
|
+
and recognition live in ``pythonnative.gestures``. Time-based
|
|
714
|
+
deadlines (long press, multi-tap windows) are polled with a
|
|
715
|
+
main-looper ``Handler`` scheduled at the arbiter's
|
|
716
|
+
``next_deadline()``. When a pan activates, the parent is asked
|
|
717
|
+
not to intercept so an enclosing ScrollView can't steal the drag.
|
|
461
718
|
"""
|
|
462
719
|
state = _state_of(view)
|
|
463
720
|
if not isinstance(specs, (list, tuple)) or not specs:
|
|
@@ -476,13 +733,27 @@ def _wire_gestures(view: Any, specs: Any) -> None:
|
|
|
476
733
|
|
|
477
734
|
|
|
478
735
|
def _schedule_arbiter_poll(view: Any, state: Dict[str, Any]) -> None:
|
|
479
|
-
"""Schedule a main-looper poll at the arbiter's next deadline
|
|
736
|
+
"""Schedule a main-looper poll at the arbiter's next deadline.
|
|
737
|
+
|
|
738
|
+
Deadlines cover every time-based recognizer decision: long-press
|
|
739
|
+
activation and the multi-tap windows that flush an Exclusive
|
|
740
|
+
double-tap/single-tap buffer. Called after every pointer event
|
|
741
|
+
(not just DOWN) because a deadline can appear on UP, when no
|
|
742
|
+
further touch events arrive to drive the arbiter forward.
|
|
743
|
+
|
|
744
|
+
A pending poll at or before the requested deadline is reused
|
|
745
|
+
rather than stacking duplicate runnables; each poll reschedules
|
|
746
|
+
itself for the following deadline until none remains.
|
|
747
|
+
"""
|
|
480
748
|
arbiter = state.get("arbiter")
|
|
481
749
|
if arbiter is None:
|
|
482
750
|
return
|
|
483
751
|
deadline = arbiter.next_deadline()
|
|
484
752
|
if deadline is None:
|
|
485
753
|
return
|
|
754
|
+
pending = state.get("poll_deadline")
|
|
755
|
+
if pending is not None and pending <= deadline + 0.001:
|
|
756
|
+
return
|
|
486
757
|
delay_ms = max(1, int((deadline - time.monotonic()) * 1000.0))
|
|
487
758
|
try:
|
|
488
759
|
Handler = jclass("android.os.Handler")
|
|
@@ -492,9 +763,14 @@ def _schedule_arbiter_poll(view: Any, state: Dict[str, Any]) -> None:
|
|
|
492
763
|
if handler is None:
|
|
493
764
|
handler = Handler(Looper.getMainLooper())
|
|
494
765
|
state["poll_handler"] = handler
|
|
766
|
+
token = state.get("poll_token", 0) + 1
|
|
767
|
+
state["poll_token"] = token
|
|
768
|
+
state["poll_deadline"] = deadline
|
|
495
769
|
|
|
496
770
|
class _PollRunnable(dynamic_proxy(Runnable)):
|
|
497
771
|
def run(self) -> None:
|
|
772
|
+
if state.get("poll_token") == token:
|
|
773
|
+
state["poll_deadline"] = None
|
|
498
774
|
live = state.get("arbiter")
|
|
499
775
|
if live is not None:
|
|
500
776
|
live.poll(time.monotonic())
|
|
@@ -502,6 +778,7 @@ def _schedule_arbiter_poll(view: Any, state: Dict[str, Any]) -> None:
|
|
|
502
778
|
|
|
503
779
|
handler.postDelayed(_PollRunnable(), delay_ms)
|
|
504
780
|
except Exception:
|
|
781
|
+
state["poll_deadline"] = None
|
|
505
782
|
diagnostics.swallowed("android._schedule_arbiter_poll")
|
|
506
783
|
|
|
507
784
|
|
|
@@ -512,6 +789,8 @@ def _bind_touch_stream(view: Any, state: Dict[str, Any]) -> None:
|
|
|
512
789
|
|
|
513
790
|
class _GestureTouchProxy(dynamic_proxy(OnTouchListener)):
|
|
514
791
|
def onTouch(self, v: Any, event: Any) -> bool:
|
|
792
|
+
if _pointer_events_blocked(v):
|
|
793
|
+
return False
|
|
515
794
|
return bool(_feed_motion_event(v, state, event))
|
|
516
795
|
|
|
517
796
|
view.setOnTouchListener(_GestureTouchProxy())
|
|
@@ -536,7 +815,6 @@ def _feed_motion_event(view: Any, state: Dict[str, Any], event: Any) -> bool:
|
|
|
536
815
|
idx = int(event.getActionIndex())
|
|
537
816
|
pid = int(event.getPointerId(idx))
|
|
538
817
|
arbiter.pointer_down(pid, float(event.getX(idx)) / density, float(event.getY(idx)) / density, t)
|
|
539
|
-
_schedule_arbiter_poll(view, state)
|
|
540
818
|
elif action == 2: # MOVE
|
|
541
819
|
for i in range(int(event.getPointerCount())):
|
|
542
820
|
pid = int(event.getPointerId(i))
|
|
@@ -554,6 +832,10 @@ def _feed_motion_event(view: Any, state: Dict[str, Any], event: Any) -> bool:
|
|
|
554
832
|
arbiter.pointer_up(pid, float(event.getX(idx)) / density, float(event.getY(idx)) / density, t)
|
|
555
833
|
elif action == 3: # CANCEL
|
|
556
834
|
arbiter.cancel(t)
|
|
835
|
+
# Every event can move the next time-based deadline (long-press
|
|
836
|
+
# activation, multi-tap windows opened on UP), so reschedule
|
|
837
|
+
# unconditionally; the scheduler dedups pending polls.
|
|
838
|
+
_schedule_arbiter_poll(view, state)
|
|
557
839
|
return True
|
|
558
840
|
except Exception:
|
|
559
841
|
return True
|
|
@@ -770,6 +1052,13 @@ class AndroidViewHandler(ViewHandler):
|
|
|
770
1052
|
except Exception:
|
|
771
1053
|
diagnostics.swallowed("android.AndroidViewHandler.set_frame")
|
|
772
1054
|
native_view.setLayoutParams(lp)
|
|
1055
|
+
# ``hit_slop`` needs the frame (and an attached parent, which
|
|
1056
|
+
# exists by the time frames are applied) to size its
|
|
1057
|
+
# TouchDelegate rect, so record the frame and recompute here.
|
|
1058
|
+
state = _state_of(native_view)
|
|
1059
|
+
state["frame"] = (x, y, width, height)
|
|
1060
|
+
if (state.get("props") or {}).get("hit_slop") is not None or state.get("hit_slop_active"):
|
|
1061
|
+
_update_hit_slop(native_view)
|
|
773
1062
|
except Exception:
|
|
774
1063
|
diagnostics.swallowed("android.AndroidViewHandler.set_frame")
|
|
775
1064
|
|
|
@@ -802,7 +1091,13 @@ class AndroidViewHandler(ViewHandler):
|
|
|
802
1091
|
return (0.0, 0.0)
|
|
803
1092
|
|
|
804
1093
|
def set_animated_property(self, native_view: Any, prop_name: str, value: Any) -> None:
|
|
805
|
-
"""Apply one Python-driven animation frame immediately.
|
|
1094
|
+
"""Apply one Python-driven animation frame immediately.
|
|
1095
|
+
|
|
1096
|
+
Handles float transform props (dp-scaled translations, scales,
|
|
1097
|
+
rotation degrees, opacity) and interpolated color frames for
|
|
1098
|
+
``background_color`` / ``color`` ("#AARRGGBB" / "#RRGGBB"
|
|
1099
|
+
strings or ints). Unknown props are silently ignored.
|
|
1100
|
+
"""
|
|
806
1101
|
if native_view is None:
|
|
807
1102
|
return
|
|
808
1103
|
try:
|
|
@@ -822,7 +1117,12 @@ class AndroidViewHandler(ViewHandler):
|
|
|
822
1117
|
elif prop_name == "rotate":
|
|
823
1118
|
native_view.setRotation(float(value))
|
|
824
1119
|
elif prop_name == "background_color":
|
|
825
|
-
native_view
|
|
1120
|
+
_set_animated_background(native_view, value)
|
|
1121
|
+
elif prop_name == "color":
|
|
1122
|
+
# Interpolations emit "#AARRGGBB" strings; parse_color_int
|
|
1123
|
+
# accepts strings and raw ints alike. Views without
|
|
1124
|
+
# setTextColor fall into the except below and skip.
|
|
1125
|
+
native_view.setTextColor(parse_color_int(value))
|
|
826
1126
|
except Exception:
|
|
827
1127
|
diagnostics.swallowed("android.AndroidViewHandler.set_animated_property")
|
|
828
1128
|
|
|
@@ -867,11 +1167,13 @@ class FlexContainerHandler(AndroidViewHandler):
|
|
|
867
1167
|
All flex semantics (direction, alignment, distribution, padding)
|
|
868
1168
|
are computed by the layout engine and applied via
|
|
869
1169
|
[`set_frame`][pythonnative.native_views.android.AndroidViewHandler.set_frame].
|
|
870
|
-
The container itself is just a positioning surface
|
|
1170
|
+
The container itself is just a positioning surface (the template's
|
|
1171
|
+
``PNFrameLayout`` when available, so ``pointer_events`` gets real
|
|
1172
|
+
touch interception).
|
|
871
1173
|
"""
|
|
872
1174
|
|
|
873
1175
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
874
|
-
return
|
|
1176
|
+
return _new_container()
|
|
875
1177
|
|
|
876
1178
|
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
877
1179
|
_insert_view(parent, child, index)
|
|
@@ -1959,7 +2261,7 @@ class SafeAreaViewHandler(FlexContainerHandler):
|
|
|
1959
2261
|
"""Safe-area container using FrameLayout with ``fitsSystemWindows``."""
|
|
1960
2262
|
|
|
1961
2263
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
1962
|
-
fl =
|
|
2264
|
+
fl = _new_container()
|
|
1963
2265
|
fl.setFitsSystemWindows(True)
|
|
1964
2266
|
return fl
|
|
1965
2267
|
|
|
@@ -2381,7 +2683,7 @@ class PressableHandler(FlexContainerHandler):
|
|
|
2381
2683
|
_TAP_SLOP_DP = 12.0
|
|
2382
2684
|
|
|
2383
2685
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2384
|
-
fl =
|
|
2686
|
+
fl = _new_container()
|
|
2385
2687
|
fl.setClickable(True)
|
|
2386
2688
|
fl.setFocusable(True)
|
|
2387
2689
|
self._bind_press_stream(fl)
|
|
@@ -2410,6 +2712,8 @@ class PressableHandler(FlexContainerHandler):
|
|
|
2410
2712
|
|
|
2411
2713
|
class _PressTouchProxy(dynamic_proxy(OnTouchListener)):
|
|
2412
2714
|
def onTouch(self, view: Any, event: Any) -> bool:
|
|
2715
|
+
if _pointer_events_blocked(view):
|
|
2716
|
+
return False
|
|
2413
2717
|
state = _state_of(view)
|
|
2414
2718
|
_feed_motion_event(view, state, event)
|
|
2415
2719
|
action = int(event.getActionMasked())
|