pythonnative 0.16.0__py3-none-any.whl → 0.17.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 +26 -2
- pythonnative/alerts.py +254 -68
- pythonnative/animated.py +191 -173
- pythonnative/hooks.py +271 -1
- pythonnative/native_modules/camera.py +118 -121
- pythonnative/native_modules/file_system.py +3 -3
- pythonnative/native_modules/location.py +90 -109
- pythonnative/native_modules/notifications.py +148 -126
- pythonnative/native_views/android.py +93 -59
- pythonnative/native_views/ios.py +65 -36
- pythonnative/net.py +244 -0
- pythonnative/runtime.py +487 -0
- pythonnative/storage.py +400 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.0.dist-info}/METADATA +3 -1
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.0.dist-info}/RECORD +19 -16
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.0.dist-info}/top_level.txt +0 -0
pythonnative/animated.py
CHANGED
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
"""Animated values + animation drivers + animated component wrappers.
|
|
2
2
|
|
|
3
|
-
Modeled on React Native's
|
|
3
|
+
Modeled on React Native's ``Animated`` API but with an
|
|
4
|
+
``async``-aware completion contract. The core primitives are:
|
|
4
5
|
|
|
5
6
|
- [`AnimatedValue`][pythonnative.animated.AnimatedValue]: a numeric
|
|
6
7
|
cell with subscribers; animations mutate it over time.
|
|
7
8
|
- ``Animated.timing`` / ``Animated.spring`` / ``Animated.decay``:
|
|
8
|
-
animation factories.
|
|
9
|
+
animation factories. The objects they return implement
|
|
10
|
+
``__await__``, so you can write ``await Animated.timing(v, to=1.0)``
|
|
11
|
+
to suspend until the animation finishes.
|
|
9
12
|
- ``Animated.sequence`` / ``Animated.parallel`` / ``Animated.delay``:
|
|
10
|
-
composition.
|
|
13
|
+
composition; also awaitable.
|
|
11
14
|
- ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``:
|
|
12
15
|
components whose ``style`` may contain ``AnimatedValue`` instances.
|
|
13
|
-
The component subscribes to the value during mount and forwards
|
|
14
|
-
changes directly to the underlying native handler's
|
|
15
|
-
``set_animated_property`` hook (bypassing the reconciler so
|
|
16
|
-
per-frame work doesn't go through full Python reconciliation).
|
|
17
16
|
|
|
18
17
|
Driver:
|
|
19
18
|
|
|
20
|
-
- A single background thread ticks at ~60 Hz, advancing every
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
``set_animated_property(view, prop, target, duration_ms, easing)``
|
|
27
|
-
call when the animation starts, so UIKit / Android can interpolate
|
|
28
|
-
at GPU 60 Hz without per-frame Python work. The Python ticker
|
|
29
|
-
then keeps the reactive ``AnimatedValue.value`` reading
|
|
30
|
-
approximately synchronized for any non-native consumers.
|
|
19
|
+
- A single background thread ticks at ~60 Hz, advancing every active
|
|
20
|
+
animation by ``dt``.
|
|
21
|
+
- Animations expose two APIs:
|
|
22
|
+
- ``handle.start()`` — fire-and-forget. Returns ``self``.
|
|
23
|
+
- ``await handle`` (or ``await handle.run()``) — wait for the
|
|
24
|
+
animation to complete; cancellation cancels the animation.
|
|
31
25
|
|
|
32
26
|
Example:
|
|
33
27
|
```python
|
|
34
28
|
import pythonnative as pn
|
|
35
29
|
|
|
30
|
+
|
|
36
31
|
@pn.component
|
|
37
32
|
def FadeIn():
|
|
38
|
-
opacity = pn.
|
|
33
|
+
opacity = pn.use_animated_value(0.0)
|
|
39
34
|
|
|
40
|
-
def fade_in():
|
|
41
|
-
pn.Animated.timing(opacity, to=1.0, duration=400)
|
|
35
|
+
async def fade_in():
|
|
36
|
+
await pn.Animated.timing(opacity, to=1.0, duration=400)
|
|
37
|
+
await pn.Animated.timing(opacity, to=0.5, duration=200)
|
|
42
38
|
|
|
43
|
-
pn.
|
|
39
|
+
pn.use_async_effect(fade_in, [])
|
|
44
40
|
|
|
45
41
|
return pn.Animated.View(
|
|
46
42
|
pn.Text("Hello!"),
|
|
@@ -51,6 +47,7 @@ Example:
|
|
|
51
47
|
|
|
52
48
|
from __future__ import annotations
|
|
53
49
|
|
|
50
|
+
import asyncio
|
|
54
51
|
import math
|
|
55
52
|
import threading
|
|
56
53
|
import time
|
|
@@ -58,14 +55,13 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
58
55
|
|
|
59
56
|
from .element import Element
|
|
60
57
|
from .hooks import use_effect, use_ref
|
|
58
|
+
from .runtime import resolve_future
|
|
61
59
|
from .style import StyleProp, resolve_style
|
|
62
60
|
|
|
63
61
|
# Maximum frame rate at which the Python ticker drives animations.
|
|
64
|
-
# We aim for 60 Hz but back off when no animation is active.
|
|
65
62
|
_TARGET_FPS = 60.0
|
|
66
63
|
_FRAME_DT = 1.0 / _TARGET_FPS
|
|
67
64
|
|
|
68
|
-
# Easing functions: t in [0, 1] -> [0, 1].
|
|
69
65
|
_EASINGS: Dict[str, Callable[[float], float]] = {
|
|
70
66
|
"linear": lambda t: t,
|
|
71
67
|
"ease_in": lambda t: t * t,
|
|
@@ -104,13 +100,14 @@ def _resolve_easing(name: Any) -> Callable[[float], float]:
|
|
|
104
100
|
class AnimatedValue:
|
|
105
101
|
"""A subscribable numeric cell driven by animations.
|
|
106
102
|
|
|
107
|
-
Direct mutation via
|
|
108
|
-
|
|
103
|
+
Direct mutation via
|
|
104
|
+
[`set_value`][pythonnative.animated.AnimatedValue.set_value]
|
|
105
|
+
fires subscribers immediately; animations call ``set_value`` from
|
|
109
106
|
the ticker thread.
|
|
110
107
|
|
|
111
108
|
Subscribers are ``(prop_name, callback)`` tuples. Each animated
|
|
112
|
-
component (e.g.,
|
|
113
|
-
AnimatedValue prop in its style during mount.
|
|
109
|
+
component (e.g., ``Animated.View``) subscribes once per
|
|
110
|
+
``AnimatedValue`` prop in its style during mount.
|
|
114
111
|
"""
|
|
115
112
|
|
|
116
113
|
__slots__ = ("_value", "_subscribers", "_lock")
|
|
@@ -126,11 +123,7 @@ class AnimatedValue:
|
|
|
126
123
|
return self._value
|
|
127
124
|
|
|
128
125
|
def set_value(self, new_value: float) -> None:
|
|
129
|
-
"""Set the value immediately and fire all subscribers.
|
|
130
|
-
|
|
131
|
-
Used by user code for instant snaps; animations also call this
|
|
132
|
-
once per tick to update the value.
|
|
133
|
-
"""
|
|
126
|
+
"""Set the value immediately and fire all subscribers."""
|
|
134
127
|
new_value = float(new_value)
|
|
135
128
|
with self._lock:
|
|
136
129
|
self._value = new_value
|
|
@@ -146,8 +139,7 @@ class AnimatedValue:
|
|
|
146
139
|
|
|
147
140
|
Returns an unsubscribe callable. ``prop`` is metadata only —
|
|
148
141
|
it lets the subscriber differentiate this binding from others
|
|
149
|
-
on the same AnimatedValue
|
|
150
|
-
multiple props on multiple views).
|
|
142
|
+
on the same ``AnimatedValue``.
|
|
151
143
|
"""
|
|
152
144
|
with self._lock:
|
|
153
145
|
self._subscribers.append((prop, callback))
|
|
@@ -176,9 +168,9 @@ class AnimatedValue:
|
|
|
176
168
|
class _AnimationManager:
|
|
177
169
|
"""Single-threaded driver for all currently-running animations.
|
|
178
170
|
|
|
179
|
-
Holds a list of ``
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
Holds a list of ``_RunningAnimation`` instances and ticks them at
|
|
172
|
+
~60 Hz. The thread starts on first use and idles when nothing is
|
|
173
|
+
active.
|
|
182
174
|
"""
|
|
183
175
|
|
|
184
176
|
def __init__(self) -> None:
|
|
@@ -214,7 +206,6 @@ class _AnimationManager:
|
|
|
214
206
|
with self._lock:
|
|
215
207
|
active = list(self._animations)
|
|
216
208
|
if not active:
|
|
217
|
-
# Idle: sleep longer until something starts.
|
|
218
209
|
time.sleep(0.05)
|
|
219
210
|
last = time.monotonic()
|
|
220
211
|
continue
|
|
@@ -237,21 +228,28 @@ _manager = _AnimationManager()
|
|
|
237
228
|
|
|
238
229
|
|
|
239
230
|
class _RunningAnimation:
|
|
240
|
-
"""Base class for in-flight animations; advance() returns True when done."""
|
|
231
|
+
"""Base class for in-flight animations; ``advance()`` returns True when done."""
|
|
241
232
|
|
|
242
|
-
def __init__(self, value: AnimatedValue
|
|
233
|
+
def __init__(self, value: AnimatedValue) -> None:
|
|
243
234
|
self.value = value
|
|
244
|
-
self.
|
|
235
|
+
self._completion_futures: List[asyncio.Future[None]] = []
|
|
236
|
+
self._completed = False
|
|
237
|
+
|
|
238
|
+
def add_completion_future(self, future: asyncio.Future[None]) -> None:
|
|
239
|
+
"""Register ``future`` to be resolved when the animation ends."""
|
|
240
|
+
self._completion_futures.append(future)
|
|
241
|
+
if self._completed:
|
|
242
|
+
resolve_future(future, None)
|
|
245
243
|
|
|
246
244
|
def advance(self, dt: float) -> bool:
|
|
247
245
|
raise NotImplementedError
|
|
248
246
|
|
|
249
247
|
def _finish(self) -> None:
|
|
250
|
-
if self.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
248
|
+
if self._completed:
|
|
249
|
+
return
|
|
250
|
+
self._completed = True
|
|
251
|
+
for fut in self._completion_futures:
|
|
252
|
+
resolve_future(fut, None)
|
|
255
253
|
|
|
256
254
|
|
|
257
255
|
class _TimingAnimation(_RunningAnimation):
|
|
@@ -261,9 +259,8 @@ class _TimingAnimation(_RunningAnimation):
|
|
|
261
259
|
to: float,
|
|
262
260
|
duration: float,
|
|
263
261
|
easing: Callable[[float], float],
|
|
264
|
-
on_complete: Optional[Callable[[], None]],
|
|
265
262
|
) -> None:
|
|
266
|
-
super().__init__(value
|
|
263
|
+
super().__init__(value)
|
|
267
264
|
self._from = value.value
|
|
268
265
|
self._to = float(to)
|
|
269
266
|
self._duration = max(0.001, float(duration) / 1000.0)
|
|
@@ -292,9 +289,8 @@ class _SpringAnimation(_RunningAnimation):
|
|
|
292
289
|
stiffness: float,
|
|
293
290
|
damping: float,
|
|
294
291
|
mass: float,
|
|
295
|
-
on_complete: Optional[Callable[[], None]],
|
|
296
292
|
) -> None:
|
|
297
|
-
super().__init__(value
|
|
293
|
+
super().__init__(value)
|
|
298
294
|
self._to = float(to)
|
|
299
295
|
self._velocity = 0.0
|
|
300
296
|
self._stiffness = float(stiffness)
|
|
@@ -316,20 +312,13 @@ class _SpringAnimation(_RunningAnimation):
|
|
|
316
312
|
|
|
317
313
|
|
|
318
314
|
class _DecayAnimation(_RunningAnimation):
|
|
319
|
-
def __init__(
|
|
320
|
-
|
|
321
|
-
value: AnimatedValue,
|
|
322
|
-
velocity: float,
|
|
323
|
-
deceleration: float,
|
|
324
|
-
on_complete: Optional[Callable[[], None]],
|
|
325
|
-
) -> None:
|
|
326
|
-
super().__init__(value, on_complete)
|
|
315
|
+
def __init__(self, value: AnimatedValue, velocity: float, deceleration: float) -> None:
|
|
316
|
+
super().__init__(value)
|
|
327
317
|
self._velocity = float(velocity)
|
|
328
318
|
self._deceleration = float(deceleration)
|
|
329
319
|
self._rest_threshold = 0.001
|
|
330
320
|
|
|
331
321
|
def advance(self, dt: float) -> bool:
|
|
332
|
-
# Exponential decay of velocity.
|
|
333
322
|
self._velocity *= math.exp(-self._deceleration * dt * 1000.0)
|
|
334
323
|
new_x = self.value.value + self._velocity * dt
|
|
335
324
|
self.value.set_value(new_x)
|
|
@@ -339,94 +328,150 @@ class _DecayAnimation(_RunningAnimation):
|
|
|
339
328
|
return False
|
|
340
329
|
|
|
341
330
|
|
|
342
|
-
class
|
|
343
|
-
|
|
331
|
+
class _DelayAnimation(_RunningAnimation):
|
|
332
|
+
def __init__(self, duration_ms: float) -> None:
|
|
333
|
+
super().__init__(AnimatedValue(0.0))
|
|
334
|
+
self._elapsed = 0.0
|
|
335
|
+
self._duration = max(0.001, duration_ms / 1000.0)
|
|
344
336
|
|
|
345
|
-
def
|
|
346
|
-
self.
|
|
347
|
-
self.
|
|
337
|
+
def advance(self, dt: float) -> bool:
|
|
338
|
+
self._elapsed += dt
|
|
339
|
+
if self._elapsed >= self._duration:
|
|
340
|
+
self._finish()
|
|
341
|
+
return True
|
|
342
|
+
return False
|
|
348
343
|
|
|
349
|
-
def start(self, on_complete: Optional[Callable[[], None]] = None) -> None:
|
|
350
|
-
if self._mode == "parallel":
|
|
351
|
-
remaining = [len(self._items)]
|
|
352
|
-
lock = threading.Lock()
|
|
353
|
-
|
|
354
|
-
def _one_done() -> None:
|
|
355
|
-
with lock:
|
|
356
|
-
remaining[0] -= 1
|
|
357
|
-
if remaining[0] <= 0 and on_complete is not None:
|
|
358
|
-
try:
|
|
359
|
-
on_complete()
|
|
360
|
-
except Exception:
|
|
361
|
-
pass
|
|
362
|
-
|
|
363
|
-
for item in self._items:
|
|
364
|
-
if item is None:
|
|
365
|
-
_one_done()
|
|
366
|
-
continue
|
|
367
|
-
try:
|
|
368
|
-
item.start(_one_done)
|
|
369
|
-
except Exception:
|
|
370
|
-
_one_done()
|
|
371
|
-
return
|
|
372
344
|
|
|
373
|
-
|
|
374
|
-
|
|
345
|
+
# ======================================================================
|
|
346
|
+
# Public animation handles
|
|
347
|
+
# ======================================================================
|
|
375
348
|
|
|
376
|
-
def _next() -> None:
|
|
377
|
-
i = index[0]
|
|
378
|
-
if i >= len(self._items):
|
|
379
|
-
if on_complete is not None:
|
|
380
|
-
try:
|
|
381
|
-
on_complete()
|
|
382
|
-
except Exception:
|
|
383
|
-
pass
|
|
384
|
-
return
|
|
385
|
-
item = self._items[i]
|
|
386
|
-
index[0] += 1
|
|
387
|
-
if item is None:
|
|
388
|
-
_next()
|
|
389
|
-
return
|
|
390
|
-
try:
|
|
391
|
-
item.start(_next)
|
|
392
|
-
except Exception:
|
|
393
|
-
_next()
|
|
394
349
|
|
|
395
|
-
|
|
350
|
+
class _AwaitableAnimation:
|
|
351
|
+
"""Base for awaitable animation handles.
|
|
352
|
+
|
|
353
|
+
Subclasses implement :meth:`start` and :meth:`stop`. Awaiting the
|
|
354
|
+
handle (``await handle``) starts the animation if necessary and
|
|
355
|
+
suspends until it completes. Cancelling the awaiting task calls
|
|
356
|
+
:meth:`stop`.
|
|
357
|
+
|
|
358
|
+
Calling :meth:`start` returns ``self`` so handles can be chained
|
|
359
|
+
or stashed: ``handle = pn.Animated.timing(...).start()``.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
def start(self) -> "_AwaitableAnimation":
|
|
363
|
+
raise NotImplementedError
|
|
396
364
|
|
|
397
365
|
def stop(self) -> None:
|
|
398
|
-
|
|
366
|
+
raise NotImplementedError
|
|
367
|
+
|
|
368
|
+
def run(self) -> "_AwaitableAnimation":
|
|
369
|
+
"""Return ``self`` for explicit ``await handle.run()`` style.
|
|
370
|
+
|
|
371
|
+
Equivalent to ``await handle`` directly; provided because some
|
|
372
|
+
readers prefer the slightly more explicit form, particularly
|
|
373
|
+
when storing the awaitable before resolving it.
|
|
374
|
+
"""
|
|
375
|
+
return self
|
|
376
|
+
|
|
377
|
+
async def _drive(self) -> None:
|
|
378
|
+
raise NotImplementedError
|
|
379
|
+
|
|
380
|
+
def __await__(self) -> Any:
|
|
381
|
+
try:
|
|
382
|
+
asyncio.get_running_loop()
|
|
383
|
+
except RuntimeError as exc:
|
|
384
|
+
raise RuntimeError(
|
|
385
|
+
"Animations can only be awaited from inside an asyncio task; "
|
|
386
|
+
"use handle.start() to fire-and-forget instead."
|
|
387
|
+
) from exc
|
|
388
|
+
|
|
389
|
+
async def _runner() -> None:
|
|
399
390
|
try:
|
|
400
|
-
|
|
401
|
-
except
|
|
402
|
-
|
|
391
|
+
await self._drive()
|
|
392
|
+
except asyncio.CancelledError:
|
|
393
|
+
self.stop()
|
|
394
|
+
raise
|
|
403
395
|
|
|
396
|
+
return _runner().__await__()
|
|
404
397
|
|
|
405
|
-
class _AnimationHandle:
|
|
406
|
-
"""Public handle returned by `Animated.timing` / `.spring` / `.decay`.
|
|
407
398
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
399
|
+
class _AnimationHandle(_AwaitableAnimation):
|
|
400
|
+
"""Public handle returned by ``Animated.timing`` / ``.spring`` / ``.decay``.
|
|
401
|
+
|
|
402
|
+
Wraps a ``_RunningAnimation`` factory so each ``.start()`` call
|
|
403
|
+
creates a fresh in-flight animation (matches React Native — the
|
|
404
|
+
``Animated.timing`` return value is reusable).
|
|
411
405
|
"""
|
|
412
406
|
|
|
413
|
-
def __init__(self, factory: Callable[[
|
|
407
|
+
def __init__(self, factory: Callable[[], _RunningAnimation]) -> None:
|
|
414
408
|
self._factory = factory
|
|
415
409
|
self._current: Optional[_RunningAnimation] = None
|
|
416
410
|
|
|
417
|
-
def start(self
|
|
418
|
-
"""Begin the animation
|
|
411
|
+
def start(self) -> "_AnimationHandle":
|
|
412
|
+
"""Begin the animation. Returns ``self`` for chaining."""
|
|
419
413
|
self.stop()
|
|
420
|
-
anim = self._factory(
|
|
414
|
+
anim = self._factory()
|
|
421
415
|
self._current = anim
|
|
422
416
|
_manager.add(anim)
|
|
417
|
+
return self
|
|
423
418
|
|
|
424
419
|
def stop(self) -> None:
|
|
425
420
|
"""Cancel the running instance (no-op if not running)."""
|
|
426
421
|
if self._current is not None:
|
|
422
|
+
self._current._finish()
|
|
427
423
|
_manager.remove(self._current)
|
|
428
424
|
self._current = None
|
|
429
425
|
|
|
426
|
+
async def _drive(self) -> None:
|
|
427
|
+
if self._current is None:
|
|
428
|
+
self.start()
|
|
429
|
+
loop = asyncio.get_running_loop()
|
|
430
|
+
future: asyncio.Future[None] = loop.create_future()
|
|
431
|
+
assert self._current is not None
|
|
432
|
+
self._current.add_completion_future(future)
|
|
433
|
+
await future
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class _CompositeAnimation(_AwaitableAnimation):
|
|
437
|
+
"""Run a list of animations in sequence or in parallel."""
|
|
438
|
+
|
|
439
|
+
def __init__(self, items: List[Any], mode: str) -> None:
|
|
440
|
+
self._items = list(items)
|
|
441
|
+
self._mode = mode
|
|
442
|
+
|
|
443
|
+
def start(self) -> "_CompositeAnimation":
|
|
444
|
+
"""Schedule the composite on the framework runtime, fire-and-forget."""
|
|
445
|
+
from .runtime import run_async
|
|
446
|
+
|
|
447
|
+
run_async(self._drive())
|
|
448
|
+
return self
|
|
449
|
+
|
|
450
|
+
def stop(self) -> None:
|
|
451
|
+
for item in self._items:
|
|
452
|
+
try:
|
|
453
|
+
item.stop()
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
async def _drive(self) -> None:
|
|
458
|
+
if self._mode == "parallel":
|
|
459
|
+
await asyncio.gather(*(self._await_item(item) for item in self._items))
|
|
460
|
+
return
|
|
461
|
+
for item in self._items:
|
|
462
|
+
await self._await_item(item)
|
|
463
|
+
|
|
464
|
+
@staticmethod
|
|
465
|
+
async def _await_item(item: Any) -> None:
|
|
466
|
+
if item is None:
|
|
467
|
+
return
|
|
468
|
+
if isinstance(item, _AwaitableAnimation):
|
|
469
|
+
await item
|
|
470
|
+
else:
|
|
471
|
+
# Plain awaitables and coroutines are supported too — lets
|
|
472
|
+
# users mix in ``asyncio.sleep`` or other awaitables.
|
|
473
|
+
await item
|
|
474
|
+
|
|
430
475
|
|
|
431
476
|
# ======================================================================
|
|
432
477
|
# Animated component wrappers
|
|
@@ -434,10 +479,10 @@ class _AnimationHandle:
|
|
|
434
479
|
|
|
435
480
|
|
|
436
481
|
def _resolve_style_with_values(style: StyleProp) -> Tuple[Dict[str, Any], Dict[str, AnimatedValue]]:
|
|
437
|
-
"""
|
|
482
|
+
"""Split ``style`` into a plain dict and animated bindings.
|
|
438
483
|
|
|
439
|
-
AnimatedValue entries in the style are replaced with their
|
|
440
|
-
|
|
484
|
+
AnimatedValue entries in the style are replaced with their current
|
|
485
|
+
numeric value in ``plain_style`` and recorded in
|
|
441
486
|
``animated_bindings`` so the wrapping component can subscribe
|
|
442
487
|
after mount.
|
|
443
488
|
"""
|
|
@@ -457,11 +502,7 @@ def _make_animated_factory(
|
|
|
457
502
|
element_type: str,
|
|
458
503
|
accept_children: bool,
|
|
459
504
|
) -> Callable[..., Element]:
|
|
460
|
-
"""Build an animated wrapper for ``element_type``.
|
|
461
|
-
|
|
462
|
-
The returned factory is used as the public
|
|
463
|
-
``Animated.View`` / ``Animated.Text`` / ``Animated.Image``.
|
|
464
|
-
"""
|
|
505
|
+
"""Build an animated wrapper for ``element_type``."""
|
|
465
506
|
from .hooks import component # local import to avoid cycle
|
|
466
507
|
|
|
467
508
|
@component
|
|
@@ -482,9 +523,9 @@ def _make_animated_factory(
|
|
|
482
523
|
return lambda: None
|
|
483
524
|
|
|
484
525
|
for prop, value in bindings.items():
|
|
485
|
-
|
|
526
|
+
|
|
486
527
|
def _on_change(new_val: float, _prop: str = prop, _view: Any = view) -> None:
|
|
487
|
-
handler = _get_handler_for(
|
|
528
|
+
handler = _get_handler_for(_view)
|
|
488
529
|
if handler is None:
|
|
489
530
|
return
|
|
490
531
|
setter = getattr(handler, "set_animated_property", None)
|
|
@@ -516,7 +557,6 @@ def _make_animated_factory(
|
|
|
516
557
|
if element_type == "Image":
|
|
517
558
|
source = args[0] if args else kwargs.pop("source", "")
|
|
518
559
|
return _Image(source, style=plain_style, ref=ref)
|
|
519
|
-
# View
|
|
520
560
|
children = list(args) if accept_children else []
|
|
521
561
|
return _View(*children, style=plain_style, ref=ref)
|
|
522
562
|
|
|
@@ -524,29 +564,19 @@ def _make_animated_factory(
|
|
|
524
564
|
|
|
525
565
|
|
|
526
566
|
def _animated_prop_name(prop: str) -> str:
|
|
527
|
-
"""Map a style key to the name expected by
|
|
567
|
+
"""Map a style key to the name expected by ``set_animated_property``."""
|
|
528
568
|
if prop == "opacity":
|
|
529
569
|
return "opacity"
|
|
530
570
|
if prop == "background_color":
|
|
531
571
|
return "background_color"
|
|
532
|
-
# Transform shorthand keys: ``translate_x``, ``translate_y``,
|
|
533
|
-
# ``scale``, ``scale_x``, ``scale_y``, ``rotate``.
|
|
534
572
|
if prop in ("translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"):
|
|
535
573
|
return prop
|
|
536
574
|
return prop
|
|
537
575
|
|
|
538
576
|
|
|
539
577
|
def _get_handler_for(native_view: Any) -> Any:
|
|
540
|
-
"""Best-effort lookup of the registered handler for ``native_view``.
|
|
541
|
-
|
|
542
|
-
Animated bindings need a handler reference to call
|
|
543
|
-
`set_animated_property`. Since the registry is keyed by element
|
|
544
|
-
type and we only have the native view, we fall back to looking
|
|
545
|
-
up the most recently registered "View" handler — works in
|
|
546
|
-
practice because all animated targets are flex containers,
|
|
547
|
-
images, or text views, and every iOS/Android handler subclass
|
|
548
|
-
inherits the same `set_animated_property` from the base.
|
|
549
|
-
"""
|
|
578
|
+
"""Best-effort lookup of the registered handler for ``native_view``."""
|
|
579
|
+
del native_view
|
|
550
580
|
try:
|
|
551
581
|
from .native_views import get_registry
|
|
552
582
|
|
|
@@ -570,8 +600,8 @@ def _get_handler_for(native_view: Any) -> Any:
|
|
|
570
600
|
class _AnimatedNamespace:
|
|
571
601
|
"""Public ``Animated`` namespace.
|
|
572
602
|
|
|
573
|
-
Exposes the
|
|
574
|
-
component wrappers (
|
|
603
|
+
Exposes the ``Value`` type, animation factories, composers, and
|
|
604
|
+
component wrappers (``View``, ``Text``, ``Image``).
|
|
575
605
|
"""
|
|
576
606
|
|
|
577
607
|
Value = AnimatedValue
|
|
@@ -586,8 +616,8 @@ class _AnimatedNamespace:
|
|
|
586
616
|
) -> _AnimationHandle:
|
|
587
617
|
"""Linearly interpolate ``value`` to ``to`` over ``duration`` ms."""
|
|
588
618
|
|
|
589
|
-
def _factory(
|
|
590
|
-
return _TimingAnimation(value, to, duration, _resolve_easing(easing)
|
|
619
|
+
def _factory() -> _RunningAnimation:
|
|
620
|
+
return _TimingAnimation(value, to, duration, _resolve_easing(easing))
|
|
591
621
|
|
|
592
622
|
return _AnimationHandle(_factory)
|
|
593
623
|
|
|
@@ -602,8 +632,8 @@ class _AnimatedNamespace:
|
|
|
602
632
|
) -> _AnimationHandle:
|
|
603
633
|
"""Run a damped harmonic spring toward ``to``."""
|
|
604
634
|
|
|
605
|
-
def _factory(
|
|
606
|
-
return _SpringAnimation(value, to, stiffness, damping, mass
|
|
635
|
+
def _factory() -> _RunningAnimation:
|
|
636
|
+
return _SpringAnimation(value, to, stiffness, damping, mass)
|
|
607
637
|
|
|
608
638
|
return _AnimationHandle(_factory)
|
|
609
639
|
|
|
@@ -616,8 +646,8 @@ class _AnimatedNamespace:
|
|
|
616
646
|
) -> _AnimationHandle:
|
|
617
647
|
"""Decelerate ``value`` from its current velocity until it rests."""
|
|
618
648
|
|
|
619
|
-
def _factory(
|
|
620
|
-
return _DecayAnimation(value, velocity, deceleration
|
|
649
|
+
def _factory() -> _RunningAnimation:
|
|
650
|
+
return _DecayAnimation(value, velocity, deceleration)
|
|
621
651
|
|
|
622
652
|
return _AnimationHandle(_factory)
|
|
623
653
|
|
|
@@ -635,21 +665,8 @@ class _AnimatedNamespace:
|
|
|
635
665
|
def delay(duration: float) -> _AnimationHandle:
|
|
636
666
|
"""Wait ``duration`` ms before continuing in a sequence."""
|
|
637
667
|
|
|
638
|
-
def _factory(
|
|
639
|
-
|
|
640
|
-
def __init__(self, on_complete: Optional[Callable[[], None]]) -> None:
|
|
641
|
-
super().__init__(AnimatedValue(0.0), on_complete)
|
|
642
|
-
self._elapsed = 0.0
|
|
643
|
-
self._duration = max(0.001, duration / 1000.0)
|
|
644
|
-
|
|
645
|
-
def advance(self, dt: float) -> bool:
|
|
646
|
-
self._elapsed += dt
|
|
647
|
-
if self._elapsed >= self._duration:
|
|
648
|
-
self._finish()
|
|
649
|
-
return True
|
|
650
|
-
return False
|
|
651
|
-
|
|
652
|
-
return _Delay(on_complete)
|
|
668
|
+
def _factory() -> _RunningAnimation:
|
|
669
|
+
return _DelayAnimation(duration)
|
|
653
670
|
|
|
654
671
|
return _AnimationHandle(_factory)
|
|
655
672
|
|
|
@@ -662,7 +679,7 @@ Animated = _AnimatedNamespace()
|
|
|
662
679
|
|
|
663
680
|
|
|
664
681
|
def use_animated_value(initial: float = 0.0) -> AnimatedValue:
|
|
665
|
-
"""Return an [`AnimatedValue`][pythonnative.AnimatedValue]
|
|
682
|
+
"""Return an [`AnimatedValue`][pythonnative.AnimatedValue] that is stable across renders.
|
|
666
683
|
|
|
667
684
|
Convenience wrapper for the common pattern
|
|
668
685
|
``pn.use_memo(lambda: AnimatedValue(initial), [])``. The same
|
|
@@ -679,14 +696,15 @@ def use_animated_value(initial: float = 0.0) -> AnimatedValue:
|
|
|
679
696
|
```python
|
|
680
697
|
import pythonnative as pn
|
|
681
698
|
|
|
699
|
+
|
|
682
700
|
@pn.component
|
|
683
701
|
def FadeIn():
|
|
684
702
|
opacity = pn.use_animated_value(0.0)
|
|
685
703
|
|
|
686
|
-
def fade_in():
|
|
687
|
-
pn.Animated.timing(opacity, to=1.0, duration=300)
|
|
704
|
+
async def fade_in():
|
|
705
|
+
await pn.Animated.timing(opacity, to=1.0, duration=300)
|
|
688
706
|
|
|
689
|
-
pn.
|
|
707
|
+
pn.use_async_effect(fade_in, [])
|
|
690
708
|
return pn.Animated.View(
|
|
691
709
|
pn.Text("Hello"),
|
|
692
710
|
style=pn.style(opacity=opacity),
|