mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat: glass views
This commit is contained in:
@@ -10,7 +10,7 @@ export function navigatingTo(args: EventData) {
|
|||||||
export class GlassEffectModel extends Observable {
|
export class GlassEffectModel extends Observable {
|
||||||
iosGlassEffectInteractive: GlassEffectConfig = {
|
iosGlassEffectInteractive: GlassEffectConfig = {
|
||||||
interactive: true,
|
interactive: true,
|
||||||
tint: '#faabab',
|
// tint: '#faabab',
|
||||||
variant: 'clear',
|
variant: 'clear',
|
||||||
};
|
};
|
||||||
currentEffect: GlassEffectConfig = {
|
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['share'].animate({ opacity: this.glassMerged ? 1 : 0, duration: 300, curve: CoreTypes.AnimationCurve.easeInOut }).catch(() => {});
|
||||||
|
|
||||||
this.glassTargetLabels['like'].text = this.glassMerged ? 'Done' : 'Like';
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,22 +7,23 @@
|
|||||||
|
|
||||||
|
|
||||||
<GridLayout backgroundColor="#000">
|
<GridLayout backgroundColor="#000">
|
||||||
|
<Image src="res://bg1.jpg" stretch="aspectFill" iosOverflowSafeArea="true" width="100%" height="100%" />
|
||||||
<Image src="https://cdn.wallpapersafari.com/89/64/c6MnRY.jpg" stretch="aspectFill" iosOverflowSafeArea="true" />
|
<!-- test color changes over light/dark backgrounds for text -->
|
||||||
<!-- <ContentView backgroundColor="#000" height="300" verticalAlignment="bottom"/> -->
|
<!-- <ContentView backgroundColor="#000" height="300" verticalAlignment="bottom"/> -->
|
||||||
|
|
||||||
<ScrollView >
|
<ScrollView backgroundColor="transparent">
|
||||||
<StackLayout>
|
<StackLayout>
|
||||||
<GridLayout height="400" tap="{{cycleImage}}"/>
|
|
||||||
<GridLayout rows="*,auto,auto,auto,auto,auto,*">
|
<GridLayout rows="*,auto,auto,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}}"/>
|
<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">
|
<LiquidGlass row="3" width="300" height="100" borderRadius="32" iosGlassEffect="{{iosGlassEffectInteractive}}" >
|
||||||
<Label text="Glass Interactive" fontSize="22" class="p-4 font-weight-bold text-center" />
|
|
||||||
|
<Label text="Glass Interactive" fontSize="22" class="font-weight-bold text-center c-white" />
|
||||||
|
|
||||||
</LiquidGlass>
|
</LiquidGlass>
|
||||||
|
|
||||||
<!--
|
|
||||||
<LiquidGlassContainer row="4" tap="{{toggleMergeGlass}}" horizontalAlignment="center" verticalAlignment="middle" class="m-t-20" width="300" height="100">
|
<LiquidGlassContainer row="4" tap="{{toggleMergeGlass}}" horizontalAlignment="center" verticalAlignment="middle" class="m-t-20" width="300" height="100">
|
||||||
<LiquidGlass id="glass1" loaded="{{loadedGlass}}" borderRadius="50" width="100" height="100">
|
<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}}" />
|
<Label id="share" text="Share" fontSize="22" class="font-weight-bold text-center" width="100" height="100" loaded="{{loadedGlassLabels}}" />
|
||||||
@@ -30,29 +31,26 @@
|
|||||||
<LiquidGlass id="glass2" loaded="{{loadedGlass}}" borderRadius="50" width="100" height="100">
|
<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}}" />
|
<Label id="like" text="Like" fontSize="22" class="font-weight-bold text-center" loaded="{{loadedGlassLabels}}" />
|
||||||
</LiquidGlass>
|
</LiquidGlass>
|
||||||
</LiquidGlassContainer> -->
|
</LiquidGlassContainer>
|
||||||
</GridLayout>
|
</GridLayout>
|
||||||
|
|
||||||
<GridLayout height="600"/>
|
<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>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
|
||||||
<!-- Alternative setup -->
|
|
||||||
<GridLayout rows="*,auto,auto,auto,*">
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
</GridLayout>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1322,6 +1322,10 @@ export type GlassEffectConfig = {
|
|||||||
variant?: GlassEffectVariant;
|
variant?: GlassEffectVariant;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
tint?: string | Color;
|
tint?: string | Color;
|
||||||
|
/**
|
||||||
|
* (LiquidGlassContainer only) spacing between child elements (default is 8)
|
||||||
|
*/
|
||||||
|
spacing?: number;
|
||||||
/**
|
/**
|
||||||
* Duration in milliseconds to animate effect changes (default is 300ms)
|
* Duration in milliseconds to animate effect changes (default is 300ms)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,36 +1,45 @@
|
|||||||
import type { NativeScriptUIView } from '../../utils';
|
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';
|
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
|
||||||
|
|
||||||
export class LiquidGlassContainer extends LiquidGlassContainerCommon {
|
export class LiquidGlassContainer extends LiquidGlassContainerCommon {
|
||||||
public nativeViewProtected: UIVisualEffectView;
|
public nativeViewProtected: UIVisualEffectView;
|
||||||
|
private _contentHost: UIView;
|
||||||
|
|
||||||
createNativeView() {
|
createNativeView() {
|
||||||
|
// Keep UIVisualEffectView as the root to preserve interactive container effect
|
||||||
const effect = UIGlassContainerEffect.alloc().init();
|
const effect = UIGlassContainerEffect.alloc().init();
|
||||||
effect.spacing = 8;
|
effect.spacing = 8;
|
||||||
const glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
|
const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
|
||||||
glassEffectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
|
effectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
|
||||||
glassEffectView.clipsToBounds = true;
|
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 {
|
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
|
||||||
const parentNativeView = this.nativeViewProtected;
|
const parentNativeView = this._contentHost;
|
||||||
const childNativeView: NativeScriptUIView = <NativeScriptUIView>child.nativeViewProtected;
|
const childNativeView: NativeScriptUIView = <NativeScriptUIView>child.nativeViewProtected;
|
||||||
|
|
||||||
if (parentNativeView && childNativeView) {
|
if (parentNativeView && childNativeView) {
|
||||||
if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
|
if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
|
||||||
// parentNativeView.addSubview(childNativeView);
|
parentNativeView.addSubview(childNativeView);
|
||||||
this.nativeViewProtected.contentView.addSubview(childNativeView);
|
|
||||||
} else {
|
} else {
|
||||||
// parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
|
parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
|
||||||
this.nativeViewProtected.contentView.insertSubviewAtIndex(childNativeView, atIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add outer shadow layer manually as it belongs to parent layer tree (this is needed for reusable views)
|
// Add outer shadow layer manually as it belongs to parent layer tree (this is needed for reusable views)
|
||||||
if (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
|
if (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
|
||||||
parentNativeView.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
|
this.nativeViewProtected.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -38,4 +47,27 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
|
|||||||
|
|
||||||
return false;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,48 @@
|
|||||||
import type { NativeScriptUIView } from '../../utils';
|
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';
|
import { LiquidGlassCommon } from './liquid-glass-common';
|
||||||
|
|
||||||
export class LiquidGlass extends LiquidGlassCommon {
|
export class LiquidGlass extends LiquidGlassCommon {
|
||||||
public nativeViewProtected: UIVisualEffectView;
|
public nativeViewProtected: UIVisualEffectView;
|
||||||
|
private _contentHost: UIView;
|
||||||
|
|
||||||
createNativeView() {
|
createNativeView() {
|
||||||
|
console.log('createNativeView');
|
||||||
|
// Use UIVisualEffectView as the root so interactive effects can track touches
|
||||||
const effect = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.Clear);
|
const effect = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.Clear);
|
||||||
effect.interactive = true;
|
effect.interactive = true;
|
||||||
const glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
|
const effectView = UIVisualEffectView.alloc().initWithEffect(effect);
|
||||||
glassEffectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
|
effectView.frame = CGRectMake(0, 0, 0, 0);
|
||||||
glassEffectView.clipsToBounds = true;
|
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 {
|
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
|
||||||
const parentNativeView = this.nativeViewProtected;
|
const parentNativeView = this._contentHost;
|
||||||
const childNativeView: NativeScriptUIView = <NativeScriptUIView>child.nativeViewProtected;
|
const childNativeView: NativeScriptUIView = <NativeScriptUIView>child.nativeViewProtected;
|
||||||
|
|
||||||
if (parentNativeView && childNativeView) {
|
if (parentNativeView && childNativeView) {
|
||||||
if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
|
if (typeof atIndex !== 'number' || atIndex >= parentNativeView.subviews.count) {
|
||||||
// parentNativeView.addSubview(childNativeView);
|
parentNativeView.addSubview(childNativeView);
|
||||||
this.nativeViewProtected.contentView.addSubview(childNativeView);
|
|
||||||
} else {
|
} else {
|
||||||
// parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
|
parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
|
||||||
this.nativeViewProtected.contentView.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) {
|
if (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
|
||||||
parentNativeView.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
|
this.nativeViewProtected.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -38,4 +50,44 @@ export class LiquidGlass extends LiquidGlassCommon {
|
|||||||
|
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user