pythonnative 0.22.1__py3-none-any.whl → 0.24.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 +43 -50
- pythonnative/animated.py +1 -1
- pythonnative/appearance.py +124 -0
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +873 -466
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +454 -49
- pythonnative/hot_reload.py +9 -1
- pythonnative/images.py +196 -0
- pythonnative/mutations.py +3 -12
- pythonnative/native_views/__init__.py +1 -7
- pythonnative/native_views/android.py +651 -46
- pythonnative/native_views/desktop.py +116 -20
- pythonnative/native_views/ios.py +974 -52
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +74 -7
- pythonnative/project/config.py +1 -9
- pythonnative/reconciler.py +863 -441
- pythonnative/screen.py +409 -27
- pythonnative/storage.py +3 -3
- pythonnative/style.py +148 -6
- 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/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- pythonnative/virtual_rows.py +137 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
pythonnative/style.py
CHANGED
|
@@ -34,9 +34,11 @@ Example:
|
|
|
34
34
|
```
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
+
import difflib
|
|
37
38
|
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
|
|
38
39
|
|
|
39
|
-
from .
|
|
40
|
+
from . import diagnostics
|
|
41
|
+
from .hooks import Context, create_context, use_color_scheme, use_context
|
|
40
42
|
|
|
41
43
|
# ======================================================================
|
|
42
44
|
# Atomic value types
|
|
@@ -74,6 +76,30 @@ EdgeValue = Union[Dimension, EdgeInsets]
|
|
|
74
76
|
[`EdgeInsets`][pythonnative.EdgeInsets] dict."""
|
|
75
77
|
|
|
76
78
|
|
|
79
|
+
class AccessibilityState(TypedDict, total=False):
|
|
80
|
+
"""Current widget state exposed to assistive technology.
|
|
81
|
+
|
|
82
|
+
Passed via the ``accessibility_state=`` prop on interactive
|
|
83
|
+
components. All keys are optional; omitted keys are treated as
|
|
84
|
+
unset (not ``False``).
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
disabled: The widget is visible but not interactive.
|
|
88
|
+
selected: The widget is currently selected (e.g. the active
|
|
89
|
+
tab).
|
|
90
|
+
checked: Toggle/checkbox state. ``"mixed"`` represents a
|
|
91
|
+
tri-state checkbox.
|
|
92
|
+
busy: The widget is loading or otherwise temporarily busy.
|
|
93
|
+
expanded: A disclosure/accordion is currently expanded.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
disabled: bool
|
|
97
|
+
selected: bool
|
|
98
|
+
checked: Union[bool, Literal["mixed"]]
|
|
99
|
+
busy: bool
|
|
100
|
+
expanded: bool
|
|
101
|
+
|
|
102
|
+
|
|
77
103
|
class ShadowOffset(TypedDict):
|
|
78
104
|
"""Shadow displacement in points."""
|
|
79
105
|
|
|
@@ -136,6 +162,17 @@ TransformSpec = Union[TransformEntry, List[TransformEntry]]
|
|
|
136
162
|
# ----------------------------------------------------------------------
|
|
137
163
|
|
|
138
164
|
FlexDirection = Literal["row", "column", "row_reverse", "column_reverse"]
|
|
165
|
+
FlexWrap = Literal["nowrap", "wrap", "wrap_reverse"]
|
|
166
|
+
AlignContent = Literal[
|
|
167
|
+
"flex_start",
|
|
168
|
+
"center",
|
|
169
|
+
"flex_end",
|
|
170
|
+
"stretch",
|
|
171
|
+
"space_between",
|
|
172
|
+
"space_around",
|
|
173
|
+
"space_evenly",
|
|
174
|
+
]
|
|
175
|
+
LayoutDirection = Literal["ltr", "rtl"]
|
|
139
176
|
JustifyContent = Literal[
|
|
140
177
|
"flex_start",
|
|
141
178
|
"center",
|
|
@@ -241,9 +278,12 @@ class Style(TypedDict, total=False):
|
|
|
241
278
|
flex_shrink: float
|
|
242
279
|
flex_basis: Dimension
|
|
243
280
|
flex_direction: FlexDirection
|
|
281
|
+
flex_wrap: FlexWrap
|
|
244
282
|
justify_content: JustifyContent
|
|
245
283
|
align_items: AlignItems
|
|
246
284
|
align_self: AlignSelf
|
|
285
|
+
align_content: AlignContent
|
|
286
|
+
direction: LayoutDirection
|
|
247
287
|
|
|
248
288
|
# --- Layout: position ---
|
|
249
289
|
position: Position
|
|
@@ -251,6 +291,8 @@ class Style(TypedDict, total=False):
|
|
|
251
291
|
right: Dimension
|
|
252
292
|
bottom: Dimension
|
|
253
293
|
left: Dimension
|
|
294
|
+
start: Dimension
|
|
295
|
+
end: Dimension
|
|
254
296
|
|
|
255
297
|
# --- Layout: spacing ---
|
|
256
298
|
padding: EdgeValue
|
|
@@ -258,6 +300,8 @@ class Style(TypedDict, total=False):
|
|
|
258
300
|
padding_bottom: Dimension
|
|
259
301
|
padding_left: Dimension
|
|
260
302
|
padding_right: Dimension
|
|
303
|
+
padding_start: Dimension
|
|
304
|
+
padding_end: Dimension
|
|
261
305
|
padding_horizontal: Dimension
|
|
262
306
|
padding_vertical: Dimension
|
|
263
307
|
margin: EdgeValue
|
|
@@ -265,10 +309,14 @@ class Style(TypedDict, total=False):
|
|
|
265
309
|
margin_bottom: Dimension
|
|
266
310
|
margin_left: Dimension
|
|
267
311
|
margin_right: Dimension
|
|
312
|
+
margin_start: Dimension
|
|
313
|
+
margin_end: Dimension
|
|
268
314
|
margin_horizontal: Dimension
|
|
269
315
|
margin_vertical: Dimension
|
|
270
316
|
spacing: float
|
|
271
317
|
gap: float
|
|
318
|
+
row_gap: float
|
|
319
|
+
column_gap: float
|
|
272
320
|
|
|
273
321
|
# --- Visual: clipping & overflow ---
|
|
274
322
|
overflow: Overflow
|
|
@@ -283,6 +331,14 @@ class Style(TypedDict, total=False):
|
|
|
283
331
|
# --- Visual: borders ---
|
|
284
332
|
border_width: float
|
|
285
333
|
border_radius: float
|
|
334
|
+
border_top_width: float
|
|
335
|
+
border_right_width: float
|
|
336
|
+
border_bottom_width: float
|
|
337
|
+
border_left_width: float
|
|
338
|
+
border_top_color: Color
|
|
339
|
+
border_right_color: Color
|
|
340
|
+
border_bottom_color: Color
|
|
341
|
+
border_left_color: Color
|
|
286
342
|
|
|
287
343
|
# --- Visual: typography ---
|
|
288
344
|
font_size: float
|
|
@@ -370,6 +426,40 @@ def resolve_style(value: StyleProp) -> Dict[str, Any]:
|
|
|
370
426
|
return result
|
|
371
427
|
|
|
372
428
|
|
|
429
|
+
_KNOWN_STYLE_KEYS = frozenset(Style.__annotations__)
|
|
430
|
+
"""Every key declared on the [`Style`][pythonnative.Style] TypedDict."""
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None:
|
|
434
|
+
"""Warn (once per key) about style keys no built-in handler reads.
|
|
435
|
+
|
|
436
|
+
Dev-mode only; production skips the scan entirely. Typos get a
|
|
437
|
+
"did you mean" suggestion via fuzzy matching against the declared
|
|
438
|
+
[`Style`][pythonnative.Style] keys. Unknown keys still flow through
|
|
439
|
+
to handlers untouched, so custom components that read extra keys
|
|
440
|
+
keep working (at the cost of one dev warning).
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
style_dict: The flattened style dict about to be merged into an
|
|
444
|
+
element's props.
|
|
445
|
+
owner: Element type name for the warning message (e.g.
|
|
446
|
+
``"Text"``).
|
|
447
|
+
"""
|
|
448
|
+
if not diagnostics.is_dev() or not style_dict:
|
|
449
|
+
return
|
|
450
|
+
for key in style_dict:
|
|
451
|
+
if key in _KNOWN_STYLE_KEYS:
|
|
452
|
+
continue
|
|
453
|
+
matches = difflib.get_close_matches(str(key), _KNOWN_STYLE_KEYS, n=1)
|
|
454
|
+
hint = f" Did you mean {matches[0]!r}?" if matches else ""
|
|
455
|
+
diagnostics.warn_once(
|
|
456
|
+
f"Unknown style key {key!r}"
|
|
457
|
+
+ (f" on {owner}" if owner else "")
|
|
458
|
+
+ f".{hint} Built-in components ignore keys they don't recognize.",
|
|
459
|
+
key=f"style:{owner}:{key}",
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
373
463
|
# ======================================================================
|
|
374
464
|
# StyleSheet
|
|
375
465
|
# ======================================================================
|
|
@@ -481,6 +571,7 @@ DEFAULT_LIGHT_THEME: Dict[str, Any] = {
|
|
|
481
571
|
"spacing_large": 16,
|
|
482
572
|
"border_radius": 8,
|
|
483
573
|
}
|
|
574
|
+
"""Built-in light theme selected by [`use_theme`][pythonnative.use_theme]."""
|
|
484
575
|
|
|
485
576
|
DEFAULT_DARK_THEME: Dict[str, Any] = {
|
|
486
577
|
"primary_color": "#0A84FF",
|
|
@@ -500,17 +591,65 @@ DEFAULT_DARK_THEME: Dict[str, Any] = {
|
|
|
500
591
|
"spacing_large": 16,
|
|
501
592
|
"border_radius": 8,
|
|
502
593
|
}
|
|
594
|
+
"""Built-in dark theme selected by [`use_theme`][pythonnative.use_theme]."""
|
|
595
|
+
|
|
596
|
+
_FOLLOW_SYSTEM_THEME = object()
|
|
597
|
+
"""Sentinel default for `ThemeContext`: resolve from the color scheme."""
|
|
503
598
|
|
|
504
|
-
ThemeContext: Context = create_context(
|
|
505
|
-
"""
|
|
599
|
+
ThemeContext: Context = create_context(_FOLLOW_SYSTEM_THEME)
|
|
600
|
+
"""Theme context that follows the system color scheme by default.
|
|
506
601
|
|
|
507
|
-
|
|
602
|
+
Without a provider, [`use_theme`][pythonnative.use_theme] resolves to
|
|
603
|
+
[`DEFAULT_LIGHT_THEME`][pythonnative.style.DEFAULT_LIGHT_THEME] or
|
|
604
|
+
[`DEFAULT_DARK_THEME`][pythonnative.style.DEFAULT_DARK_THEME] based on
|
|
605
|
+
the current appearance. Wrap a subtree in
|
|
508
606
|
[`Provider(ThemeContext, my_theme, ...)`][pythonnative.Provider] to
|
|
509
|
-
|
|
510
|
-
via [`
|
|
607
|
+
pin an explicit theme for that subtree, then read it inside
|
|
608
|
+
descendants via [`use_theme`][pythonnative.use_theme] (or
|
|
609
|
+
[`use_context(ThemeContext)`][pythonnative.use_context]).
|
|
511
610
|
"""
|
|
512
611
|
|
|
513
612
|
|
|
613
|
+
def default_theme(scheme: str) -> Dict[str, Any]:
|
|
614
|
+
"""Return the built-in theme dict for ``scheme`` (``"light"`` / ``"dark"``)."""
|
|
615
|
+
return DEFAULT_DARK_THEME if scheme == "dark" else DEFAULT_LIGHT_THEME
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def use_theme() -> Dict[str, Any]:
|
|
619
|
+
"""Return the active theme, following the system appearance by default.
|
|
620
|
+
|
|
621
|
+
If an ancestor mounted a ``Provider(ThemeContext, ...)``, that
|
|
622
|
+
theme is returned as-is. Otherwise the built-in light or dark
|
|
623
|
+
theme is selected from the effective color scheme (via
|
|
624
|
+
[`use_color_scheme`][pythonnative.use_color_scheme], so the
|
|
625
|
+
component re-renders when the system appearance flips).
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
The active theme dict.
|
|
629
|
+
|
|
630
|
+
Raises:
|
|
631
|
+
RuntimeError: If called outside a `@component` function.
|
|
632
|
+
|
|
633
|
+
Example:
|
|
634
|
+
```python
|
|
635
|
+
import pythonnative as pn
|
|
636
|
+
|
|
637
|
+
@pn.component
|
|
638
|
+
def Card():
|
|
639
|
+
theme = pn.use_theme()
|
|
640
|
+
return pn.View(
|
|
641
|
+
pn.Text("Hello", style=pn.style(color=theme["text_color"])),
|
|
642
|
+
style=pn.style(background_color=theme["surface_color"]),
|
|
643
|
+
)
|
|
644
|
+
```
|
|
645
|
+
"""
|
|
646
|
+
scheme = use_color_scheme()
|
|
647
|
+
theme = use_context(ThemeContext)
|
|
648
|
+
if theme is _FOLLOW_SYSTEM_THEME:
|
|
649
|
+
return default_theme(scheme)
|
|
650
|
+
return theme
|
|
651
|
+
|
|
652
|
+
|
|
514
653
|
__all__ = [
|
|
515
654
|
"AlignItems",
|
|
516
655
|
"AlignSelf",
|
|
@@ -543,6 +682,9 @@ __all__ = [
|
|
|
543
682
|
"TransformScaleY",
|
|
544
683
|
"TransformSpec",
|
|
545
684
|
"TransformTranslate",
|
|
685
|
+
"default_theme",
|
|
546
686
|
"resolve_style",
|
|
547
687
|
"style",
|
|
688
|
+
"use_theme",
|
|
689
|
+
"validate_style_keys",
|
|
548
690
|
]
|
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
|
|
8
8
|
import android.view.View
|
|
9
9
|
import android.view.ViewGroup
|
|
10
10
|
import android.widget.FrameLayout
|
|
11
|
+
import androidx.activity.OnBackPressedCallback
|
|
11
12
|
import androidx.core.os.bundleOf
|
|
12
13
|
import androidx.fragment.app.Fragment
|
|
13
14
|
import com.chaquo.python.PyObject
|
|
@@ -54,6 +55,27 @@ class ScreenFragment : Fragment() {
|
|
|
54
55
|
} catch (e: Exception) {
|
|
55
56
|
Log.e(TAG, "Failed to instantiate screen", e)
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
// Offer the system back action to Python (`use_back_handler`)
|
|
60
|
+
// before running the default pop/finish behavior.
|
|
61
|
+
requireActivity().onBackPressedDispatcher.addCallback(
|
|
62
|
+
this,
|
|
63
|
+
object : OnBackPressedCallback(true) {
|
|
64
|
+
override fun handleOnBackPressed() {
|
|
65
|
+
val consumed = try {
|
|
66
|
+
screen?.callAttr("on_back_pressed")?.toBoolean() ?: false
|
|
67
|
+
} catch (e: Exception) {
|
|
68
|
+
Log.w(TAG, "on_back_pressed failed", e)
|
|
69
|
+
false
|
|
70
|
+
}
|
|
71
|
+
if (!consumed) {
|
|
72
|
+
isEnabled = false
|
|
73
|
+
requireActivity().onBackPressedDispatcher.onBackPressed()
|
|
74
|
+
isEnabled = true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
)
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
override fun onCreateView(
|