From efb048381176e572c04207f8b3cb017ad77c6243 Mon Sep 17 00:00:00 2001 From: VeinDevTtv Date: Sun, 5 Oct 2025 12:08:07 -0700 Subject: [PATCH 1/5] fix(android): coerce string width/height in ImageAssetOptions - Add parsePositiveInt(JSONObject, String --- .../java/org/nativescript/widgets/Utils.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java index d32fed9f41..832c5f69d7 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java @@ -170,6 +170,36 @@ static class ImageAssetOptions { boolean autoScaleFactor; } + private static int parsePositiveInt(JSONObject object, String key) { + if (object == null || key == null) { + return 0; + } + try { + if (!object.has(key)) { + return 0; + } + Object value = object.get(key); + if (value instanceof Number) { + int n = ((Number) value).intValue(); + return n > 0 ? n : 0; + } + if (value instanceof String) { + String s = ((String) value).trim(); + if (s.length() == 0) { + return 0; + } + try { + int n = Integer.parseInt(s); + return n > 0 ? n : 0; + } catch (NumberFormatException ignored) { + return 0; + } + } + } catch (Exception ignored) { + } + return 0; + } + private static final Executor executors = Executors.newCachedThreadPool(); @@ -297,8 +327,8 @@ public void run() { try { JSONObject object = new JSONObject(options); - opts.width = object.optInt("width", 0); - opts.height = object.optInt("height", 0); + opts.width = parsePositiveInt(object, "width"); + opts.height = parsePositiveInt(object, "height"); opts.keepAspectRatio = object.optBoolean("keepAspectRatio", true); opts.autoScaleFactor = object.optBoolean("autoScaleFactor", true); } catch (JSONException ignored) { From ac7d30045681861c6f92b2e7958fcdbc9257c7f0 Mon Sep 17 00:00:00 2001 From: VeinDevTtv Date: Wed, 5 Nov 2025 14:37:41 -0800 Subject: [PATCH 2/5] fix: ProxyViewContainer.hidden property now properly hides children - Added _applyHiddenToChildren method to propagate hidden state to all children - Overrode hiddenProperty for ProxyViewContainer to call _applyHiddenToChildren when value changes - Updated _addViewToNativeVisualTree to apply hidden state to new children when container is hidden - Preserves isCollapsed behavior for layout calculations Fixes #10912 --- .../core/ui/proxy-view-container/index.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/core/ui/proxy-view-container/index.ts b/packages/core/ui/proxy-view-container/index.ts index aafcf82f97..4f7a6697da 100644 --- a/packages/core/ui/proxy-view-container/index.ts +++ b/packages/core/ui/proxy-view-container/index.ts @@ -2,6 +2,7 @@ import { View, CSSType } from '../core/view'; import { LayoutBase } from '../layouts/layout-base'; import { Property } from '../core/properties'; import { Trace } from '../../trace'; +import { hiddenProperty } from '../core/view-base'; /** * Proxy view container that adds all its native children directly to the parent. @@ -103,6 +104,11 @@ export class ProxyViewContainer extends LayoutBase { } super._addViewToNativeVisualTree(child); + // Apply hidden state to new child if the container is hidden + if (this.hidden) { + child.hidden = true; + } + layoutProperties.forEach((propName) => { const proxyPropName = makeProxyPropName(propName); child[proxyPropName] = child[propName]; @@ -231,6 +237,18 @@ export class ProxyViewContainer extends LayoutBase { child[propName] = value; child[proxyPropName] = value; } + + /** + * Apply the hidden state to all child views. + * When ProxyViewContainer is hidden, all its children should also be hidden + * since they are directly added to the parent's native view tree. + */ + private _applyHiddenToChildren(hidden: boolean): void { + this.eachChildView((child) => { + child.hidden = hidden; + return true; + }); + } } // Layout propeties to be proxyed to the child views @@ -277,6 +295,25 @@ for (const name of layoutProperties) { proxyProperty.register(ProxyViewContainer); } +// Override the hidden property to propagate it to all children +// Since ProxyViewContainer has no native view, we need to apply the hidden state +// to all children so they are visually hidden when the container is hidden. +const proxyHiddenProperty = new Property({ + name: 'hidden', + defaultValue: false, + affectsLayout: __APPLE__, + valueConverter: hiddenProperty.valueConverter, + valueChanged(target, oldValue, newValue) { + // Call the base implementation to set isCollapsed for layout calculations + if (target) { + target.isCollapsed = !!newValue; + } + // Apply hidden state to all children + target._applyHiddenToChildren(!!newValue); + }, +}); +proxyHiddenProperty.register(ProxyViewContainer); + function makeProxyPropName(propName) { return `_proxy:${propName}`; } From a702d84c388d11e303381f98c552af2cf6fc17ab Mon Sep 17 00:00:00 2001 From: VeinDevTtv Date: Wed, 5 Nov 2025 14:47:12 -0800 Subject: [PATCH 3/5] test: add tests for ProxyViewContainer.hidden property - Test hiding existing children when proxy.hidden = true - Test showing children when proxy.hidden = false - Test hiding new children added after setting hidden = true - Test multiple children scenarios Fixes #10912 --- .../proxy-view-container-tests.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/apps/automated/src/ui/proxy-view-container/proxy-view-container-tests.ts b/apps/automated/src/ui/proxy-view-container/proxy-view-container-tests.ts index d82af831ec..2fc01674a7 100644 --- a/apps/automated/src/ui/proxy-view-container/proxy-view-container-tests.ts +++ b/apps/automated/src/ui/proxy-view-container/proxy-view-container-tests.ts @@ -273,6 +273,141 @@ export function test_proxy_inside_listview_itemTemplate_crash() { helper.buildUIAndRunTest(list, testAction); } +export function test_proxy_hidden_hides_existing_children() { + const outer = new StackLayout(); + const proxy = new ProxyViewContainer(); + const btn1 = createBtn('1'); + const btn2 = createBtn('2'); + const btn3 = createBtn('3'); + + function testAction(views: Array) { + outer.addChild(btn1); + outer.addChild(proxy); + proxy.addChild(btn2); + proxy.addChild(btn3); + outer.addChild(createBtn('4')); + + // Initially all children should be visible + assertViewHidden(btn1, false, 'btn1 should be visible initially'); + assertViewHidden(btn2, false, 'btn2 should be visible initially'); + assertViewHidden(btn3, false, 'btn3 should be visible initially'); + + // Hide the proxy container + proxy.hidden = true; + + // Proxy's isCollapsed should be set + TKUnit.assertTrue(proxy.isCollapsed, 'Proxy isCollapsed should be true when hidden'); + + // All proxy children should be hidden + assertViewHidden(btn2, true, 'btn2 should be hidden when proxy is hidden'); + assertViewHidden(btn3, true, 'btn3 should be hidden when proxy is hidden'); + + // Other children should remain visible + assertViewHidden(btn1, false, 'btn1 should remain visible'); + } + + helper.buildUIAndRunTest(outer, testAction); +} + +export function test_proxy_hidden_shows_children_when_set_to_false() { + const outer = new StackLayout(); + const proxy = new ProxyViewContainer(); + const btn1 = createBtn('1'); + const btn2 = createBtn('2'); + + function testAction(views: Array) { + outer.addChild(proxy); + proxy.addChild(btn1); + proxy.addChild(btn2); + + // Hide the proxy + proxy.hidden = true; + assertViewHidden(btn1, true, 'btn1 should be hidden'); + assertViewHidden(btn2, true, 'btn2 should be hidden'); + + // Show the proxy again + proxy.hidden = false; + + // Proxy's isCollapsed should be false + TKUnit.assertFalse(proxy.isCollapsed, 'Proxy isCollapsed should be false when not hidden'); + + // All proxy children should be visible again + assertViewHidden(btn1, false, 'btn1 should be visible when proxy is shown'); + assertViewHidden(btn2, false, 'btn2 should be visible when proxy is shown'); + } + + helper.buildUIAndRunTest(outer, testAction); +} + +export function test_proxy_hidden_hides_new_children_added_after_setting_hidden() { + const outer = new StackLayout(); + const proxy = new ProxyViewContainer(); + const btn1 = createBtn('1'); + + function testAction(views: Array) { + outer.addChild(proxy); + + // Hide the proxy first + proxy.hidden = true; + + // Add a child after hiding + proxy.addChild(btn1); + + // The new child should be hidden + assertViewHidden(btn1, true, 'New child should be hidden when added to hidden proxy'); + + // Add another child + const btn2 = createBtn('2'); + proxy.addChild(btn2); + assertViewHidden(btn2, true, 'Another new child should be hidden when added to hidden proxy'); + } + + helper.buildUIAndRunTest(outer, testAction); +} + +export function test_proxy_hidden_with_multiple_children() { + const outer = new StackLayout(); + const proxy = new ProxyViewContainer(); + const btn1 = createBtn('1'); + const btn2 = createBtn('2'); + const btn3 = createBtn('3'); + const btn4 = createBtn('4'); + + function testAction(views: Array) { + outer.addChild(proxy); + proxy.addChild(btn1); + proxy.addChild(btn2); + proxy.addChild(btn3); + proxy.addChild(btn4); + + // All should be visible initially + assertViewHidden(btn1, false, 'btn1 should be visible'); + assertViewHidden(btn2, false, 'btn2 should be visible'); + assertViewHidden(btn3, false, 'btn3 should be visible'); + assertViewHidden(btn4, false, 'btn4 should be visible'); + + // Hide the proxy + proxy.hidden = true; + + // All should be hidden + assertViewHidden(btn1, true, 'btn1 should be hidden'); + assertViewHidden(btn2, true, 'btn2 should be hidden'); + assertViewHidden(btn3, true, 'btn3 should be hidden'); + assertViewHidden(btn4, true, 'btn4 should be hidden'); + + // Show again + proxy.hidden = false; + + // All should be visible again + assertViewHidden(btn1, false, 'btn1 should be visible again'); + assertViewHidden(btn2, false, 'btn2 should be visible again'); + assertViewHidden(btn3, false, 'btn3 should be visible again'); + assertViewHidden(btn4, false, 'btn4 should be visible again'); + } + + helper.buildUIAndRunTest(outer, testAction); +} + // TODO: Proxy as a direct child to of TabItem is not supported. Not sure if we want to support it. //export function test_proxy_inside_tab() { // const proxy = new ProxyViewContainer(); @@ -336,3 +471,16 @@ function assertNativeChildren(layout: LayoutBase, arr: Array) { TKUnit.assert(false, 'No native view to assert'); } } + +function assertViewHidden(view: View, expectedHidden: boolean, message: string) { + if (view.android) { + const visibility = view.android.getVisibility(); + const isHidden = visibility === android.view.View.GONE; + TKUnit.assertEqual(isHidden, expectedHidden, message); + } else if (view.ios) { + const isHidden = view.ios.hidden; + TKUnit.assertEqual(isHidden, expectedHidden, message); + } else { + TKUnit.assert(false, 'No native view to assert hidden state'); + } +} From ae0a22dce0f251cc8ea4367865d91dac3cde5e84 Mon Sep 17 00:00:00 2001 From: VeinDevTtv Date: Thu, 6 Nov 2025 15:33:12 -0800 Subject: [PATCH 4/5] fix: resolve merge conflicts in Utils.java - Accept main branch improvements for parsePositiveInt method - Use doubleValue() with Math.floor() for better numeric parsing - Add null check with object.isNull(key) - Use specific JSONException instead of generic Exception - Add documentation comment for numeric coercion --- .../main/java/org/nativescript/widgets/Utils.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java index 832c5f69d7..29a3da916c 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java @@ -175,13 +175,13 @@ private static int parsePositiveInt(JSONObject object, String key) { return 0; } try { - if (!object.has(key)) { + if (!object.has(key) || object.isNull(key)) { return 0; } Object value = object.get(key); if (value instanceof Number) { - int n = ((Number) value).intValue(); - return n > 0 ? n : 0; + int parsed = (int) Math.floor(((Number) value).doubleValue()); + return parsed > 0 ? parsed : 0; } if (value instanceof String) { String s = ((String) value).trim(); @@ -189,13 +189,13 @@ private static int parsePositiveInt(JSONObject object, String key) { return 0; } try { - int n = Integer.parseInt(s); - return n > 0 ? n : 0; + int parsed = Integer.parseInt(s); + return parsed > 0 ? parsed : 0; } catch (NumberFormatException ignored) { return 0; } } - } catch (Exception ignored) { + } catch (JSONException ignored) { } return 0; } @@ -327,6 +327,7 @@ public void run() { try { JSONObject object = new JSONObject(options); + // Coerce numeric strings or numbers; fallback to 0 for invalid values opts.width = parsePositiveInt(object, "width"); opts.height = parsePositiveInt(object, "height"); opts.keepAspectRatio = object.optBoolean("keepAspectRatio", true); From a1e67fb6464b9dba6157b1b3134d17f9a5716d7a Mon Sep 17 00:00:00 2001 From: VeinDevTtv Date: Thu, 6 Nov 2025 16:16:00 -0800 Subject: [PATCH 5/5] fix: allow ProxyViewContainer hidden property to build - import booleanConverter instead of using hiddenProperty.valueConverter - expose _applyHiddenToChildren so property handler can invoke it --- packages/core/ui/proxy-view-container/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/ui/proxy-view-container/index.ts b/packages/core/ui/proxy-view-container/index.ts index 4f7a6697da..6623ed6676 100644 --- a/packages/core/ui/proxy-view-container/index.ts +++ b/packages/core/ui/proxy-view-container/index.ts @@ -2,7 +2,7 @@ import { View, CSSType } from '../core/view'; import { LayoutBase } from '../layouts/layout-base'; import { Property } from '../core/properties'; import { Trace } from '../../trace'; -import { hiddenProperty } from '../core/view-base'; +import { hiddenProperty, booleanConverter } from '../core/view-base'; /** * Proxy view container that adds all its native children directly to the parent. @@ -243,7 +243,7 @@ export class ProxyViewContainer extends LayoutBase { * When ProxyViewContainer is hidden, all its children should also be hidden * since they are directly added to the parent's native view tree. */ - private _applyHiddenToChildren(hidden: boolean): void { + protected _applyHiddenToChildren(hidden: boolean): void { this.eachChildView((child) => { child.hidden = hidden; return true; @@ -302,7 +302,7 @@ const proxyHiddenProperty = new Property({ name: 'hidden', defaultValue: false, affectsLayout: __APPLE__, - valueConverter: hiddenProperty.valueConverter, + valueConverter: booleanConverter, valueChanged(target, oldValue, newValue) { // Call the base implementation to set isCollapsed for layout calculations if (target) {