feat: glass effects containers

This commit is contained in:
Nathan Walker
2025-09-15 10:18:06 -07:00
parent abe0b7a9cd
commit f2ec80f4ff
19 changed files with 374 additions and 53 deletions

View File

@@ -2,10 +2,11 @@ import { ControlStateChangeListener } from '../core/control-state-change';
import { ButtonBase } from './button-common';
import { View, PseudoClassHandler } from '../core/view';
import { borderTopWidthProperty, borderRightWidthProperty, borderBottomWidthProperty, borderLeftWidthProperty, paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty } from '../styling/style-properties';
import { textAlignmentProperty, whiteSpaceProperty, textOverflowProperty } from '../text-base';
import { textAlignmentProperty, whiteSpaceProperty, textOverflowProperty, textProperty } from '../text-base';
import { resetSymbol } from '../text-base/text-base-common';
import { layout } from '../../utils';
import { SDK_VERSION } from '../../utils/constants';
import { CoreTypes } from '../../core-types';
import { Color } from '../../color';
export * from './button-common';
@@ -208,6 +209,34 @@ export class Button extends ButtonBase {
};
}
// [textProperty.setNative](value: string | number | symbol) {
// if (SDK_VERSION >= 26) {
// const config = UIButtonConfiguration.plainButtonConfiguration();
// // const attrs = {};
// // attrs[NSFontAttributeName] = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline);
// // config.attributedTitle = NSAttributedString.alloc().initWithStringAttributes(this.text, {
// // [NSFontAttributeName]: UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline),
// // } as any);
// //attributes: AttributeContainer([
// // .font : UIFont.preferredFont(forTextStyle: .headline)
// // ])
// this.nativeViewProtected.tintColor = UIColor.labelColor;
// config.contentInsets = NSDirectionalEdgeInsetsFromString('8,12,8,12');
// // semantic; flips as needed over glass
// config.baseForegroundColor = UIColor.labelColor;
// this.nativeViewProtected.configuration = config;
// this._requestLayoutOnTextChanged();
// } else {
// const reset = value === resetSymbol;
// if (!reset && this.formattedText) {
// return;
// }
// this._setNativeText(reset);
// this._requestLayoutOnTextChanged();
// }
// }
[textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) {
switch (value) {
case 'left':

View File

@@ -2,7 +2,7 @@
import { Point, Position, View as ViewDefinition } from '.';
// Requires
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, iosGlassEffectProperty, GlassEffectType, GlassEffectVariant } from './view-common';
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, iosGlassEffectProperty, GlassEffectType, GlassEffectVariant, GlassEffectConfig } from './view-common';
import { ShowModalOptions, hiddenProperty } from '../view-base';
import { Trace } from '../../../trace';
import { layout, ios as iosUtils } from '../../../utils';
@@ -901,42 +901,47 @@ export class View extends ViewCommon implements ViewDefinition {
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));
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 {
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;
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;
}
}
}
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);
if (!this._glassEffectView) {
this._glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
// this._glassEffectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Light;
// 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);
});
} else {
// animate effect changes
UIView.animateWithDurationAnimations(duration, () => {
this._glassEffectView.effect = effect;
});
}
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) {

View File

@@ -105,6 +105,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public visionHoverStyle: string | VisionHoverOptions;
public visionIgnoreHoverStyle: boolean;
/**
* iOS 26+ Glass
*/
iosGlassEffect: GlassEffectType;
protected _closeModalCallback: Function;
public _manager: any;
public _modalParent: ViewCommon;
@@ -1317,8 +1322,16 @@ iosIgnoreSafeAreaProperty.register(ViewCommon);
/**
* Glass effects
*/
export type GlassEffectVariant = 'regular' | 'clear' | 'identity';
export type GlassEffectConfig = { variant?: GlassEffectVariant; interactive?: boolean; tint: string | Color };
export type GlassEffectVariant = 'regular' | 'clear' | 'identity' | 'none';
export type GlassEffectConfig = {
variant?: GlassEffectVariant;
interactive?: boolean;
tint?: string | Color;
/**
* Duration in milliseconds to animate effect changes (default is 300ms)
*/
animateChangeDuration?: number;
};
export type GlassEffectType = GlassEffectVariant | GlassEffectConfig;
export const iosGlassEffectProperty = new Property<ViewCommon, GlassEffectType>({
name: 'iosGlassEffect',

View File

@@ -6,3 +6,5 @@ export { RootLayout, getRootLayout, getRootLayoutById, RootLayoutOptions, ShadeC
export { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout';
export { LayoutBase } from './layout-base';
export { LiquidGlass } from './liquid-glass';
export { LiquidGlassContainer } from './liquid-glass-container';

View File

@@ -7,3 +7,5 @@ export type { RootLayoutOptions, ShadeCoverOptions } from './root-layout';
export { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout';
export { LayoutBase } from './layout-base';
export { LiquidGlass } from './liquid-glass';
export { LiquidGlassContainer } from './liquid-glass-container';

View File

@@ -0,0 +1,3 @@
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
export class LiquidGlassContainer extends LiquidGlassContainerCommon {}

View File

@@ -0,0 +1,3 @@
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
export class LiquidGlassContainer extends LiquidGlassContainerCommon {}

View File

@@ -0,0 +1,41 @@
import type { NativeScriptUIView } from '../../utils';
import { View } from '../../core/view';
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
export class LiquidGlassContainer extends LiquidGlassContainerCommon {
public nativeViewProtected: UIVisualEffectView;
createNativeView() {
const effect = UIGlassContainerEffect.alloc().init();
effect.spacing = 8;
const glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
glassEffectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
glassEffectView.clipsToBounds = true;
return glassEffectView;
}
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
const parentNativeView = this.nativeViewProtected;
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);
} else {
// 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 (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
parentNativeView.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,3 @@
import { GridLayout } from '../grid-layout';
export class LiquidGlassContainerCommon extends GridLayout {}

View File

@@ -0,0 +1,3 @@
import { LiquidGlassCommon } from './liquid-glass-common';
export class LiquidGlass extends LiquidGlassCommon {}

View File

@@ -0,0 +1,3 @@
import { LiquidGlassCommon } from './liquid-glass-common';
export class LiquidGlass extends LiquidGlassCommon {}

View File

@@ -0,0 +1,41 @@
import type { NativeScriptUIView } from '../../utils';
import { View } from '../../core/view';
import { LiquidGlassCommon } from './liquid-glass-common';
export class LiquidGlass extends LiquidGlassCommon {
public nativeViewProtected: UIVisualEffectView;
createNativeView() {
const effect = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.Clear);
effect.interactive = true;
const glassEffectView = UIVisualEffectView.alloc().initWithEffect(effect);
glassEffectView.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark;
glassEffectView.clipsToBounds = true;
return glassEffectView;
}
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
const parentNativeView = this.nativeViewProtected;
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);
} else {
// 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 (childNativeView.outerShadowContainerLayer && !childNativeView.outerShadowContainerLayer.superlayer) {
parentNativeView.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,3 @@
import { GridLayout } from '../grid-layout';
export class LiquidGlassCommon extends GridLayout {}

View File

@@ -12,14 +12,12 @@ import { Span } from './span';
import { colorProperty, fontInternalProperty, fontScaleInternalProperty, Length } from '../styling/style-properties';
import { StrokeCSSValues } from '../styling/css-stroke';
import { isString, isNullOrUndefined } from '../../utils/types';
import { iOSNativeHelper, layout } from '../../utils';
import { Trace } from '../../trace';
import { layout } from '../../utils';
import { SDK_VERSION } from '../../utils/constants';
import { CoreTypes } from '../../core-types';
export * from './text-base-common';
const majorVersion = iOSNativeHelper.MajorVersion;
@NativeClass
class UILabelClickHandlerImpl extends NSObject {
private _owner: WeakRef<TextBase>;
@@ -350,7 +348,7 @@ export class TextBase extends TextBaseCommon {
const text = getTransformedText(isNullOrUndefined(this.text) ? '' : `${this.text}`, this.textTransform);
this.nativeTextViewProtected.nativeScriptSetTextDecorationAndTransformTextDecorationLetterSpacingLineHeight(text, this.style.textDecoration || '', letterSpacing, lineHeight);
if (!this.style?.color && majorVersion >= 13 && UIColor.labelColor) {
if (!this.style?.color && SDK_VERSION >= 13 && UIColor.labelColor) {
this._setColor(UIColor.labelColor);
}
}