Last active
January 15, 2026 10:17
-
-
Save lotawei/b6485d75ac2b9ee7e6096df8fce0c0e0 to your computer and use it in GitHub Desktop.
extensionusage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'dart:math' as math; | |
| import 'dart:ui'; | |
| void main() { | |
| runApp(MaterialApp(home: ChainableExample())); | |
| } | |
| class ChainableExample extends StatelessWidget { | |
| const ChainableExample({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar(title: const Text('链式调用示例')), | |
| body: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| _buildSectionTitle('动画效果'), | |
| _buildAnimationDemo(), | |
| _buildSectionTitle('基础文本样式'), | |
| _buildTextDemo(), | |
| _buildSectionTitle('容器装饰'), | |
| _buildContainerDemo(), | |
| _buildSectionTitle('组合布局'), | |
| _buildLayoutDemo(), | |
| _buildSectionTitle('按钮样式'), | |
| _buildButtonDemo(), | |
| _buildSectionTitle('卡片组件'), | |
| _buildCardDemo(), | |
| ], | |
| ).scrollable().safeArea(), | |
| ); | |
| } | |
| Widget _buildSectionTitle(String title) => Text(title) | |
| .fontSize(14) | |
| .textColor(Colors.grey) | |
| .paddingOnly(left: 16, top: 16, bottom: 8); | |
| Widget _buildTextDemo() => [ | |
| // String 扩展快速创建 | |
| '你好木木哒'.text.fontSize(24).bold().textColor(Colors.blue), | |
| 16.vGap, | |
| // Text 链式样式 | |
| const Text('下划线+斜体').underline().italic().textColor(Colors.orange), | |
| 8.vGap, | |
| const Text('带阴影文字').fontSize(20).bold().shadow(), | |
| 8.vGap, | |
| const Text('这是一段很长的文字需要省略显示的效果测试').ellipsis(), | |
| ].toColumn(crossAxisAlignment: CrossAxisAlignment.start).paddingAll(16); | |
| Widget _buildContainerDemo() => [ | |
| // 背景+圆角 | |
| Text('背景+圆角') | |
| .textColor(Colors.white) | |
| .paddingAll(12) | |
| .decorated(color: Colors.black, borderRadius: 8.circular), | |
| 12.vGap, | |
| // 渐变背景 | |
| const Text('渐变背景') | |
| .textColor(Colors.white) | |
| .paddingAll(12) | |
| .linearGradient(colors: [Colors.blue, Colors.purple]) | |
| .cornerRadius(8), | |
| 12.vGap, | |
| // 阴影+边框 | |
| const Text('阴影+边框') | |
| .paddingAll(12) | |
| .border(color: Colors.blue, width: 2, radius: 8) | |
| .shadow(blurRadius: 8), | |
| 12.vGap, | |
| // 模糊效果 | |
| const Text('模糊效果').paddingAll(12).blur(sigmaX: 1, sigmaY: 1), | |
| ].toColumn(crossAxisAlignment: CrossAxisAlignment.start).paddingAll(16); | |
| Widget _buildLayoutDemo() => | |
| Row( | |
| children: [ | |
| // 圆形图标 | |
| const Icon(Icons.star, color: Colors.amber, size: 30) | |
| .background(Colors.red) | |
| .paddingAll(12) | |
| .background(Colors.amber.shade50) | |
| .circle(), | |
| 12.hGap, | |
| // 文字扩展填充 | |
| const Text('Rating: 4.5').fontSize(16).expanded(), | |
| // 右侧图标 | |
| const Icon(Icons.arrow_forward_ios, size: 16).opacity(0.5), | |
| ], | |
| ) | |
| .paddingAll(16) | |
| .background(Colors.grey.shade100) | |
| .cornerRadius(12) | |
| .margin(const EdgeInsets.symmetric(horizontal: 16)); | |
| Widget _buildButtonDemo() => [ | |
| // 主按钮 | |
| const Text('主按钮') | |
| .fontSize(16) | |
| .textColor(Colors.white) | |
| .bold() | |
| .center() | |
| .height(48) | |
| .fullWidth() | |
| .background(Colors.blue) | |
| .cornerRadius(24) | |
| .onTap(() => debugPrint('主按钮点击')), | |
| 12.vGap, | |
| // 次按钮 | |
| const Text('次按钮') | |
| .fontSize(16) | |
| .textColor(Colors.blue) | |
| .center() | |
| .height(48) | |
| .fullWidth() | |
| .border(color: Colors.blue, width: 1.5, radius: 24) | |
| .onTap(() => debugPrint('次按钮点击')), | |
| 12.vGap, | |
| // 禁用按钮 | |
| const Text('禁用按钮') | |
| .fontSize(16) | |
| .textColor(Colors.grey) | |
| .center() | |
| .height(48) | |
| .fullWidth() | |
| .background(Colors.grey.shade200) | |
| .cornerRadius(24) | |
| .ignorePointer(), | |
| ].toColumn().paddingSymmetric(horizontal: 16); | |
| Widget _buildCardDemo() => | |
| [ | |
| const Text('卡片标题').fontSize(18).bold(), | |
| 8.vGap, | |
| const Text( | |
| '这是卡片内容描述,支持多行显示。', | |
| ).fontSize(14).textColor(Colors.grey.shade600).lines(2), | |
| 16.vGap, | |
| Row( | |
| children: [ | |
| const Text('查看详情').textColor(Colors.blue).expanded(), | |
| const Icon(Icons.arrow_forward, color: Colors.blue, size: 18), | |
| ], | |
| ), | |
| ] | |
| .toColumn(crossAxisAlignment: CrossAxisAlignment.start) | |
| .paddingAll(16) | |
| .decorated( | |
| color: Colors.white, | |
| borderRadius: 12.circular, | |
| boxShadow: [ | |
| BoxShadow( | |
| color: Colors.black.withOpacity(0.08), | |
| blurRadius: 10, | |
| offset: const Offset(0, 4), | |
| ), | |
| ], | |
| ) | |
| .margin(const EdgeInsets.symmetric(horizontal: 16)); | |
| Widget _buildAnimationDemo() => [ | |
| // 淡入效果 - 自动播放 | |
| const Text('淡入(自动)') | |
| .paddingAll(12) | |
| .background(Colors.green.shade100) | |
| .cornerRadius(8) | |
| .animate(Anim.fadeIn(duration: const Duration(milliseconds: 500))), | |
| 12.vGap, | |
| // 从左滑入 - 自动播放 | |
| const Text('滑入(自动)') | |
| .paddingAll(12) | |
| .background(Colors.orange.shade100) | |
| .cornerRadius(8) | |
| .animate(Anim.slideLeft(delay: const Duration(milliseconds: 200))), | |
| 12.vGap, | |
| // 弹跳 - 点击触发 | |
| const Text('点击弹跳') | |
| .paddingAll(12) | |
| .background(Colors.purple.shade100) | |
| .cornerRadius(8) | |
| .animate(Anim.bounce(trigger: AnimTrigger.onTap)), | |
| 12.vGap, | |
| // 缩放 - 点击触发 | |
| const Text('点击缩放') | |
| .paddingAll(12) | |
| .background(Colors.blue.shade100) | |
| .cornerRadius(8) | |
| .animate(Anim.scale(trigger: AnimTrigger.onTap)), | |
| 12.vGap, | |
| // 抖动 - 点击触发 | |
| const Text('点击抖动') | |
| .paddingAll(12) | |
| .background(Colors.red.shade100) | |
| .cornerRadius(8) | |
| .animate(Anim.shake(trigger: AnimTrigger.onTap)), | |
| 12.vGap, | |
| // 旋转 - 点击触发 | |
| Row( | |
| children: [ | |
| const Icon( | |
| Icons.refresh, | |
| color: Colors.teal, | |
| size: 40, | |
| ).animate(Anim.rotate(trigger: AnimTrigger.onTap)), | |
| 12.hGap, | |
| const Text('点击旋转').textColor(Colors.grey), | |
| ], | |
| ), | |
| 12.vGap, | |
| // 脉冲循环 - 点击切换 | |
| Row( | |
| children: [ | |
| const Icon( | |
| Icons.favorite, | |
| color: Colors.red, | |
| size: 40, | |
| ).animate(Anim.pulse(repeat: true, trigger: AnimTrigger.onTap)), | |
| 12.hGap, | |
| const Text('点击切换脉冲').textColor(Colors.grey), | |
| ], | |
| ), | |
| 12.vGap, | |
| // 闪烁循环 - 点击切换 | |
| Row( | |
| children: [ | |
| const Icon( | |
| Icons.notifications, | |
| color: Colors.amber, | |
| size: 40, | |
| ).animate(Anim.blink(repeat: true, trigger: AnimTrigger.onTap)), | |
| 12.hGap, | |
| const Text('点击切换闪烁').textColor(Colors.grey), | |
| ], | |
| ).background(Colors.black.withOpacity(0.1)), | |
| 12.vGap, | |
| // 淡入+滑入组合 - 点击触发 | |
| const Text('点击淡入滑入') | |
| .paddingAll(12) | |
| .background(Colors.cyan.shade100) | |
| .cornerRadius(8) | |
| .animate(Anim.fadeSlide(trigger: AnimTrigger.onTap)), | |
| ].toColumn(crossAxisAlignment: CrossAxisAlignment.start).paddingAll(16); | |
| } | |
| // ==================== 基础扩展 ==================== | |
| extension WidgetModifier on Widget { | |
| // 尺寸相关 | |
| Widget width(double width) => SizedBox(width: width, child: this); | |
| Widget height(double height) => SizedBox(height: height, child: this); | |
| Widget size(double width, double height) => | |
| SizedBox(width: width, height: height, child: this); | |
| Widget square(double size) => | |
| SizedBox(width: size, height: size, child: this); | |
| // Padding 相关 | |
| Widget padding(EdgeInsetsGeometry padding) => | |
| Padding(padding: padding, child: this); | |
| Widget paddingAll(double value) => | |
| Padding(padding: EdgeInsets.all(value), child: this); | |
| Widget paddingSymmetric({double horizontal = 0, double vertical = 0}) => | |
| Padding( | |
| padding: EdgeInsets.symmetric( | |
| horizontal: horizontal, | |
| vertical: vertical, | |
| ), | |
| child: this, | |
| ); | |
| Widget paddingOnly({ | |
| double left = 0, | |
| double top = 0, | |
| double right = 0, | |
| double bottom = 0, | |
| }) => Padding( | |
| padding: EdgeInsets.only( | |
| left: left, | |
| top: top, | |
| right: right, | |
| bottom: bottom, | |
| ), | |
| child: this, | |
| ); | |
| // Margin 相关(通过 Container 实现) | |
| Widget margin(EdgeInsetsGeometry margin) => | |
| Container(margin: margin, child: this); | |
| Widget marginAll(double value) => | |
| Container(margin: EdgeInsets.all(value), child: this); | |
| // 对齐相关 | |
| Widget align(AlignmentGeometry alignment) => | |
| Align(alignment: alignment, child: this); | |
| Widget center() => Center(child: this); | |
| Widget alignLeft() => Align(alignment: Alignment.centerLeft, child: this); | |
| Widget alignRight() => Align(alignment: Alignment.centerRight, child: this); | |
| Widget alignTop() => Align(alignment: Alignment.topCenter, child: this); | |
| Widget alignBottom() => Align(alignment: Alignment.bottomCenter, child: this); | |
| // 背景和装饰 | |
| Widget background(Color color) => Container(color: color, child: this); | |
| Widget decorated({ | |
| Color? color, | |
| DecorationImage? image, | |
| Border? border, | |
| BorderRadius? borderRadius, | |
| List<BoxShadow>? boxShadow, | |
| Gradient? gradient, | |
| }) => Container( | |
| decoration: BoxDecoration( | |
| color: color, | |
| image: image, | |
| border: border, | |
| borderRadius: borderRadius, | |
| boxShadow: boxShadow, | |
| gradient: gradient, | |
| ), | |
| child: this, | |
| ); | |
| Widget cornerRadius(double radius) => | |
| ClipRRect(borderRadius: BorderRadius.circular(radius), child: this); | |
| Widget rounded() => | |
| ClipRRect(borderRadius: BorderRadius.circular(8), child: this); | |
| Widget circle() => ClipOval(child: this); | |
| // 边框 | |
| Widget border({ | |
| Color color = Colors.black, | |
| double width = 1, | |
| double radius = 0, | |
| }) => Container( | |
| decoration: BoxDecoration( | |
| border: Border.all(color: color, width: width), | |
| borderRadius: BorderRadius.circular(radius), | |
| ), | |
| child: this, | |
| ); | |
| // 阴影 | |
| Widget shadow({ | |
| Color color = Colors.black26, | |
| double blurRadius = 4, | |
| Offset offset = const Offset(0, 2), | |
| }) => Container( | |
| decoration: BoxDecoration( | |
| boxShadow: [ | |
| BoxShadow(color: color, blurRadius: blurRadius, offset: offset), | |
| ], | |
| ), | |
| child: this, | |
| ); | |
| // 手势相关 | |
| Widget onTap(VoidCallback onTap) => | |
| GestureDetector(onTap: onTap, child: this); | |
| Widget onLongPress(VoidCallback onLongPress) => | |
| GestureDetector(onLongPress: onLongPress, child: this); | |
| Widget inkWell({ | |
| VoidCallback? onTap, | |
| VoidCallback? onLongPress, | |
| Color? splashColor, | |
| }) => InkWell( | |
| onTap: onTap, | |
| onLongPress: onLongPress, | |
| splashColor: splashColor, | |
| child: this, | |
| ); | |
| // 可见性 | |
| Widget visible(bool visible) => Visibility(visible: visible, child: this); | |
| Widget opacity(double opacity) => Opacity(opacity: opacity, child: this); | |
| // 旋转和变换 | |
| Widget rotate(double angle) => Transform.rotate(angle: angle, child: this); | |
| Widget scale(double scale) => Transform.scale(scale: scale, child: this); | |
| // 灵活布局 | |
| Widget expanded({int flex = 1}) => Expanded(flex: flex, child: this); | |
| Widget flexible({int flex = 1, FlexFit fit = FlexFit.loose}) => | |
| Flexible(flex: flex, fit: fit, child: this); | |
| // 滚动 | |
| Widget scrollable({Axis axis = Axis.vertical}) => | |
| SingleChildScrollView(scrollDirection: axis, child: this); | |
| // Hero 动画 | |
| Widget hero(String tag) => Hero(tag: tag, child: this); | |
| // SafeArea | |
| Widget safeArea({ | |
| bool top = true, | |
| bool bottom = true, | |
| bool left = true, | |
| bool right = true, | |
| }) => | |
| SafeArea(top: top, bottom: bottom, left: left, right: right, child: this); | |
| // 约束 | |
| Widget constrained({ | |
| double? minWidth, | |
| double? maxWidth, | |
| double? minHeight, | |
| double? maxHeight, | |
| }) => ConstrainedBox( | |
| constraints: BoxConstraints( | |
| minWidth: minWidth ?? 0, | |
| maxWidth: maxWidth ?? double.infinity, | |
| minHeight: minHeight ?? 0, | |
| maxHeight: maxHeight ?? double.infinity, | |
| ), | |
| child: this, | |
| ); | |
| Widget aspectRatio(double aspectRatio) => | |
| AspectRatio(aspectRatio: aspectRatio, child: this); | |
| // 卡片效果 | |
| Widget card({ | |
| Color? color, | |
| double elevation = 1, | |
| ShapeBorder? shape, | |
| EdgeInsetsGeometry? margin, | |
| }) => Card( | |
| color: color, | |
| elevation: elevation, | |
| shape: shape, | |
| margin: margin, | |
| child: this, | |
| ); | |
| // 定位相关 | |
| Widget positioned({ | |
| double? left, | |
| double? top, | |
| double? right, | |
| double? bottom, | |
| double? width, | |
| double? height, | |
| }) => Positioned( | |
| left: left, | |
| top: top, | |
| right: right, | |
| bottom: bottom, | |
| width: width, | |
| height: height, | |
| child: this, | |
| ); | |
| Widget positionedFill({ | |
| double left = 0, | |
| double top = 0, | |
| double right = 0, | |
| double bottom = 0, | |
| }) => Positioned.fill( | |
| left: left, | |
| top: top, | |
| right: right, | |
| bottom: bottom, | |
| child: this, | |
| ); | |
| // FittedBox | |
| Widget fitted({BoxFit fit = BoxFit.contain}) => | |
| FittedBox(fit: fit, child: this); | |
| // IgnorePointer | |
| Widget ignorePointer({bool ignoring = true}) => | |
| IgnorePointer(ignoring: ignoring, child: this); | |
| Widget absorbPointer({bool absorbing = true}) => | |
| AbsorbPointer(absorbing: absorbing, child: this); | |
| // Material 效果 | |
| Widget material({ | |
| Color color = Colors.transparent, | |
| double elevation = 0, | |
| BorderRadius? borderRadius, | |
| }) => Material( | |
| color: color, | |
| elevation: elevation, | |
| borderRadius: borderRadius, | |
| child: this, | |
| ); | |
| // Tooltip | |
| Widget tooltip(String message) => Tooltip(message: message, child: this); | |
| // Semantics | |
| Widget semantics({String? label, bool? button}) => | |
| Semantics(label: label, button: button, child: this); | |
| // 条件显示 | |
| Widget when(bool condition) => condition ? this : const SizedBox.shrink(); | |
| // 包装 Builder | |
| Widget builder(Widget Function(Widget child) builder) => builder(this); | |
| // 动画相关 | |
| Widget animatedOpacity({ | |
| required double opacity, | |
| Duration duration = const Duration(milliseconds: 300), | |
| Curve curve = Curves.easeInOut, | |
| }) => AnimatedOpacity( | |
| opacity: opacity, | |
| duration: duration, | |
| curve: curve, | |
| child: this, | |
| ); | |
| Widget animatedScale({ | |
| required double scale, | |
| Duration duration = const Duration(milliseconds: 300), | |
| Curve curve = Curves.easeInOut, | |
| }) => AnimatedScale( | |
| scale: scale, | |
| duration: duration, | |
| curve: curve, | |
| child: this, | |
| ); | |
| Widget animatedSlide({ | |
| required Offset offset, | |
| Duration duration = const Duration(milliseconds: 300), | |
| Curve curve = Curves.easeInOut, | |
| }) => AnimatedSlide( | |
| offset: offset, | |
| duration: duration, | |
| curve: curve, | |
| child: this, | |
| ); | |
| // ColorFiltered | |
| Widget colorFiltered(ColorFilter colorFilter) => | |
| ColorFiltered(colorFilter: colorFilter, child: this); | |
| Widget grayscale() => ColorFiltered( | |
| colorFilter: const ColorFilter.matrix(<double>[ | |
| 0.2126, | |
| 0.7152, | |
| 0.0722, | |
| 0, | |
| 0, | |
| 0.2126, | |
| 0.7152, | |
| 0.0722, | |
| 0, | |
| 0, | |
| 0.2126, | |
| 0.7152, | |
| 0.0722, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 1, | |
| 0, | |
| ]), | |
| child: this, | |
| ); | |
| // ShaderMask | |
| Widget shaderMask({ | |
| required Shader Function(Rect bounds) shaderCallback, | |
| BlendMode blendMode = BlendMode.modulate, | |
| }) => ShaderMask( | |
| shaderCallback: shaderCallback, | |
| blendMode: blendMode, | |
| child: this, | |
| ); | |
| // RepaintBoundary (性能优化) | |
| Widget repaintBoundary() => RepaintBoundary(child: this); | |
| // Offstage | |
| Widget offstage({bool offstage = true}) => | |
| Offstage(offstage: offstage, child: this); | |
| // SliverToBoxAdapter | |
| Widget sliverBox() => SliverToBoxAdapter(child: this); | |
| // 无限宽度/高度 | |
| Widget fullWidth() => SizedBox(width: double.infinity, child: this); | |
| Widget fullHeight() => SizedBox(height: double.infinity, child: this); | |
| Widget fullSize() => SizedBox.expand(child: this); | |
| // FractionallySizedBox | |
| Widget fractionallySized({ | |
| double? widthFactor, | |
| double? heightFactor, | |
| AlignmentGeometry alignment = Alignment.center, | |
| }) => FractionallySizedBox( | |
| widthFactor: widthFactor, | |
| heightFactor: heightFactor, | |
| alignment: alignment, | |
| child: this, | |
| ); | |
| // UnconstrainedBox | |
| Widget unconstrained({Axis? constrainedAxis}) => | |
| UnconstrainedBox(constrainedAxis: constrainedAxis, child: this); | |
| // LimitedBox | |
| Widget limitedBox({ | |
| double maxWidth = double.infinity, | |
| double maxHeight = double.infinity, | |
| }) => LimitedBox(maxWidth: maxWidth, maxHeight: maxHeight, child: this); | |
| // 更多手势 | |
| Widget onDoubleTap(VoidCallback onDoubleTap) => | |
| GestureDetector(onDoubleTap: onDoubleTap, child: this); | |
| Widget onPanUpdate(void Function(DragUpdateDetails) onPanUpdate) => | |
| GestureDetector(onPanUpdate: onPanUpdate, child: this); | |
| Widget onScaleUpdate(void Function(ScaleUpdateDetails) onScaleUpdate) => | |
| GestureDetector(onScaleUpdate: onScaleUpdate, child: this); | |
| // Dismissible | |
| Widget dismissible({ | |
| required Key key, | |
| VoidCallback? onDismissed, | |
| DismissDirection direction = DismissDirection.horizontal, | |
| Widget? background, | |
| }) => Dismissible( | |
| key: key, | |
| onDismissed: (_) => onDismissed?.call(), | |
| direction: direction, | |
| background: background ?? Container(color: Colors.red), | |
| child: this, | |
| ); | |
| // Draggable | |
| Widget draggable<T extends Object>({ | |
| required T data, | |
| Widget? feedback, | |
| Widget? childWhenDragging, | |
| }) => Draggable<T>( | |
| data: data, | |
| feedback: feedback ?? this, | |
| childWhenDragging: childWhenDragging, | |
| child: this, | |
| ); | |
| // 更多动画 | |
| Widget animatedContainer({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Curve curve = Curves.easeInOut, | |
| Color? color, | |
| EdgeInsetsGeometry? padding, | |
| EdgeInsetsGeometry? margin, | |
| double? width, | |
| double? height, | |
| BoxDecoration? decoration, | |
| }) => AnimatedContainer( | |
| duration: duration, | |
| curve: curve, | |
| color: decoration == null ? color : null, | |
| padding: padding, | |
| margin: margin, | |
| width: width, | |
| height: height, | |
| decoration: decoration, | |
| child: this, | |
| ); | |
| Widget animatedPadding({ | |
| required EdgeInsetsGeometry padding, | |
| Duration duration = const Duration(milliseconds: 300), | |
| Curve curve = Curves.easeInOut, | |
| }) => AnimatedPadding( | |
| padding: padding, | |
| duration: duration, | |
| curve: curve, | |
| child: this, | |
| ); | |
| Widget animatedAlign({ | |
| required AlignmentGeometry alignment, | |
| Duration duration = const Duration(milliseconds: 300), | |
| Curve curve = Curves.easeInOut, | |
| }) => AnimatedAlign( | |
| alignment: alignment, | |
| duration: duration, | |
| curve: curve, | |
| child: this, | |
| ); | |
| Widget animatedPositioned({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Curve curve = Curves.easeInOut, | |
| double? left, | |
| double? top, | |
| double? right, | |
| double? bottom, | |
| double? width, | |
| double? height, | |
| }) => AnimatedPositioned( | |
| duration: duration, | |
| curve: curve, | |
| left: left, | |
| top: top, | |
| right: right, | |
| bottom: bottom, | |
| width: width, | |
| height: height, | |
| child: this, | |
| ); | |
| // 渐变背景 | |
| Widget linearGradient({ | |
| required List<Color> colors, | |
| AlignmentGeometry begin = Alignment.centerLeft, | |
| AlignmentGeometry end = Alignment.centerRight, | |
| }) => Container( | |
| decoration: BoxDecoration( | |
| gradient: LinearGradient(colors: colors, begin: begin, end: end), | |
| ), | |
| child: this, | |
| ); | |
| Widget radialGradient({ | |
| required List<Color> colors, | |
| double radius = 0.5, | |
| AlignmentGeometry center = Alignment.center, | |
| }) => Container( | |
| decoration: BoxDecoration( | |
| gradient: RadialGradient(colors: colors, radius: radius, center: center), | |
| ), | |
| child: this, | |
| ); | |
| // 模糊效果 | |
| Widget blur({double sigmaX = 5, double sigmaY = 5}) => ImageFiltered( | |
| imageFilter: ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY), | |
| child: this, | |
| ); | |
| // ClipPath | |
| Widget clipPath(CustomClipper<Path> clipper) => | |
| ClipPath(clipper: clipper, child: this); | |
| // 旋转角度(度数) | |
| Widget rotateDegrees(double degrees) => | |
| Transform.rotate(angle: degrees * 3.14159265359 / 180, child: this); | |
| // 平移 | |
| Widget translate({double x = 0, double y = 0}) => | |
| Transform.translate(offset: Offset(x, y), child: this); | |
| // 倾斜 | |
| Widget skew({double x = 0, double y = 0}) => | |
| Transform(transform: Matrix4.skew(x, y), child: this); | |
| // 长按菜单 | |
| Widget contextMenu({required List<PopupMenuEntry> items}) => GestureDetector( | |
| onLongPressStart: (details) { | |
| // 需要在 StatefulWidget 中使用 showMenu | |
| }, | |
| child: this, | |
| ); | |
| // 刷新指示器 | |
| Widget refreshIndicator({ | |
| required Future<void> Function() onRefresh, | |
| Color? color, | |
| }) => RefreshIndicator(onRefresh: onRefresh, color: color, child: this); | |
| // 通知监听 | |
| Widget notificationListener<T extends Notification>({ | |
| required bool Function(T) onNotification, | |
| }) => NotificationListener<T>(onNotification: onNotification, child: this); | |
| // 主题相关 | |
| Widget themed( | |
| Widget Function(BuildContext context, ThemeData theme) builder, | |
| ) => Builder(builder: (context) => builder(context, Theme.of(context))); | |
| // MediaQuery 响应式 | |
| Widget responsive({ | |
| Widget Function(BuildContext)? onMobile, | |
| Widget Function(BuildContext)? onTablet, | |
| Widget Function(BuildContext)? onDesktop, | |
| }) => LayoutBuilder( | |
| builder: (context, constraints) { | |
| if (constraints.maxWidth < 600 && onMobile != null) { | |
| return onMobile(context); | |
| } else if (constraints.maxWidth < 1200 && onTablet != null) { | |
| return onTablet(context); | |
| } else if (onDesktop != null) { | |
| return onDesktop(context); | |
| } | |
| return this; | |
| }, | |
| ); | |
| // 根据条件构建 | |
| Widget ifElse( | |
| bool condition, | |
| Widget Function(Widget) ifTrue, [ | |
| Widget Function(Widget)? ifFalse, | |
| ]) => condition ? ifTrue(this) : (ifFalse?.call(this) ?? this); | |
| // 应用多个修饰器 | |
| Widget apply(Widget Function(Widget) modifier) => modifier(this); | |
| // Sliver 系列 | |
| Widget sliverPadding(EdgeInsetsGeometry padding) => SliverPadding( | |
| padding: padding, | |
| sliver: SliverToBoxAdapter(child: this), | |
| ); | |
| // ==================== iOS UIView.animate 风格动画 ==================== | |
| /// 淡入动画 (类似 iOS fadeIn) | |
| Widget fadeIn({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeIn, | |
| }) => _FadeInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| child: this, | |
| ); | |
| /// 淡出动画 | |
| Widget fadeOut({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| }) => _FadeOutWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| child: this, | |
| ); | |
| /// 缩放进入 (从小变大) | |
| Widget scaleIn({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOutBack, | |
| double begin = 0.0, | |
| }) => _ScaleInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| begin: begin, | |
| child: this, | |
| ); | |
| /// 从左滑入 | |
| Widget slideInFromLeft({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| }) => _SlideInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| beginOffset: const Offset(-1, 0), | |
| child: this, | |
| ); | |
| /// 从右滑入 | |
| Widget slideInFromRight({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| }) => _SlideInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| beginOffset: const Offset(1, 0), | |
| child: this, | |
| ); | |
| /// 从上滑入 | |
| Widget slideInFromTop({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| }) => _SlideInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| beginOffset: const Offset(0, -1), | |
| child: this, | |
| ); | |
| /// 从下滑入 | |
| Widget slideInFromBottom({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| }) => _SlideInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| beginOffset: const Offset(0, 1), | |
| child: this, | |
| ); | |
| /// 弹跳效果 | |
| Widget bounceIn({ | |
| Duration duration = const Duration(milliseconds: 500), | |
| Duration delay = Duration.zero, | |
| }) => _ScaleInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: Curves.elasticOut, | |
| begin: 0.3, | |
| child: this, | |
| ); | |
| /// 旋转进入 | |
| Widget rotateIn({ | |
| Duration duration = const Duration(milliseconds: 400), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| double turns = 1, | |
| }) => _RotateInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| turns: turns, | |
| child: this, | |
| ); | |
| /// 抖动效果 (类似错误提示) | |
| Widget shake({ | |
| Duration duration = const Duration(milliseconds: 500), | |
| Duration delay = Duration.zero, | |
| double intensity = 10, | |
| }) => _ShakeWidget( | |
| duration: duration, | |
| delay: delay, | |
| intensity: intensity, | |
| child: this, | |
| ); | |
| /// 脉冲效果 (循环) | |
| Widget pulse({ | |
| Duration duration = const Duration(milliseconds: 1000), | |
| double minScale = 0.95, | |
| double maxScale = 1.05, | |
| }) => _PulseWidget( | |
| duration: duration, | |
| minScale: minScale, | |
| maxScale: maxScale, | |
| child: this, | |
| ); | |
| /// 闪烁效果 (循环) | |
| Widget blink({ | |
| Duration duration = const Duration(milliseconds: 1000), | |
| double minOpacity = 0.3, | |
| }) => _BlinkWidget(duration: duration, minOpacity: minOpacity, child: this); | |
| /// 组合动画:淡入+滑入 | |
| Widget fadeSlideIn({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| Offset beginOffset = const Offset(0, 0.2), | |
| }) => _FadeSlideInWidget( | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| beginOffset: beginOffset, | |
| child: this, | |
| ); | |
| // ==================== 统一动画 API ==================== | |
| /// 统一动画接口 | |
| /// ```dart | |
| /// widget.animate(Anim.fadeIn(duration: 300.ms)) | |
| /// widget.animate(Anim.scale(trigger: AnimTrigger.onTap)) | |
| /// widget.animate(Anim.pulse(repeat: true)) | |
| /// ``` | |
| Widget animate(Anim anim) => _UnifiedAnimWidget(anim: anim, child: this); | |
| } | |
| // ==================== 动画配置 ==================== | |
| /// 动画触发方式 | |
| enum AnimTrigger { | |
| /// 自动播放(出现时) | |
| auto, | |
| /// 点击触发 | |
| onTap, | |
| /// 长按触发 | |
| onLongPress, | |
| } | |
| /// 统一动画配置 | |
| class Anim { | |
| final AnimType type; | |
| final Duration duration; | |
| final Duration delay; | |
| final Curve curve; | |
| final bool repeat; | |
| final AnimTrigger trigger; | |
| final Offset? slideOffset; | |
| final double? scaleBegin; | |
| final double? turns; | |
| final double? intensity; | |
| const Anim._({ | |
| required this.type, | |
| this.duration = const Duration(milliseconds: 300), | |
| this.delay = Duration.zero, | |
| this.curve = Curves.easeOut, | |
| this.repeat = false, | |
| this.trigger = AnimTrigger.auto, | |
| this.slideOffset, | |
| this.scaleBegin, | |
| this.turns, | |
| this.intensity, | |
| }); | |
| /// 淡入 | |
| static Anim fadeIn({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeIn, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.fadeIn, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 淡出 | |
| static Anim fadeOut({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.fadeOut, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 缩放 | |
| static Anim scale({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOutBack, | |
| double begin = 0.0, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.scale, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| scaleBegin: begin, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 弹跳 | |
| static Anim bounce({ | |
| Duration duration = const Duration(milliseconds: 500), | |
| Duration delay = Duration.zero, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.bounce, | |
| duration: duration, | |
| delay: delay, | |
| curve: Curves.elasticOut, | |
| scaleBegin: 0.3, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 从左滑入 | |
| static Anim slideLeft({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.slide, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| slideOffset: const Offset(-1, 0), | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 从右滑入 | |
| static Anim slideRight({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.slide, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| slideOffset: const Offset(1, 0), | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 从上滑入 | |
| static Anim slideTop({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.slide, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| slideOffset: const Offset(0, -1), | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 从下滑入 | |
| static Anim slideBottom({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.slide, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| slideOffset: const Offset(0, 1), | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 旋转 | |
| static Anim rotate({ | |
| Duration duration = const Duration(milliseconds: 400), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| double turns = 1, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.rotate, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| turns: turns, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 抖动 | |
| static Anim shake({ | |
| Duration duration = const Duration(milliseconds: 500), | |
| Duration delay = Duration.zero, | |
| double intensity = 10, | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.shake, | |
| duration: duration, | |
| delay: delay, | |
| intensity: intensity, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 脉冲 | |
| static Anim pulse({ | |
| Duration duration = const Duration(milliseconds: 1000), | |
| bool repeat = true, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.pulse, | |
| duration: duration, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 闪烁 | |
| static Anim blink({ | |
| Duration duration = const Duration(milliseconds: 1000), | |
| bool repeat = true, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.blink, | |
| duration: duration, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| /// 淡入+滑入组合 | |
| static Anim fadeSlide({ | |
| Duration duration = const Duration(milliseconds: 300), | |
| Duration delay = Duration.zero, | |
| Curve curve = Curves.easeOut, | |
| Offset offset = const Offset(0, 0.2), | |
| bool repeat = false, | |
| AnimTrigger trigger = AnimTrigger.auto, | |
| }) => Anim._( | |
| type: AnimType.fadeSlide, | |
| duration: duration, | |
| delay: delay, | |
| curve: curve, | |
| slideOffset: offset, | |
| repeat: repeat, | |
| trigger: trigger, | |
| ); | |
| } | |
| enum AnimType { | |
| fadeIn, | |
| fadeOut, | |
| scale, | |
| bounce, | |
| slide, | |
| rotate, | |
| shake, | |
| pulse, | |
| blink, | |
| fadeSlide, | |
| } | |
| // ==================== Text 专用扩展 ==================== | |
| extension TextModifier on Text { | |
| Text fontSize(double size) => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith(fontSize: size), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text fontWeight(FontWeight weight) => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith(fontWeight: weight), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text bold() => fontWeight(FontWeight.bold); | |
| Text textColor(Color color) => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith(color: color), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text aligned(TextAlign align) => Text( | |
| data ?? '', | |
| style: style, | |
| textAlign: align, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text lines(int count) => Text( | |
| data ?? '', | |
| style: style, | |
| textAlign: textAlign, | |
| maxLines: count, | |
| overflow: overflow ?? TextOverflow.ellipsis, | |
| ); | |
| Text underline() => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith( | |
| decoration: TextDecoration.underline, | |
| ), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text lineThrough() => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith( | |
| decoration: TextDecoration.lineThrough, | |
| ), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text italic() => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith(fontStyle: FontStyle.italic), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text letterSpacing(double spacing) => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith(letterSpacing: spacing), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text lineHeight(double height) => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith(height: height), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text fontFamily(String family) => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith(fontFamily: family), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text shadow({ | |
| Color color = Colors.black26, | |
| double blurRadius = 2, | |
| Offset offset = const Offset(1, 1), | |
| }) => Text( | |
| data ?? '', | |
| style: (style ?? const TextStyle()).copyWith( | |
| shadows: [Shadow(color: color, blurRadius: blurRadius, offset: offset)], | |
| ), | |
| textAlign: textAlign, | |
| maxLines: maxLines, | |
| overflow: overflow, | |
| ); | |
| Text ellipsis() => Text( | |
| data ?? '', | |
| style: style, | |
| textAlign: textAlign, | |
| maxLines: maxLines ?? 1, | |
| overflow: TextOverflow.ellipsis, | |
| ); | |
| } | |
| // ==================== Icon 专用扩展 ==================== | |
| extension IconModifier on Icon { | |
| Icon iconSize(double size) => Icon(icon, size: size, color: color); | |
| Icon iconColor(Color color) => Icon(icon, size: size, color: color); | |
| } | |
| // ==================== Image 专用扩展 ==================== | |
| extension ImageModifier on Image { | |
| Widget imageSize(double width, double height) => | |
| SizedBox(width: width, height: height, child: this); | |
| Widget imageFit(BoxFit fit) => SizedBox( | |
| child: Image(image: image, fit: fit), | |
| ); | |
| } | |
| // ==================== Container 专用扩展 ==================== | |
| extension ContainerModifier on Container { | |
| Container backgroundColor(Color color) => Container( | |
| key: key, | |
| alignment: alignment, | |
| padding: padding, | |
| color: decoration == null ? color : null, | |
| decoration: decoration != null | |
| ? (decoration as BoxDecoration).copyWith(color: color) | |
| : null, | |
| margin: margin, | |
| width: constraints?.maxWidth, | |
| height: constraints?.maxHeight, | |
| child: child, | |
| ); | |
| } | |
| // ==================== List<Widget> 扩展 ==================== | |
| extension WidgetListModifier on List<Widget> { | |
| Widget toColumn({ | |
| MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, | |
| CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, | |
| MainAxisSize mainAxisSize = MainAxisSize.max, | |
| }) => Column( | |
| mainAxisAlignment: mainAxisAlignment, | |
| crossAxisAlignment: crossAxisAlignment, | |
| mainAxisSize: mainAxisSize, | |
| children: this, | |
| ); | |
| Widget toRow({ | |
| MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, | |
| CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, | |
| MainAxisSize mainAxisSize = MainAxisSize.max, | |
| }) => Row( | |
| mainAxisAlignment: mainAxisAlignment, | |
| crossAxisAlignment: crossAxisAlignment, | |
| mainAxisSize: mainAxisSize, | |
| children: this, | |
| ); | |
| Widget toStack({ | |
| AlignmentGeometry alignment = AlignmentDirectional.topStart, | |
| StackFit fit = StackFit.loose, | |
| }) => Stack(alignment: alignment, fit: fit, children: this); | |
| Widget toWrap({ | |
| double spacing = 0, | |
| double runSpacing = 0, | |
| WrapAlignment alignment = WrapAlignment.start, | |
| }) => Wrap( | |
| spacing: spacing, | |
| runSpacing: runSpacing, | |
| alignment: alignment, | |
| children: this, | |
| ); | |
| Widget toListView({ | |
| Axis scrollDirection = Axis.vertical, | |
| EdgeInsetsGeometry? padding, | |
| bool shrinkWrap = false, | |
| }) => ListView( | |
| scrollDirection: scrollDirection, | |
| padding: padding, | |
| shrinkWrap: shrinkWrap, | |
| children: this, | |
| ); | |
| // 添加间距 | |
| List<Widget> withSpacing(double spacing) { | |
| if (isEmpty) return this; | |
| return expand((widget) sync* { | |
| yield widget; | |
| yield SizedBox(width: spacing, height: spacing); | |
| }).toList()..removeLast(); | |
| } | |
| List<Widget> withDivider({Widget? divider}) { | |
| if (isEmpty) return this; | |
| final div = divider ?? const Divider(height: 1); | |
| return expand((widget) sync* { | |
| yield widget; | |
| yield div; | |
| }).toList()..removeLast(); | |
| } | |
| } | |
| // ==================== num 扩展 (用于间距) ==================== | |
| extension NumSpacingModifier on num { | |
| Widget get hGap => SizedBox(width: toDouble()); | |
| Widget get vGap => SizedBox(height: toDouble()); | |
| Widget get gap => SizedBox(width: toDouble(), height: toDouble()); | |
| EdgeInsets get all => EdgeInsets.all(toDouble()); | |
| EdgeInsets get horizontal => EdgeInsets.symmetric(horizontal: toDouble()); | |
| EdgeInsets get vertical => EdgeInsets.symmetric(vertical: toDouble()); | |
| EdgeInsets get left => EdgeInsets.only(left: toDouble()); | |
| EdgeInsets get right => EdgeInsets.only(right: toDouble()); | |
| EdgeInsets get top => EdgeInsets.only(top: toDouble()); | |
| EdgeInsets get bottom => EdgeInsets.only(bottom: toDouble()); | |
| BorderRadius get circular => BorderRadius.circular(toDouble()); | |
| Radius get radius => Radius.circular(toDouble()); | |
| Duration get ms => Duration(milliseconds: toInt()); | |
| Duration get seconds => Duration(seconds: toInt()); | |
| } | |
| // ==================== String 扩展 (快速创建 Text) ==================== | |
| extension StringTextModifier on String { | |
| Text get text => Text(this); | |
| Text textStyle(TextStyle style) => Text(this, style: style); | |
| } | |
| // ==================== Color 扩展 ==================== | |
| extension ColorModifier on Color { | |
| Color darken([double amount = 0.1]) { | |
| final hsl = HSLColor.fromColor(this); | |
| return hsl | |
| .withLightness((hsl.lightness - amount).clamp(0.0, 1.0)) | |
| .toColor(); | |
| } | |
| Color lighten([double amount = 0.1]) { | |
| final hsl = HSLColor.fromColor(this); | |
| return hsl | |
| .withLightness((hsl.lightness + amount).clamp(0.0, 1.0)) | |
| .toColor(); | |
| } | |
| Color withOpacity10() => withValues(alpha: 0.1); | |
| Color withOpacity20() => withValues(alpha: 0.2); | |
| Color withOpacity50() => withValues(alpha: 0.5); | |
| Color withOpacity80() => withValues(alpha: 0.8); | |
| } | |
| // ==================== 动画 Widget 实现 ==================== | |
| /// 淡入动画 | |
| class _FadeInWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final Duration delay; | |
| final Curve curve; | |
| const _FadeInWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.delay, | |
| required this.curve, | |
| }); | |
| @override | |
| State<_FadeInWidget> createState() => _FadeInWidgetState(); | |
| } | |
| class _FadeInWidgetState extends State<_FadeInWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = CurvedAnimation(parent: _controller, curve: widget.curve); | |
| Future.delayed(widget.delay, () { | |
| if (mounted) _controller.forward(); | |
| }); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => | |
| FadeTransition(opacity: _animation, child: widget.child); | |
| } | |
| /// 淡出动画 | |
| class _FadeOutWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final Duration delay; | |
| final Curve curve; | |
| const _FadeOutWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.delay, | |
| required this.curve, | |
| }); | |
| @override | |
| State<_FadeOutWidget> createState() => _FadeOutWidgetState(); | |
| } | |
| class _FadeOutWidgetState extends State<_FadeOutWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController( | |
| duration: widget.duration, | |
| vsync: this, | |
| value: 1.0, | |
| ); | |
| _animation = CurvedAnimation(parent: _controller, curve: widget.curve); | |
| Future.delayed(widget.delay, () { | |
| if (mounted) _controller.reverse(); | |
| }); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => | |
| FadeTransition(opacity: _animation, child: widget.child); | |
| } | |
| /// 缩放进入动画 | |
| class _ScaleInWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final Duration delay; | |
| final Curve curve; | |
| final double begin; | |
| const _ScaleInWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.delay, | |
| required this.curve, | |
| required this.begin, | |
| }); | |
| @override | |
| State<_ScaleInWidget> createState() => _ScaleInWidgetState(); | |
| } | |
| class _ScaleInWidgetState extends State<_ScaleInWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<double>( | |
| begin: widget.begin, | |
| end: 1.0, | |
| ).animate(CurvedAnimation(parent: _controller, curve: widget.curve)); | |
| Future.delayed(widget.delay, () { | |
| if (mounted) _controller.forward(); | |
| }); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => | |
| ScaleTransition(scale: _animation, child: widget.child); | |
| } | |
| /// 滑入动画 | |
| class _SlideInWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final Duration delay; | |
| final Curve curve; | |
| final Offset beginOffset; | |
| const _SlideInWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.delay, | |
| required this.curve, | |
| required this.beginOffset, | |
| }); | |
| @override | |
| State<_SlideInWidget> createState() => _SlideInWidgetState(); | |
| } | |
| class _SlideInWidgetState extends State<_SlideInWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<Offset> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<Offset>( | |
| begin: widget.beginOffset, | |
| end: Offset.zero, | |
| ).animate(CurvedAnimation(parent: _controller, curve: widget.curve)); | |
| Future.delayed(widget.delay, () { | |
| if (mounted) _controller.forward(); | |
| }); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => | |
| SlideTransition(position: _animation, child: widget.child); | |
| } | |
| /// 旋转进入动画 | |
| class _RotateInWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final Duration delay; | |
| final Curve curve; | |
| final double turns; | |
| const _RotateInWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.delay, | |
| required this.curve, | |
| required this.turns, | |
| }); | |
| @override | |
| State<_RotateInWidget> createState() => _RotateInWidgetState(); | |
| } | |
| class _RotateInWidgetState extends State<_RotateInWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<double>( | |
| begin: widget.turns, | |
| end: 0.0, | |
| ).animate(CurvedAnimation(parent: _controller, curve: widget.curve)); | |
| Future.delayed(widget.delay, () { | |
| if (mounted) _controller.forward(); | |
| }); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => | |
| RotationTransition(turns: _animation, child: widget.child); | |
| } | |
| /// 抖动动画 | |
| class _ShakeWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final Duration delay; | |
| final double intensity; | |
| const _ShakeWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.delay, | |
| required this.intensity, | |
| }); | |
| @override | |
| State<_ShakeWidget> createState() => _ShakeWidgetState(); | |
| } | |
| class _ShakeWidgetState extends State<_ShakeWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<double>(begin: 0, end: 1).animate(_controller); | |
| Future.delayed(widget.delay, () { | |
| if (mounted) _controller.forward(); | |
| }); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return AnimatedBuilder( | |
| animation: _animation, | |
| builder: (context, child) { | |
| final sineValue = math.sin(_animation.value * 3.14159 * 4); | |
| return Transform.translate( | |
| offset: Offset( | |
| sineValue * widget.intensity * (1 - _animation.value), | |
| 0, | |
| ), | |
| child: child, | |
| ); | |
| }, | |
| child: widget.child, | |
| ); | |
| } | |
| } | |
| /// 脉冲动画 (循环) | |
| class _PulseWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final double minScale; | |
| final double maxScale; | |
| const _PulseWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.minScale, | |
| required this.maxScale, | |
| }); | |
| @override | |
| State<_PulseWidget> createState() => _PulseWidgetState(); | |
| } | |
| class _PulseWidgetState extends State<_PulseWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<double>( | |
| begin: widget.minScale, | |
| end: widget.maxScale, | |
| ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); | |
| _controller.repeat(reverse: true); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => | |
| ScaleTransition(scale: _animation, child: widget.child); | |
| } | |
| /// 闪烁动画 (循环) | |
| class _BlinkWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final double minOpacity; | |
| const _BlinkWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.minOpacity, | |
| }); | |
| @override | |
| State<_BlinkWidget> createState() => _BlinkWidgetState(); | |
| } | |
| class _BlinkWidgetState extends State<_BlinkWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<double>( | |
| begin: 1.0, | |
| end: widget.minOpacity, | |
| ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); | |
| _controller.repeat(reverse: true); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => | |
| FadeTransition(opacity: _animation, child: widget.child); | |
| } | |
| /// 淡入+滑入组合动画 | |
| class _FadeSlideInWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| final Duration delay; | |
| final Curve curve; | |
| final Offset beginOffset; | |
| const _FadeSlideInWidget({ | |
| required this.child, | |
| required this.duration, | |
| required this.delay, | |
| required this.curve, | |
| required this.beginOffset, | |
| }); | |
| @override | |
| State<_FadeSlideInWidget> createState() => _FadeSlideInWidgetState(); | |
| } | |
| class _FadeSlideInWidgetState extends State<_FadeSlideInWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _fadeAnimation; | |
| late Animation<Offset> _slideAnimation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _fadeAnimation = CurvedAnimation(parent: _controller, curve: widget.curve); | |
| _slideAnimation = Tween<Offset>( | |
| begin: widget.beginOffset, | |
| end: Offset.zero, | |
| ).animate(CurvedAnimation(parent: _controller, curve: widget.curve)); | |
| Future.delayed(widget.delay, () { | |
| if (mounted) _controller.forward(); | |
| }); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) => FadeTransition( | |
| opacity: _fadeAnimation, | |
| child: SlideTransition(position: _slideAnimation, child: widget.child), | |
| ); | |
| } | |
| // ==================== 统一动画 Widget ==================== | |
| class _UnifiedAnimWidget extends StatefulWidget { | |
| final Anim anim; | |
| final Widget child; | |
| const _UnifiedAnimWidget({required this.anim, required this.child}); | |
| @override | |
| State<_UnifiedAnimWidget> createState() => _UnifiedAnimWidgetState(); | |
| } | |
| class _UnifiedAnimWidgetState extends State<_UnifiedAnimWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController( | |
| duration: widget.anim.duration, | |
| vsync: this, | |
| ); | |
| if (widget.anim.trigger == AnimTrigger.auto) { | |
| Future.delayed(widget.anim.delay, () { | |
| if (mounted) { | |
| if (widget.anim.repeat) { | |
| _controller.repeat(reverse: true); | |
| } else { | |
| _controller.forward(); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| void _playAnimation() { | |
| if (widget.anim.repeat) { | |
| if (_controller.isAnimating) { | |
| _controller.stop(); | |
| _controller.value = 0; | |
| } else { | |
| _controller.repeat(reverse: true); | |
| } | |
| } else { | |
| _controller.forward(from: 0); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| Widget animatedChild = _buildAnimatedChild(); | |
| if (widget.anim.trigger == AnimTrigger.onTap) { | |
| return GestureDetector(onTap: _playAnimation, child: animatedChild); | |
| } else if (widget.anim.trigger == AnimTrigger.onLongPress) { | |
| return GestureDetector(onLongPress: _playAnimation, child: animatedChild); | |
| } | |
| return animatedChild; | |
| } | |
| Widget _buildAnimatedChild() { | |
| final anim = widget.anim; | |
| final curve = CurvedAnimation(parent: _controller, curve: anim.curve); | |
| switch (anim.type) { | |
| case AnimType.fadeIn: | |
| return FadeTransition(opacity: curve, child: widget.child); | |
| case AnimType.fadeOut: | |
| final fadeOut = Tween<double>(begin: 1.0, end: 0.0).animate(curve); | |
| return FadeTransition(opacity: fadeOut, child: widget.child); | |
| case AnimType.scale: | |
| final scale = Tween<double>( | |
| begin: anim.scaleBegin ?? 0.0, | |
| end: 1.0, | |
| ).animate(curve); | |
| return ScaleTransition(scale: scale, child: widget.child); | |
| case AnimType.bounce: | |
| final bounce = Tween<double>( | |
| begin: anim.scaleBegin ?? 0.3, | |
| end: 1.0, | |
| ).animate(curve); | |
| return ScaleTransition(scale: bounce, child: widget.child); | |
| case AnimType.slide: | |
| final slide = Tween<Offset>( | |
| begin: anim.slideOffset ?? const Offset(0, 1), | |
| end: Offset.zero, | |
| ).animate(curve); | |
| return SlideTransition(position: slide, child: widget.child); | |
| case AnimType.rotate: | |
| final rotate = Tween<double>( | |
| begin: anim.turns ?? 1, | |
| end: 0.0, | |
| ).animate(curve); | |
| return RotationTransition(turns: rotate, child: widget.child); | |
| case AnimType.shake: | |
| return AnimatedBuilder( | |
| animation: _controller, | |
| builder: (context, child) { | |
| final sineValue = math.sin(_controller.value * math.pi * 4); | |
| return Transform.translate( | |
| offset: Offset( | |
| sineValue * (anim.intensity ?? 10) * (1 - _controller.value), | |
| 0, | |
| ), | |
| child: child, | |
| ); | |
| }, | |
| child: widget.child, | |
| ); | |
| case AnimType.pulse: | |
| final pulse = Tween<double>(begin: 0.95, end: 1.05).animate(curve); | |
| return ScaleTransition(scale: pulse, child: widget.child); | |
| case AnimType.blink: | |
| final blink = Tween<double>(begin: 1.0, end: 0.3).animate(curve); | |
| return FadeTransition(opacity: blink, child: widget.child); | |
| case AnimType.fadeSlide: | |
| final fade = CurvedAnimation(parent: _controller, curve: anim.curve); | |
| final slide = Tween<Offset>( | |
| begin: anim.slideOffset ?? const Offset(0, 0.2), | |
| end: Offset.zero, | |
| ).animate(curve); | |
| return FadeTransition( | |
| opacity: fade, | |
| child: SlideTransition(position: slide, child: widget.child), | |
| ); | |
| } | |
| } | |
| } | |
| // ==================== 点击触发动画 ==================== | |
| extension TapAnimationModifier on Widget { | |
| /// 点击缩放效果 (类似 iOS 按钮点击) | |
| Widget tapScale({ | |
| double scale = 0.95, | |
| Duration duration = const Duration(milliseconds: 100), | |
| }) => _TapScaleWidget(scale: scale, duration: duration, child: this); | |
| /// 点击透明度效果 | |
| Widget tapOpacity({ | |
| double opacity = 0.6, | |
| Duration duration = const Duration(milliseconds: 100), | |
| }) => _TapOpacityWidget(opacity: opacity, duration: duration, child: this); | |
| /// 点击弹跳效果 | |
| Widget tapBounce({Duration duration = const Duration(milliseconds: 150)}) => | |
| _TapBounceWidget(duration: duration, child: this); | |
| /// 点击涟漪效果 (带回调) | |
| Widget tapRipple({VoidCallback? onTap, Color? color}) => Material( | |
| color: Colors.transparent, | |
| child: InkWell( | |
| onTap: onTap, | |
| splashColor: color, | |
| borderRadius: BorderRadius.circular(8), | |
| child: this, | |
| ), | |
| ); | |
| } | |
| /// 点击缩放 | |
| class _TapScaleWidget extends StatefulWidget { | |
| final Widget child; | |
| final double scale; | |
| final Duration duration; | |
| const _TapScaleWidget({ | |
| required this.child, | |
| required this.scale, | |
| required this.duration, | |
| }); | |
| @override | |
| State<_TapScaleWidget> createState() => _TapScaleWidgetState(); | |
| } | |
| class _TapScaleWidgetState extends State<_TapScaleWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<double>( | |
| begin: 1.0, | |
| end: widget.scale, | |
| ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| void _onTapDown(TapDownDetails _) => _controller.forward(); | |
| void _onTapUp(TapUpDetails _) => _controller.reverse(); | |
| void _onTapCancel() => _controller.reverse(); | |
| @override | |
| Widget build(BuildContext context) => GestureDetector( | |
| onTapDown: _onTapDown, | |
| onTapUp: _onTapUp, | |
| onTapCancel: _onTapCancel, | |
| child: ScaleTransition(scale: _animation, child: widget.child), | |
| ); | |
| } | |
| /// 点击透明度 | |
| class _TapOpacityWidget extends StatefulWidget { | |
| final Widget child; | |
| final double opacity; | |
| final Duration duration; | |
| const _TapOpacityWidget({ | |
| required this.child, | |
| required this.opacity, | |
| required this.duration, | |
| }); | |
| @override | |
| State<_TapOpacityWidget> createState() => _TapOpacityWidgetState(); | |
| } | |
| class _TapOpacityWidgetState extends State<_TapOpacityWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = Tween<double>( | |
| begin: 1.0, | |
| end: widget.opacity, | |
| ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| void _onTapDown(TapDownDetails _) => _controller.forward(); | |
| void _onTapUp(TapUpDetails _) => _controller.reverse(); | |
| void _onTapCancel() => _controller.reverse(); | |
| @override | |
| Widget build(BuildContext context) => GestureDetector( | |
| onTapDown: _onTapDown, | |
| onTapUp: _onTapUp, | |
| onTapCancel: _onTapCancel, | |
| child: FadeTransition(opacity: _animation, child: widget.child), | |
| ); | |
| } | |
| /// 点击弹跳 | |
| class _TapBounceWidget extends StatefulWidget { | |
| final Widget child; | |
| final Duration duration; | |
| const _TapBounceWidget({required this.child, required this.duration}); | |
| @override | |
| State<_TapBounceWidget> createState() => _TapBounceWidgetState(); | |
| } | |
| class _TapBounceWidgetState extends State<_TapBounceWidget> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| late Animation<double> _animation; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController(duration: widget.duration, vsync: this); | |
| _animation = TweenSequence<double>([ | |
| TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.9), weight: 1), | |
| TweenSequenceItem(tween: Tween(begin: 0.9, end: 1.05), weight: 1), | |
| TweenSequenceItem(tween: Tween(begin: 1.05, end: 1.0), weight: 1), | |
| ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| void _onTap() { | |
| _controller.forward(from: 0); | |
| } | |
| @override | |
| Widget build(BuildContext context) => GestureDetector( | |
| onTap: _onTap, | |
| child: ScaleTransition(scale: _animation, child: widget.child), | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment