Skip to content
Merged
108 changes: 106 additions & 2 deletions apps/toolbox/src/pages/glass-effects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable, EventData, Page, CoreTypes, GlassEffectConfig } from '@nativescript/core';
import { Observable, EventData, Page, CoreTypes, GlassEffectConfig, View, Label, Animation, LiquidGlassContainer } from '@nativescript/core';

let page: Page;

Expand All @@ -10,7 +10,111 @@ export function navigatingTo(args: EventData) {
export class GlassEffectModel extends Observable {
iosGlassEffectInteractive: GlassEffectConfig = {
interactive: true,
tint: '#faabab',
// tint: '#faabab',
variant: 'clear',
};
currentEffect: GlassEffectConfig = {
variant: 'none',
interactive: false,
// tint: '#ccc',
};

toggleGlassEffect(args) {
const btn = args.object as View;
this.currentEffect =
this.currentEffect.variant === 'none'
? {
variant: 'clear',
interactive: true,
// tint: '#faabab',
}
: {
variant: 'none',
interactive: false,
// tint: '#ccc',
};
btn.iosGlassEffect = this.currentEffect;
}

glassMerged = false;
glassTargets = {};
loadedGlass(args) {
const glass = args.object as View;
switch (glass.id) {
case 'glass1':
glass.translateX = 10;
break;
case 'glass2':
glass.translateX = 70;

break;
}
this.glassTargets[glass.id] = glass;
}

glassTargetLabels: { [key: string]: Label } = {};
loadedGlassLabels(args) {
const label = args.object as Label;
this.glassTargetLabels[label.id] = label;
}

async toggleMergeGlass(args) {
if (!this.glassTargets['glass1'] || !this.glassTargets['glass2']) {
return;
}
const container = args?.object as LiquidGlassContainer | undefined;
this.glassMerged = !this.glassMerged;
const glass1 = this.glassTargets['glass1'];
const glass2 = this.glassTargets['glass2'];

// Use relative deltas for translate; the container will bake them into frames post-animation
const d1 = this.glassMerged ? 25 : -25; // left bubble moves inward/outward
const d2 = this.glassMerged ? -25 : 25; // right bubble moves inward/outward

if (!this.glassMerged) {
this.glassTargetLabels['like'].text = 'Like';
}

const animateAll = new Animation([
{ target: glass1, translate: { x: d1, y: 0 }, duration: 300, curve: CoreTypes.AnimationCurve.easeOut },
{ target: glass2, translate: { x: d2, y: 0 }, duration: 300, curve: CoreTypes.AnimationCurve.easeOut },
{
target: this.glassTargetLabels['share'],
opacity: this.glassMerged ? 0 : 1,
duration: 300,
},
]);
animateAll.play().then(() => {
if (this.glassMerged) {
this.glassTargetLabels['like'].text = 'Done';
}

// Ask container to stabilize frames so UIGlassContainerEffect samples correct positions
setTimeout(() => container?.stabilizeLayout?.(), 0);
});

// 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);
}
}
58 changes: 44 additions & 14 deletions apps/toolbox/src/pages/glass-effects.xml
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar>
<ActionBar title="Glass Effects" class="action-bar">
<ActionBar title="Glass Effects" color="white">
</ActionBar>
</Page.actionBar>

<GridLayout>
<GridLayout backgroundColor="#000">
<!-- test color changes over light/dark backgrounds for text -->
<!-- <ContentView backgroundColor="#000" height="300" verticalAlignment="bottom"/> -->
<Image src="res://bg1.jpg" stretch="aspectFill" iosOverflowSafeArea="true" />

<Image src="https://cdn.wallpapersafari.com/89/64/c6MnRY.jpg" stretch="aspectFill" iosOverflowSafeArea="true" />
<ScrollView backgroundColor="transparent">
<StackLayout>
<GridLayout rows="*,auto,auto,auto,auto,auto,*">

<GridLayout rows="*,auto,auto,auto,*">
<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}}"/>

<GridLayout row="1" width="300" height="150" iosGlassEffect="regular" horizontalAlignment="center" verticalAlignment="middle">
<Label class="text-center c-white" fontWeight="bold" fontSize="18" text="Glass Effects Regular" />
</GridLayout>
<LiquidGlass row="3" width="300" height="100" borderRadius="32" iosGlassEffect="{{iosGlassEffectInteractive}}" >

<GridLayout row="2" width="300" height="150" iosGlassEffect="clear" horizontalAlignment="center" verticalAlignment="middle" class="m-t-10">
<Label class="text-center c-white" fontWeight="bold" fontSize="18" text="Glass Effects Clear" />
</GridLayout>
<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}}" />
</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>
</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" />
</GridLayout>

<GridLayout row="2" width="300" height="100" iosGlassEffect="clear" horizontalAlignment="center" verticalAlignment="middle" class="m-t-10" borderRadius="32">
<Label class="text-center c-white" fontWeight="bold" fontSize="18" text="Glass Effects Clear" />
</GridLayout>

<GridLayout row="3" width="300" height="100" iosGlassEffect="{{iosGlassEffectInteractive}}" horizontalAlignment="center" verticalAlignment="middle" class="m-t-10" borderRadius="32">
<Label class="text-center c-white" fontWeight="bold" fontSize="18" text="Glass Effects Interactive" />
</GridLayout>
</GridLayout>

<!-- make scrollable to view glass on scroll -->
<ContentView height="500"/>
</StackLayout>

</ScrollView>

<GridLayout row="3" width="300" height="150" iosGlassEffect="{{iosGlassEffectInteractive}}" horizontalAlignment="center" verticalAlignment="middle" class="m-t-10">
<Label class="text-center c-white" fontWeight="bold" fontSize="18" text="Glass Effects Interactive" />
</GridLayout>
</GridLayout>

</GridLayout>
</Page>
1 change: 0 additions & 1 deletion packages/core/ui/button/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { borderTopWidthProperty, borderRightWidthProperty, borderBottomWidthProp
import { textAlignmentProperty, whiteSpaceProperty, textOverflowProperty } from '../text-base';
import { layout } from '../../utils';
import { CoreTypes } from '../../core-types';
import { Color } from '../../color';

export * from './button-common';

Expand Down
120 changes: 84 additions & 36 deletions packages/core/ui/core/view/index.ios.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Point, Position } from './view-interfaces';
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, iosGlassEffectProperty, GlassEffectType, GlassEffectVariant, statusBarStyleProperty } from './view-common';
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, iosGlassEffectProperty, GlassEffectType, GlassEffectVariant, GlassEffectConfig, statusBarStyleProperty } from './view-common';
import { isAccessibilityServiceEnabled } from '../../../application';
import { updateA11yPropertiesCallback } from '../../../application/helpers-common';
import { ShowModalOptions, hiddenProperty } from '../view-base';
Expand Down Expand Up @@ -904,6 +904,62 @@ export class View extends ViewCommon {
}
}

protected _applyGlassEffect(
value: GlassEffectType,
options: {
effectType: 'glass' | 'container';
targetView?: UIVisualEffectView;
toGlassStyleFn?: (variant?: GlassEffectVariant) => number;
onCreate?: (effectView: UIVisualEffectView, effect: UIVisualEffect) => void;
onUpdate?: (effectView: UIVisualEffectView, effect: UIVisualEffect, duration: number) => void;
},
): UIVisualEffectView | undefined {
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;

let effect: UIGlassEffect | UIGlassContainerEffect | UIVisualEffect;

// Create the appropriate effect based on type and variant
if (!value || ['identity', 'none'].includes(variant)) {
effect = UIVisualEffect.new();
} else {
if (options.effectType === 'glass') {
const styleFn = options.toGlassStyleFn || this.toUIGlassStyle.bind(this);
effect = UIGlassEffect.effectWithStyle(styleFn(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;
}
}
} else if (options.effectType === 'container') {
effect = UIGlassContainerEffect.alloc().init();
(effect as UIGlassContainerEffect).spacing = config?.spacing ?? 8;
}
}

// Handle creating new effect view or updating existing one
if (options.targetView) {
// Update existing effect view
if (options.onUpdate) {
options.onUpdate(options.targetView, effect, duration);
} else {
// Default update behavior: animate effect changes
UIView.animateWithDurationAnimations(duration, () => {
options.targetView.effect = effect;
});
}
return undefined;
} else if (options.onCreate) {
// Create new effect view and let caller handle setup
const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
options.onCreate(effectView, effect);
return effectView;
}
return undefined;
}
[statusBarStyleProperty.getDefault]() {
return this.style.statusBarStyle;
}
Expand All @@ -929,45 +985,37 @@ export class View extends ViewCommon {
if (!this.nativeViewProtected || !supportsGlass()) {
return;
}
if (this._glassEffectView) {
this._glassEffectView.removeFromSuperview();
this._glassEffectView = null;
}
if (!value) {
return;
}
let effect: UIGlassEffect;
if (typeof value === 'string') {
effect = UIGlassEffect.effectWithStyle(this.toUIGlassStyle(value));

if (!this._glassEffectView) {
// Create new glass effect view
this._glassEffectView = this._applyGlassEffect(value, {
effectType: 'glass',
onCreate: (effectView, effect) => {
// let touches pass to content
effectView.userInteractionEnabled = false;
effectView.clipsToBounds = true;
// size & autoresize
if (this._glassEffectMeasure) {
clearTimeout(this._glassEffectMeasure);
}
this._glassEffectMeasure = setTimeout(() => {
const size = this.nativeViewProtected.bounds.size;
effectView.frame = CGRectMake(0, 0, size.width, size.height);
effectView.autoresizingMask = 2;
this.nativeViewProtected.insertSubviewAtIndex(effectView, 0);
});
},
});
} else {
if (value.variant === 'identity') {
return;
}
effect = UIGlassEffect.effectWithStyle(this.toUIGlassStyle(value.variant));
if (value.interactive) {
effect.interactive = true;
}
if (value.tint) {
effect.tintColor = typeof value.tint === 'string' ? new Color(value.tint).ios : value.tint;
}
// Update existing glass effect view
this._applyGlassEffect(value, {
effectType: 'glass',
targetView: this._glassEffectView,
});
}
this._glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
// let touches pass to content
this._glassEffectView.userInteractionEnabled = false;
this._glassEffectView.clipsToBounds = true;
// size & autoresize
if (this._glassEffectMeasure) {
clearTimeout(this._glassEffectMeasure);
}
this._glassEffectMeasure = setTimeout(() => {
const size = this.nativeViewProtected.bounds.size;
this._glassEffectView.frame = CGRectMake(0, 0, size.width, size.height);
this._glassEffectView.autoresizingMask = 2;
this.nativeViewProtected.insertSubviewAtIndex(this._glassEffectView, 0);
});
}

public toUIGlassStyle(value?: GlassEffectVariant) {
toUIGlassStyle(value?: GlassEffectVariant) {
if (supportsGlass()) {
switch (value) {
case 'regular':
Expand Down
Loading