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
pythonnative/storage.py
CHANGED
|
@@ -321,7 +321,7 @@ class AsyncStorage:
|
|
|
321
321
|
"""Return the JSON-decoded value stored at ``key``, or ``None``.
|
|
322
322
|
|
|
323
323
|
If the stored value isn't valid JSON, returns ``None`` rather
|
|
324
|
-
than raising
|
|
324
|
+
than raising; assume the entry was written by another
|
|
325
325
|
process or an older version of the app.
|
|
326
326
|
"""
|
|
327
327
|
raw = await AsyncStorage.get(key)
|
|
@@ -347,7 +347,7 @@ def use_persisted_state(
|
|
|
347
347
|
Backed by [`AsyncStorage`][pythonnative.storage.AsyncStorage]:
|
|
348
348
|
behaves like ``use_state`` but loads the prior value (if any) on
|
|
349
349
|
mount and persists every subsequent update. Until the load
|
|
350
|
-
completes the value is ``initial
|
|
350
|
+
completes the value is ``initial``, the same fallback React
|
|
351
351
|
Native users get with ``AsyncStorage.getItem``.
|
|
352
352
|
|
|
353
353
|
The setter accepts either a value or a ``current -> new``
|
|
@@ -362,7 +362,7 @@ def use_persisted_state(
|
|
|
362
362
|
initial: Value used before the first load completes.
|
|
363
363
|
|
|
364
364
|
Returns:
|
|
365
|
-
``(value, setter)
|
|
365
|
+
``(value, setter)``, same shape as
|
|
366
366
|
[`use_state`][pythonnative.use_state].
|
|
367
367
|
|
|
368
368
|
Example:
|
pythonnative/style.py
CHANGED
|
@@ -36,7 +36,7 @@ Example:
|
|
|
36
36
|
|
|
37
37
|
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
|
|
38
38
|
|
|
39
|
-
from .hooks import Context, create_context
|
|
39
|
+
from .hooks import Context, create_context, use_color_scheme, use_context
|
|
40
40
|
|
|
41
41
|
# ======================================================================
|
|
42
42
|
# Atomic value types
|
|
@@ -208,7 +208,7 @@ class Style(TypedDict, total=False):
|
|
|
208
208
|
[`Literal`][typing.Literal]. ``Style`` is a `total=False` TypedDict
|
|
209
209
|
so any subset of keys is valid at construction time.
|
|
210
210
|
|
|
211
|
-
Custom native components may accept additional, unlisted keys
|
|
211
|
+
Custom native components may accept additional, unlisted keys;
|
|
212
212
|
they are ignored by the built-in handlers but flow through the
|
|
213
213
|
reconciler unmodified, so third-party handlers can read them.
|
|
214
214
|
|
|
@@ -283,6 +283,14 @@ class Style(TypedDict, total=False):
|
|
|
283
283
|
# --- Visual: borders ---
|
|
284
284
|
border_width: float
|
|
285
285
|
border_radius: float
|
|
286
|
+
border_top_width: float
|
|
287
|
+
border_right_width: float
|
|
288
|
+
border_bottom_width: float
|
|
289
|
+
border_left_width: float
|
|
290
|
+
border_top_color: Color
|
|
291
|
+
border_right_color: Color
|
|
292
|
+
border_bottom_color: Color
|
|
293
|
+
border_left_color: Color
|
|
286
294
|
|
|
287
295
|
# --- Visual: typography ---
|
|
288
296
|
font_size: float
|
|
@@ -481,6 +489,7 @@ DEFAULT_LIGHT_THEME: Dict[str, Any] = {
|
|
|
481
489
|
"spacing_large": 16,
|
|
482
490
|
"border_radius": 8,
|
|
483
491
|
}
|
|
492
|
+
"""Built-in light theme selected by [`use_theme`][pythonnative.use_theme]."""
|
|
484
493
|
|
|
485
494
|
DEFAULT_DARK_THEME: Dict[str, Any] = {
|
|
486
495
|
"primary_color": "#0A84FF",
|
|
@@ -500,17 +509,65 @@ DEFAULT_DARK_THEME: Dict[str, Any] = {
|
|
|
500
509
|
"spacing_large": 16,
|
|
501
510
|
"border_radius": 8,
|
|
502
511
|
}
|
|
512
|
+
"""Built-in dark theme selected by [`use_theme`][pythonnative.use_theme]."""
|
|
503
513
|
|
|
504
|
-
|
|
505
|
-
"""
|
|
514
|
+
_FOLLOW_SYSTEM_THEME = object()
|
|
515
|
+
"""Sentinel default for `ThemeContext`: resolve from the color scheme."""
|
|
506
516
|
|
|
507
|
-
|
|
517
|
+
ThemeContext: Context = create_context(_FOLLOW_SYSTEM_THEME)
|
|
518
|
+
"""Theme context that follows the system color scheme by default.
|
|
519
|
+
|
|
520
|
+
Without a provider, [`use_theme`][pythonnative.use_theme] resolves to
|
|
521
|
+
[`DEFAULT_LIGHT_THEME`][pythonnative.style.DEFAULT_LIGHT_THEME] or
|
|
522
|
+
[`DEFAULT_DARK_THEME`][pythonnative.style.DEFAULT_DARK_THEME] based on
|
|
523
|
+
the current appearance. Wrap a subtree in
|
|
508
524
|
[`Provider(ThemeContext, my_theme, ...)`][pythonnative.Provider] to
|
|
509
|
-
|
|
510
|
-
via [`
|
|
525
|
+
pin an explicit theme for that subtree, then read it inside
|
|
526
|
+
descendants via [`use_theme`][pythonnative.use_theme] (or
|
|
527
|
+
[`use_context(ThemeContext)`][pythonnative.use_context]).
|
|
511
528
|
"""
|
|
512
529
|
|
|
513
530
|
|
|
531
|
+
def default_theme(scheme: str) -> Dict[str, Any]:
|
|
532
|
+
"""Return the built-in theme dict for ``scheme`` (``"light"`` / ``"dark"``)."""
|
|
533
|
+
return DEFAULT_DARK_THEME if scheme == "dark" else DEFAULT_LIGHT_THEME
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def use_theme() -> Dict[str, Any]:
|
|
537
|
+
"""Return the active theme, following the system appearance by default.
|
|
538
|
+
|
|
539
|
+
If an ancestor mounted a ``Provider(ThemeContext, ...)``, that
|
|
540
|
+
theme is returned as-is. Otherwise the built-in light or dark
|
|
541
|
+
theme is selected from the effective color scheme (via
|
|
542
|
+
[`use_color_scheme`][pythonnative.use_color_scheme], so the
|
|
543
|
+
component re-renders when the system appearance flips).
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
The active theme dict.
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
RuntimeError: If called outside a `@component` function.
|
|
550
|
+
|
|
551
|
+
Example:
|
|
552
|
+
```python
|
|
553
|
+
import pythonnative as pn
|
|
554
|
+
|
|
555
|
+
@pn.component
|
|
556
|
+
def Card():
|
|
557
|
+
theme = pn.use_theme()
|
|
558
|
+
return pn.View(
|
|
559
|
+
pn.Text("Hello", style=pn.style(color=theme["text_color"])),
|
|
560
|
+
style=pn.style(background_color=theme["surface_color"]),
|
|
561
|
+
)
|
|
562
|
+
```
|
|
563
|
+
"""
|
|
564
|
+
scheme = use_color_scheme()
|
|
565
|
+
theme = use_context(ThemeContext)
|
|
566
|
+
if theme is _FOLLOW_SYSTEM_THEME:
|
|
567
|
+
return default_theme(scheme)
|
|
568
|
+
return theme
|
|
569
|
+
|
|
570
|
+
|
|
514
571
|
__all__ = [
|
|
515
572
|
"AlignItems",
|
|
516
573
|
"AlignSelf",
|
|
@@ -543,6 +600,8 @@ __all__ = [
|
|
|
543
600
|
"TransformScaleY",
|
|
544
601
|
"TransformSpec",
|
|
545
602
|
"TransformTranslate",
|
|
603
|
+
"default_theme",
|
|
546
604
|
"resolve_style",
|
|
547
605
|
"style",
|
|
606
|
+
"use_theme",
|
|
548
607
|
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.pythonnative.android_template
|
|
2
|
+
|
|
3
|
+
import android.os.Build
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.accessibility.AccessibilityNodeInfo
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Accessibility delegate that exposes PythonNative's `test_id` and
|
|
9
|
+
* `accessibility_state` props through the Android accessibility tree.
|
|
10
|
+
*
|
|
11
|
+
* `test_id` is surfaced as the node's view-id resource name, which
|
|
12
|
+
* UI Automator-based tools (Maestro, UiAutomator2, Appium) match as
|
|
13
|
+
* `resource-id`. State flags map onto the closest
|
|
14
|
+
* [AccessibilityNodeInfo] equivalents so TalkBack announces them.
|
|
15
|
+
*
|
|
16
|
+
* Instances are created and configured from Python (the Android view
|
|
17
|
+
* handlers); fields left null are not applied.
|
|
18
|
+
*/
|
|
19
|
+
class PNAccessibilityDelegate : View.AccessibilityDelegate() {
|
|
20
|
+
var testId: String? = null
|
|
21
|
+
var stateDisabled: Boolean? = null
|
|
22
|
+
var stateSelected: Boolean? = null
|
|
23
|
+
var stateChecked: Boolean? = null
|
|
24
|
+
var stateBusy: Boolean? = null
|
|
25
|
+
var stateExpanded: Boolean? = null
|
|
26
|
+
|
|
27
|
+
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
|
|
28
|
+
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
29
|
+
testId?.let { info.viewIdResourceName = it }
|
|
30
|
+
stateDisabled?.let { info.isEnabled = !it }
|
|
31
|
+
stateSelected?.let { info.isSelected = it }
|
|
32
|
+
stateChecked?.let {
|
|
33
|
+
info.isCheckable = true
|
|
34
|
+
info.isChecked = it
|
|
35
|
+
}
|
|
36
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
37
|
+
val states = mutableListOf<String>()
|
|
38
|
+
if (stateBusy == true) states.add("busy")
|
|
39
|
+
when (stateExpanded) {
|
|
40
|
+
true -> states.add("expanded")
|
|
41
|
+
false -> states.add("collapsed")
|
|
42
|
+
null -> {}
|
|
43
|
+
}
|
|
44
|
+
if (states.isNotEmpty()) info.stateDescription = states.joinToString(", ")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package com.pythonnative.android_template
|
|
2
|
+
|
|
3
|
+
import android.graphics.Canvas
|
|
4
|
+
import android.graphics.ColorFilter
|
|
5
|
+
import android.graphics.Paint
|
|
6
|
+
import android.graphics.PixelFormat
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.graphics.drawable.Drawable
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Background drawable that renders an optional rounded fill plus four
|
|
12
|
+
* independent border strips. Used for PythonNative's
|
|
13
|
+
* `border_<side>_width` / `border_<side>_color` style props, which
|
|
14
|
+
* `GradientDrawable`'s single uniform stroke cannot express.
|
|
15
|
+
*
|
|
16
|
+
* Widths are in pixels; sides are ordered left, top, right, bottom.
|
|
17
|
+
*/
|
|
18
|
+
class PNBorderDrawable(
|
|
19
|
+
private val hasBackground: Boolean,
|
|
20
|
+
private val backgroundColor: Int,
|
|
21
|
+
private val cornerRadius: Float,
|
|
22
|
+
private val widths: FloatArray,
|
|
23
|
+
private val colors: IntArray,
|
|
24
|
+
) : Drawable() {
|
|
25
|
+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
26
|
+
|
|
27
|
+
override fun draw(canvas: Canvas) {
|
|
28
|
+
val b = RectF(bounds)
|
|
29
|
+
if (hasBackground) {
|
|
30
|
+
paint.style = Paint.Style.FILL
|
|
31
|
+
paint.color = backgroundColor
|
|
32
|
+
if (cornerRadius > 0f) {
|
|
33
|
+
canvas.drawRoundRect(b, cornerRadius, cornerRadius, paint)
|
|
34
|
+
} else {
|
|
35
|
+
canvas.drawRect(b, paint)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
paint.style = Paint.Style.FILL
|
|
39
|
+
if (widths[0] > 0f) {
|
|
40
|
+
paint.color = colors[0]
|
|
41
|
+
canvas.drawRect(b.left, b.top, b.left + widths[0], b.bottom, paint)
|
|
42
|
+
}
|
|
43
|
+
if (widths[1] > 0f) {
|
|
44
|
+
paint.color = colors[1]
|
|
45
|
+
canvas.drawRect(b.left, b.top, b.right, b.top + widths[1], paint)
|
|
46
|
+
}
|
|
47
|
+
if (widths[2] > 0f) {
|
|
48
|
+
paint.color = colors[2]
|
|
49
|
+
canvas.drawRect(b.right - widths[2], b.top, b.right, b.bottom, paint)
|
|
50
|
+
}
|
|
51
|
+
if (widths[3] > 0f) {
|
|
52
|
+
paint.color = colors[3]
|
|
53
|
+
canvas.drawRect(b.left, b.bottom - widths[3], b.right, b.bottom, paint)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun setAlpha(alpha: Int) {
|
|
58
|
+
paint.alpha = alpha
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun setColorFilter(colorFilter: ColorFilter?) {
|
|
62
|
+
paint.colorFilter = colorFilter
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Deprecated("Deprecated in Java")
|
|
66
|
+
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
|
67
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
package com.pythonnative.android_template;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.util.TypedValue;
|
|
5
|
+
import android.view.ViewGroup;
|
|
6
|
+
import android.widget.FrameLayout;
|
|
7
|
+
|
|
8
|
+
import androidx.annotation.NonNull;
|
|
9
|
+
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
10
|
+
import androidx.recyclerview.widget.RecyclerView;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RecyclerView-backed virtualized list used by PythonNative's Android bridge.
|
|
14
|
+
*
|
|
15
|
+
* <p>Chaquopy can proxy Java interfaces from Python, but it can't subclass Java
|
|
16
|
+
* abstract classes such as RecyclerView.Adapter or RecyclerView.ViewHolder. This
|
|
17
|
+
* helper owns those abstract-class implementations in Java and delegates row
|
|
18
|
+
* content back to Python through the small Delegate interface below.</p>
|
|
19
|
+
*
|
|
20
|
+
* <p>Rows may have per-position heights (section lists interleave headers and
|
|
21
|
+
* items); the delegate reports each row's height in dp. Scroll changes are
|
|
22
|
+
* forwarded to the delegate in dp so the Python side can drive on_scroll,
|
|
23
|
+
* on_end_reached, and viewability callbacks with one code path per platform.</p>
|
|
24
|
+
*/
|
|
25
|
+
public class PNVirtualListView extends RecyclerView {
|
|
26
|
+
public interface Delegate {
|
|
27
|
+
int getCount();
|
|
28
|
+
|
|
29
|
+
float getRowHeightDp(int position);
|
|
30
|
+
|
|
31
|
+
void mountRow(int position, FrameLayout container, float widthDp, float heightDp);
|
|
32
|
+
|
|
33
|
+
void onRowPress(int position);
|
|
34
|
+
|
|
35
|
+
void onRowRecycled(FrameLayout container);
|
|
36
|
+
|
|
37
|
+
void onScrolled(float offsetDp, float extentDp, float rangeDp);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private final float density;
|
|
41
|
+
private final RowAdapter rowAdapter;
|
|
42
|
+
private Delegate delegate;
|
|
43
|
+
|
|
44
|
+
public PNVirtualListView(@NonNull Context context, @NonNull Delegate delegate) {
|
|
45
|
+
super(context);
|
|
46
|
+
this.delegate = delegate;
|
|
47
|
+
this.density = context.getResources().getDisplayMetrics().density;
|
|
48
|
+
setLayoutManager(new LinearLayoutManager(context));
|
|
49
|
+
rowAdapter = new RowAdapter();
|
|
50
|
+
setAdapter(rowAdapter);
|
|
51
|
+
addOnScrollListener(new OnScrollListener() {
|
|
52
|
+
@Override
|
|
53
|
+
public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
|
|
54
|
+
PNVirtualListView.this.delegate.onScrolled(
|
|
55
|
+
rv.computeVerticalScrollOffset() / PNVirtualListView.this.density,
|
|
56
|
+
rv.computeVerticalScrollExtent() / PNVirtualListView.this.density,
|
|
57
|
+
rv.computeVerticalScrollRange() / PNVirtualListView.this.density
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public void setDelegate(@NonNull Delegate delegate) {
|
|
64
|
+
this.delegate = delegate;
|
|
65
|
+
rowAdapter.notifyDataSetChanged();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public void notifyDataChanged() {
|
|
69
|
+
rowAdapter.notifyDataSetChanged();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public void scrollToOffsetDp(float offsetDp, boolean animated) {
|
|
73
|
+
int targetPx = Math.round(offsetDp * density);
|
|
74
|
+
int currentPx = computeVerticalScrollOffset();
|
|
75
|
+
if (animated) {
|
|
76
|
+
smoothScrollBy(0, targetPx - currentPx);
|
|
77
|
+
} else {
|
|
78
|
+
scrollBy(0, targetPx - currentPx);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public void scrollToIndex(int position, boolean animated) {
|
|
83
|
+
int clamped = Math.max(0, Math.min(position, Math.max(0, delegate.getCount() - 1)));
|
|
84
|
+
if (animated) {
|
|
85
|
+
smoothScrollToPosition(clamped);
|
|
86
|
+
} else {
|
|
87
|
+
scrollToPosition(clamped);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private int rowHeightPx(int position) {
|
|
92
|
+
return Math.max(
|
|
93
|
+
1,
|
|
94
|
+
Math.round(TypedValue.applyDimension(
|
|
95
|
+
TypedValue.COMPLEX_UNIT_DIP,
|
|
96
|
+
delegate.getRowHeightDp(position),
|
|
97
|
+
getResources().getDisplayMetrics()
|
|
98
|
+
))
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private float currentWidthDp(FrameLayout container) {
|
|
103
|
+
int widthPx = container.getWidth();
|
|
104
|
+
if (widthPx <= 0) {
|
|
105
|
+
widthPx = getWidth();
|
|
106
|
+
}
|
|
107
|
+
if (widthPx <= 0) {
|
|
108
|
+
widthPx = getResources().getDisplayMetrics().widthPixels;
|
|
109
|
+
}
|
|
110
|
+
return widthPx / density;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private class RowHolder extends RecyclerView.ViewHolder {
|
|
114
|
+
final FrameLayout container;
|
|
115
|
+
|
|
116
|
+
RowHolder(@NonNull FrameLayout container) {
|
|
117
|
+
super(container);
|
|
118
|
+
this.container = container;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private class RowAdapter extends RecyclerView.Adapter<RowHolder> {
|
|
123
|
+
@Override
|
|
124
|
+
public int getItemCount() {
|
|
125
|
+
return delegate.getCount();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@NonNull
|
|
129
|
+
@Override
|
|
130
|
+
public RowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
131
|
+
FrameLayout container = new FrameLayout(parent.getContext());
|
|
132
|
+
container.setLayoutParams(
|
|
133
|
+
new RecyclerView.LayoutParams(
|
|
134
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
135
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
return new RowHolder(container);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Override
|
|
142
|
+
public void onBindViewHolder(@NonNull RowHolder holder, int position) {
|
|
143
|
+
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.container.getLayoutParams();
|
|
144
|
+
int heightPx = rowHeightPx(position);
|
|
145
|
+
if (params.height != heightPx) {
|
|
146
|
+
params.height = heightPx;
|
|
147
|
+
holder.container.setLayoutParams(params);
|
|
148
|
+
}
|
|
149
|
+
holder.container.removeAllViews();
|
|
150
|
+
holder.container.setOnClickListener(v -> {
|
|
151
|
+
int pos = holder.getBindingAdapterPosition();
|
|
152
|
+
if (pos != RecyclerView.NO_POSITION) {
|
|
153
|
+
delegate.onRowPress(pos);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
delegate.mountRow(
|
|
157
|
+
position,
|
|
158
|
+
holder.container,
|
|
159
|
+
currentWidthDp(holder.container),
|
|
160
|
+
delegate.getRowHeightDp(position)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Override
|
|
165
|
+
public void onViewRecycled(@NonNull RowHolder holder) {
|
|
166
|
+
holder.container.removeAllViews();
|
|
167
|
+
holder.container.setOnClickListener(null);
|
|
168
|
+
delegate.onRowRecycled(holder.container);
|
|
169
|
+
super.onViewRecycled(holder);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -253,8 +253,8 @@
|
|
|
253
253
|
isa = XCRemoteSwiftPackageReference;
|
|
254
254
|
repositoryURL = "https://github.com/pvieito/PythonKit";
|
|
255
255
|
requirement = {
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
kind = upToNextMinorVersion;
|
|
257
|
+
minimumVersion = 0.5.1;
|
|
258
258
|
};
|
|
259
259
|
};
|
|
260
260
|
/* End XCRemoteSwiftPackageReference section */
|
|
@@ -15,7 +15,7 @@ final class ios_templateUITests: XCTestCase {
|
|
|
15
15
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
|
16
16
|
continueAfterFailure = false
|
|
17
17
|
|
|
18
|
-
// In UI tests it
|
|
18
|
+
// In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
override func tearDownWithError() throws {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Row subtrees for natively virtualized lists.
|
|
2
|
+
|
|
3
|
+
Native virtualized lists (Android ``RecyclerView`` via
|
|
4
|
+
``PNVirtualListView``, iOS ``UITableView``) own row lifecycle on the
|
|
5
|
+
platform side: the platform decides when a row enters the viewport,
|
|
6
|
+
hands PythonNative an empty native container, and recycles that
|
|
7
|
+
container when the row scrolls away.
|
|
8
|
+
|
|
9
|
+
Each visible row hosts a full PythonNative subtree driven by its own
|
|
10
|
+
nested [`Reconciler`][pythonnative.reconciler.Reconciler]. The
|
|
11
|
+
[`RowSubtree`][pythonnative.virtual_rows.RowSubtree] class wraps that
|
|
12
|
+
per-row reconciler: it mounts the row element into fresh native views,
|
|
13
|
+
re-reconciles in place when the platform rebinds the same container to
|
|
14
|
+
a different row index (recycling), and tears everything down when the
|
|
15
|
+
list is destroyed.
|
|
16
|
+
|
|
17
|
+
State inside rows works: hooks like ``use_state`` mark the row's own
|
|
18
|
+
reconciler dirty and a guarded flush re-renders just that row.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Callable, Dict
|
|
22
|
+
|
|
23
|
+
from .element import Element
|
|
24
|
+
|
|
25
|
+
__all__ = ["RowSubtree", "RowHostPool"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RowSubtree:
|
|
29
|
+
"""One list row's mounted PythonNative subtree.
|
|
30
|
+
|
|
31
|
+
Owns a nested reconciler whose viewport is the row's cell size.
|
|
32
|
+
The native root view produced by
|
|
33
|
+
[`mount`][pythonnative.virtual_rows.RowSubtree.mount] is what the
|
|
34
|
+
platform handler attaches to the recycled cell container.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
from .native_views import get_registry
|
|
39
|
+
from .reconciler import Reconciler
|
|
40
|
+
|
|
41
|
+
self._reconciler = Reconciler(get_registry())
|
|
42
|
+
# Guarded synchronous flush so ``use_state`` inside a row
|
|
43
|
+
# re-renders the row. Re-entrant requests (a setter firing
|
|
44
|
+
# during the flush itself) are queued and drained after.
|
|
45
|
+
self._flushing = False
|
|
46
|
+
self._flush_queued = False
|
|
47
|
+
self._reconciler._screen_re_render = self._request_flush
|
|
48
|
+
self.native_root: Any = None
|
|
49
|
+
|
|
50
|
+
def _request_flush(self) -> None:
|
|
51
|
+
if self._flushing:
|
|
52
|
+
self._flush_queued = True
|
|
53
|
+
return
|
|
54
|
+
self._flushing = True
|
|
55
|
+
try:
|
|
56
|
+
for _ in range(8):
|
|
57
|
+
self._flush_queued = False
|
|
58
|
+
self.native_root = self._reconciler.flush_dirty()
|
|
59
|
+
if not self._flush_queued:
|
|
60
|
+
break
|
|
61
|
+
finally:
|
|
62
|
+
self._flushing = False
|
|
63
|
+
|
|
64
|
+
def mount(self, element: Element, width: float, height: float) -> Any:
|
|
65
|
+
"""Mount ``element`` at the given cell size; returns the native root."""
|
|
66
|
+
self.native_root = self._reconciler.mount(element)
|
|
67
|
+
self._reconciler.set_viewport_size(float(width), float(height))
|
|
68
|
+
return self.native_root
|
|
69
|
+
|
|
70
|
+
def rebind(self, element: Element, width: float, height: float) -> Any:
|
|
71
|
+
"""Reconcile the subtree to ``element`` (cell recycled to a new row).
|
|
72
|
+
|
|
73
|
+
Cheaper than unmount + mount when consecutive rows share a
|
|
74
|
+
shape, which is the overwhelmingly common case in lists.
|
|
75
|
+
Returns the (possibly replaced) native root.
|
|
76
|
+
"""
|
|
77
|
+
self._reconciler.set_viewport_size(float(width), float(height))
|
|
78
|
+
self.native_root = self._reconciler.reconcile(element)
|
|
79
|
+
return self.native_root
|
|
80
|
+
|
|
81
|
+
def unmount(self) -> None:
|
|
82
|
+
"""Destroy the subtree and release its native views."""
|
|
83
|
+
try:
|
|
84
|
+
self._reconciler.unmount()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
self.native_root = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class RowHostPool:
|
|
91
|
+
"""Per-list bookkeeping of row subtrees keyed by native container.
|
|
92
|
+
|
|
93
|
+
Platform handlers create one pool per virtualized list view. Keys
|
|
94
|
+
are platform-stable container identities (Java identity hash on
|
|
95
|
+
Android, ``contentView`` pointer on iOS).
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
self._subtrees: Dict[int, RowSubtree] = {}
|
|
100
|
+
|
|
101
|
+
def bind(
|
|
102
|
+
self,
|
|
103
|
+
container_key: int,
|
|
104
|
+
make_element: Callable[[], Element],
|
|
105
|
+
width: float,
|
|
106
|
+
height: float,
|
|
107
|
+
) -> Any:
|
|
108
|
+
"""Mount or rebind the subtree for ``container_key``.
|
|
109
|
+
|
|
110
|
+
Returns the subtree's native root. The caller must (re)attach
|
|
111
|
+
it to the container: platforms strip a recycled cell's
|
|
112
|
+
children before rebinding, so even an unchanged root needs
|
|
113
|
+
re-attachment.
|
|
114
|
+
"""
|
|
115
|
+
element = make_element()
|
|
116
|
+
subtree = self._subtrees.get(container_key)
|
|
117
|
+
if subtree is None:
|
|
118
|
+
subtree = RowSubtree()
|
|
119
|
+
self._subtrees[container_key] = subtree
|
|
120
|
+
return subtree.mount(element, width, height)
|
|
121
|
+
return subtree.rebind(element, width, height)
|
|
122
|
+
|
|
123
|
+
def release(self, container_key: int) -> None:
|
|
124
|
+
"""Unmount the subtree for one recycled container, if any."""
|
|
125
|
+
subtree = self._subtrees.pop(container_key, None)
|
|
126
|
+
if subtree is not None:
|
|
127
|
+
subtree.unmount()
|
|
128
|
+
|
|
129
|
+
def release_all(self) -> None:
|
|
130
|
+
"""Unmount every subtree (list destroyed)."""
|
|
131
|
+
subtrees = list(self._subtrees.values())
|
|
132
|
+
self._subtrees.clear()
|
|
133
|
+
for subtree in subtrees:
|
|
134
|
+
subtree.unmount()
|
|
135
|
+
|
|
136
|
+
def __len__(self) -> int:
|
|
137
|
+
return len(self._subtrees)
|