pythonnative 0.20.0__py3-none-any.whl → 0.22.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 +14 -3
- pythonnative/animated.py +420 -135
- pythonnative/cli/pn.py +450 -956
- pythonnative/components.py +519 -235
- pythonnative/events.py +210 -0
- pythonnative/gestures.py +875 -0
- pythonnative/layout.py +463 -149
- pythonnative/mutations.py +130 -0
- pythonnative/native_views/__init__.py +161 -97
- pythonnative/native_views/android.py +1050 -1124
- pythonnative/native_views/base.py +108 -18
- pythonnative/native_views/desktop.py +460 -417
- pythonnative/native_views/ios.py +1918 -1916
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +504 -0
- pythonnative/project/builder.py +555 -0
- pythonnative/project/config.py +642 -0
- pythonnative/project/doctor.py +233 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +344 -0
- pythonnative/project/permissions.py +343 -0
- pythonnative/project/runtime_assets.py +272 -0
- pythonnative/reconciler.py +540 -470
- pythonnative/screen.py +5 -2
- pythonnative/sdk/_components.py +2 -2
- pythonnative/templates/android_template/app/build.gradle +2 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
pythonnative/animated.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""Animated values
|
|
1
|
+
"""Animated values, native-driven animation, and animated components.
|
|
2
2
|
|
|
3
|
-
Modeled on React Native's ``Animated`` API
|
|
4
|
-
|
|
3
|
+
Modeled on React Native's ``Animated`` API with an ``async``-aware
|
|
4
|
+
completion contract. The core primitives are:
|
|
5
5
|
|
|
6
6
|
- [`AnimatedValue`][pythonnative.animated.AnimatedValue]: a numeric
|
|
7
|
-
cell
|
|
7
|
+
cell attached to native view properties; animations drive it over
|
|
8
|
+
time.
|
|
8
9
|
- ``Animated.timing`` / ``Animated.spring`` / ``Animated.decay``:
|
|
9
10
|
animation factories. The objects they return implement
|
|
10
11
|
``__await__``, so you can write ``await Animated.timing(v, to=1.0)``
|
|
@@ -14,14 +15,24 @@ Modeled on React Native's ``Animated`` API but with an
|
|
|
14
15
|
- ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``:
|
|
15
16
|
components whose ``style`` may contain ``AnimatedValue`` instances.
|
|
16
17
|
|
|
17
|
-
Driver:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
Driver architecture (the **native driver**):
|
|
19
|
+
|
|
20
|
+
When an animation starts, PythonNative compiles its spec (curve,
|
|
21
|
+
duration, target value) and offers it to the platform handler of every
|
|
22
|
+
native view the value is attached to
|
|
23
|
+
([`ViewHandler.start_animation`][pythonnative.native_views.base.ViewHandler.start_animation]).
|
|
24
|
+
|
|
25
|
+
- **Accepted** (iOS Core Animation, Android ``ViewPropertyAnimator`` /
|
|
26
|
+
``DynamicAnimation``): the platform animates the property entirely
|
|
27
|
+
natively — no Python code runs per frame. Python receives exactly one
|
|
28
|
+
callback when the animation settles, updates the
|
|
29
|
+
[`AnimatedValue`][pythonnative.animated.AnimatedValue], and resolves
|
|
30
|
+
any awaiting tasks.
|
|
31
|
+
- **Declined** (desktop preview, unattached values, callable easings,
|
|
32
|
+
values feeding Python-side listeners): a single background thread
|
|
33
|
+
ticks the animation at ~60 Hz from Python, pushing each frame through
|
|
34
|
+
``set_animated_property``. Semantics are identical; only the frame
|
|
35
|
+
source differs.
|
|
25
36
|
|
|
26
37
|
Example:
|
|
27
38
|
```python
|
|
@@ -48,6 +59,7 @@ Example:
|
|
|
48
59
|
from __future__ import annotations
|
|
49
60
|
|
|
50
61
|
import asyncio
|
|
62
|
+
import itertools
|
|
51
63
|
import math
|
|
52
64
|
import threading
|
|
53
65
|
import time
|
|
@@ -58,11 +70,13 @@ from .hooks import use_effect, use_ref
|
|
|
58
70
|
from .runtime import resolve_future
|
|
59
71
|
from .style import StyleProp, resolve_style
|
|
60
72
|
|
|
61
|
-
# Maximum frame rate at which the Python ticker drives
|
|
73
|
+
# Maximum frame rate at which the Python fallback ticker drives
|
|
74
|
+
# animations (native-driven animations run at the display's refresh
|
|
75
|
+
# rate, managed by the platform).
|
|
62
76
|
_TARGET_FPS = 60.0
|
|
63
77
|
_FRAME_DT = 1.0 / _TARGET_FPS
|
|
64
78
|
|
|
65
|
-
# Upper bound on how much wall-clock time the
|
|
79
|
+
# Upper bound on how much wall-clock time the fallback loop will try to
|
|
66
80
|
# catch up on in a single iteration after thread starvation. At 60 fps
|
|
67
81
|
# this is ~333 ms of simulated motion; further drift is dropped to keep
|
|
68
82
|
# the loop responsive.
|
|
@@ -98,30 +112,48 @@ def _resolve_easing(name: Any) -> Callable[[float], float]:
|
|
|
98
112
|
return _EASINGS.get(str(name), _EASINGS["ease_in_out"])
|
|
99
113
|
|
|
100
114
|
|
|
115
|
+
def _backend() -> Any:
|
|
116
|
+
"""Return the active native-view registry (the animation backend)."""
|
|
117
|
+
from .native_views import get_registry
|
|
118
|
+
|
|
119
|
+
return get_registry()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Process-unique ids for native animations, so completion callbacks can
|
|
123
|
+
# be routed without holding references on the native side.
|
|
124
|
+
_anim_id_counter = itertools.count(1)
|
|
125
|
+
|
|
126
|
+
|
|
101
127
|
# ======================================================================
|
|
102
128
|
# AnimatedValue
|
|
103
129
|
# ======================================================================
|
|
104
130
|
|
|
105
131
|
|
|
106
132
|
class AnimatedValue:
|
|
107
|
-
"""A
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
"""A numeric cell that can be attached to native view properties.
|
|
134
|
+
|
|
135
|
+
Animated components (``Animated.View`` et al.) **attach** the value
|
|
136
|
+
to ``(tag, prop)`` bindings after mount. Setting the value pushes
|
|
137
|
+
the new number to every attached native view through the registry's
|
|
138
|
+
``set_animated_property`` — and when an animation can be driven
|
|
139
|
+
natively, the platform animates those same bindings directly.
|
|
140
|
+
|
|
141
|
+
Python-side listeners registered via
|
|
142
|
+
[`add_listener`][pythonnative.animated.AnimatedValue.add_listener]
|
|
143
|
+
observe every Python-driven change. Natively-driven animations
|
|
144
|
+
intentionally skip per-frame Python callbacks (that's the point);
|
|
145
|
+
listeners see the final settled value.
|
|
117
146
|
"""
|
|
118
147
|
|
|
119
|
-
__slots__ = ("_value", "_subscribers", "_lock")
|
|
148
|
+
__slots__ = ("_value", "_subscribers", "_attachments", "_lock", "_native_group")
|
|
120
149
|
|
|
121
150
|
def __init__(self, initial: float = 0.0) -> None:
|
|
122
151
|
self._value = float(initial)
|
|
123
152
|
self._subscribers: List[Tuple[str, Callable[[float], None]]] = []
|
|
153
|
+
self._attachments: List[Tuple[int, str]] = []
|
|
124
154
|
self._lock = threading.Lock()
|
|
155
|
+
# The in-flight native animation group driving this value, if any.
|
|
156
|
+
self._native_group: Optional["_NativeAnimationGroup"] = None
|
|
125
157
|
|
|
126
158
|
@property
|
|
127
159
|
def value(self) -> float:
|
|
@@ -129,23 +161,65 @@ class AnimatedValue:
|
|
|
129
161
|
return self._value
|
|
130
162
|
|
|
131
163
|
def set_value(self, new_value: float) -> None:
|
|
132
|
-
"""Set the value immediately
|
|
133
|
-
|
|
164
|
+
"""Set the value immediately, pushing to native views and listeners."""
|
|
165
|
+
self._apply(float(new_value), push_native=True)
|
|
166
|
+
|
|
167
|
+
def _apply(self, new_value: float, push_native: bool) -> None:
|
|
134
168
|
with self._lock:
|
|
135
169
|
self._value = new_value
|
|
136
170
|
subs = list(self._subscribers)
|
|
171
|
+
attachments = list(self._attachments)
|
|
172
|
+
if push_native and attachments:
|
|
173
|
+
try:
|
|
174
|
+
backend = _backend()
|
|
175
|
+
for tag, prop in attachments:
|
|
176
|
+
backend.set_animated_property(tag, prop, new_value)
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
137
179
|
for prop, cb in subs:
|
|
138
180
|
try:
|
|
139
181
|
cb(new_value)
|
|
140
182
|
except Exception:
|
|
141
183
|
pass
|
|
142
184
|
|
|
185
|
+
# -- bindings ------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def attach(self, tag: int, prop: str) -> Callable[[], None]:
|
|
188
|
+
"""Bind this value to ``prop`` of the native view under ``tag``.
|
|
189
|
+
|
|
190
|
+
The current value is pushed immediately so the view reflects it
|
|
191
|
+
even if no animation is running. Returns a detach callable.
|
|
192
|
+
"""
|
|
193
|
+
binding = (tag, prop)
|
|
194
|
+
with self._lock:
|
|
195
|
+
self._attachments.append(binding)
|
|
196
|
+
try:
|
|
197
|
+
_backend().set_animated_property(tag, prop, self._value)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
def _detach() -> None:
|
|
202
|
+
with self._lock:
|
|
203
|
+
try:
|
|
204
|
+
self._attachments.remove(binding)
|
|
205
|
+
except ValueError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
return _detach
|
|
209
|
+
|
|
210
|
+
def attachments(self) -> List[Tuple[int, str]]:
|
|
211
|
+
"""Snapshot of the current ``(tag, prop)`` bindings."""
|
|
212
|
+
with self._lock:
|
|
213
|
+
return list(self._attachments)
|
|
214
|
+
|
|
215
|
+
# -- listeners -----------------------------------------------------
|
|
216
|
+
|
|
143
217
|
def add_listener(self, prop: str, callback: Callable[[float], None]) -> Callable[[], None]:
|
|
144
|
-
"""Register ``callback`` for changes to this value.
|
|
218
|
+
"""Register ``callback`` for Python-driven changes to this value.
|
|
145
219
|
|
|
146
|
-
Returns an unsubscribe callable. ``prop`` is metadata only —
|
|
147
|
-
|
|
148
|
-
|
|
220
|
+
Returns an unsubscribe callable. ``prop`` is metadata only — it
|
|
221
|
+
lets the subscriber differentiate this binding from others on
|
|
222
|
+
the same ``AnimatedValue``.
|
|
149
223
|
"""
|
|
150
224
|
with self._lock:
|
|
151
225
|
self._subscribers.append((prop, callback))
|
|
@@ -159,6 +233,24 @@ class AnimatedValue:
|
|
|
159
233
|
|
|
160
234
|
return _unsubscribe
|
|
161
235
|
|
|
236
|
+
def has_listeners(self) -> bool:
|
|
237
|
+
"""Whether any Python-side listeners are registered."""
|
|
238
|
+
with self._lock:
|
|
239
|
+
return bool(self._subscribers)
|
|
240
|
+
|
|
241
|
+
# -- native handoff ------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def _adopt_native_group(self, group: Optional["_NativeAnimationGroup"]) -> None:
|
|
244
|
+
previous = self._native_group
|
|
245
|
+
self._native_group = group
|
|
246
|
+
if previous is not None and previous is not group:
|
|
247
|
+
previous.cancel()
|
|
248
|
+
|
|
249
|
+
def stop_animation(self) -> None:
|
|
250
|
+
"""Cancel any in-flight animation on this value (native or Python)."""
|
|
251
|
+
self._adopt_native_group(None)
|
|
252
|
+
_manager.cancel_for_value(self)
|
|
253
|
+
|
|
162
254
|
def __float__(self) -> float:
|
|
163
255
|
return self._value
|
|
164
256
|
|
|
@@ -167,16 +259,16 @@ class AnimatedValue:
|
|
|
167
259
|
|
|
168
260
|
|
|
169
261
|
# ======================================================================
|
|
170
|
-
#
|
|
262
|
+
# Python fallback driver
|
|
171
263
|
# ======================================================================
|
|
172
264
|
|
|
173
265
|
|
|
174
266
|
class _AnimationManager:
|
|
175
|
-
"""Single-threaded driver for
|
|
267
|
+
"""Single-threaded fallback driver for Python-ticked animations.
|
|
176
268
|
|
|
177
269
|
Holds a list of ``_RunningAnimation`` instances and ticks them at
|
|
178
270
|
~60 Hz. The thread starts on first use and idles when nothing is
|
|
179
|
-
active.
|
|
271
|
+
active. Native-driven animations never touch this loop.
|
|
180
272
|
"""
|
|
181
273
|
|
|
182
274
|
def __init__(self) -> None:
|
|
@@ -197,6 +289,15 @@ class _AnimationManager:
|
|
|
197
289
|
except ValueError:
|
|
198
290
|
pass
|
|
199
291
|
|
|
292
|
+
def cancel_for_value(self, value: AnimatedValue) -> None:
|
|
293
|
+
"""Cancel every queued/running Python-driven animation on ``value``."""
|
|
294
|
+
with self._lock:
|
|
295
|
+
stale = [a for a in self._animations if a.value is value]
|
|
296
|
+
for anim in stale:
|
|
297
|
+
self._animations.remove(anim)
|
|
298
|
+
for anim in stale:
|
|
299
|
+
anim._finish()
|
|
300
|
+
|
|
200
301
|
def _ensure_thread_locked(self) -> None:
|
|
201
302
|
if self._thread is not None and self._thread.is_alive():
|
|
202
303
|
return
|
|
@@ -207,16 +308,16 @@ class _AnimationManager:
|
|
|
207
308
|
last = time.monotonic()
|
|
208
309
|
# Clamping the per-tick dt is important for numerical stability:
|
|
209
310
|
# an underdamped spring with a 0.3 s step explodes immediately,
|
|
210
|
-
# and
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
311
|
+
# and the animation thread can be starved for several frames
|
|
312
|
+
# during render bursts. We integrate physics on a clamped dt
|
|
313
|
+
# (max 2 target frames) and sub-step when wall-clock has
|
|
314
|
+
# advanced more than that, so the perceived motion still tracks
|
|
315
|
+
# real time at most a couple of frames behind. After an extreme
|
|
316
|
+
# starvation (e.g. the app was backgrounded for seconds) we cap
|
|
317
|
+
# the catch-up at ``_MAX_CATCHUP_FRAMES`` worth of physics; any
|
|
318
|
+
# further wall-clock drift is dropped on the floor, which keeps
|
|
319
|
+
# the loop responsive instead of spinning forward through
|
|
320
|
+
# hundreds of substeps.
|
|
220
321
|
max_step = _FRAME_DT * 2.0
|
|
221
322
|
max_catchup = _FRAME_DT * _MAX_CATCHUP_FRAMES
|
|
222
323
|
while not self._stopped:
|
|
@@ -249,12 +350,12 @@ _manager = _AnimationManager()
|
|
|
249
350
|
|
|
250
351
|
|
|
251
352
|
# ======================================================================
|
|
252
|
-
#
|
|
353
|
+
# Python-driven animation primitives (the fallback path)
|
|
253
354
|
# ======================================================================
|
|
254
355
|
|
|
255
356
|
|
|
256
357
|
class _RunningAnimation:
|
|
257
|
-
"""Base class for
|
|
358
|
+
"""Base class for Python-ticked animations; ``advance()`` returns True when done."""
|
|
258
359
|
|
|
259
360
|
def __init__(self, value: AnimatedValue) -> None:
|
|
260
361
|
self.value = value
|
|
@@ -315,10 +416,11 @@ class _SpringAnimation(_RunningAnimation):
|
|
|
315
416
|
stiffness: float,
|
|
316
417
|
damping: float,
|
|
317
418
|
mass: float,
|
|
419
|
+
initial_velocity: float = 0.0,
|
|
318
420
|
) -> None:
|
|
319
421
|
super().__init__(value)
|
|
320
422
|
self._to = float(to)
|
|
321
|
-
self._velocity =
|
|
423
|
+
self._velocity = float(initial_velocity)
|
|
322
424
|
self._stiffness = float(stiffness)
|
|
323
425
|
self._damping = float(damping)
|
|
324
426
|
self._mass = float(mass)
|
|
@@ -368,6 +470,163 @@ class _DelayAnimation(_RunningAnimation):
|
|
|
368
470
|
return False
|
|
369
471
|
|
|
370
472
|
|
|
473
|
+
# ======================================================================
|
|
474
|
+
# Native-driven animation group
|
|
475
|
+
# ======================================================================
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class _NativeAnimationGroup:
|
|
479
|
+
"""One logical animation fanned out to N natively-animated views.
|
|
480
|
+
|
|
481
|
+
Each attached ``(tag, prop)`` binding gets its own ``anim_id``; the
|
|
482
|
+
group completes when the platform reports completion for all of
|
|
483
|
+
them. Cancellation asks each platform handler for the current
|
|
484
|
+
presentation value so the ``AnimatedValue`` lands wherever the view
|
|
485
|
+
visually was.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
def __init__(self, value: AnimatedValue, final_value: float) -> None:
|
|
489
|
+
self.value = value
|
|
490
|
+
self.final_value = final_value
|
|
491
|
+
self._targets: Dict[int, Tuple[int, str]] = {} # anim_id -> (tag, prop)
|
|
492
|
+
self._pending: set = set()
|
|
493
|
+
self._completion_futures: List[asyncio.Future[None]] = []
|
|
494
|
+
self._completed = False
|
|
495
|
+
self._lock = threading.Lock()
|
|
496
|
+
|
|
497
|
+
def add_target(self, anim_id: int, tag: int, prop: str) -> None:
|
|
498
|
+
with self._lock:
|
|
499
|
+
self._targets[anim_id] = (tag, prop)
|
|
500
|
+
self._pending.add(anim_id)
|
|
501
|
+
_native_groups[anim_id] = self
|
|
502
|
+
|
|
503
|
+
def add_completion_future(self, future: asyncio.Future[None]) -> None:
|
|
504
|
+
with self._lock:
|
|
505
|
+
done = self._completed
|
|
506
|
+
if not done:
|
|
507
|
+
self._completion_futures.append(future)
|
|
508
|
+
if done:
|
|
509
|
+
resolve_future(future, None)
|
|
510
|
+
|
|
511
|
+
def target_completed(self, anim_id: int, finished: bool) -> None:
|
|
512
|
+
with self._lock:
|
|
513
|
+
self._pending.discard(anim_id)
|
|
514
|
+
remaining = len(self._pending)
|
|
515
|
+
_native_groups.pop(anim_id, None)
|
|
516
|
+
if remaining == 0:
|
|
517
|
+
self._settle(self.final_value if finished else None)
|
|
518
|
+
|
|
519
|
+
def cancel(self) -> None:
|
|
520
|
+
"""Cancel all in-flight native animations, syncing to presentation values."""
|
|
521
|
+
with self._lock:
|
|
522
|
+
targets = dict(self._targets)
|
|
523
|
+
self._pending.clear()
|
|
524
|
+
presentation: Optional[float] = None
|
|
525
|
+
try:
|
|
526
|
+
backend = _backend()
|
|
527
|
+
for anim_id, (tag, _prop) in targets.items():
|
|
528
|
+
_native_groups.pop(anim_id, None)
|
|
529
|
+
current = backend.cancel_animation(tag, anim_id)
|
|
530
|
+
if current is not None:
|
|
531
|
+
try:
|
|
532
|
+
presentation = float(current)
|
|
533
|
+
except (TypeError, ValueError):
|
|
534
|
+
pass
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
self._settle(presentation)
|
|
538
|
+
|
|
539
|
+
def _settle(self, end_value: Optional[float]) -> None:
|
|
540
|
+
with self._lock:
|
|
541
|
+
if self._completed:
|
|
542
|
+
return
|
|
543
|
+
self._completed = True
|
|
544
|
+
futures = list(self._completion_futures)
|
|
545
|
+
self._completion_futures.clear()
|
|
546
|
+
if self.value._native_group is self:
|
|
547
|
+
self.value._native_group = None
|
|
548
|
+
if end_value is not None:
|
|
549
|
+
# The native side already shows this value; update the
|
|
550
|
+
# Python cell (and listeners) without re-pushing.
|
|
551
|
+
self.value._apply(end_value, push_native=False)
|
|
552
|
+
for fut in futures:
|
|
553
|
+
resolve_future(fut, None)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# anim_id -> group, for routing completion callbacks from platform handlers.
|
|
557
|
+
_native_groups: Dict[int, _NativeAnimationGroup] = {}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def native_animation_completed(anim_id: int, finished: bool = True) -> None:
|
|
561
|
+
"""Report a natively-driven animation as settled.
|
|
562
|
+
|
|
563
|
+
Called by platform handlers from their completion callbacks (iOS
|
|
564
|
+
``UIView`` completion blocks, Android ``withEndAction`` /
|
|
565
|
+
``DynamicAnimation.OnAnimationEndListener``). Safe to call from any
|
|
566
|
+
thread; unknown ids are ignored (e.g. an animation cancelled
|
|
567
|
+
moments before its completion fired).
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
anim_id: The id passed to ``ViewHandler.start_animation``.
|
|
571
|
+
finished: ``False`` when the platform reports the animation was
|
|
572
|
+
interrupted rather than running to completion.
|
|
573
|
+
"""
|
|
574
|
+
group = _native_groups.get(anim_id)
|
|
575
|
+
if group is not None:
|
|
576
|
+
group.target_completed(anim_id, finished)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _projected_final_value(spec: Dict[str, Any]) -> float:
|
|
580
|
+
"""Compute where an animation will settle, from its spec."""
|
|
581
|
+
kind = spec.get("kind")
|
|
582
|
+
if kind == "decay":
|
|
583
|
+
# v(t) = v0 · e^(−k·1000·t) ⇒ ∫v dt = v0 / (k·1000)
|
|
584
|
+
v0 = float(spec.get("velocity", 0.0))
|
|
585
|
+
k = max(1e-6, float(spec.get("deceleration", 0.997)))
|
|
586
|
+
return float(spec.get("from", 0.0)) + v0 / (k * 1000.0)
|
|
587
|
+
return float(spec.get("to", spec.get("from", 0.0)))
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _start_native(value: AnimatedValue, spec: Dict[str, Any]) -> Optional[_NativeAnimationGroup]:
|
|
591
|
+
"""Offer ``spec`` to the platform for every binding of ``value``.
|
|
592
|
+
|
|
593
|
+
Returns the live group when **all** bindings accepted the native
|
|
594
|
+
animation; otherwise rolls back any accepted targets and returns
|
|
595
|
+
``None`` so the caller falls back to the Python ticker.
|
|
596
|
+
"""
|
|
597
|
+
targets = value.attachments()
|
|
598
|
+
if not targets:
|
|
599
|
+
return None
|
|
600
|
+
if value.has_listeners():
|
|
601
|
+
# Python listeners want per-frame values; only the ticker
|
|
602
|
+
# provides those.
|
|
603
|
+
return None
|
|
604
|
+
try:
|
|
605
|
+
backend = _backend()
|
|
606
|
+
except Exception:
|
|
607
|
+
return None
|
|
608
|
+
|
|
609
|
+
group = _NativeAnimationGroup(value, _projected_final_value(spec))
|
|
610
|
+
accepted: List[Tuple[int, int]] = [] # (anim_id, tag)
|
|
611
|
+
for tag, prop in targets:
|
|
612
|
+
anim_id = next(_anim_id_counter)
|
|
613
|
+
try:
|
|
614
|
+
ok = backend.start_animation(tag, anim_id, prop, spec)
|
|
615
|
+
except Exception:
|
|
616
|
+
ok = False
|
|
617
|
+
if not ok:
|
|
618
|
+
for prev_id, prev_tag in accepted:
|
|
619
|
+
_native_groups.pop(prev_id, None)
|
|
620
|
+
try:
|
|
621
|
+
backend.cancel_animation(prev_tag, prev_id)
|
|
622
|
+
except Exception:
|
|
623
|
+
pass
|
|
624
|
+
return None
|
|
625
|
+
group.add_target(anim_id, tag, prop)
|
|
626
|
+
accepted.append((anim_id, tag))
|
|
627
|
+
return group
|
|
628
|
+
|
|
629
|
+
|
|
371
630
|
# ======================================================================
|
|
372
631
|
# Public animation handles
|
|
373
632
|
# ======================================================================
|
|
@@ -425,37 +684,66 @@ class _AwaitableAnimation:
|
|
|
425
684
|
class _AnimationHandle(_AwaitableAnimation):
|
|
426
685
|
"""Public handle returned by ``Animated.timing`` / ``.spring`` / ``.decay``.
|
|
427
686
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
``Animated.timing``
|
|
687
|
+
Each ``.start()`` call snapshots the value's current state, prefers
|
|
688
|
+
the native driver, and falls back to a fresh Python-ticked
|
|
689
|
+
animation otherwise (matches React Native — the ``Animated.timing``
|
|
690
|
+
return value is reusable).
|
|
431
691
|
"""
|
|
432
692
|
|
|
433
|
-
def __init__(
|
|
434
|
-
self
|
|
435
|
-
|
|
693
|
+
def __init__(
|
|
694
|
+
self,
|
|
695
|
+
value: Optional[AnimatedValue],
|
|
696
|
+
spec_factory: Callable[[], Dict[str, Any]],
|
|
697
|
+
fallback_factory: Callable[[], _RunningAnimation],
|
|
698
|
+
native_eligible: bool = True,
|
|
699
|
+
) -> None:
|
|
700
|
+
self._value = value
|
|
701
|
+
self._spec_factory = spec_factory
|
|
702
|
+
self._fallback_factory = fallback_factory
|
|
703
|
+
self._native_eligible = native_eligible
|
|
704
|
+
self._python_anim: Optional[_RunningAnimation] = None
|
|
705
|
+
self._native_group: Optional[_NativeAnimationGroup] = None
|
|
436
706
|
|
|
437
707
|
def start(self) -> "_AnimationHandle":
|
|
438
708
|
"""Begin the animation. Returns ``self`` for chaining."""
|
|
439
709
|
self.stop()
|
|
440
|
-
|
|
441
|
-
|
|
710
|
+
if self._value is not None and self._native_eligible:
|
|
711
|
+
spec = self._spec_factory()
|
|
712
|
+
group = _start_native(self._value, spec)
|
|
713
|
+
if group is not None:
|
|
714
|
+
self._native_group = group
|
|
715
|
+
self._value._adopt_native_group(group)
|
|
716
|
+
return self
|
|
717
|
+
anim = self._fallback_factory()
|
|
718
|
+
self._python_anim = anim
|
|
442
719
|
_manager.add(anim)
|
|
443
720
|
return self
|
|
444
721
|
|
|
445
722
|
def stop(self) -> None:
|
|
446
723
|
"""Cancel the running instance (no-op if not running)."""
|
|
447
|
-
if self.
|
|
448
|
-
self.
|
|
449
|
-
|
|
450
|
-
self.
|
|
724
|
+
if self._native_group is not None:
|
|
725
|
+
group = self._native_group
|
|
726
|
+
self._native_group = None
|
|
727
|
+
if self._value is not None and self._value._native_group is group:
|
|
728
|
+
self._value._native_group = None
|
|
729
|
+
group.cancel()
|
|
730
|
+
if self._python_anim is not None:
|
|
731
|
+
anim = self._python_anim
|
|
732
|
+
self._python_anim = None
|
|
733
|
+
anim._finish()
|
|
734
|
+
_manager.remove(anim)
|
|
451
735
|
|
|
452
736
|
async def _drive(self) -> None:
|
|
453
|
-
if self.
|
|
737
|
+
if self._native_group is None and self._python_anim is None:
|
|
454
738
|
self.start()
|
|
455
739
|
loop = asyncio.get_running_loop()
|
|
456
740
|
future: asyncio.Future[None] = loop.create_future()
|
|
457
|
-
|
|
458
|
-
|
|
741
|
+
if self._native_group is not None:
|
|
742
|
+
self._native_group.add_completion_future(future)
|
|
743
|
+
elif self._python_anim is not None:
|
|
744
|
+
self._python_anim.add_completion_future(future)
|
|
745
|
+
else:
|
|
746
|
+
return
|
|
459
747
|
await future
|
|
460
748
|
|
|
461
749
|
|
|
@@ -491,12 +779,9 @@ class _CompositeAnimation(_AwaitableAnimation):
|
|
|
491
779
|
async def _await_item(item: Any) -> None:
|
|
492
780
|
if item is None:
|
|
493
781
|
return
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
# Plain awaitables and coroutines are supported too — lets
|
|
498
|
-
# users mix in ``asyncio.sleep`` or other awaitables.
|
|
499
|
-
await item
|
|
782
|
+
# ``_AwaitableAnimation`` and plain awaitables/coroutines are
|
|
783
|
+
# both supported — lets users mix in ``asyncio.sleep``.
|
|
784
|
+
await item
|
|
500
785
|
|
|
501
786
|
|
|
502
787
|
# ======================================================================
|
|
@@ -509,7 +794,7 @@ def _resolve_style_with_values(style: StyleProp) -> Tuple[Dict[str, Any], Dict[s
|
|
|
509
794
|
|
|
510
795
|
AnimatedValue entries in the style are replaced with their current
|
|
511
796
|
numeric value in ``plain_style`` and recorded in
|
|
512
|
-
``animated_bindings`` so the wrapping component can
|
|
797
|
+
``animated_bindings`` so the wrapping component can attach them
|
|
513
798
|
after mount.
|
|
514
799
|
"""
|
|
515
800
|
flat = resolve_style(style)
|
|
@@ -537,36 +822,25 @@ def _make_animated_factory(
|
|
|
537
822
|
from .components import Text as _Text
|
|
538
823
|
from .components import View as _View
|
|
539
824
|
|
|
825
|
+
# ``@component`` packs positional children into the ``children``
|
|
826
|
+
# prop (this function declares ``*args``), and the reconciler
|
|
827
|
+
# re-invokes it with keyword props only — so at render time the
|
|
828
|
+
# payload arrives in ``kwargs``, never in ``args``.
|
|
829
|
+
children = list(args) or list(kwargs.pop("children", ()) or ())
|
|
830
|
+
|
|
540
831
|
style = kwargs.pop("style", None)
|
|
541
832
|
plain_style, bindings = _resolve_style_with_values(style)
|
|
542
833
|
|
|
543
834
|
ref = use_ref(None)
|
|
544
835
|
|
|
545
|
-
def
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if view is None:
|
|
836
|
+
def _attach_bindings() -> Callable[[], None]:
|
|
837
|
+
tag = ref.get("_pn_tag")
|
|
838
|
+
if tag is None:
|
|
549
839
|
return lambda: None
|
|
550
|
-
|
|
551
|
-
for prop, value in bindings.items():
|
|
552
|
-
|
|
553
|
-
def _on_change(new_val: float, _prop: str = prop, _view: Any = view) -> None:
|
|
554
|
-
handler = _get_handler_for(_view)
|
|
555
|
-
if handler is None:
|
|
556
|
-
return
|
|
557
|
-
setter = getattr(handler, "set_animated_property", None)
|
|
558
|
-
if setter is None:
|
|
559
|
-
return
|
|
560
|
-
try:
|
|
561
|
-
setter(_view, _animated_prop_name(_prop), new_val)
|
|
562
|
-
except Exception:
|
|
563
|
-
pass
|
|
564
|
-
|
|
565
|
-
unsub = value.add_listener(prop, _on_change)
|
|
566
|
-
unsubs.append(unsub)
|
|
840
|
+
detachers = [value.attach(tag, _animated_prop_name(prop)) for prop, value in bindings.items()]
|
|
567
841
|
|
|
568
842
|
def _cleanup() -> None:
|
|
569
|
-
for fn in
|
|
843
|
+
for fn in detachers:
|
|
570
844
|
try:
|
|
571
845
|
fn()
|
|
572
846
|
except Exception:
|
|
@@ -574,50 +848,27 @@ def _make_animated_factory(
|
|
|
574
848
|
|
|
575
849
|
return _cleanup
|
|
576
850
|
|
|
577
|
-
# Re-
|
|
578
|
-
use_effect(
|
|
851
|
+
# Re-attach whenever the binding set changes identity.
|
|
852
|
+
use_effect(_attach_bindings, [tuple(sorted((k, id(v)) for k, v in bindings.items()))])
|
|
579
853
|
|
|
580
854
|
if element_type == "Text":
|
|
581
|
-
text =
|
|
582
|
-
return _Text(text, style=plain_style, ref=ref)
|
|
855
|
+
text = children[0] if children else kwargs.pop("text", "")
|
|
856
|
+
return _Text(text, style=plain_style, ref=ref, **kwargs)
|
|
583
857
|
if element_type == "Image":
|
|
584
|
-
source =
|
|
585
|
-
return _Image(source, style=plain_style, ref=ref)
|
|
586
|
-
|
|
587
|
-
|
|
858
|
+
source = children[0] if children else kwargs.pop("source", "")
|
|
859
|
+
return _Image(source, style=plain_style, ref=ref, **kwargs)
|
|
860
|
+
if not accept_children:
|
|
861
|
+
children = []
|
|
862
|
+
return _View(*children, style=plain_style, ref=ref, **kwargs)
|
|
588
863
|
|
|
589
864
|
return _animated
|
|
590
865
|
|
|
591
866
|
|
|
592
867
|
def _animated_prop_name(prop: str) -> str:
|
|
593
868
|
"""Map a style key to the name expected by ``set_animated_property``."""
|
|
594
|
-
if prop == "opacity":
|
|
595
|
-
return "opacity"
|
|
596
|
-
if prop == "background_color":
|
|
597
|
-
return "background_color"
|
|
598
|
-
if prop in ("translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"):
|
|
599
|
-
return prop
|
|
600
869
|
return prop
|
|
601
870
|
|
|
602
871
|
|
|
603
|
-
def _get_handler_for(native_view: Any) -> Any:
|
|
604
|
-
"""Best-effort lookup of the registered handler for ``native_view``."""
|
|
605
|
-
del native_view
|
|
606
|
-
try:
|
|
607
|
-
from .native_views import get_registry
|
|
608
|
-
|
|
609
|
-
registry = get_registry()
|
|
610
|
-
handlers = getattr(registry, "_handlers", {})
|
|
611
|
-
handler = handlers.get("View")
|
|
612
|
-
if handler is not None:
|
|
613
|
-
return handler
|
|
614
|
-
if handlers:
|
|
615
|
-
return next(iter(handlers.values()))
|
|
616
|
-
return None
|
|
617
|
-
except Exception:
|
|
618
|
-
return None
|
|
619
|
-
|
|
620
|
-
|
|
621
872
|
# ======================================================================
|
|
622
873
|
# Public API
|
|
623
874
|
# ======================================================================
|
|
@@ -640,12 +891,22 @@ class _AnimatedNamespace:
|
|
|
640
891
|
duration: float = 300.0,
|
|
641
892
|
easing: Any = "ease_in_out",
|
|
642
893
|
) -> _AnimationHandle:
|
|
643
|
-
"""
|
|
644
|
-
|
|
645
|
-
def
|
|
894
|
+
"""Interpolate ``value`` to ``to`` over ``duration`` ms with ``easing``."""
|
|
895
|
+
|
|
896
|
+
def _spec() -> Dict[str, Any]:
|
|
897
|
+
return {
|
|
898
|
+
"kind": "timing",
|
|
899
|
+
"from": value.value,
|
|
900
|
+
"to": float(to),
|
|
901
|
+
"duration_ms": float(duration),
|
|
902
|
+
"easing": str(easing),
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
def _fallback() -> _RunningAnimation:
|
|
646
906
|
return _TimingAnimation(value, to, duration, _resolve_easing(easing))
|
|
647
907
|
|
|
648
|
-
|
|
908
|
+
# Callable easings can't cross the bridge; tick them in Python.
|
|
909
|
+
return _AnimationHandle(value, _spec, _fallback, native_eligible=not callable(easing))
|
|
649
910
|
|
|
650
911
|
@staticmethod
|
|
651
912
|
def spring(
|
|
@@ -655,13 +916,25 @@ class _AnimatedNamespace:
|
|
|
655
916
|
stiffness: float = 100.0,
|
|
656
917
|
damping: float = 10.0,
|
|
657
918
|
mass: float = 1.0,
|
|
919
|
+
initial_velocity: float = 0.0,
|
|
658
920
|
) -> _AnimationHandle:
|
|
659
921
|
"""Run a damped harmonic spring toward ``to``."""
|
|
660
922
|
|
|
661
|
-
def
|
|
662
|
-
return
|
|
923
|
+
def _spec() -> Dict[str, Any]:
|
|
924
|
+
return {
|
|
925
|
+
"kind": "spring",
|
|
926
|
+
"from": value.value,
|
|
927
|
+
"to": float(to),
|
|
928
|
+
"stiffness": float(stiffness),
|
|
929
|
+
"damping": float(damping),
|
|
930
|
+
"mass": float(mass),
|
|
931
|
+
"initial_velocity": float(initial_velocity),
|
|
932
|
+
}
|
|
663
933
|
|
|
664
|
-
|
|
934
|
+
def _fallback() -> _RunningAnimation:
|
|
935
|
+
return _SpringAnimation(value, to, stiffness, damping, mass, initial_velocity)
|
|
936
|
+
|
|
937
|
+
return _AnimationHandle(value, _spec, _fallback)
|
|
665
938
|
|
|
666
939
|
@staticmethod
|
|
667
940
|
def decay(
|
|
@@ -670,12 +943,20 @@ class _AnimatedNamespace:
|
|
|
670
943
|
velocity: float,
|
|
671
944
|
deceleration: float = 0.997,
|
|
672
945
|
) -> _AnimationHandle:
|
|
673
|
-
"""Decelerate ``value`` from
|
|
946
|
+
"""Decelerate ``value`` from ``velocity`` (units/ms) until it rests."""
|
|
947
|
+
|
|
948
|
+
def _spec() -> Dict[str, Any]:
|
|
949
|
+
return {
|
|
950
|
+
"kind": "decay",
|
|
951
|
+
"from": value.value,
|
|
952
|
+
"velocity": float(velocity),
|
|
953
|
+
"deceleration": float(deceleration),
|
|
954
|
+
}
|
|
674
955
|
|
|
675
|
-
def
|
|
956
|
+
def _fallback() -> _RunningAnimation:
|
|
676
957
|
return _DecayAnimation(value, velocity, deceleration)
|
|
677
958
|
|
|
678
|
-
return _AnimationHandle(
|
|
959
|
+
return _AnimationHandle(value, _spec, _fallback)
|
|
679
960
|
|
|
680
961
|
@staticmethod
|
|
681
962
|
def parallel(animations: List[Any]) -> _CompositeAnimation:
|
|
@@ -691,10 +972,13 @@ class _AnimatedNamespace:
|
|
|
691
972
|
def delay(duration: float) -> _AnimationHandle:
|
|
692
973
|
"""Wait ``duration`` ms before continuing in a sequence."""
|
|
693
974
|
|
|
694
|
-
def
|
|
975
|
+
def _spec() -> Dict[str, Any]:
|
|
976
|
+
return {"kind": "delay", "duration_ms": float(duration)}
|
|
977
|
+
|
|
978
|
+
def _fallback() -> _RunningAnimation:
|
|
695
979
|
return _DelayAnimation(duration)
|
|
696
980
|
|
|
697
|
-
return _AnimationHandle(
|
|
981
|
+
return _AnimationHandle(None, _spec, _fallback)
|
|
698
982
|
|
|
699
983
|
View = staticmethod(_make_animated_factory("View", accept_children=True))
|
|
700
984
|
Text = staticmethod(_make_animated_factory("Text", accept_children=False))
|
|
@@ -746,4 +1030,5 @@ __all__ = [
|
|
|
746
1030
|
"AnimatedValue",
|
|
747
1031
|
"Animated",
|
|
748
1032
|
"use_animated_value",
|
|
1033
|
+
"native_animation_completed",
|
|
749
1034
|
]
|