Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.');
});
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