pythonnative 0.22.0__py3-none-any.whl → 0.23.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 +16 -2
- pythonnative/animated.py +6 -6
- pythonnative/appearance.py +124 -0
- pythonnative/cli/pn.py +1 -1
- pythonnative/components.py +718 -48
- pythonnative/events.py +5 -5
- pythonnative/gestures.py +3 -3
- pythonnative/hooks.py +44 -3
- pythonnative/hot_reload.py +4 -4
- pythonnative/images.py +196 -0
- pythonnative/layout.py +3 -3
- pythonnative/mutations.py +4 -13
- pythonnative/native_modules/camera.py +1 -1
- pythonnative/native_modules/haptics.py +2 -2
- pythonnative/native_modules/location.py +1 -1
- pythonnative/native_modules/permissions.py +2 -2
- pythonnative/native_modules/secure_store.py +1 -1
- pythonnative/native_views/__init__.py +1 -7
- pythonnative/native_views/android.py +592 -64
- pythonnative/native_views/base.py +3 -3
- pythonnative/native_views/desktop.py +85 -26
- pythonnative/native_views/ios.py +762 -74
- pythonnative/navigation.py +4 -4
- pythonnative/net.py +3 -3
- pythonnative/platform.py +1 -1
- pythonnative/platform_metrics.py +5 -5
- pythonnative/preview.py +34 -3
- pythonnative/project/builder.py +1 -1
- pythonnative/project/config.py +4 -12
- pythonnative/project/doctor.py +2 -2
- pythonnative/project/icons.py +1 -1
- pythonnative/project/ios.py +1 -1
- pythonnative/project/permissions.py +2 -2
- pythonnative/project/runtime_assets.py +3 -3
- pythonnative/reconciler.py +9 -9
- pythonnative/runtime.py +8 -8
- pythonnative/screen.py +90 -5
- pythonnative/sdk/_components.py +1 -1
- pythonnative/storage.py +3 -3
- pythonnative/style.py +66 -7
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +2 -2
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +1 -1
- pythonnative/virtual_rows.py +137 -0
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/METADATA +13 -13
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/RECORD +53 -47
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,7 @@ and frame application. Handlers are registered with the
|
|
|
8
8
|
|
|
9
9
|
**Batched protocol**: the registry applies the reconciler's mutation
|
|
10
10
|
ops; handlers receive callable-free props. User callbacks never reach
|
|
11
|
-
this module
|
|
11
|
+
this module; every interaction (clicks, text changes, scrolls,
|
|
12
12
|
gestures) is forwarded through
|
|
13
13
|
[`dispatch_event`][pythonnative.events.dispatch_event] keyed by the
|
|
14
14
|
view's reconciler-assigned tag.
|
|
@@ -45,7 +45,26 @@ from ..gestures import make_arbiter
|
|
|
45
45
|
from ..utils import get_android_context
|
|
46
46
|
from .base import ViewHandler, _safe_max, parse_color_int
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
_SIDE_BORDER_WIDTH_KEYS = (
|
|
49
|
+
"border_left_width",
|
|
50
|
+
"border_top_width",
|
|
51
|
+
"border_right_width",
|
|
52
|
+
"border_bottom_width",
|
|
53
|
+
)
|
|
54
|
+
_SIDE_BORDER_COLOR_KEYS = (
|
|
55
|
+
"border_left_color",
|
|
56
|
+
"border_top_color",
|
|
57
|
+
"border_right_color",
|
|
58
|
+
"border_bottom_color",
|
|
59
|
+
)
|
|
60
|
+
_DRAWABLE_STYLE_KEYS = (
|
|
61
|
+
"background_color",
|
|
62
|
+
"border_radius",
|
|
63
|
+
"border_width",
|
|
64
|
+
"border_color",
|
|
65
|
+
*_SIDE_BORDER_WIDTH_KEYS,
|
|
66
|
+
*_SIDE_BORDER_COLOR_KEYS,
|
|
67
|
+
)
|
|
49
68
|
|
|
50
69
|
|
|
51
70
|
# ======================================================================
|
|
@@ -57,6 +76,35 @@ def _ctx() -> Any:
|
|
|
57
76
|
return get_android_context()
|
|
58
77
|
|
|
59
78
|
|
|
79
|
+
def _pn_runtime_class(class_name: str) -> Any:
|
|
80
|
+
"""Resolve a PythonNative Android helper class for the running app.
|
|
81
|
+
|
|
82
|
+
The Android template's helper classes (e.g.
|
|
83
|
+
``PNAccessibilityDelegate``, ``PNBorderDrawable``) live in the
|
|
84
|
+
app's own package, which the ``pn`` CLI relocates to the configured
|
|
85
|
+
``application_id`` at build time. Deriving the package from the
|
|
86
|
+
runtime ``Context`` (rather than hardcoding the template package)
|
|
87
|
+
keeps these lookups correct for any app id.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
class_name: The class name within the app package, e.g.
|
|
91
|
+
``"PNBorderDrawable"``.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The resolved Java class.
|
|
95
|
+
"""
|
|
96
|
+
package = _ctx().getPackageName()
|
|
97
|
+
return jclass(f"{package}.{class_name}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _signed_color(value: int) -> int:
|
|
101
|
+
"""Clamp a Python color int into Java's signed 32-bit range."""
|
|
102
|
+
value &= 0xFFFFFFFF
|
|
103
|
+
if value >= 0x80000000:
|
|
104
|
+
value -= 0x100000000
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
60
108
|
def _density() -> float:
|
|
61
109
|
return float(_ctx().getResources().getDisplayMetrics().density)
|
|
62
110
|
|
|
@@ -118,6 +166,52 @@ def _has_event(view: Any, name: str) -> bool:
|
|
|
118
166
|
return name in event_names(merged)
|
|
119
167
|
|
|
120
168
|
|
|
169
|
+
def _apply_side_border(view: Any, props: Dict[str, Any]) -> bool:
|
|
170
|
+
"""Apply per-side borders via the template's ``PNBorderDrawable``.
|
|
171
|
+
|
|
172
|
+
Activated when any ``border_<side>_width`` key is present. In that
|
|
173
|
+
mode the drawable owns all four edges: each side's width falls
|
|
174
|
+
back to the uniform ``border_width`` (else 0) and its color to
|
|
175
|
+
``border_color`` (else black), and the background fill and corner
|
|
176
|
+
radius are baked into the same drawable.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
``True`` if the drawable was applied; ``False`` when no
|
|
180
|
+
per-side key is present or the helper class is unavailable
|
|
181
|
+
(older generated projects), in which case the caller falls
|
|
182
|
+
back to the uniform ``GradientDrawable`` path.
|
|
183
|
+
"""
|
|
184
|
+
if not any(props.get(k) is not None for k in _SIDE_BORDER_WIDTH_KEYS):
|
|
185
|
+
return False
|
|
186
|
+
try:
|
|
187
|
+
PNBorderDrawable = _pn_runtime_class("PNBorderDrawable")
|
|
188
|
+
except Exception:
|
|
189
|
+
return False
|
|
190
|
+
try:
|
|
191
|
+
base_width = float(props.get("border_width") or 0.0)
|
|
192
|
+
base_color = props.get("border_color") or "#000000"
|
|
193
|
+
widths = [
|
|
194
|
+
float(_dp(float(props[k]))) if props.get(k) is not None else float(_dp(base_width))
|
|
195
|
+
for k in _SIDE_BORDER_WIDTH_KEYS
|
|
196
|
+
]
|
|
197
|
+
colors = [
|
|
198
|
+
_signed_color(parse_color_int(props[k] if props.get(k) is not None else base_color))
|
|
199
|
+
for k in _SIDE_BORDER_COLOR_KEYS
|
|
200
|
+
]
|
|
201
|
+
has_bg = props.get("background_color") is not None
|
|
202
|
+
bg = _signed_color(parse_color_int(props["background_color"])) if has_bg else 0
|
|
203
|
+
radius = float(_dp(float(props.get("border_radius") or 0.0)))
|
|
204
|
+
drawable = PNBorderDrawable(has_bg, bg, radius, widths, colors)
|
|
205
|
+
view.setBackground(drawable)
|
|
206
|
+
try:
|
|
207
|
+
view.invalidate()
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
return True
|
|
211
|
+
except Exception:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
121
215
|
def _apply_border(view: Any, props: Dict[str, Any]) -> None:
|
|
122
216
|
"""Apply border_radius / border_width / border_color via a GradientDrawable.
|
|
123
217
|
|
|
@@ -126,7 +220,12 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
|
|
|
126
220
|
background to a ``GradientDrawable`` (the "shape" XML primitive)
|
|
127
221
|
that renders the corner radius and stroke. We preserve any
|
|
128
222
|
existing ``background_color`` by re-baking it into the drawable.
|
|
223
|
+
Per-side borders route through
|
|
224
|
+
[`_apply_side_border`][pythonnative.native_views.android._apply_side_border]
|
|
225
|
+
instead.
|
|
129
226
|
"""
|
|
227
|
+
if _apply_side_border(view, props):
|
|
228
|
+
return
|
|
130
229
|
has_border = any(k in props for k in ("border_radius", "border_width", "border_color"))
|
|
131
230
|
has_bg = "background_color" in props and props["background_color"] is not None
|
|
132
231
|
if not has_border and not has_bg:
|
|
@@ -170,16 +269,46 @@ def _apply_border(view: Any, props: Dict[str, Any]) -> None:
|
|
|
170
269
|
|
|
171
270
|
|
|
172
271
|
def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
|
|
173
|
-
"""Apply elevation
|
|
272
|
+
"""Apply shadow props via elevation plus tinted outline shadows.
|
|
273
|
+
|
|
274
|
+
Android renders view shadows from ``elevation``; iOS-style
|
|
275
|
+
``shadow_radius`` maps onto it so cross-platform styles work.
|
|
276
|
+
On API 28+ the ambient/spot shadow colors are tinted with
|
|
277
|
+
``shadow_color`` (with ``shadow_opacity`` baked into the alpha
|
|
278
|
+
channel), which is as close to iOS's layer shadows as the
|
|
279
|
+
platform allows. ``shadow_offset`` has no Android equivalent and
|
|
280
|
+
is ignored.
|
|
281
|
+
"""
|
|
174
282
|
elevation = props.get("elevation")
|
|
175
283
|
if elevation is None and "shadow_radius" in props:
|
|
176
284
|
elevation = props.get("shadow_radius")
|
|
285
|
+
if elevation is None and (props.get("shadow_color") is not None or props.get("shadow_opacity") is not None):
|
|
286
|
+
# Shadow requested without an explicit size: use a Material
|
|
287
|
+
# card-like default so the shadow is actually visible.
|
|
288
|
+
elevation = 4.0
|
|
177
289
|
if elevation is None:
|
|
178
290
|
return
|
|
179
291
|
try:
|
|
180
292
|
view.setElevation(float(_dp(float(elevation))))
|
|
181
293
|
except Exception:
|
|
182
294
|
pass
|
|
295
|
+
color = props.get("shadow_color")
|
|
296
|
+
if color is None:
|
|
297
|
+
return
|
|
298
|
+
try:
|
|
299
|
+
Build = jclass("android.os.Build")
|
|
300
|
+
if int(Build.VERSION.SDK_INT) < 28:
|
|
301
|
+
return
|
|
302
|
+
argb = parse_color_int(color) & 0xFFFFFFFF
|
|
303
|
+
opacity = props.get("shadow_opacity")
|
|
304
|
+
if opacity is not None:
|
|
305
|
+
alpha = int(max(0.0, min(1.0, float(opacity))) * 255)
|
|
306
|
+
argb = (argb & 0x00FFFFFF) | (alpha << 24)
|
|
307
|
+
signed = _signed_color(argb)
|
|
308
|
+
view.setOutlineAmbientShadowColor(signed)
|
|
309
|
+
view.setOutlineSpotShadowColor(signed)
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
183
312
|
|
|
184
313
|
|
|
185
314
|
def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
|
|
@@ -228,7 +357,7 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
|
|
|
228
357
|
|
|
229
358
|
|
|
230
359
|
def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
|
|
231
|
-
"""Apply
|
|
360
|
+
"""Apply accessibility props (label / accessible / state / live region / test_id)."""
|
|
232
361
|
if "accessible" in props:
|
|
233
362
|
try:
|
|
234
363
|
View = jclass("android.view.View")
|
|
@@ -243,10 +372,48 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
|
|
|
243
372
|
view.setContentDescription(str(label) if label is not None else None)
|
|
244
373
|
except Exception:
|
|
245
374
|
pass
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
375
|
+
if "accessibility_live_region" in props:
|
|
376
|
+
try:
|
|
377
|
+
mode = {"none": 0, "polite": 1, "assertive": 2}.get(
|
|
378
|
+
str(props["accessibility_live_region"] or "none").lower(), 0
|
|
379
|
+
)
|
|
380
|
+
view.setAccessibilityLiveRegion(mode)
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
state = props.get("accessibility_state")
|
|
385
|
+
if isinstance(state, dict) and "selected" in state:
|
|
386
|
+
try:
|
|
387
|
+
view.setSelected(bool(state["selected"]))
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
test_id = props.get("test_id")
|
|
391
|
+
if test_id is None and not isinstance(state, dict):
|
|
392
|
+
return
|
|
393
|
+
# test_id (exposed as `resource-id` to UI Automator tools) and the
|
|
394
|
+
# remaining state flags need an AccessibilityDelegate, which Python
|
|
395
|
+
# can't subclass directly (Chaquopy proxies interfaces only); the
|
|
396
|
+
# template ships PNAccessibilityDelegate for this. Older generated
|
|
397
|
+
# projects without the class simply skip these props.
|
|
398
|
+
try:
|
|
399
|
+
vs = _state_of(view)
|
|
400
|
+
delegate = vs.get("a11y_delegate")
|
|
401
|
+
if delegate is None:
|
|
402
|
+
PNAccessibilityDelegate = _pn_runtime_class("PNAccessibilityDelegate")
|
|
403
|
+
delegate = PNAccessibilityDelegate()
|
|
404
|
+
view.setAccessibilityDelegate(delegate)
|
|
405
|
+
vs["a11y_delegate"] = delegate
|
|
406
|
+
if test_id is not None:
|
|
407
|
+
delegate.setTestId(str(test_id))
|
|
408
|
+
view.setTag(str(test_id))
|
|
409
|
+
if isinstance(state, dict):
|
|
410
|
+
delegate.setStateDisabled(state.get("disabled"))
|
|
411
|
+
delegate.setStateSelected(state.get("selected"))
|
|
412
|
+
delegate.setStateChecked(state.get("checked"))
|
|
413
|
+
delegate.setStateBusy(state.get("busy"))
|
|
414
|
+
delegate.setStateExpanded(state.get("expanded"))
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
250
417
|
|
|
251
418
|
|
|
252
419
|
def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
@@ -689,7 +856,7 @@ class AndroidViewHandler(ViewHandler):
|
|
|
689
856
|
|
|
690
857
|
|
|
691
858
|
class FlexContainerHandler(AndroidViewHandler):
|
|
692
|
-
"""Container for flex layout
|
|
859
|
+
"""Container for flex layout, a bare `FrameLayout`.
|
|
693
860
|
|
|
694
861
|
All flex semantics (direction, alignment, distribution, padding)
|
|
695
862
|
are computed by the layout engine and applied via
|
|
@@ -758,13 +925,93 @@ def _typeface_for(weight: Any, italic: bool) -> Any:
|
|
|
758
925
|
return style
|
|
759
926
|
|
|
760
927
|
|
|
928
|
+
def _build_spannable(spans: Any) -> Any:
|
|
929
|
+
"""Build a ``SpannableStringBuilder`` from a rich-text span list.
|
|
930
|
+
|
|
931
|
+
Each span dict carries ``text`` plus optional per-span overrides
|
|
932
|
+
(``color``, ``background_color``, ``font_size``, ``font_family``,
|
|
933
|
+
``bold`` / ``italic`` / ``font_weight``, ``text_decoration``).
|
|
934
|
+
Spans without overrides inherit the TextView's base styling.
|
|
935
|
+
"""
|
|
936
|
+
SpannableStringBuilder = jclass("android.text.SpannableStringBuilder")
|
|
937
|
+
Typeface = jclass("android.graphics.Typeface")
|
|
938
|
+
FLAG = 33 # Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
939
|
+
|
|
940
|
+
builder = SpannableStringBuilder()
|
|
941
|
+
for span in spans:
|
|
942
|
+
if not isinstance(span, dict):
|
|
943
|
+
continue
|
|
944
|
+
text = str(span.get("text", ""))
|
|
945
|
+
if not text:
|
|
946
|
+
continue
|
|
947
|
+
start = builder.length()
|
|
948
|
+
builder.append(text)
|
|
949
|
+
end = builder.length()
|
|
950
|
+
|
|
951
|
+
def _set(obj: Any) -> None:
|
|
952
|
+
builder.setSpan(obj, start, end, FLAG)
|
|
953
|
+
|
|
954
|
+
try:
|
|
955
|
+
if span.get("color") is not None:
|
|
956
|
+
ForegroundColorSpan = jclass("android.text.style.ForegroundColorSpan")
|
|
957
|
+
_set(ForegroundColorSpan(parse_color_int(span["color"])))
|
|
958
|
+
if span.get("background_color") is not None:
|
|
959
|
+
BackgroundColorSpan = jclass("android.text.style.BackgroundColorSpan")
|
|
960
|
+
_set(BackgroundColorSpan(parse_color_int(span["background_color"])))
|
|
961
|
+
if span.get("font_size") is not None:
|
|
962
|
+
AbsoluteSizeSpan = jclass("android.text.style.AbsoluteSizeSpan")
|
|
963
|
+
_set(AbsoluteSizeSpan(int(float(span["font_size"])), True)) # dip units
|
|
964
|
+
bold = bool(span.get("bold"))
|
|
965
|
+
weight = span.get("font_weight")
|
|
966
|
+
if not bold and weight is not None:
|
|
967
|
+
try:
|
|
968
|
+
bold = str(weight) in ("bold", "600", "700", "800", "900")
|
|
969
|
+
except Exception:
|
|
970
|
+
bold = False
|
|
971
|
+
italic = bool(span.get("italic"))
|
|
972
|
+
if bold or italic:
|
|
973
|
+
StyleSpan = jclass("android.text.style.StyleSpan")
|
|
974
|
+
if bold and italic:
|
|
975
|
+
_set(StyleSpan(Typeface.BOLD_ITALIC))
|
|
976
|
+
elif bold:
|
|
977
|
+
_set(StyleSpan(Typeface.BOLD))
|
|
978
|
+
else:
|
|
979
|
+
_set(StyleSpan(Typeface.ITALIC))
|
|
980
|
+
if span.get("font_family"):
|
|
981
|
+
TypefaceSpan = jclass("android.text.style.TypefaceSpan")
|
|
982
|
+
_set(TypefaceSpan(str(span["font_family"])))
|
|
983
|
+
decoration = span.get("text_decoration")
|
|
984
|
+
if decoration == "underline":
|
|
985
|
+
UnderlineSpan = jclass("android.text.style.UnderlineSpan")
|
|
986
|
+
_set(UnderlineSpan())
|
|
987
|
+
elif decoration == "line_through":
|
|
988
|
+
StrikethroughSpan = jclass("android.text.style.StrikethroughSpan")
|
|
989
|
+
_set(StrikethroughSpan())
|
|
990
|
+
except Exception:
|
|
991
|
+
pass
|
|
992
|
+
return builder
|
|
993
|
+
|
|
994
|
+
|
|
761
995
|
class TextHandler(AndroidViewHandler):
|
|
762
996
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
763
997
|
return jclass("android.widget.TextView")(_ctx())
|
|
764
998
|
|
|
765
999
|
def _apply(self, tv: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
766
|
-
if "text" in props:
|
|
767
|
-
|
|
1000
|
+
if "spans" in props or "text" in props:
|
|
1001
|
+
# ``update()`` merges changed props into the view state
|
|
1002
|
+
# before calling ``_apply``, so the merged dict always has
|
|
1003
|
+
# the latest text/spans pair (covering rich -> plain
|
|
1004
|
+
# transitions where only ``spans`` changed).
|
|
1005
|
+
merged = _state_of(tv).get("props") or props
|
|
1006
|
+
spans = merged.get("spans")
|
|
1007
|
+
if spans:
|
|
1008
|
+
try:
|
|
1009
|
+
tv.setText(_build_spannable(spans))
|
|
1010
|
+
except Exception:
|
|
1011
|
+
tv.setText(str(merged.get("text") or ""))
|
|
1012
|
+
else:
|
|
1013
|
+
text = merged.get("text")
|
|
1014
|
+
tv.setText(str(text) if text is not None else "")
|
|
768
1015
|
if "font_size" in props and props["font_size"] is not None:
|
|
769
1016
|
tv.setTextSize(float(props["font_size"]))
|
|
770
1017
|
if "color" in props and props["color"] is not None:
|
|
@@ -844,7 +1091,7 @@ class ButtonHandler(AndroidViewHandler):
|
|
|
844
1091
|
|
|
845
1092
|
|
|
846
1093
|
class ScrollViewHandler(AndroidViewHandler):
|
|
847
|
-
"""Scroll container
|
|
1094
|
+
"""Scroll container: wraps a single child whose height is unbounded.
|
|
848
1095
|
|
|
849
1096
|
Uses ``androidx.core.widget.NestedScrollView`` (vertical) or
|
|
850
1097
|
``android.widget.HorizontalScrollView`` so nested scroll views
|
|
@@ -871,7 +1118,7 @@ class ScrollViewHandler(AndroidViewHandler):
|
|
|
871
1118
|
sv = jclass("android.widget.ScrollView")(_ctx())
|
|
872
1119
|
|
|
873
1120
|
# Vertical scroll views are *always* wrapped in a (disabled)
|
|
874
|
-
# SwipeRefreshLayout. Wrapping later is impossible
|
|
1121
|
+
# SwipeRefreshLayout. Wrapping later is impossible; the
|
|
875
1122
|
# reconciler may reuse this view for a screen that adds a
|
|
876
1123
|
# ``refresh_control`` prop afterwards (e.g. navigation swapping
|
|
877
1124
|
# screens of the same shape), and re-parenting a mounted view
|
|
@@ -1214,7 +1461,7 @@ class TextInputHandler(AndroidViewHandler):
|
|
|
1214
1461
|
# Map the cross-platform ``return_key_type`` to Android's
|
|
1215
1462
|
# ``EditorInfo.IME_ACTION_*`` so the soft keyboard renders the
|
|
1216
1463
|
# right action key. iOS has a richer set (Google / Yahoo /
|
|
1217
|
-
# Join / Route) with no direct AOSP equivalents
|
|
1464
|
+
# Join / Route) with no direct AOSP equivalents; fall back
|
|
1218
1465
|
# to ``IME_ACTION_DONE`` for those.
|
|
1219
1466
|
try:
|
|
1220
1467
|
EditorInfo = jclass("android.view.inputmethod.EditorInfo")
|
|
@@ -1244,7 +1491,7 @@ class TextInputHandler(AndroidViewHandler):
|
|
|
1244
1491
|
Single-line inputs always dismiss the keyboard on the action
|
|
1245
1492
|
key (matching React Native's Android default) and fire
|
|
1246
1493
|
``on_submit`` first. Multi-line inputs only consume the action
|
|
1247
|
-
when an ``on_submit`` handler exists
|
|
1494
|
+
when an ``on_submit`` handler exists; otherwise Enter inserts
|
|
1248
1495
|
a newline.
|
|
1249
1496
|
"""
|
|
1250
1497
|
try:
|
|
@@ -1355,8 +1602,13 @@ class ImageHandler(AndroidViewHandler):
|
|
|
1355
1602
|
iv.setImageTintList(ColorStateList.valueOf(parse_color_int(props["tint_color"])))
|
|
1356
1603
|
except Exception:
|
|
1357
1604
|
pass
|
|
1605
|
+
if "placeholder_color" in props and props["placeholder_color"] is not None:
|
|
1606
|
+
try:
|
|
1607
|
+
iv.setBackgroundColor(parse_color_int(props["placeholder_color"]))
|
|
1608
|
+
except Exception:
|
|
1609
|
+
pass
|
|
1358
1610
|
if "source" in props and props["source"]:
|
|
1359
|
-
self._load_source(iv, props["source"])
|
|
1611
|
+
self._load_source(iv, str(props["source"]))
|
|
1360
1612
|
if "scale_type" in props and props["scale_type"]:
|
|
1361
1613
|
ScaleType = jclass("android.widget.ImageView$ScaleType")
|
|
1362
1614
|
mapping = {
|
|
@@ -1372,43 +1624,39 @@ class ImageHandler(AndroidViewHandler):
|
|
|
1372
1624
|
|
|
1373
1625
|
def _load_source(self, iv: Any, source: str) -> None:
|
|
1374
1626
|
try:
|
|
1375
|
-
if source.startswith(
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
bitmap = BitmapFactory.decodeStream(stream)
|
|
1396
|
-
stream.close()
|
|
1397
|
-
|
|
1398
|
-
class SetImage(dynamic_proxy(Runnable)):
|
|
1399
|
-
def __init__(self, view: Any, bmp: Any) -> None:
|
|
1400
|
-
super().__init__()
|
|
1401
|
-
self.view = view
|
|
1402
|
-
self.bmp = bmp
|
|
1403
|
-
|
|
1404
|
-
def run(self) -> None:
|
|
1405
|
-
self.view.setImageBitmap(self.bmp)
|
|
1627
|
+
if source.startswith("data:"):
|
|
1628
|
+
self._load_data_uri(iv, source)
|
|
1629
|
+
elif source.startswith(("http://", "https://")):
|
|
1630
|
+
from ..images import fetch
|
|
1631
|
+
|
|
1632
|
+
state = _state_of(iv)
|
|
1633
|
+
state["pending_uri"] = source
|
|
1634
|
+
|
|
1635
|
+
def _on_ready(path: str) -> None:
|
|
1636
|
+
if _state_of(iv).get("pending_uri") != source:
|
|
1637
|
+
return # a newer source superseded this load
|
|
1638
|
+
bitmap = self._decode_downsampled(iv, path)
|
|
1639
|
+
if bitmap is None:
|
|
1640
|
+
_fire(iv, "on_error", "decode failed")
|
|
1641
|
+
return
|
|
1642
|
+
try:
|
|
1643
|
+
iv.setImageBitmap(bitmap)
|
|
1644
|
+
_fire(iv, "on_load")
|
|
1645
|
+
except Exception:
|
|
1646
|
+
pass
|
|
1406
1647
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1648
|
+
def _on_error(message: str) -> None:
|
|
1649
|
+
if _state_of(iv).get("pending_uri") == source:
|
|
1650
|
+
_fire(iv, "on_error", message)
|
|
1410
1651
|
|
|
1411
|
-
|
|
1652
|
+
fetch(source, _on_ready, _on_error)
|
|
1653
|
+
elif source.startswith("/"):
|
|
1654
|
+
bitmap = self._decode_downsampled(iv, source)
|
|
1655
|
+
if bitmap is not None:
|
|
1656
|
+
iv.setImageBitmap(bitmap)
|
|
1657
|
+
_fire(iv, "on_load")
|
|
1658
|
+
else:
|
|
1659
|
+
_fire(iv, "on_error", "decode failed")
|
|
1412
1660
|
else:
|
|
1413
1661
|
ctx = _ctx()
|
|
1414
1662
|
res = ctx.getResources()
|
|
@@ -1417,9 +1665,76 @@ class ImageHandler(AndroidViewHandler):
|
|
|
1417
1665
|
res_id = res.getIdentifier(res_name, "drawable", pkg)
|
|
1418
1666
|
if res_id != 0:
|
|
1419
1667
|
iv.setImageResource(res_id)
|
|
1668
|
+
_fire(iv, "on_load")
|
|
1669
|
+
else:
|
|
1670
|
+
_fire(iv, "on_error", f"drawable {res_name!r} not found")
|
|
1420
1671
|
except Exception:
|
|
1421
1672
|
pass
|
|
1422
1673
|
|
|
1674
|
+
def _load_data_uri(self, iv: Any, source: str) -> None:
|
|
1675
|
+
"""Decode an inline ``data:`` URI (base64 payload) into the view."""
|
|
1676
|
+
try:
|
|
1677
|
+
import base64
|
|
1678
|
+
|
|
1679
|
+
payload = source.split(",", 1)[1] if "," in source else ""
|
|
1680
|
+
raw = base64.b64decode(payload)
|
|
1681
|
+
BitmapFactory = jclass("android.graphics.BitmapFactory")
|
|
1682
|
+
bitmap = BitmapFactory.decodeByteArray(raw, 0, len(raw))
|
|
1683
|
+
if bitmap is not None:
|
|
1684
|
+
iv.setImageBitmap(bitmap)
|
|
1685
|
+
_fire(iv, "on_load")
|
|
1686
|
+
else:
|
|
1687
|
+
_fire(iv, "on_error", "data URI decode failed")
|
|
1688
|
+
except Exception:
|
|
1689
|
+
_fire(iv, "on_error", "data URI decode failed")
|
|
1690
|
+
|
|
1691
|
+
def _decode_downsampled(self, iv: Any, path: str) -> Any:
|
|
1692
|
+
"""Decode ``path`` with ``inSampleSize`` sized to the target view.
|
|
1693
|
+
|
|
1694
|
+
A 4000px photo displayed in a 200dp thumbnail should not hold a
|
|
1695
|
+
48 MB bitmap; ``inSampleSize`` decodes at the nearest power-of-2
|
|
1696
|
+
fraction that still covers the view (falling back to the screen
|
|
1697
|
+
width when the view hasn't been laid out yet).
|
|
1698
|
+
"""
|
|
1699
|
+
try:
|
|
1700
|
+
BitmapFactory = jclass("android.graphics.BitmapFactory")
|
|
1701
|
+
Options = jclass("android.graphics.BitmapFactory$Options")
|
|
1702
|
+
|
|
1703
|
+
bounds = Options()
|
|
1704
|
+
bounds.inJustDecodeBounds = True
|
|
1705
|
+
BitmapFactory.decodeFile(path, bounds)
|
|
1706
|
+
src_w = int(bounds.outWidth)
|
|
1707
|
+
src_h = int(bounds.outHeight)
|
|
1708
|
+
if src_w <= 0 or src_h <= 0:
|
|
1709
|
+
return None
|
|
1710
|
+
|
|
1711
|
+
target_w = 0
|
|
1712
|
+
target_h = 0
|
|
1713
|
+
try:
|
|
1714
|
+
target_w = int(iv.getWidth())
|
|
1715
|
+
target_h = int(iv.getHeight())
|
|
1716
|
+
except Exception:
|
|
1717
|
+
pass
|
|
1718
|
+
if target_w <= 0:
|
|
1719
|
+
try:
|
|
1720
|
+
metrics = _ctx().getResources().getDisplayMetrics()
|
|
1721
|
+
target_w = int(metrics.widthPixels)
|
|
1722
|
+
target_h = int(metrics.heightPixels)
|
|
1723
|
+
except Exception:
|
|
1724
|
+
target_w, target_h = src_w, src_h
|
|
1725
|
+
if target_h <= 0:
|
|
1726
|
+
target_h = target_w
|
|
1727
|
+
|
|
1728
|
+
sample = 1
|
|
1729
|
+
while (src_w // (sample * 2)) >= target_w and (src_h // (sample * 2)) >= target_h:
|
|
1730
|
+
sample *= 2
|
|
1731
|
+
|
|
1732
|
+
opts = Options()
|
|
1733
|
+
opts.inSampleSize = sample
|
|
1734
|
+
return BitmapFactory.decodeFile(path, opts)
|
|
1735
|
+
except Exception:
|
|
1736
|
+
return None
|
|
1737
|
+
|
|
1423
1738
|
|
|
1424
1739
|
class SwitchHandler(AndroidViewHandler):
|
|
1425
1740
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
@@ -1616,7 +1931,7 @@ class WebViewHandler(AndroidViewHandler):
|
|
|
1616
1931
|
class SpacerHandler(AndroidViewHandler):
|
|
1617
1932
|
"""Empty layout placeholder used as a flexible gap.
|
|
1618
1933
|
|
|
1619
|
-
All sizing semantics live in the layout engine
|
|
1934
|
+
All sizing semantics live in the layout engine; ``Spacer``
|
|
1620
1935
|
behaves identically to a `View` with the same style props (e.g.,
|
|
1621
1936
|
``flex: 1`` for an expanding spacer, ``size`` for a fixed gap).
|
|
1622
1937
|
"""
|
|
@@ -1638,7 +1953,7 @@ class SafeAreaViewHandler(FlexContainerHandler):
|
|
|
1638
1953
|
|
|
1639
1954
|
|
|
1640
1955
|
# ======================================================================
|
|
1641
|
-
# Modal
|
|
1956
|
+
# Modal: actually presents a Dialog with the children inside
|
|
1642
1957
|
# ======================================================================
|
|
1643
1958
|
|
|
1644
1959
|
|
|
@@ -1959,7 +2274,7 @@ class TabBarHandler(AndroidViewHandler):
|
|
|
1959
2274
|
|
|
1960
2275
|
|
|
1961
2276
|
# ======================================================================
|
|
1962
|
-
# Pressable
|
|
2277
|
+
# Pressable: visual feedback + tap callbacks + gestures
|
|
1963
2278
|
# ======================================================================
|
|
1964
2279
|
|
|
1965
2280
|
|
|
@@ -2073,7 +2388,7 @@ class PressableHandler(FlexContainerHandler):
|
|
|
2073
2388
|
|
|
2074
2389
|
|
|
2075
2390
|
# ======================================================================
|
|
2076
|
-
# StatusBar
|
|
2391
|
+
# StatusBar: global side effect
|
|
2077
2392
|
# ======================================================================
|
|
2078
2393
|
|
|
2079
2394
|
|
|
@@ -2131,7 +2446,7 @@ def _present_alert(
|
|
|
2131
2446
|
) -> None:
|
|
2132
2447
|
"""Present an AlertDialog or BottomSheet (``style='action_sheet'``).
|
|
2133
2448
|
|
|
2134
|
-
Safe to call from any thread
|
|
2449
|
+
Safe to call from any thread; the AlertDialog work is automatically
|
|
2135
2450
|
marshalled to the main looper via
|
|
2136
2451
|
[`pythonnative.runtime.call_on_main_thread`][pythonnative.runtime.call_on_main_thread].
|
|
2137
2452
|
Returns immediately; the dialog appears on the next main-loop tick.
|
|
@@ -2227,12 +2542,12 @@ def _present_alert(
|
|
|
2227
2542
|
|
|
2228
2543
|
|
|
2229
2544
|
# ======================================================================
|
|
2230
|
-
# Picker
|
|
2545
|
+
# Picker: native dropdown / select widget
|
|
2231
2546
|
# ======================================================================
|
|
2232
2547
|
|
|
2233
2548
|
|
|
2234
2549
|
class PickerHandler(AndroidViewHandler):
|
|
2235
|
-
"""``Picker`` element handler
|
|
2550
|
+
"""``Picker`` element handler, native ``Spinner`` dropdown."""
|
|
2236
2551
|
|
|
2237
2552
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2238
2553
|
sp = jclass("android.widget.Spinner")(_ctx())
|
|
@@ -2300,12 +2615,12 @@ class PickerHandler(AndroidViewHandler):
|
|
|
2300
2615
|
|
|
2301
2616
|
|
|
2302
2617
|
# ======================================================================
|
|
2303
|
-
# Checkbox
|
|
2618
|
+
# Checkbox: native CheckBox with an optional inline label
|
|
2304
2619
|
# ======================================================================
|
|
2305
2620
|
|
|
2306
2621
|
|
|
2307
2622
|
class CheckboxHandler(AndroidViewHandler):
|
|
2308
|
-
"""``Checkbox`` element handler
|
|
2623
|
+
"""``Checkbox`` element handler, native ``CheckBox`` widget.
|
|
2309
2624
|
|
|
2310
2625
|
Programmatic ``value`` updates are wrapped in a per-view
|
|
2311
2626
|
"suppress" guard so pushing a new state via ``setChecked`` never
|
|
@@ -2348,12 +2663,12 @@ class CheckboxHandler(AndroidViewHandler):
|
|
|
2348
2663
|
|
|
2349
2664
|
|
|
2350
2665
|
# ======================================================================
|
|
2351
|
-
# SegmentedControl
|
|
2666
|
+
# SegmentedControl: horizontal toggle row (no UISegmentedControl on AOSP)
|
|
2352
2667
|
# ======================================================================
|
|
2353
2668
|
|
|
2354
2669
|
|
|
2355
2670
|
class SegmentedControlHandler(AndroidViewHandler):
|
|
2356
|
-
"""``SegmentedControl`` element
|
|
2671
|
+
"""``SegmentedControl`` element, a horizontal row of toggle buttons.
|
|
2357
2672
|
|
|
2358
2673
|
Android has no ``UISegmentedControl`` equivalent, so the control is
|
|
2359
2674
|
built from a horizontal ``LinearLayout`` holding one ``Button`` per
|
|
@@ -2492,12 +2807,12 @@ class SegmentedControlHandler(AndroidViewHandler):
|
|
|
2492
2807
|
|
|
2493
2808
|
|
|
2494
2809
|
# ======================================================================
|
|
2495
|
-
# DatePicker
|
|
2810
|
+
# DatePicker: trigger button opening native date/time dialogs
|
|
2496
2811
|
# ======================================================================
|
|
2497
2812
|
|
|
2498
2813
|
|
|
2499
2814
|
class DatePickerHandler(AndroidViewHandler):
|
|
2500
|
-
"""``DatePicker`` element
|
|
2815
|
+
"""``DatePicker`` element, a trigger ``Button`` opening native dialogs.
|
|
2501
2816
|
|
|
2502
2817
|
The button text reflects the current ISO ``value`` (or a
|
|
2503
2818
|
placeholder). Tapping it opens a ``DatePickerDialog`` (``mode``
|
|
@@ -2662,6 +2977,217 @@ class DatePickerHandler(AndroidViewHandler):
|
|
|
2662
2977
|
_fire(btn, "on_change", iso)
|
|
2663
2978
|
|
|
2664
2979
|
|
|
2980
|
+
# ======================================================================
|
|
2981
|
+
# VirtualList — RecyclerView-backed native virtualization
|
|
2982
|
+
# ======================================================================
|
|
2983
|
+
#
|
|
2984
|
+
# The heavy lifting lives in the template's ``PNVirtualListView.java``:
|
|
2985
|
+
# Chaquopy cannot subclass abstract Java classes (RecyclerView.Adapter,
|
|
2986
|
+
# RecyclerView.ViewHolder), so the Java side owns the adapter and view
|
|
2987
|
+
# holders and calls back into Python through the small ``Delegate``
|
|
2988
|
+
# interface. Each visible row hosts a nested-reconciler subtree (see
|
|
2989
|
+
# ``pythonnative.virtual_rows``): the delegate's ``mountRow`` renders
|
|
2990
|
+
# the row element and attaches its native root to the recycled cell
|
|
2991
|
+
# container; ``onRowRecycled`` tears the subtree down.
|
|
2992
|
+
|
|
2993
|
+
# Per-list mutable state, keyed by the RecyclerView's Java identity.
|
|
2994
|
+
# The delegate proxy closes over this dict so prop updates (new
|
|
2995
|
+
# ``render_row``, new ``count``) take effect without re-wiring Java.
|
|
2996
|
+
_pn_vlist_info: Dict[int, Dict[str, Any]] = {}
|
|
2997
|
+
|
|
2998
|
+
|
|
2999
|
+
class VirtualListHandler(AndroidViewHandler):
|
|
3000
|
+
"""Natively virtualized list backed by ``RecyclerView``.
|
|
3001
|
+
|
|
3002
|
+
Expects props:
|
|
3003
|
+
|
|
3004
|
+
- ``count``: total number of rows.
|
|
3005
|
+
- ``row_height``: uniform row extent in points, or
|
|
3006
|
+
- ``row_heights``: per-row extents (section lists interleave
|
|
3007
|
+
headers and items of different heights).
|
|
3008
|
+
- ``render_row``: ``render_row(index) -> Element`` producing one
|
|
3009
|
+
row's subtree. Called lazily as rows enter the viewport.
|
|
3010
|
+
- ``shows_scroll_indicator``: hide the scroll bar when ``False``.
|
|
3011
|
+
|
|
3012
|
+
Events (dispatched by tag): ``on_row_press`` with the row index,
|
|
3013
|
+
``on_scroll`` with ``{"x", "y", "extent", "range"}`` in points.
|
|
3014
|
+
|
|
3015
|
+
Commands: ``scroll_to_offset`` / ``scroll_to_index`` /
|
|
3016
|
+
``scroll_to_end`` / ``get_scroll_offset``.
|
|
3017
|
+
"""
|
|
3018
|
+
|
|
3019
|
+
def _build(self, props: Dict[str, Any]) -> Any:
|
|
3020
|
+
from ..virtual_rows import RowHostPool
|
|
3021
|
+
|
|
3022
|
+
PNVirtualListView = _pn_runtime_class("PNVirtualListView")
|
|
3023
|
+
info: Dict[str, Any] = {
|
|
3024
|
+
"count": int(props.get("count", 0) or 0),
|
|
3025
|
+
"row_height": float(props.get("row_height", 44.0) or 44.0),
|
|
3026
|
+
"row_heights": props.get("row_heights"),
|
|
3027
|
+
"render_row": props.get("render_row"),
|
|
3028
|
+
"pool": RowHostPool(),
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
class _Delegate(dynamic_proxy(PNVirtualListView.Delegate)):
|
|
3032
|
+
def getCount(self) -> int:
|
|
3033
|
+
return int(info["count"])
|
|
3034
|
+
|
|
3035
|
+
def getRowHeightDp(self, position: int) -> float:
|
|
3036
|
+
heights = info.get("row_heights")
|
|
3037
|
+
if heights and 0 <= position < len(heights):
|
|
3038
|
+
return float(heights[position])
|
|
3039
|
+
return float(info["row_height"])
|
|
3040
|
+
|
|
3041
|
+
def mountRow(self, position: int, container: Any, width_dp: float, height_dp: float) -> None:
|
|
3042
|
+
render_row = info.get("render_row")
|
|
3043
|
+
if render_row is None:
|
|
3044
|
+
return
|
|
3045
|
+
try:
|
|
3046
|
+
pool = info["pool"]
|
|
3047
|
+
root = pool.bind(
|
|
3048
|
+
_java_id(container),
|
|
3049
|
+
lambda: render_row(int(position)),
|
|
3050
|
+
float(width_dp),
|
|
3051
|
+
float(height_dp),
|
|
3052
|
+
)
|
|
3053
|
+
if root is not None:
|
|
3054
|
+
_insert_view(container, root, 0)
|
|
3055
|
+
# The layout engine frames only the *descendants*
|
|
3056
|
+
# of a subtree root (screen hosts normally size
|
|
3057
|
+
# the root themselves), so the row root would
|
|
3058
|
+
# otherwise keep the 0x0 params ``_insert_view``
|
|
3059
|
+
# assigns and render as nothing. Fill the cell.
|
|
3060
|
+
FrameLP = jclass("android.widget.FrameLayout$LayoutParams")
|
|
3061
|
+
root.setLayoutParams(FrameLP(FrameLP.MATCH_PARENT, FrameLP.MATCH_PARENT))
|
|
3062
|
+
except Exception:
|
|
3063
|
+
import traceback
|
|
3064
|
+
|
|
3065
|
+
traceback.print_exc()
|
|
3066
|
+
|
|
3067
|
+
def onRowPress(self, position: int) -> None:
|
|
3068
|
+
view = info.get("view")
|
|
3069
|
+
if view is not None:
|
|
3070
|
+
_fire(view, "on_row_press", int(position))
|
|
3071
|
+
|
|
3072
|
+
def onRowRecycled(self, container: Any) -> None:
|
|
3073
|
+
try:
|
|
3074
|
+
info["pool"].release(_java_id(container))
|
|
3075
|
+
except Exception:
|
|
3076
|
+
pass
|
|
3077
|
+
|
|
3078
|
+
def onScrolled(self, offset_dp: float, extent_dp: float, range_dp: float) -> None:
|
|
3079
|
+
view = info.get("view")
|
|
3080
|
+
if view is not None:
|
|
3081
|
+
_fire(
|
|
3082
|
+
view,
|
|
3083
|
+
"on_scroll",
|
|
3084
|
+
{
|
|
3085
|
+
"x": 0.0,
|
|
3086
|
+
"y": float(offset_dp),
|
|
3087
|
+
"extent": float(extent_dp),
|
|
3088
|
+
"range": float(range_dp),
|
|
3089
|
+
},
|
|
3090
|
+
)
|
|
3091
|
+
|
|
3092
|
+
delegate = _Delegate()
|
|
3093
|
+
view = PNVirtualListView(_ctx(), delegate)
|
|
3094
|
+
info["view"] = view
|
|
3095
|
+
# Keep the proxy alive: Java holds only a weak-ish reference
|
|
3096
|
+
# through the field, and Chaquopy proxies are collectible once
|
|
3097
|
+
# the Python side drops them.
|
|
3098
|
+
info["delegate"] = delegate
|
|
3099
|
+
_pn_vlist_info[_java_id(view)] = info
|
|
3100
|
+
return view
|
|
3101
|
+
|
|
3102
|
+
def _apply(self, view: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
3103
|
+
_apply_common_visual(view, props)
|
|
3104
|
+
info = _pn_vlist_info.get(_java_id(view))
|
|
3105
|
+
if info is None:
|
|
3106
|
+
return
|
|
3107
|
+
data_changed = False
|
|
3108
|
+
if "count" in props:
|
|
3109
|
+
info["count"] = int(props.get("count") or 0)
|
|
3110
|
+
data_changed = True
|
|
3111
|
+
if "row_height" in props and props["row_height"] is not None:
|
|
3112
|
+
info["row_height"] = float(props["row_height"])
|
|
3113
|
+
data_changed = True
|
|
3114
|
+
if "row_heights" in props:
|
|
3115
|
+
info["row_heights"] = props.get("row_heights")
|
|
3116
|
+
data_changed = True
|
|
3117
|
+
if "render_row" in props:
|
|
3118
|
+
info["render_row"] = props.get("render_row")
|
|
3119
|
+
data_changed = True
|
|
3120
|
+
if "shows_scroll_indicator" in props:
|
|
3121
|
+
show = props["shows_scroll_indicator"] is not False
|
|
3122
|
+
try:
|
|
3123
|
+
view.setVerticalScrollBarEnabled(show)
|
|
3124
|
+
except Exception:
|
|
3125
|
+
pass
|
|
3126
|
+
if data_changed and not initial:
|
|
3127
|
+
try:
|
|
3128
|
+
view.notifyDataChanged()
|
|
3129
|
+
except Exception:
|
|
3130
|
+
pass
|
|
3131
|
+
|
|
3132
|
+
def _teardown(self, native_view: Any) -> None:
|
|
3133
|
+
info = _pn_vlist_info.pop(_java_id(native_view), None)
|
|
3134
|
+
if info is not None:
|
|
3135
|
+
info.get("pool").release_all()
|
|
3136
|
+
info["view"] = None
|
|
3137
|
+
|
|
3138
|
+
def measure_intrinsic(
|
|
3139
|
+
self,
|
|
3140
|
+
native_view: Any,
|
|
3141
|
+
max_width: float,
|
|
3142
|
+
max_height: float,
|
|
3143
|
+
) -> Tuple[float, float]:
|
|
3144
|
+
# Fill the available space, like a ScrollView clamped to its
|
|
3145
|
+
# parent. Inside an unbounded axis (nested in another scroll
|
|
3146
|
+
# view) collapse to 0, matching React Native's behavior for
|
|
3147
|
+
# nested virtualized lists.
|
|
3148
|
+
w = max_width if math.isfinite(max_width) else 0.0
|
|
3149
|
+
h = max_height if math.isfinite(max_height) else 0.0
|
|
3150
|
+
return (max(0.0, w), max(0.0, h))
|
|
3151
|
+
|
|
3152
|
+
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
3153
|
+
density = _density() or 1.0
|
|
3154
|
+
if name == "scroll_to_offset":
|
|
3155
|
+
try:
|
|
3156
|
+
native_view.scrollToOffsetDp(
|
|
3157
|
+
float(args.get("y", 0.0) or 0.0),
|
|
3158
|
+
args.get("animated", True) is not False,
|
|
3159
|
+
)
|
|
3160
|
+
except Exception:
|
|
3161
|
+
pass
|
|
3162
|
+
return None
|
|
3163
|
+
if name == "scroll_to_index":
|
|
3164
|
+
try:
|
|
3165
|
+
native_view.scrollToIndex(
|
|
3166
|
+
int(args.get("index", 0) or 0),
|
|
3167
|
+
args.get("animated", True) is not False,
|
|
3168
|
+
)
|
|
3169
|
+
except Exception:
|
|
3170
|
+
pass
|
|
3171
|
+
return None
|
|
3172
|
+
if name == "scroll_to_end":
|
|
3173
|
+
info = _pn_vlist_info.get(_java_id(native_view))
|
|
3174
|
+
count = int(info.get("count", 0)) if info else 0
|
|
3175
|
+
try:
|
|
3176
|
+
native_view.scrollToIndex(max(0, count - 1), args.get("animated", True) is not False)
|
|
3177
|
+
except Exception:
|
|
3178
|
+
pass
|
|
3179
|
+
return None
|
|
3180
|
+
if name == "get_scroll_offset":
|
|
3181
|
+
try:
|
|
3182
|
+
return {
|
|
3183
|
+
"x": 0.0,
|
|
3184
|
+
"y": float(native_view.computeVerticalScrollOffset()) / density,
|
|
3185
|
+
}
|
|
3186
|
+
except Exception:
|
|
3187
|
+
return {"x": 0.0, "y": 0.0}
|
|
3188
|
+
return None
|
|
3189
|
+
|
|
3190
|
+
|
|
2665
3191
|
# ======================================================================
|
|
2666
3192
|
# Registration
|
|
2667
3193
|
# ======================================================================
|
|
@@ -2694,6 +3220,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
2694
3220
|
registry.register("Checkbox", CheckboxHandler())
|
|
2695
3221
|
registry.register("SegmentedControl", SegmentedControlHandler())
|
|
2696
3222
|
registry.register("DatePicker", DatePickerHandler())
|
|
3223
|
+
registry.register("VirtualList", VirtualListHandler())
|
|
2697
3224
|
|
|
2698
3225
|
|
|
2699
3226
|
__all__ = [
|
|
@@ -2720,5 +3247,6 @@ __all__ = [
|
|
|
2720
3247
|
"CheckboxHandler",
|
|
2721
3248
|
"SegmentedControlHandler",
|
|
2722
3249
|
"DatePickerHandler",
|
|
3250
|
+
"VirtualListHandler",
|
|
2723
3251
|
"register_handlers",
|
|
2724
3252
|
]
|