mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(ios): iosGlassEffect and LiquidGlass containers
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import type { NativeScriptUIView } from '../../utils';
|
||||
import { GlassEffectConfig, GlassEffectType, GlassEffectVariant, iosGlassEffectProperty, View } from '../../core/view';
|
||||
import { GlassEffectType, iosGlassEffectProperty, View } from '../../core/view';
|
||||
import { LiquidGlassContainerCommon } from './liquid-glass-container-common';
|
||||
import { toUIGlassStyle } from '../liquid-glass';
|
||||
|
||||
export class LiquidGlassContainer extends LiquidGlassContainerCommon {
|
||||
public nativeViewProtected: UIVisualEffectView;
|
||||
private _contentHost: UIView;
|
||||
private _normalizing = false;
|
||||
|
||||
createNativeView() {
|
||||
// Keep UIVisualEffectView as the root to preserve interactive container effect
|
||||
@@ -15,7 +17,7 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
|
||||
effectView.clipsToBounds = true;
|
||||
effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
|
||||
|
||||
// Add a host view for children so GridLayout can lay them out normally
|
||||
// Add a host view for children so parent can lay them out normally
|
||||
const host = UIView.new();
|
||||
host.frame = effectView.bounds;
|
||||
host.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
|
||||
@@ -42,32 +44,91 @@ export class LiquidGlassContainer extends LiquidGlassContainerCommon {
|
||||
this.nativeViewProtected.layer.insertSublayerBelow(childNativeView.outerShadowContainerLayer, childNativeView.layer);
|
||||
}
|
||||
|
||||
// Normalize in case the child comes in with a residual translate from a previous state
|
||||
this._scheduleNormalize();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
// When children animate with translate (layer transform), UIVisualEffectView-based
|
||||
// container effects may recompute based on the underlying frames (not transforms),
|
||||
// which can cause jumps. Normalize any residual translation into the
|
||||
// child's frame so the effect uses the final visual positions.
|
||||
public onLayout(left: number, top: number, right: number, bottom: number): void {
|
||||
super.onLayout(left, top, right, bottom);
|
||||
|
||||
// Try to fold any pending translates into frames on each layout pass
|
||||
this._normalizeChildrenTransforms();
|
||||
}
|
||||
|
||||
// Allow callers to stabilize layout after custom animations
|
||||
public stabilizeLayout() {
|
||||
this._normalizeChildrenTransforms(true);
|
||||
}
|
||||
|
||||
private _scheduleNormalize() {
|
||||
if (this._normalizing) return;
|
||||
this._normalizing = true;
|
||||
// Next tick to allow any pending frame/transform updates to settle
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this._normalizeChildrenTransforms();
|
||||
} finally {
|
||||
this._normalizing = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _normalizeChildrenTransforms(force = false) {
|
||||
let changed = false;
|
||||
const count = this.getChildrenCount?.() ?? 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const child = this.getChildAt(i) as View | undefined;
|
||||
if (!child) continue;
|
||||
const tx = child.translateX || 0;
|
||||
const ty = child.translateY || 0;
|
||||
if (!tx && !ty) continue;
|
||||
|
||||
const native = child.nativeViewProtected as UIView;
|
||||
if (!native) continue;
|
||||
|
||||
// Skip if the child is still animating (unless forced)
|
||||
if (!force) {
|
||||
const keys = native.layer.animationKeys ? native.layer.animationKeys() : null;
|
||||
const hasAnimations = !!(keys && keys.count > 0);
|
||||
if (hasAnimations) continue;
|
||||
}
|
||||
|
||||
const frame = native.frame;
|
||||
native.transform = CGAffineTransformIdentity;
|
||||
native.frame = CGRectMake(frame.origin.x + tx, frame.origin.y + ty, frame.size.width, frame.size.height);
|
||||
|
||||
child.translateX = 0;
|
||||
child.translateY = 0;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (effect) {
|
||||
// animate effect changes
|
||||
UIView.animateWithDurationAnimations(duration, () => {
|
||||
this.nativeViewProtected.effect = effect;
|
||||
});
|
||||
if (changed) {
|
||||
// Ask the effect view to re-evaluate its internal state using updated frames
|
||||
const nv = this.nativeViewProtected;
|
||||
if (nv) {
|
||||
nv.setNeedsLayout();
|
||||
nv.layoutIfNeeded();
|
||||
// Also request layout on contentView in case the effect inspects it directly
|
||||
nv.contentView?.setNeedsLayout?.();
|
||||
nv.contentView?.layoutIfNeeded?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[iosGlassEffectProperty.setNative](value: GlassEffectType) {
|
||||
this._applyGlassEffect(value, {
|
||||
effectType: 'container',
|
||||
targetView: this.nativeViewProtected,
|
||||
toGlassStyleFn: toUIGlassStyle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GridLayout } from '../grid-layout';
|
||||
import { AbsoluteLayout } from '../absolute-layout';
|
||||
|
||||
export class LiquidGlassContainerCommon extends GridLayout {}
|
||||
export class LiquidGlassContainerCommon extends AbsoluteLayout {
|
||||
stabilizeLayout() {}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { LiquidGlassCommon } from './liquid-glass-common';
|
||||
import type { GlassEffectVariant } from '../../core/view';
|
||||
|
||||
export class LiquidGlass extends LiquidGlassCommon {}
|
||||
|
||||
export function toUIGlassStyle(value?: GlassEffectVariant) {
|
||||
// Can support when Android equivalent is available
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { LiquidGlassCommon } from './liquid-glass-common';
|
||||
|
||||
export class LiquidGlass extends LiquidGlassCommon {}
|
||||
|
||||
/**
|
||||
* Convert GlassEffectVariant to corresponding platform glass style.
|
||||
* @param value GlassEffectVariant | undefined
|
||||
* @returns 0 | 1 | UIGlassEffectStyle
|
||||
*/
|
||||
export function toUIGlassStyle(value?: GlassEffectVariant): 0 | 1 | UIGlassEffectStyle;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NativeScriptUIView } from '../../utils';
|
||||
import { supportsGlass } from '../../../utils/constants';
|
||||
import { GlassEffectConfig, GlassEffectType, GlassEffectVariant, iosGlassEffectProperty, View } from '../../core/view';
|
||||
import { Color } from '../../../color';
|
||||
import { type GlassEffectType, type GlassEffectVariant, iosGlassEffectProperty, View } from '../../core/view';
|
||||
import { LiquidGlassCommon } from './liquid-glass-common';
|
||||
|
||||
export class LiquidGlass extends LiquidGlassCommon {
|
||||
@@ -9,7 +8,6 @@ export class LiquidGlass extends LiquidGlassCommon {
|
||||
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;
|
||||
@@ -18,7 +16,7 @@ export class LiquidGlass extends LiquidGlassCommon {
|
||||
effectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
|
||||
effectView.clipsToBounds = true;
|
||||
|
||||
// Host for all children so GridLayout (derived) layout works as usual
|
||||
// Host for all children so parent layout works as usual
|
||||
const host = UIView.new();
|
||||
host.frame = effectView.bounds;
|
||||
host.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
|
||||
@@ -52,42 +50,22 @@ export class LiquidGlass extends LiquidGlassCommon {
|
||||
}
|
||||
|
||||
[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;
|
||||
this._applyGlassEffect(value, {
|
||||
effectType: 'glass',
|
||||
targetView: this.nativeViewProtected,
|
||||
toGlassStyleFn: toUIGlassStyle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function 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