diff --git a/apps/toolbox/src/pages/glass-effects.ts b/apps/toolbox/src/pages/glass-effects.ts
index a1ca9c0e6..d2d7075e8 100644
--- a/apps/toolbox/src/pages/glass-effects.ts
+++ b/apps/toolbox/src/pages/glass-effects.ts
@@ -10,7 +10,7 @@ export function navigatingTo(args: EventData) {
export class GlassEffectModel extends Observable {
iosGlassEffectInteractive: GlassEffectConfig = {
interactive: true,
- tint: '#faabab',
+ // tint: '#faabab',
variant: 'clear',
};
currentEffect: GlassEffectConfig = {
@@ -71,5 +71,29 @@ export class GlassEffectModel extends Observable {
this.glassTargetLabels['share'].animate({ opacity: this.glassMerged ? 1 : 0, duration: 300, curve: CoreTypes.AnimationCurve.easeInOut }).catch(() => {});
this.glassTargetLabels['like'].text = this.glassMerged ? 'Done' : 'Like';
+
+ // for testing, on tap, can see glass effect changes animating differences
+ this.testGlassBindingChanges();
+ }
+
+ testGlassBindingChanges() {
+ setTimeout(() => {
+ this.iosGlassEffectInteractive = {
+ interactive: false,
+ variant: 'regular',
+ // can even animate tint changes (requires starting of transparent tint)
+ // tint: '#faabab',
+ };
+ this.notifyPropertyChange('iosGlassEffectInteractive', this.iosGlassEffectInteractive);
+ setTimeout(() => {
+ this.iosGlassEffectInteractive = {
+ interactive: true,
+ variant: 'clear',
+ // by setting tint to transparent, it will animate on next change
+ // tint: '#00000000',
+ };
+ this.notifyPropertyChange('iosGlassEffectInteractive', this.iosGlassEffectInteractive);
+ }, 1500);
+ }, 1500);
}
}
diff --git a/apps/toolbox/src/pages/glass-effects.xml b/apps/toolbox/src/pages/glass-effects.xml
index 60f44ca96..303c29e74 100644
--- a/apps/toolbox/src/pages/glass-effects.xml
+++ b/apps/toolbox/src/pages/glass-effects.xml
@@ -7,22 +7,23 @@
-
-
+
+
-
+
-
-
-
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts
index f513e613f..823c1b487 100644
--- a/packages/core/ui/core/view/view-common.ts
+++ b/packages/core/ui/core/view/view-common.ts
@@ -1322,6 +1322,10 @@ export type GlassEffectConfig = {
variant?: GlassEffectVariant;
interactive?: boolean;
tint?: string | Color;
+ /**
+ * (LiquidGlassContainer only) spacing between child elements (default is 8)
+ */
+ spacing?: number;
/**
* Duration in milliseconds to animate effect changes (default is 300ms)
*/
diff --git a/packages/core/ui/layouts/liquid-glass-container/index.ios.ts b/packages/core/ui/layouts/liquid-glass-container/index.ios.ts
index 585805441..7cac4cdb3 100644
--- a/packages/core/ui/layouts/liquid-glass-container/index.ios.ts
+++ b/packages/core/ui/layouts/liquid-glass-container/index.ios.ts
@@ -1,36 +1,45 @@
import type { NativeScriptUIView } from '../../utils';
-import { View } from '../../core/view';
+import { GlassEffectConfig, GlassEffectType, GlassEffectVariant, iosGlassEffectProperty, View } from '../../core/view';
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
export class LiquidGlassContainer extends LiquidGlassContainerCommon {
public nativeViewProtected: UIVisualEffectView;
+ private _contentHost: UIView;
createNativeView() {
+ // Keep UIVisualEffectView as the root to preserve interactive container effect
const effect = UIGlassContainerEffect.alloc().init();
effect.spacing = 8;
- const glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
- glassEffectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
- glassEffectView.clipsToBounds = true;
+ const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
+ effectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
+ effectView.clipsToBounds = true;
+ effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
- return glassEffectView;
+ // Add a host view for children so GridLayout can lay them out normally
+ const host = UIView.new();
+ host.frame = effectView.bounds;
+ host.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
+ host.userInteractionEnabled = true;
+ effectView.contentView.addSubview(host);
+ this._contentHost = host;
+
+ return effectView;
}
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
- const parentNativeView = this.nativeViewProtected;
+ const parentNativeView = this._contentHost;
const childNativeView: NativeScriptUIView = child.nativeViewProtected;
if (parentNativeView && childNativeView) {
if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
- // parentNativeView.addSubview(childNativeView);
- this.nativeViewProtected.contentView.addSubview(childNativeView);
+ parentNativeView.addSubview(childNativeView);
} else {
- // parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
- this.nativeViewProtected.contentView.insertSubviewAtIndex(childNativeView, atIndex);
+ parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
}
// Add outer shadow layer manually as it belongs to parent layer tree (this is needed for reusable views)
if (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
- parentNativeView.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
+ this.nativeViewProtected.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
}
return true;
@@ -38,4 +47,27 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
return false;
}
+
+ [iosGlassEffectProperty.setNative](value: GlassEffectType) {
+ console.log('iosGlassEffectProperty:', value);
+ let effect: UIGlassContainerEffect | UIVisualEffect;
+ const config: GlassEffectConfig | null = typeof value !== 'string' ? value : null;
+ const variant = config ? config.variant : (value as GlassEffectVariant);
+ const defaultDuration = 0.3;
+ const duration = config ? (config.animateChangeDuration ?? defaultDuration) : defaultDuration;
+ if (!value || ['identity', 'none'].includes(variant)) {
+ // empty effect
+ effect = UIVisualEffect.new();
+ } else {
+ effect = UIGlassContainerEffect.alloc().init();
+ (effect as UIGlassContainerEffect).spacing = config?.spacing ?? 8;
+ }
+
+ if (effect) {
+ // animate effect changes
+ UIView.animateWithDurationAnimations(duration, () => {
+ this.nativeViewProtected.effect = effect;
+ });
+ }
+ }
}
diff --git a/packages/core/ui/layouts/liquid-glass/index.ios.ts b/packages/core/ui/layouts/liquid-glass/index.ios.ts
index 71b59ab77..9dddbc42a 100644
--- a/packages/core/ui/layouts/liquid-glass/index.ios.ts
+++ b/packages/core/ui/layouts/liquid-glass/index.ios.ts
@@ -1,36 +1,48 @@
import type { NativeScriptUIView } from '../../utils';
-import { View } from '../../core/view';
+import { supportsGlass } from '../../../utils/constants';
+import { GlassEffectConfig, GlassEffectType, GlassEffectVariant, iosGlassEffectProperty, View } from '../../core/view';
+import { Color } from '../../../color';
import { LiquidGlassCommon } from './liquid-glass-common';
export class LiquidGlass extends LiquidGlassCommon {
public nativeViewProtected: UIVisualEffectView;
+ private _contentHost: UIView;
createNativeView() {
+ console.log('createNativeView');
+ // Use UIVisualEffectView as the root so interactive effects can track touches
const effect = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.Clear);
effect.interactive = true;
- const glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
- glassEffectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
- glassEffectView.clipsToBounds = true;
+ const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
+ effectView.frame = CGRectMake(0, 0, 0, 0);
+ effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
+ effectView.clipsToBounds = true;
- return glassEffectView;
+ // Host for all children so GridLayout (derived) layout works as usual
+ const host = UIView.new();
+ host.frame = effectView.bounds;
+ host.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
+ host.userInteractionEnabled = true;
+ effectView.contentView.addSubview(host);
+ this._contentHost = host;
+
+ return effectView;
}
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
- const parentNativeView = this.nativeViewProtected;
+ const parentNativeView = this._contentHost;
const childNativeView: NativeScriptUIView = child.nativeViewProtected;
if (parentNativeView && childNativeView) {
if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
- // parentNativeView.addSubview(childNativeView);
- this.nativeViewProtected.contentView.addSubview(childNativeView);
+ parentNativeView.addSubview(childNativeView);
} else {
- // parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
- this.nativeViewProtected.contentView.insertSubviewAtIndex(childNativeView, atIndex);
+ parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
}
- // Add outer shadow layer manually as it belongs to parent layer tree (this is needed for reusable views)
+ // If the child has an outer shadow layer, ensure it is attached under the child's layer
if (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
- parentNativeView.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
+ this.nativeViewProtected.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
}
return true;
@@ -38,4 +50,44 @@ export class LiquidGlass extends LiquidGlassCommon {
return false;
}
+
+ [iosGlassEffectProperty.setNative](value: GlassEffectType) {
+ console.log('iosGlassEffectProperty:', value);
+ let effect: UIGlassEffect | UIVisualEffect;
+ const config: GlassEffectConfig | null = typeof value !== 'string' ? value : null;
+ const variant = config ? config.variant : (value as GlassEffectVariant);
+ const defaultDuration = 0.3;
+ const duration = config ? (config.animateChangeDuration ?? defaultDuration) : defaultDuration;
+ if (!value || ['identity', 'none'].includes(variant)) {
+ // empty effect
+ effect = UIVisualEffect.new();
+ } else {
+ effect = UIGlassEffect.effectWithStyle(this.toUIGlassStyle(variant));
+ if (config) {
+ (effect as UIGlassEffect).interactive = !!config.interactive;
+ if (config.tint) {
+ (effect as UIGlassEffect).tintColor = typeof config.tint === 'string' ? new Color(config.tint).ios : config.tint;
+ }
+ }
+ }
+
+ if (effect) {
+ // animate effect changes
+ UIView.animateWithDurationAnimations(duration, () => {
+ this.nativeViewProtected.effect = effect;
+ });
+ }
+ }
+
+ public toUIGlassStyle(value?: GlassEffectVariant) {
+ if (supportsGlass()) {
+ switch (value) {
+ case 'regular':
+ return UIGlassEffectStyle?.Regular ?? 0;
+ case 'clear':
+ return UIGlassEffectStyle?.Clear ?? 1;
+ }
+ }
+ return 1;
+ }
}