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:
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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 = <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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = <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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user