287 lines
9.3 KiB
Dart
287 lines
9.3 KiB
Dart
/// ============================================================
|
||
/// 闲言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;
|
||
}
|