Files
xianyan/lib/shared/widgets/glass_container.dart
2026-05-22 02:04:46 +08:00

287 lines
9.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — Liquid Glass 容器组件 (纯Dart渲染版)
/// 创建时间: 2026-04-20
/// 更新时间: 2026-05-22
/// 作用: iOS 26 风格毛玻璃容器纯Dart实现零shader依赖
/// 上次更新: 添加RepaintBoundary减少BackdropFilter重绘范围
/// ============================================================
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:xianyan/core/utils/platform_utils.dart'
show isOhos, OhosDeviceCapabilities;
import '../../core/theme/app_theme.dart';
import '../../core/theme/app_radius.dart';
import '../../core/theme/app_spacing.dart';
/// Glass 深度层级
enum GlassDepth { base, elevated, prominent, alert }
/// 卡片样式ID — 与 ThemeSettingsState.cardStyleId 对应
enum CardStyle { standard, borderless, shadow, minimal }
/// Liquid Glass 容器 — 纯 Dart 毛玻璃效果
///
/// 使用 BackdropFilter + LinearGradient 实现iOS风格毛玻璃
/// 不依赖任何 GPU shader兼容所有设备。
/// 读取 AppThemeExtension.glassBlurMultiplier 实现毛玻璃强度全局生效。
/// 读取 AppThemeExtension.cardStyleId 实现卡片样式全局切换。
class GlassContainer extends StatelessWidget {
const GlassContainer({
super.key,
required this.child,
this.depth = GlassDepth.base,
this.borderRadius,
this.padding,
this.margin,
this.width,
this.height,
this.borderColor,
this.showBorder = true,
this.cardStyle,
});
final Widget child;
final GlassDepth depth;
final BorderRadius? borderRadius;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final double? width;
final double? height;
final Color? borderColor;
final bool showBorder;
final CardStyle? cardStyle;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final dynamicRadius = AppRadius.of(context);
final effectiveRadius = borderRadius ?? dynamicRadius.lgBorder;
final effectivePadding = padding ?? const EdgeInsets.all(AppSpacing.md);
final multiplier = ext.glassBlurMultiplier;
final effectiveCardStyle = cardStyle ?? _resolveCardStyle(ext.cardStyleId);
final cfg = _configForDepth(depth, ext.isDark);
if (effectiveCardStyle == CardStyle.borderless) {
return Container(
width: width,
height: height,
margin: margin,
decoration: BoxDecoration(
borderRadius: effectiveRadius,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [cfg.surfaceTop, cfg.surfaceBottom],
stops: const [0.0, 1.0],
),
),
clipBehavior: Clip.antiAlias,
child: _maybeBackdropFilter(
sigma: cfg.blurSigma * multiplier,
child: RepaintBoundary(
child: Padding(padding: effectivePadding, child: child),
),
),
);
}
if (effectiveCardStyle == CardStyle.shadow) {
return Container(
width: width,
height: height,
margin: margin,
decoration: BoxDecoration(
borderRadius: effectiveRadius,
color: ext.bgCard,
boxShadow: [
BoxShadow(
color: cfg.shadowColor,
blurRadius: cfg.shadowBlur,
offset: cfg.shadowOffset,
spreadRadius: cfg.shadowSpread,
),
],
),
child: Padding(padding: effectivePadding, child: child),
);
}
if (effectiveCardStyle == CardStyle.minimal) {
return Container(
width: width,
height: height,
margin: margin,
decoration: BoxDecoration(
borderRadius: effectiveRadius,
color: ext.bgCard.withValues(alpha: 0.6),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: Padding(padding: effectivePadding, child: child),
);
}
return Container(
width: width,
height: height,
margin: margin,
decoration: BoxDecoration(
borderRadius: effectiveRadius,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [cfg.surfaceTop, cfg.surfaceBottom],
stops: const [0.0, 1.0],
),
border: showBorder
? Border.all(
color: borderColor ?? cfg.borderCol,
width: cfg.borderWidth,
)
: null,
boxShadow: [
BoxShadow(
color: cfg.shadowColor,
blurRadius: cfg.shadowBlur,
offset: cfg.shadowOffset,
spreadRadius: cfg.shadowSpread,
),
],
),
clipBehavior: Clip.antiAlias,
child: _maybeBackdropFilter(
sigma: cfg.blurSigma * multiplier,
child: RepaintBoundary(
child: Padding(padding: effectivePadding, child: child),
),
),
);
}
/// 根据设备特性决定是否使用BackdropFilter
/// 鸿蒙端低端设备不支持毛玻璃时降级为普通Container
Widget _maybeBackdropFilter({required double sigma, required Widget child}) {
if (isOhos && !OhosDeviceCapabilities.supportsBackdropFilter) {
return child;
}
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
child: child,
);
}
CardStyle _resolveCardStyle(String id) {
return switch (id) {
'borderless' => CardStyle.borderless,
'shadow' => CardStyle.shadow,
'minimal' => CardStyle.minimal,
_ => CardStyle.standard,
};
}
_GlassConfig _configForDepth(GlassDepth depth, bool isDark) {
return switch (depth) {
GlassDepth.base => _GlassConfig(
blurSigma: isDark ? 20 : 18,
surfaceTop: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.12)
: const Color(0xFFE8ECF0).withValues(alpha: 0.72),
surfaceBottom: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.04)
: const Color(0xFFD8E0E8).withValues(alpha: 0.45),
borderCol: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.14)
: const Color(0xFFC8D4E0).withValues(alpha: 0.50),
borderWidth: 0.5,
shadowColor: Colors.black.withValues(alpha: isDark ? 0.20 : 0.10),
shadowBlur: 12,
shadowOffset: const Offset(0, 3),
shadowSpread: 0,
),
GlassDepth.elevated => _GlassConfig(
blurSigma: isDark ? 26 : 22,
surfaceTop: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.15)
: const Color(0xFFE4ECF2).withValues(alpha: 0.78),
surfaceBottom: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.05)
: const Color(0xFFD0DAE4).withValues(alpha: 0.50),
borderCol: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.16)
: const Color(0xFFC0CEE0).withValues(alpha: 0.55),
borderWidth: 0.6,
shadowColor: Colors.black.withValues(alpha: isDark ? 0.25 : 0.12),
shadowBlur: 18,
shadowOffset: const Offset(0, 5),
shadowSpread: 0,
),
GlassDepth.prominent => _GlassConfig(
blurSigma: isDark ? 34 : 30,
surfaceTop: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.18)
: const Color(0xFFDFEAF4).withValues(alpha: 0.85),
surfaceBottom: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.06)
: const Color(0xFFCAD6E2).withValues(alpha: 0.55),
borderCol: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.19)
: const Color(0xFFBCCAE0).withValues(alpha: 0.60),
borderWidth: 0.7,
shadowColor: Colors.black.withValues(alpha: isDark ? 0.32 : 0.16),
shadowBlur: 26,
shadowOffset: const Offset(0, 7),
shadowSpread: 1,
),
GlassDepth.alert => _GlassConfig(
blurSigma: isDark ? 42 : 38,
surfaceTop: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.22)
: const Color(0xFFDAE8F4).withValues(alpha: 0.90),
surfaceBottom: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.08)
: const Color(0xFFC4D2E0).withValues(alpha: 0.60),
borderCol: isDark
? const Color(0xFFFFFFFF).withValues(alpha: 0.22)
: const Color(0xFFB4C6DC).withValues(alpha: 0.65),
borderWidth: 0.8,
shadowColor: Colors.black.withValues(alpha: isDark ? 0.40 : 0.20),
shadowBlur: 34,
shadowOffset: const Offset(0, 10),
shadowSpread: 2,
),
};
}
}
class _GlassConfig {
const _GlassConfig({
required this.blurSigma,
required this.surfaceTop,
required this.surfaceBottom,
required this.borderCol,
required this.borderWidth,
required this.shadowColor,
required this.shadowBlur,
required this.shadowOffset,
required this.shadowSpread,
});
final double blurSigma;
final Color surfaceTop;
final Color surfaceBottom;
final Color borderCol;
final double borderWidth;
final Color shadowColor;
final double shadowBlur;
final Offset shadowOffset;
final double shadowSpread;
}