Skip to content

Commit 2a090cc

Browse files
authored
fix(ios): liquid glass flexbox (#11134)
1 parent 938090e commit 2a090cc

File tree

6 files changed

+174
-17
lines changed

6 files changed

+174
-17
lines changed

apps/automated/src/ui/layouts/flexbox-layout-tests.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,3 +1746,41 @@ export const testFlexboxLayout_does_not_crash_with_proxy_view_container = test(a
17461746
// Omit testDivider_directionRow_verticalBeginning
17471747

17481748
// Omit divider test family, we don't draw dividers
1749+
1750+
let activity_liquidglass_flexbox_layout = () =>
1751+
getViews(
1752+
`<FlexboxLayout iosOverflowSafeArea="false" id="flexbox" width="300" height="300" flexDirection="${FlexDirection.ROW}" backgroundColor="gray">
1753+
<LiquidGlass id="text1" width="100" height="100" />
1754+
<LiquidGlassContainer id="text2" width="100" height="100" />
1755+
<Label id="text3" width="100" height="100" text="3" />
1756+
</FlexboxLayout>`,
1757+
);
1758+
1759+
export const testLiquidGlassFlexboxLayout = test(activity_liquidglass_flexbox_layout, noop, ({ flexbox, text1, text2, text3 }) => {
1760+
isTopAlignedWith(text1, flexbox);
1761+
isLeftAlignedWith(text1, flexbox);
1762+
isRightOf(text2, text1);
1763+
isTopAlignedWith(text2, flexbox);
1764+
isRightOf(text3, text2);
1765+
isTopAlignedWith(text3, flexbox);
1766+
1767+
equal(width(flexbox), width(text1) + width(text2) + width(text3));
1768+
// Layout helpers report device pixels, so fixed XML DIP sizes must be converted too.
1769+
closeEnough(height(flexbox), dipToDp(300));
1770+
});
1771+
1772+
export const testLiquidGlassViews_do_not_crash_when_updating_iosGlassEffect = test(activity_liquidglass_flexbox_layout, noop, ({ root, text1, text2 }) => {
1773+
const liquidGlass = text1 as unknown as View;
1774+
const liquidGlassContainer = text2 as unknown as View;
1775+
1776+
TKUnit.assertTrue(liquidGlass.nativeViewProtected instanceof UIVisualEffectView, 'LiquidGlass should create a UIVisualEffectView host.');
1777+
TKUnit.assertTrue(liquidGlassContainer.nativeViewProtected instanceof UIVisualEffectView, 'LiquidGlassContainer should create a UIVisualEffectView host.');
1778+
1779+
liquidGlass.iosGlassEffect = 'regular';
1780+
liquidGlassContainer.iosGlassEffect = { variant: 'clear', spacing: 12 };
1781+
liquidGlass.iosGlassEffect = 'none';
1782+
liquidGlassContainer.iosGlassEffect = 'none';
1783+
1784+
waitUntilTestElementLayoutIsValid(root);
1785+
TKUnit.assertTrue(root.isLoaded, 'Liquid glass view tree should remain loaded after glass effect updates.');
1786+
});

apps/toolbox/src/pages/glass-effects.xml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515

1616
<Button row="2" text="Toggle Glass" tap="{{toggleGlassEffect}}" horizontalAlignment="center" verticalAlignment="middle" class="c-white font-weight-bold m-y-20 p-4" fontSize="22" borderRadius="32" width="300" height="100" touchAnimation="{{touchAnimation}}" iosGlassEffect="{{currentEffect}}"/>
1717

18-
<LiquidGlass row="3" width="300" height="100" borderRadius="32" iosGlassEffect="{{iosGlassEffectInteractive}}" >
18+
<LiquidGlass row="3" width="300" height="100" borderRadius="32" iosGlassEffect="{{iosGlassEffectInteractive}}">
1919

2020
<Label text="Glass Interactive" fontSize="22" class="font-weight-bold text-center c-white" />
2121

2222
</LiquidGlass>
2323

24-
24+
2525
<LiquidGlassContainer row="4" tap="{{toggleMergeGlass}}" horizontalAlignment="left" verticalAlignment="middle" class="m-t-20" width="300" height="100" columns="*">
2626
<LiquidGlass id="glass1" loaded="{{loadedGlass}}" borderRadius="50" width="100" height="100">
2727
<Label id="share" text="Share" fontSize="22" class="font-weight-bold text-center" width="100" height="100" loaded="{{loadedGlassLabels}}" />
@@ -32,6 +32,28 @@
3232
</LiquidGlassContainer>
3333
</GridLayout>
3434

35+
<GridLayout rows="auto,auto" marginTop="20">
36+
<Label text="Inside FlexboxLayout:" fontSize="22" class="font-weight-bold text-center c-white" />
37+
<FlexboxLayout row="1" marginTop="12" alignment="center" justifyContent="center" flexDirection="column" width="350">
38+
39+
<LiquidGlass row="3" width="300" height="100" borderRadius="32" iosGlassEffect="{{iosGlassEffectInteractive}}">
40+
41+
<Label text="Glass Interactive" fontSize="22" class="font-weight-bold text-center c-white" />
42+
43+
</LiquidGlass>
44+
45+
<!-- Can try containers as well -->
46+
<!-- <LiquidGlassContainer row="4" tap="{{toggleMergeGlass}}" horizontalAlignment="left" verticalAlignment="middle" class="m-t-20" width="300" height="100" columns="*">
47+
<LiquidGlass id="glass1" loaded="{{loadedGlass}}" borderRadius="50" width="100" height="100">
48+
<Label id="share" text="Share" fontSize="22" class="font-weight-bold text-center" width="100" height="100" loaded="{{loadedGlassLabels}}" />
49+
</LiquidGlass>
50+
<LiquidGlass id="glass2" loaded="{{loadedGlass}}" borderRadius="50" width="100" height="100">
51+
<Label id="like" text="Like" fontSize="22" class="font-weight-bold text-center" loaded="{{loadedGlassLabels}}" />
52+
</LiquidGlass>
53+
</LiquidGlassContainer> -->
54+
</FlexboxLayout>
55+
</GridLayout>
56+
3557
<GridLayout rows="*,auto,auto,auto,*" class="m-t-10">
3658
<GridLayout row="1" width="300" height="100" iosGlassEffect="regular" horizontalAlignment="center" verticalAlignment="middle" borderRadius="32">
3759
<Label class="text-center c-white" fontWeight="bold" fontSize="18" text="Glass Effects Regular" />

packages/core/ui/core/view/index.ios.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -927,13 +927,11 @@ export class View extends ViewCommon {
927927
const variant = config ? config.variant : (value as GlassEffectVariant);
928928
const defaultDuration = 0.3;
929929
const duration = config ? (config.animateChangeDuration ?? defaultDuration) : defaultDuration;
930-
931-
let effect: UIGlassEffect | UIGlassContainerEffect | UIVisualEffect;
930+
const glassSupported = supportsGlass();
931+
let effect: UIVisualEffect = UIVisualEffect.new();
932932

933933
// Create the appropriate effect based on type and variant
934-
if (!value || ['identity', 'none'].includes(variant)) {
935-
effect = UIVisualEffect.new();
936-
} else {
934+
if (value && !['identity', 'none'].includes(variant) && glassSupported) {
937935
if (options.effectType === 'glass') {
938936
const styleFn = options.toGlassStyleFn || this.toUIGlassStyle.bind(this);
939937
effect = UIGlassEffect.effectWithStyle(styleFn(variant));
@@ -1086,9 +1084,9 @@ export class View extends ViewCommon {
10861084
if (supportsGlass()) {
10871085
switch (value) {
10881086
case 'regular':
1089-
return UIGlassEffectStyle?.Regular ?? 0;
1087+
return UIGlassEffectStyle.Regular;
10901088
case 'clear':
1091-
return UIGlassEffectStyle?.Clear ?? 1;
1089+
return UIGlassEffectStyle.Clear;
10921090
}
10931091
}
10941092
return 1;

packages/core/ui/layouts/liquid-glass-container/index.ios.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { NativeScriptUIView } from '../../utils';
2+
import { supportsGlass } from '../../../utils/constants';
23
import { GlassEffectType, iosGlassEffectProperty, View } from '../../core/view';
34
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
45
import { toUIGlassStyle } from '../liquid-glass';
@@ -9,11 +10,16 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
910
private _normalizing = false;
1011

1112
createNativeView() {
13+
const glassSupported = supportsGlass();
1214
// Keep UIVisualEffectView as the root to preserve interactive container effect
13-
const effect = UIGlassContainerEffect.alloc().init();
14-
effect.spacing = 8;
15+
const effect = glassSupported ? UIGlassContainerEffect.alloc().init() : UIVisualEffect.new();
16+
if (glassSupported) {
17+
(effect as UIGlassContainerEffect).spacing = 8;
18+
}
1519
const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
16-
effectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
20+
if (glassSupported) {
21+
effectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
22+
}
1723
effectView.clipsToBounds = true;
1824
effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
1925

@@ -53,12 +59,54 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
5359
return false;
5460
}
5561

62+
// When LiquidGlassContainer is a child of FlexboxLayout (or any layout that passes
63+
// a measure spec already reduced by the child's padding), AbsoluteLayout.onMeasure
64+
// would subtract our padding a second time. To prevent this double-deduction we
65+
// temporarily zero the effective padding/border values before delegating to the
66+
// AbsoluteLayout measurement, then restore them immediately after.
67+
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
68+
const pl = this.effectivePaddingLeft;
69+
const pr = this.effectivePaddingRight;
70+
const pt = this.effectivePaddingTop;
71+
const pb = this.effectivePaddingBottom;
72+
const bl = this.effectiveBorderLeftWidth;
73+
const br = this.effectiveBorderRightWidth;
74+
const bt = this.effectiveBorderTopWidth;
75+
const bb = this.effectiveBorderBottomWidth;
76+
77+
this.effectivePaddingLeft = 0;
78+
this.effectivePaddingRight = 0;
79+
this.effectivePaddingTop = 0;
80+
this.effectivePaddingBottom = 0;
81+
this.effectiveBorderLeftWidth = 0;
82+
this.effectiveBorderRightWidth = 0;
83+
this.effectiveBorderTopWidth = 0;
84+
this.effectiveBorderBottomWidth = 0;
85+
86+
try {
87+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
88+
} finally {
89+
this.effectivePaddingLeft = pl;
90+
this.effectivePaddingRight = pr;
91+
this.effectivePaddingTop = pt;
92+
this.effectivePaddingBottom = pb;
93+
this.effectiveBorderLeftWidth = bl;
94+
this.effectiveBorderRightWidth = br;
95+
this.effectiveBorderTopWidth = bt;
96+
this.effectiveBorderBottomWidth = bb;
97+
}
98+
}
99+
56100
// When children animate with translate (layer transform), UIVisualEffectView-based
57101
// container effects may recompute based on the underlying frames (not transforms),
58102
// which can cause jumps. Normalize any residual translation into the
59103
// child's frame so the effect uses the final visual positions.
60104
public onLayout(left: number, top: number, right: number, bottom: number): void {
61-
super.onLayout(left, top, right, bottom);
105+
// AbsoluteLayout.onLayout positions children using our padding as an offset.
106+
// Since the FlexboxLayout (or parent) already placed our UIVisualEffectView at
107+
// (left, top), we normalise to local coordinates so that AbsoluteLayout places
108+
// children in (0, 0, width, height) space — the coordinate space of _contentHost.
109+
super.onLayout(0, 0, right - left, bottom - top);
62110

63111
// Try to fold any pending translates into frames on each layout pass
64112
this._normalizeChildrenTransforms();

packages/core/ui/layouts/liquid-glass/index.ios.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ export class LiquidGlass extends LiquidGlassCommon {
88
private _contentHost: UIView;
99

1010
createNativeView() {
11+
const glassSupported = supportsGlass();
1112
// Use UIVisualEffectView as the root so interactive effects can track touches
12-
const effect = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.Clear);
13-
effect.interactive = true;
13+
const effect = glassSupported ? UIGlassEffect.effectWithStyle(toUIGlassStyle('clear')) : UIVisualEffect.new();
14+
if (glassSupported) {
15+
(effect as UIGlassEffect).interactive = true;
16+
}
1417
const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
1518
effectView.frame = CGRectMake(0, 0, 0, 0);
1619
effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
@@ -49,6 +52,53 @@ export class LiquidGlass extends LiquidGlassCommon {
4952
return false;
5053
}
5154

55+
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
56+
// When LiquidGlass is a child of FlexboxLayout (or any layout that passes a child
57+
// measure spec already reduced by the child's padding), GridLayout.onMeasure would
58+
// subtract our padding a second time. To prevent this double-deduction we temporarily
59+
// zero the effective padding/border values before delegating to the GridLayout
60+
// measurement, then restore them immediately after.
61+
const pl = this.effectivePaddingLeft;
62+
const pr = this.effectivePaddingRight;
63+
const pt = this.effectivePaddingTop;
64+
const pb = this.effectivePaddingBottom;
65+
const bl = this.effectiveBorderLeftWidth;
66+
const br = this.effectiveBorderRightWidth;
67+
const bt = this.effectiveBorderTopWidth;
68+
const bb = this.effectiveBorderBottomWidth;
69+
70+
this.effectivePaddingLeft = 0;
71+
this.effectivePaddingRight = 0;
72+
this.effectivePaddingTop = 0;
73+
this.effectivePaddingBottom = 0;
74+
this.effectiveBorderLeftWidth = 0;
75+
this.effectiveBorderRightWidth = 0;
76+
this.effectiveBorderTopWidth = 0;
77+
this.effectiveBorderBottomWidth = 0;
78+
79+
try {
80+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
81+
} finally {
82+
this.effectivePaddingLeft = pl;
83+
this.effectivePaddingRight = pr;
84+
this.effectivePaddingTop = pt;
85+
this.effectivePaddingBottom = pb;
86+
this.effectiveBorderLeftWidth = bl;
87+
this.effectiveBorderRightWidth = br;
88+
this.effectiveBorderTopWidth = bt;
89+
this.effectiveBorderBottomWidth = bb;
90+
}
91+
}
92+
93+
public onLayout(left: number, top: number, right: number, bottom: number): void {
94+
// GridLayout.onLayout computes column/row offsets relative to (left, top), then
95+
// adds its own padding on top. Since the FlexboxLayout (or parent) already placed
96+
// our UIVisualEffectView at (left, top), we normalise to local coordinates so that
97+
// GridLayout lays children out in (0, 0, width, height) space — which is exactly
98+
// the coordinate space of our _contentHost UIView that hosts the children.
99+
super.onLayout(0, 0, right - left, bottom - top);
100+
}
101+
52102
[iosGlassEffectProperty.setNative](value: GlassEffectType) {
53103
this._applyGlassEffect(value, {
54104
effectType: 'glass',
@@ -62,9 +112,9 @@ export function toUIGlassStyle(value?: GlassEffectVariant) {
62112
if (supportsGlass()) {
63113
switch (value) {
64114
case 'regular':
65-
return UIGlassEffectStyle?.Regular ?? 0;
115+
return UIGlassEffectStyle.Regular;
66116
case 'clear':
67-
return UIGlassEffectStyle?.Clear ?? 1;
117+
return UIGlassEffectStyle.Clear;
68118
}
69119
}
70120
return 1;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const SDK_VERSION = parseFloat(UIDevice.currentDevice.systemVersion);
2+
23
export function supportsGlass(): boolean {
34
return __APPLE__ && SDK_VERSION >= 26;
45
}

0 commit comments

Comments
 (0)