Skip to content
Merged
38 changes: 38 additions & 0 deletions apps/automated/src/ui/layouts/flexbox-layout-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1746,3 +1746,41 @@ export const testFlexboxLayout_does_not_crash_with_proxy_view_container = test(a
// Omit testDivider_directionRow_verticalBeginning

// Omit divider test family, we don't draw dividers

let activity_liquidglass_flexbox_layout = () =>
getViews(
`<FlexboxLayout iosOverflowSafeArea="false" id="flexbox" width="300" height="300" flexDirection="${FlexDirection.ROW}" backgroundColor="gray">
<LiquidGlass id="text1" width="100" height="100" />
<LiquidGlassContainer id="text2" width="100" height="100" />
<Label id="text3" width="100" height="100" text="3" />
</FlexboxLayout>`,
);

export const testLiquidGlassFlexboxLayout = test(activity_liquidglass_flexbox_layout, noop, ({ flexbox, text1, text2, text3 }) => {
isTopAlignedWith(text1, flexbox);
isLeftAlignedWith(text1, flexbox);
isRightOf(text2, text1);
isTopAlignedWith(text2, flexbox);
isRightOf(text3, text2);
isTopAlignedWith(text3, flexbox);

equal(width(flexbox), width(text1) + width(text2) + width(text3));
// Layout helpers report device pixels, so fixed XML DIP sizes must be converted too.
closeEnough(height(flexbox), dipToDp(300));
});

export const testLiquidGlassViews_do_not_crash_when_updating_iosGlassEffect = test(activity_liquidglass_flexbox_layout, noop, ({ root, text1, text2 }) => {
const liquidGlass = text1 as unknown as View;
const liquidGlassContainer = text2 as unknown as View;

TKUnit.assertTrue(liquidGlass.nativeViewProtected instanceof UIVisualEffectView, 'LiquidGlass should create a UIVisualEffectView host.');
TKUnit.assertTrue(liquidGlassContainer.nativeViewProtected instanceof UIVisualEffectView, 'LiquidGlassContainer should create a UIVisualEffectView host.');

liquidGlass.iosGlassEffect = 'regular';
liquidGlassContainer.iosGlassEffect = { variant: 'clear', spacing: 12 };
liquidGlass.iosGlassEffect = 'none';
liquidGlassContainer.iosGlassEffect = 'none';

waitUntilTestElementLayoutIsValid(root);
TKUnit.assertTrue(root.isLoaded, 'Liquid glass view tree should remain loaded after glass effect updates.');
});
26 changes: 24 additions & 2 deletions apps/toolbox/src/pages/glass-effects.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@

<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}}"/>

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

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

</LiquidGlass>


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

<GridLayout rows="auto,auto" marginTop="20">
<Label text="Inside FlexboxLayout:" fontSize="22" class="font-weight-bold text-center c-white" />
<FlexboxLayout row="1" marginTop="12" alignment="center" justifyContent="center" flexDirection="column" width="350">

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

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

</LiquidGlass>

<!-- Can try containers as well -->
<!-- <LiquidGlassContainer row="4" tap="{{toggleMergeGlass}}" horizontalAlignment="left" verticalAlignment="middle" class="m-t-20" width="300" height="100" columns="*">
<LiquidGlass id="glass1" loaded="{{loadedGlass}}" borderRadius="50" width="100" height="100">
<Label id="share" text="Share" fontSize="22" class="font-weight-bold text-center" width="100" height="100" loaded="{{loadedGlassLabels}}" />
</LiquidGlass>
<LiquidGlass id="glass2" loaded="{{loadedGlass}}" borderRadius="50" width="100" height="100">
<Label id="like" text="Like" fontSize="22" class="font-weight-bold text-center" loaded="{{loadedGlassLabels}}" />
</LiquidGlass>
</LiquidGlassContainer> -->
</FlexboxLayout>
</GridLayout>

<GridLayout rows="*,auto,auto,auto,*" class="m-t-10">
<GridLayout row="1" width="300" height="100" iosGlassEffect="regular" horizontalAlignment="center" verticalAlignment="middle" borderRadius="32">
<Label class="text-center c-white" fontWeight="bold" fontSize="18" text="Glass Effects Regular" />
Expand Down
12 changes: 5 additions & 7 deletions packages/core/ui/core/view/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,13 +927,11 @@ export class View extends ViewCommon {
const variant = config ? config.variant : (value as GlassEffectVariant);
const defaultDuration = 0.3;
const duration = config ? (config.animateChangeDuration ?? defaultDuration) : defaultDuration;

let effect: UIGlassEffect | UIGlassContainerEffect | UIVisualEffect;
const glassSupported = supportsGlass();
let effect: UIVisualEffect = UIVisualEffect.new();

// Create the appropriate effect based on type and variant
if (!value || ['identity', 'none'].includes(variant)) {
effect = UIVisualEffect.new();
} else {
if (value && !['identity', 'none'].includes(variant) && glassSupported) {
if (options.effectType === 'glass') {
const styleFn = options.toGlassStyleFn || this.toUIGlassStyle.bind(this);
effect = UIGlassEffect.effectWithStyle(styleFn(variant));
Expand Down Expand Up @@ -1086,9 +1084,9 @@ export class View extends ViewCommon {
if (supportsGlass()) {
switch (value) {
case 'regular':
return UIGlassEffectStyle?.Regular ?? 0;
return UIGlassEffectStyle.Regular;
case 'clear':
return UIGlassEffectStyle?.Clear ?? 1;
return UIGlassEffectStyle.Clear;
}
}
return 1;
Expand Down
56 changes: 52 additions & 4 deletions packages/core/ui/layouts/liquid-glass-container/index.ios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NativeScriptUIView } from '../../utils';
import { supportsGlass } from '../../../utils/constants';
import { GlassEffectType, iosGlassEffectProperty, View } from '../../core/view';
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
import { toUIGlassStyle } from '../liquid-glass';
Expand All @@ -9,11 +10,16 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
private _normalizing = false;

createNativeView() {
const glassSupported = supportsGlass();
// Keep UIVisualEffectView as the root to preserve interactive container effect
const effect = UIGlassContainerEffect.alloc().init();
effect.spacing = 8;
const effect = glassSupported ? UIGlassContainerEffect.alloc().init() : UIVisualEffect.new();
if (glassSupported) {
(effect as UIGlassContainerEffect).spacing = 8;
}
const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
effectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
if (glassSupported) {
effectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
}
effectView.clipsToBounds = true;
effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;

Expand Down Expand Up @@ -53,12 +59,54 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
return false;
}

// When LiquidGlassContainer is a child of FlexboxLayout (or any layout that passes
// a measure spec already reduced by the child's padding), AbsoluteLayout.onMeasure
// would subtract our padding a second time. To prevent this double-deduction we
// temporarily zero the effective padding/border values before delegating to the
// AbsoluteLayout measurement, then restore them immediately after.
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
const pl = this.effectivePaddingLeft;
const pr = this.effectivePaddingRight;
const pt = this.effectivePaddingTop;
const pb = this.effectivePaddingBottom;
const bl = this.effectiveBorderLeftWidth;
const br = this.effectiveBorderRightWidth;
const bt = this.effectiveBorderTopWidth;
const bb = this.effectiveBorderBottomWidth;

this.effectivePaddingLeft = 0;
this.effectivePaddingRight = 0;
this.effectivePaddingTop = 0;
this.effectivePaddingBottom = 0;
this.effectiveBorderLeftWidth = 0;
this.effectiveBorderRightWidth = 0;
this.effectiveBorderTopWidth = 0;
this.effectiveBorderBottomWidth = 0;

try {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} finally {
this.effectivePaddingLeft = pl;
this.effectivePaddingRight = pr;
this.effectivePaddingTop = pt;
this.effectivePaddingBottom = pb;
this.effectiveBorderLeftWidth = bl;
this.effectiveBorderRightWidth = br;
this.effectiveBorderTopWidth = bt;
this.effectiveBorderBottomWidth = bb;
}
}

// When children animate with translate (layer transform), UIVisualEffectView-based
// container effects may recompute based on the underlying frames (not transforms),
// which can cause jumps. Normalize any residual translation into the
// child's frame so the effect uses the final visual positions.
public onLayout(left: number, top: number, right: number, bottom: number): void {
super.onLayout(left, top, right, bottom);
// AbsoluteLayout.onLayout positions children using our padding as an offset.
// Since the FlexboxLayout (or parent) already placed our UIVisualEffectView at
// (left, top), we normalise to local coordinates so that AbsoluteLayout places
// children in (0, 0, width, height) space — the coordinate space of _contentHost.
super.onLayout(0, 0, right - left, bottom - top);

// Try to fold any pending translates into frames on each layout pass
this._normalizeChildrenTransforms();
Expand Down
58 changes: 54 additions & 4 deletions packages/core/ui/layouts/liquid-glass/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ export class LiquidGlass extends LiquidGlassCommon {
private _contentHost: UIView;

createNativeView() {
const glassSupported = supportsGlass();
// Use UIVisualEffectView as the root so interactive effects can track touches
const effect = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.Clear);
effect.interactive = true;
const effect = glassSupported ? UIGlassEffect.effectWithStyle(toUIGlassStyle('clear')) : UIVisualEffect.new();
if (glassSupported) {
(effect as UIGlassEffect).interactive = true;
}
const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
effectView.frame = CGRectMake(0, 0, 0, 0);
effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
Expand Down Expand Up @@ -49,6 +52,53 @@ export class LiquidGlass extends LiquidGlassCommon {
return false;
}

public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
// When LiquidGlass is a child of FlexboxLayout (or any layout that passes a child
// measure spec already reduced by the child's padding), GridLayout.onMeasure would
// subtract our padding a second time. To prevent this double-deduction we temporarily
// zero the effective padding/border values before delegating to the GridLayout
// measurement, then restore them immediately after.
const pl = this.effectivePaddingLeft;
const pr = this.effectivePaddingRight;
const pt = this.effectivePaddingTop;
const pb = this.effectivePaddingBottom;
const bl = this.effectiveBorderLeftWidth;
const br = this.effectiveBorderRightWidth;
const bt = this.effectiveBorderTopWidth;
const bb = this.effectiveBorderBottomWidth;

this.effectivePaddingLeft = 0;
this.effectivePaddingRight = 0;
this.effectivePaddingTop = 0;
this.effectivePaddingBottom = 0;
this.effectiveBorderLeftWidth = 0;
this.effectiveBorderRightWidth = 0;
this.effectiveBorderTopWidth = 0;
this.effectiveBorderBottomWidth = 0;

try {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} finally {
this.effectivePaddingLeft = pl;
this.effectivePaddingRight = pr;
this.effectivePaddingTop = pt;
this.effectivePaddingBottom = pb;
this.effectiveBorderLeftWidth = bl;
this.effectiveBorderRightWidth = br;
this.effectiveBorderTopWidth = bt;
this.effectiveBorderBottomWidth = bb;
}
}

public onLayout(left: number, top: number, right: number, bottom: number): void {
// GridLayout.onLayout computes column/row offsets relative to (left, top), then
// adds its own padding on top. Since the FlexboxLayout (or parent) already placed
// our UIVisualEffectView at (left, top), we normalise to local coordinates so that
// GridLayout lays children out in (0, 0, width, height) space — which is exactly
// the coordinate space of our _contentHost UIView that hosts the children.
super.onLayout(0, 0, right - left, bottom - top);
}

[iosGlassEffectProperty.setNative](value: GlassEffectType) {
this._applyGlassEffect(value, {
effectType: 'glass',
Expand All @@ -62,9 +112,9 @@ export function toUIGlassStyle(value?: GlassEffectVariant) {
if (supportsGlass()) {
switch (value) {
case 'regular':
return UIGlassEffectStyle?.Regular ?? 0;
return UIGlassEffectStyle.Regular;
case 'clear':
return UIGlassEffectStyle?.Clear ?? 1;
return UIGlassEffectStyle.Clear;
}
}
return 1;
Expand Down
1 change: 1 addition & 0 deletions packages/core/utils/constants.ios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const SDK_VERSION = parseFloat(UIDevice.currentDevice.systemVersion);

export function supportsGlass(): boolean {
return __APPLE__ && SDK_VERSION >= 26;
}
Loading