animal_island_flutter 0.1.1
animal_island_flutter: ^0.1.1 copied to clipboard
A cozy Animal Island inspired Flutter component library.
example/lib/main.dart
import 'dart:ui' as ui;
import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
void main() {
runApp(const AnimalIslandDocsApp());
}
class AnimalIslandDocsApp extends StatelessWidget {
const AnimalIslandDocsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: AnimalTheme(
child: const DocsHomePage(),
),
);
}
}
class DocsHomePage extends StatefulWidget {
const DocsHomePage({super.key});
@override
State<DocsHomePage> createState() => _DocsHomePageState();
}
class _DocsHomePageState extends State<DocsHomePage> {
var _activeIndex = -1;
var _routeLoadingActive = false;
var _routeLoadingMounted = false;
var _switchChecked = false;
var _fishValue = 'fish1';
String? _flowerValue;
String? _fruitValue;
var _disabledSelectValue = 'flower2';
var _islandChecks = <String>['beach', 'garden'];
var _critterChecks = <String>[];
var _tabsActiveKey = 'tab1';
var _typewriterReplayKey = 0;
var _loadingActive = true;
var _tableStriped = true;
var _tableLoading = false;
@override
Widget build(BuildContext context) {
final pages = _buildPages();
Widget body;
Color backgroundColor;
if (_activeIndex < 0) {
backgroundColor = const Color(0xFF7DC395);
body = _HomePage(onNavigate: _openHomeTarget);
} else {
final safeIndex = _activeIndex.clamp(0, pages.length - 1);
final activePage = pages[safeIndex];
backgroundColor = const Color(0xFFF8F4E8);
body = SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Sidebar(
pages: pages,
activeIndex: safeIndex,
onHome: () => setState(() => _activeIndex = -1),
onSelect: (index) => setState(() => _activeIndex = index),
),
Expanded(
child: _DocsDetailShell(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _DocsHeader(onOpenDialog: _openWelcomeDialog),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 64),
sliver: SliverToBoxAdapter(
child: _DocArticle(activePage),
),
),
],
),
),
),
],
),
);
}
return Scaffold(
backgroundColor: backgroundColor,
body: AnimalCursor(
child: Stack(
fit: StackFit.expand,
children: [
body,
if (_routeLoadingMounted)
Positioned.fill(
child: IgnorePointer(
ignoring: !_routeLoadingActive,
child: AnimalLoading(active: _routeLoadingActive),
),
),
],
),
),
);
}
List<_DocPage> _buildPages() {
return [
_DocPage(
group: '基础组件',
navTitle: 'Button 按钮',
title: 'Button 按钮',
summary:
'按钮组件 — 支持 primary / dashed / text / link 等类型,danger / ghost / loading / disabled 状态,icon 图标,block 块级,三种尺寸',
body: const _ButtonDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Input 输入框',
title: 'Input 输入框',
summary:
'输入框组件 — 支持三种尺寸、clearable 清除、prefix / suffix 前后缀、error / warning 校验状态、disabled 禁用',
body: const _InputDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Switch 开关',
title: 'Switch 开关',
summary: '开关组件 — 支持受控 / 非受控、自定义文案、small 尺寸、loading 状态',
body: _SwitchDoc(
checked: _switchChecked,
onChanged: (value) => setState(() => _switchChecked = value),
),
),
_DocPage(
group: '基础组件',
navTitle: 'Card 卡片',
title: 'Card 卡片',
summary: '卡片容器组件 — 支持 default / title 两种类型,13 种背景颜色',
body: const _CardDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Collapse 折叠面板',
title: 'Collapse 折叠面板',
summary: '折叠面板组件 — 支持展开/收起、默认展开、禁用状态',
body: const _CollapseDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Cursor 光标',
title: 'Cursor 光标',
summary: '光标组件 — 自定义手指光标,支持自定义尺寸、点击动画',
body: const _CursorDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Modal 弹窗',
title: 'Modal 弹窗',
summary: '模态弹窗组件 — SVG 有机形状裁切、支持标题、关闭按钮、自定义 Footer、ESC / 遮罩关闭',
body: _ModalDoc(
onBasic: _openBasicModal,
onTitle: _openTitleModal,
onFooter: _openCustomFooterModal,
onNoTypewriter: _openNoTypewriterModal,
),
),
_DocPage(
group: '基础组件',
navTitle: 'Typewriter 打字机',
title: 'Typewriter 打字机',
summary: '打字机组件 — 按字符逐个显示文本,支持多行与 Widget 富内容,不改变原有样式',
body: _TypewriterDoc(
replayKey: _typewriterReplayKey,
onReplay: () => setState(() => _typewriterReplayKey += 1),
),
),
_DocPage(
group: '基础组件',
navTitle: 'Divider 分割线',
title: 'Divider 分割线',
summary: '分割线组件 — 装饰性分割线',
body: const _DividerDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Icon 图标',
title: 'Icon 图标',
summary: '图标组件 — 动森风格图标集,包含 10 个可爱图标,支持自定义尺寸',
body: const _IconDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Select 选择器',
title: 'Select 选择器',
summary: '下拉选择器组件 — 支持自定义选项列表,高亮当前选中项',
body: _SelectDoc(
fishValue: _fishValue,
flowerValue: _flowerValue,
fruitValue: _fruitValue,
disabledValue: _disabledSelectValue,
onFishChanged: (value) => setState(() => _fishValue = value),
onFlowerChanged: (value) => setState(() => _flowerValue = value),
onFruitChanged: (value) => setState(() => _fruitValue = value),
onDisabledChanged: (value) =>
setState(() => _disabledSelectValue = value),
),
),
_DocPage(
group: '基础组件',
navTitle: 'Checkbox 多选框',
title: 'Checkbox 多选框',
summary: '多选框组件 — 支持受控/非受控、水平/垂直排列、三种尺寸、禁用单项或全部禁用',
body: _CheckboxDoc(
selectedIslands: _islandChecks,
selectedCritters: _critterChecks,
onIslandsChanged: (values) => setState(() => _islandChecks = values),
onCrittersChanged: (values) =>
setState(() => _critterChecks = values),
),
),
_DocPage(
group: '基础组件',
navTitle: 'Tabs 标签页',
title: 'Tabs 标签页',
summary: '标签页组件 — 支持受控/非受控模式切换',
body: _TabsDoc(
activeKey: _tabsActiveKey,
onChanged: (value) => setState(() => _tabsActiveKey = value),
),
),
_DocPage(
group: '基础组件',
navTitle: 'Footer 页脚',
title: 'Footer 底部装饰',
summary: '页面底部装饰图片,支持树和海两种类型',
body: const _FooterDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'CodeBlock 代码高亮',
title: 'CodeBlock 代码高亮',
summary: '代码高亮组件 — 语法高亮显示,支持自定义样式和类名',
body: const _CodeBlockDoc(),
),
_DocPage(
group: '基础组件',
navTitle: 'Loading 加载',
title: 'Loading 加载',
summary: '动森风格小岛 Loading 动画组件,支持自定义样式和类名',
body: _LoadingDoc(
active: _loadingActive,
onToggle: () => setState(() => _loadingActive = !_loadingActive),
),
),
_DocPage(
group: '基础组件',
navTitle: 'Table 表格',
title: 'Table 表格',
summary: '数据表格组件,支持斑马纹、边框、加载状态等常用功能',
body: _TableDoc(
striped: _tableStriped,
loading: _tableLoading,
onToggleStriped: () => setState(() => _tableStriped = !_tableStriped),
onLoading: _simulateTableLoading,
),
),
_DocPage(
group: '复杂组件',
navTitle: 'Time 时间',
title: 'Time 时间',
summary: '经典 HUD 风格的时间显示组件,实时更新时间',
body: const _TimeDoc(),
),
_DocPage(
group: '复杂组件',
navTitle: 'Phone 手机',
title: 'Phone 手机',
summary: '动森风格手机界面,包含对话框和背包功能',
body: const _PhoneDoc(),
),
];
}
Future<void> _simulateTableLoading() async {
if (_tableLoading) {
return;
}
setState(() => _tableLoading = true);
await Future<void>.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() => _tableLoading = false);
}
}
Future<void> _openHomeTarget(String key) async {
final index = switch (key) {
'button' => 0,
'input' => 1,
'switch' => 2,
'card' => 3,
'collapse' => 4,
'cursor' => 5,
'modal' => 6,
'typewriter' => 7,
'divider-comp' => 8,
'icon' => 9,
'select' => 10,
'checkbox' => 11,
'tabs' => 12,
'footer' => 13,
'codeblock' => 14,
'loading' => 15,
'table' => 16,
'time' => 17,
'phone' => 18,
_ => 0,
};
if (_activeIndex >= 0) {
setState(() => _activeIndex = index);
return;
}
setState(() {
_routeLoadingMounted = true;
_routeLoadingActive = true;
_activeIndex = index;
});
await Future<void>.delayed(const Duration(seconds: 2));
if (!mounted) {
return;
}
setState(() => _routeLoadingActive = false);
await Future<void>.delayed(const Duration(milliseconds: 1500));
if (mounted) {
setState(() => _routeLoadingMounted = false);
}
}
void _openWelcomeDialog() {
AnimalDialog.show<void>(
context: context,
title: const Text('博物馆捐赠'),
child: const Text('是否愿意将这条鱼捐赠给博物馆?傅达会好好照顾它的!'),
);
}
void _openBasicModal() {
AnimalDialog.show<void>(
context: context,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Text.rich(
TextSpan(
children: [
TextSpan(text: '钓到'),
TextSpan(
text: '石头',
style: TextStyle(color: Color(0xFFFD9303)),
),
TextSpan(text: '了!'),
],
),
textAlign: TextAlign.center,
),
SizedBox(height: 8),
Text('竟然连这种都能钓起来...'),
],
),
),
);
}
void _openTitleModal() {
AnimalDialog.show<void>(
context: context,
title: const Text('博物馆捐赠'),
child: const Text('是否愿意将这条鱼捐赠给博物馆呢?傅达会好好照顾它的!这可是博物馆的新展品哦~'),
);
}
void _openCustomFooterModal() {
AnimalDialog.show<void>(
context: context,
title: const Text('确认操作'),
footer: Builder(
builder: (dialogContext) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AnimalButton(
onPressed: () => Navigator.of(dialogContext).maybePop(),
child: const Text('再想想'),
),
const SizedBox(width: 12),
AnimalButton(
type: AnimalButtonType.primary,
danger: true,
onPressed: () => Navigator.of(dialogContext).maybePop(),
child: const Text('确认搬家'),
),
],
);
},
),
child: const Text('确定要让这位居民搬走吗?这个操作不可撤销。'),
);
}
void _openNoTypewriterModal() {
AnimalDialog.show<void>(
context: context,
title: const Text('天气预报'),
typewriter: false,
child: const Text('明天天气晴朗,气温 20-28°C,适合外出活动!'),
);
}
}
class _HomePage extends StatefulWidget {
const _HomePage({required this.onNavigate});
final ValueChanged<String> onNavigate;
@override
State<_HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<_HomePage>
with SingleTickerProviderStateMixin {
final _scrollController = ScrollController();
late final AnimationController _backgroundController;
var _showScrollHint = true;
@override
void initState() {
super.initState();
_backgroundController = AnimationController(
vsync: this,
duration: const Duration(seconds: 80),
)..repeat();
_scrollController.addListener(_handleScroll);
}
@override
void dispose() {
_backgroundController.dispose();
_scrollController
..removeListener(_handleScroll)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isMobile = width < 760;
return Stack(
children: [
Positioned.fill(
child: _ScrollingHomeBackground(animation: _backgroundController),
),
Positioned.fill(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
_HomeHero(
isMobile: isMobile,
onStart: () => widget.onNavigate('button'),
),
_HomeSection(
title: '特性',
description: '为什么选择 animal_island_flutter',
isMobile: isMobile,
child: const _HomeFeatureGrid(),
),
_HomeDivider(isMobile: isMobile),
_HomeSection(
title: '组件一览',
description: '点击卡片查看详细文档和在线演示',
isMobile: isMobile,
child: _HomeComponentGrid(
onNavigate: widget.onNavigate,
),
),
_HomeDivider(isMobile: isMobile),
_HomeSection(
title: '安装',
description: '在 Flutter 项目中添加依赖即可使用',
isMobile: isMobile,
child: const _HomeCodeBlock(
code: '# pubspec.yaml\n'
'dependencies:\n'
' animal_island_flutter:\n'
' path: ../animal_island_flutter',
),
),
_HomeDivider(isMobile: isMobile),
_HomeSection(
title: '快速上手',
description: '包裹 AnimalTheme 后即可使用组件',
isMobile: isMobile,
child: const _HomeCodeBlock(
code:
"import 'package:animal_island_flutter/animal_island_flutter.dart';\n"
"import 'package:flutter/material.dart';\n\n"
'class App extends StatelessWidget {\n'
' const App({super.key});\n\n'
' @override\n'
' Widget build(BuildContext context) {\n'
' return const AnimalTheme(\n'
" child: AnimalButton(child: Text('开始')),\n"
' );\n'
' }\n'
'}',
),
),
_HomeDivider(isMobile: isMobile),
_HomeSection(
title: '主题定制',
description: '通过 AnimalThemeData 覆盖 Flutter 设计令牌',
isMobile: isMobile,
child: const _HomeCodeBlock(
code: 'final theme = AnimalThemeData.fallback().copyWith(\n'
' primaryColor: const Color(0xFF19C8B9),\n'
' textColor: const Color(0xFF827157),\n'
" fontFamily: 'Nunito',\n"
' radius: 18,\n'
');\n\n'
'AnimalTheme(data: theme, child: app);',
),
),
_HomeFooter(
isMobile: isMobile,
onDocs: () => widget.onNavigate('button'),
),
],
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 40,
child: IgnorePointer(
ignoring: !_showScrollHint,
child: AnimatedOpacity(
opacity: _showScrollHint ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: const _ScrollHint(),
),
),
),
],
);
}
void _handleScroll() {
final shouldShow = _scrollController.offset <= 70;
if (shouldShow != _showScrollHint) {
setState(() => _showScrollHint = shouldShow);
}
}
}
class _ScrollingHomeBackground extends StatefulWidget {
const _ScrollingHomeBackground({required this.animation});
final Animation<double> animation;
@override
State<_ScrollingHomeBackground> createState() =>
_ScrollingHomeBackgroundState();
}
class _ScrollingHomeBackgroundState extends State<_ScrollingHomeBackground> {
ImageStream? _stream;
late final ImageStreamListener _listener;
ImageInfo? _imageInfo;
@override
void initState() {
super.initState();
_listener = ImageStreamListener(_handleImage);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_resolveImage();
}
@override
void dispose() {
_stream?.removeListener(_listener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ColoredBox(
color: const Color(0xFF7DC395),
child: CustomPaint(
painter: _HomeBackgroundPainter(
image: _imageInfo?.image,
progress: widget.animation,
),
),
);
}
void _resolveImage() {
final stream = const AssetImage(_DemoAssets.homeBg).resolve(
createLocalImageConfiguration(context),
);
if (_stream?.key == stream.key) {
return;
}
_stream?.removeListener(_listener);
_stream = stream;
_stream!.addListener(_listener);
}
void _handleImage(ImageInfo image, bool synchronousCall) {
setState(() => _imageInfo = image);
}
}
class _HomeBackgroundPainter extends CustomPainter {
const _HomeBackgroundPainter({
required this.image,
required this.progress,
}) : super(repaint: progress);
final ui.Image? image;
final Animation<double> progress;
@override
void paint(Canvas canvas, Size size) {
canvas.drawColor(const Color(0xFF7DC395), BlendMode.src);
final bg = image;
if (bg == null) {
return;
}
final sourceSize = Size(bg.width.toDouble(), bg.height.toDouble());
final scale =
(size.shortestSide / sourceSize.shortestSide).clamp(0.55, 1.0);
final imageSize = sourceSize * scale;
final dx = -progress.value * imageSize.width;
final dy = progress.value * imageSize.height;
final paint = Paint();
for (var x = dx % imageSize.width - imageSize.width;
x < size.width + imageSize.width;
x += imageSize.width) {
for (var y = dy % imageSize.height - imageSize.height;
y < size.height + imageSize.height;
y += imageSize.height) {
canvas.drawImageRect(
bg,
Offset.zero & sourceSize,
Offset(x, y) & imageSize,
paint,
);
}
}
}
@override
bool shouldRepaint(covariant _HomeBackgroundPainter oldDelegate) {
return oldDelegate.image != image || oldDelegate.progress != progress;
}
}
class _HomeHero extends StatelessWidget {
const _HomeHero({
required this.isMobile,
required this.onStart,
});
final bool isMobile;
final VoidCallback onStart;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
final textBlock = Column(
crossAxisAlignment:
isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: isMobile
? 'Animal Island Flutter'
: 'Animal\nIsland Flutter'),
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(left: 8),
child: _VersionPill(),
),
),
],
),
textAlign: isMobile ? TextAlign.center : TextAlign.left,
style: theme
.textStyle(
size: isMobile ? 37 : 60,
weight: FontWeight.w800,
color: const Color(0xFFFFF9E6),
)
.copyWith(
height: 1.1,
shadows: const [
Shadow(
color: Color(0x66000000),
offset: Offset(0, 4),
blurRadius: 1,
),
],
),
),
const SizedBox(height: 12),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: SizedBox(
height: isMobile ? 72 : 88,
child: AnimalTypewriter(
text: 'Animal风格的 Flutter 组件库,基于 Dart Widget 构建,让跨端应用充满温暖质感',
speed: const Duration(milliseconds: 60),
style: theme
.textStyle(
size: isMobile ? 14 : 17,
weight: FontWeight.w500,
color: const Color(0xFF7C5734),
)
.copyWith(height: 1.7),
),
),
),
const SizedBox(height: 28),
Align(
alignment: isMobile ? Alignment.center : Alignment.centerLeft,
child: AnimalButton(
type: AnimalButtonType.primary,
size: AnimalButtonSize.large,
onPressed: onStart,
child: const Text('开始使用 ->'),
),
),
],
);
final logo = Image.asset(
_DemoAssets.animalIcon,
width: isMobile ? 180 : 320,
height: isMobile ? 112 : 200,
fit: BoxFit.contain,
);
return ConstrainedBox(
constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height),
child: Padding(
padding: EdgeInsets.fromLTRB(
isMobile ? 24 : 40,
isMobile ? 56 : 60,
isMobile ? 24 : 40,
40,
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 880),
child: isMobile
? Column(
mainAxisSize: MainAxisSize.min,
children: [
logo,
const SizedBox(height: 32),
textBlock,
],
)
: Row(
children: [
Expanded(child: textBlock),
const SizedBox(width: 150),
logo,
],
),
),
),
),
);
}
}
class _VersionPill extends StatelessWidget {
const _VersionPill();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFE6F9F6),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'v0.8.1',
style: TextStyle(
color: Color(0xFF19C8B9),
fontSize: 12,
fontWeight: FontWeight.w600,
height: 1.2,
shadows: [],
),
),
);
}
}
class _HomeSection extends StatelessWidget {
const _HomeSection({
required this.title,
required this.description,
required this.isMobile,
required this.child,
});
final String title;
final String description;
final bool isMobile;
final Widget child;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 40,
vertical: isMobile ? 32 : 48,
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 960),
child: Column(
children: [
Text(
title,
textAlign: TextAlign.center,
style: theme.textStyle(
size: 24,
weight: FontWeight.w700,
color: const Color(0xFF725D42),
),
),
const SizedBox(height: 8),
Text(
description,
textAlign: TextAlign.center,
style: theme.textStyle(
size: 14,
weight: FontWeight.w500,
color: const Color(0xFF7C5734),
),
),
const SizedBox(height: 32),
child,
],
),
),
),
);
}
}
class _HomeFeatureGrid extends StatelessWidget {
const _HomeFeatureGrid();
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth < 460
? 1
: constraints.maxWidth < 720
? 2
: 4;
return _ResponsiveGrid(
columns: columns,
maxWidth: constraints.maxWidth,
spacing: 16,
children: [
for (final feature in _homeFeatures) _FeatureCard(feature: feature),
],
);
},
);
}
}
class _HomeComponentGrid extends StatelessWidget {
const _HomeComponentGrid({required this.onNavigate});
final ValueChanged<String> onNavigate;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth < 460
? 1
: constraints.maxWidth < 700
? 2
: constraints.maxWidth < 920
? 3
: 4;
return _ResponsiveGrid(
columns: columns,
maxWidth: constraints.maxWidth,
spacing: 12,
children: [
for (final component in _homeComponents)
_ComponentCard(
component: component,
onTap: () => onNavigate(component.key),
),
],
);
},
);
}
}
class _ResponsiveGrid extends StatelessWidget {
const _ResponsiveGrid({
required this.columns,
required this.maxWidth,
required this.spacing,
required this.children,
});
final int columns;
final double maxWidth;
final double spacing;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: [
for (final child in children)
SizedBox(
width: _itemWidth(context),
child: child,
),
],
);
}
double _itemWidth(BuildContext context) {
return (maxWidth - spacing * (columns - 1)) / columns;
}
}
class _FeatureCard extends StatefulWidget {
const _FeatureCard({required this.feature});
final _FeatureInfo feature;
@override
State<_FeatureCard> createState() => _FeatureCardState();
}
class _FeatureCardState extends State<_FeatureCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
transform: Matrix4.translationValues(0, _hovered ? -4 : 0, 0),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
decoration: BoxDecoration(
color: const Color(0xFFF7F3DF),
borderRadius: BorderRadius.circular(20),
boxShadow: _hovered
? [
BoxShadow(
color: const Color(0xFF725D42).withValues(alpha: 0.15),
offset: const Offset(0, 8),
blurRadius: 24,
),
]
: null,
),
child: Column(
children: [
AnimatedScale(
scale: _hovered ? 1.1 : 1,
duration: const Duration(milliseconds: 300),
child: Transform.rotate(
angle: _hovered ? -0.07 : 0,
child: SvgPicture.asset(
widget.feature.icon,
width: 42,
height: 42,
),
),
),
const SizedBox(height: 12),
Text(
widget.feature.title,
textAlign: TextAlign.center,
style: theme.textStyle(
size: 15,
weight: FontWeight.w700,
color: const Color(0xFF725D42),
),
),
const SizedBox(height: 6),
Text(
widget.feature.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: theme
.textStyle(
size: 13,
weight: FontWeight.w500,
color: const Color(0xFF7C5734),
)
.copyWith(height: 1.6),
),
],
),
),
);
}
}
class _ComponentCard extends StatefulWidget {
const _ComponentCard({
required this.component,
required this.onTap,
});
final _ComponentInfo component;
final VoidCallback onTap;
@override
State<_ComponentCard> createState() => _ComponentCardState();
}
class _ComponentCardState extends State<_ComponentCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 220),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xFFF7F3DF),
borderRadius: BorderRadius.circular(20),
boxShadow: _hovered ? theme.shadowBase : null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.component.name,
style: theme.textStyle(
size: 15,
weight: FontWeight.w700,
color: const Color(0xFF725D42),
),
),
const SizedBox(height: 4),
Text(
widget.component.description,
style: theme
.textStyle(
size: 12,
weight: FontWeight.w500,
color: const Color(0xFF7C5734),
)
.copyWith(height: 1.5),
),
],
),
),
),
);
}
}
class _HomeCodeBlock extends StatelessWidget {
const _HomeCodeBlock({required this.code});
final String code;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: AnimalCodeBlock(
code: code,
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 20),
),
);
}
}
class _HomeDivider extends StatelessWidget {
const _HomeDivider({required this.isMobile});
final bool isMobile;
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: isMobile ? MediaQuery.sizeOf(context).width * 0.9 : 800,
child: const AnimalDivider(),
),
);
}
}
class _HomeFooter extends StatelessWidget {
const _HomeFooter({
required this.isMobile,
required this.onDocs,
});
final bool isMobile;
final VoidCallback onDocs;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
final linkStyle = theme.textStyle(
size: 13,
weight: FontWeight.w500,
color: const Color(0xFF7C5734),
);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 40,
vertical: isMobile ? 24 : 32,
).copyWith(top: 32),
child: Column(
children: [
Wrap(
alignment: WrapAlignment.center,
spacing: 20,
runSpacing: 8,
children: [
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onDocs,
child: Text('组件文档', style: linkStyle),
),
),
Text('GitHub', style: linkStyle),
],
),
const SizedBox(height: 12),
Text(
'MIT License · Flutter + Dart',
style: theme.textStyle(
size: 12,
weight: FontWeight.w500,
color: const Color(0xFF7C5734),
),
),
],
),
);
}
}
class _ScrollHint extends StatefulWidget {
const _ScrollHint();
@override
State<_ScrollHint> createState() => _ScrollHintState();
}
class _ScrollHintState extends State<_ScrollHint>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _offset;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
_offset = Tween<double>(begin: 0, end: -8).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _offset,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _offset.value),
child: Opacity(
opacity: 0.7 + (1 - (_offset.value.abs() / 8)) * 0.3,
child: child,
),
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'向下滑动',
style: TextStyle(
color: Color(0xFFFFF9E6),
fontSize: 12,
fontWeight: FontWeight.w500,
shadows: [
Shadow(
color: Color(0x4D000000),
offset: Offset(0, 1),
blurRadius: 2,
),
],
),
),
const SizedBox(height: 4),
CustomPaint(
size: const Size(16, 16),
painter: _ArrowDownPainter(),
),
],
),
);
}
}
class _ArrowDownPainter extends CustomPainter {
const _ArrowDownPainter();
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFFFFF9E6)
..strokeWidth = 2
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke;
final path = Path()
..moveTo(size.width / 2, size.height * 0.2)
..lineTo(size.width / 2, size.height * 0.8)
..moveTo(size.width * 0.2, size.height * 0.5)
..lineTo(size.width / 2, size.height * 0.8)
..lineTo(size.width * 0.8, size.height * 0.5);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant _ArrowDownPainter oldDelegate) => false;
}
class _DocsDetailShell extends StatelessWidget {
const _DocsDetailShell({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
const _DocsTiledBackground(),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: IgnorePointer(
child: Image.asset(
_DemoAssets.guideBgLine,
fit: BoxFit.fitWidth,
alignment: Alignment.bottomCenter,
),
),
),
child,
],
);
}
}
class _DocsTiledBackground extends StatelessWidget {
const _DocsTiledBackground();
@override
Widget build(BuildContext context) {
return const DecoratedBox(
decoration: BoxDecoration(
color: Color(0xFFF8F4E8),
image: DecorationImage(
image: AssetImage(_DemoAssets.contentBg),
repeat: ImageRepeat.repeat,
fit: BoxFit.none,
alignment: Alignment.center,
),
),
);
}
}
class _Sidebar extends StatelessWidget {
const _Sidebar({
required this.pages,
required this.activeIndex,
required this.onHome,
required this.onSelect,
});
final List<_DocPage> pages;
final int activeIndex;
final VoidCallback onHome;
final ValueChanged<int> onSelect;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return SizedBox(
width: 220,
child: Stack(
fit: StackFit.expand,
children: [
const ColoredBox(color: Color(0xFFF8F4E8)),
SvgPicture.asset(
_DemoAssets.menuBg,
fit: BoxFit.cover,
alignment: Alignment.center,
),
DecoratedBox(
decoration: const BoxDecoration(
border: Border(right: BorderSide(color: Color(0xFFE8E2D6))),
),
child: Column(
children: [
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onHome,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 12),
child: Row(
children: [
SvgPicture.asset(
_DemoAssets.nook1,
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(
'集合啦!Animal',
style: theme.textStyle(
size: 15,
weight: FontWeight.w700,
),
),
],
),
),
),
),
const Divider(height: 1, color: Color(0xFFE8E2D6)),
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 24),
children: [
for (final indexed in pages.indexed) ...[
if (indexed.$1 == 0 ||
pages[indexed.$1 - 1].group != indexed.$2.group)
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Row(
children: [
const Expanded(
child: Divider(
color: Color(0xFFA0936E),
height: 1,
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 6),
child: Text(
indexed.$2.group,
style: theme.textStyle(
size: 11,
weight: FontWeight.w600,
color: const Color(0xFFA0936E),
),
),
),
const Expanded(
child: Divider(
color: Color(0xFFA0936E),
height: 1,
),
),
],
),
),
_NavItem(
title: indexed.$2.navTitle,
active: indexed.$1 == activeIndex,
onTap: () => onSelect(indexed.$1),
),
],
],
),
),
],
),
),
],
),
);
}
}
class _NavItem extends StatefulWidget {
const _NavItem({
required this.title,
required this.active,
required this.onTap,
});
final String title;
final bool active;
final VoidCallback onTap;
@override
State<_NavItem> createState() => _NavItemState();
}
class _NavItemState extends State<_NavItem> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
height: 40,
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
padding: const EdgeInsets.only(left: 26, right: 16),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: widget.active
? const Color(0xFFB7C6E5)
: (_hovered ? const Color(0xFFD6DFF0) : Colors.transparent),
borderRadius: BorderRadius.circular(12),
),
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textStyle(
size: 14,
weight: FontWeight.w600,
color: widget.active ? Colors.white : const Color(0xFF8A7B66),
),
),
),
),
);
}
}
class _DocsHeader extends StatelessWidget {
const _DocsHeader({required this.onOpenDialog});
final VoidCallback onOpenDialog;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Padding(
padding: const EdgeInsets.fromLTRB(40, 32, 40, 28),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Animal Island Flutter',
style: theme.textStyle(size: 30, weight: FontWeight.w900),
),
const SizedBox(height: 8),
Text(
'基于 animal-island-ui demo 源码编排的 Flutter 组件库文档。',
style: theme.textStyle(
color: const Color(0xFF794F27),
weight: FontWeight.w600,
),
),
],
),
),
],
),
);
}
}
class _DocArticle extends StatelessWidget {
const _DocArticle(this.page);
final _DocPage page;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
page.title,
style: theme.textStyle(
size: 24,
weight: FontWeight.w700,
color: const Color(0xFF794F27),
),
),
const SizedBox(height: 12),
SizedBox(
height: 44,
child: AnimalTypewriter(
key: ValueKey(page.title),
trigger: page.title,
speed: const Duration(milliseconds: 30),
text: page.summary,
style: theme.textStyle(
size: 14,
weight: FontWeight.w500,
color: const Color(0xFF794F27),
),
),
),
page.body,
],
);
}
}
class _ComponentDoc extends StatelessWidget {
const _ComponentDoc({
required this.title,
required this.tags,
required this.sections,
required this.code,
required this.api,
});
final String title;
final List<String> tags;
final List<_DocSection> sections;
final String code;
final List<_ApiRow> api;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 36),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE8E2D6)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ComponentTitle(title: title, tags: tags),
for (final section in sections) section,
_UsageCode(code: code),
_ApiTable(rows: api),
],
),
);
}
}
class _ComponentTitle extends StatelessWidget {
const _ComponentTitle({required this.title, required this.tags});
final String title;
final List<String> tags;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Wrap(
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
title,
style: theme.textStyle(
size: 18,
weight: FontWeight.w600,
color: const Color(0xFF725D42),
),
),
for (final tag in tags) _DocTag(tag),
],
);
}
}
class _DocTag extends StatelessWidget {
const _DocTag(this.text);
final String text;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF0E8D8),
borderRadius: BorderRadius.circular(20),
),
child: Text(
text,
style: theme.textStyle(
size: 10,
weight: FontWeight.w500,
color: const Color(0xFFA08060),
),
),
);
}
}
class _DocSection extends StatelessWidget {
const _DocSection({
required this.label,
required this.child,
this.box = _DemoBoxStyle.none,
});
final String label;
final Widget child;
final _DemoBoxStyle box;
@override
Widget build(BuildContext context) {
final hasLabel = label.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (hasLabel) _DemoLabel(label) else const SizedBox(height: 16),
_SectionBox(style: box, child: child),
],
);
}
}
class _DemoLabel extends StatelessWidget {
const _DemoLabel(this.text);
final String text;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Padding(
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: Text(
text,
style: theme.textStyle(
size: 14,
weight: FontWeight.w500,
color: const Color(0xFFA0936E),
),
),
);
}
}
class _SectionBox extends StatelessWidget {
const _SectionBox({required this.style, required this.child});
final _DemoBoxStyle style;
final Widget child;
@override
Widget build(BuildContext context) {
if (style == _DemoBoxStyle.none) {
return child;
}
final dashed = style == _DemoBoxStyle.dashed;
final radius = dashed ? 18.0 : 12.0;
return CustomPaint(
foregroundPainter: dashed
? _DashedBoxBorderPainter(
color: const Color(0xFFE0D8C8),
radius: radius,
)
: null,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: dashed ? const Color(0xFFFAF8F2) : const Color(0xFFFAF8F3),
borderRadius: BorderRadius.circular(radius),
border: dashed ? null : Border.all(color: const Color(0xFFE8E2D6)),
),
child: child,
),
);
}
}
class _DashedBoxBorderPainter extends CustomPainter {
const _DashedBoxBorderPainter({
required this.color,
required this.radius,
});
final Color color;
final double radius;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 1;
final path = Path()
..addRRect(
RRect.fromRectAndRadius(
Offset.zero & size,
Radius.circular(radius),
).deflate(0.5),
);
for (final metric in path.computeMetrics()) {
var distance = 0.0;
while (distance < metric.length) {
final next = (distance + 6).clamp(0.0, metric.length);
canvas.drawPath(metric.extractPath(distance, next), paint);
distance += 12;
}
}
}
@override
bool shouldRepaint(covariant _DashedBoxBorderPainter oldDelegate) {
return oldDelegate.color != color || oldDelegate.radius != radius;
}
}
class _UsageCode extends StatelessWidget {
const _UsageCode({required this.code});
final String code;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 36),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _DarkLabel('使用示例'),
AnimalCodeBlock(
code: code,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(20),
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
],
),
);
}
}
class _ApiTable extends StatelessWidget {
const _ApiTable({required this.rows});
final List<_ApiRow> rows;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Padding(
padding: const EdgeInsets.only(top: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _DarkLabel('API'),
Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Color(0xFF2B2118),
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
clipBehavior: Clip.antiAlias,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 820),
child: Column(
children: [
_ApiTableRow(
background: const Color(0xFF3D3028),
cells: const ['属性', '说明', '类型', '默认值'],
styles: [
_apiHeaderStyle(theme),
_apiHeaderStyle(theme),
_apiHeaderStyle(theme),
_apiHeaderStyle(theme),
],
),
for (final row in rows) _ApiDataRow(row: row),
],
),
),
),
),
],
),
);
}
}
class _ApiDataRow extends StatelessWidget {
const _ApiDataRow({required this.row});
final _ApiRow row;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
final base = theme.textStyle(
size: 13,
weight: FontWeight.w500,
color: const Color(0xFFC8BBA8),
);
return DecoratedBox(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Color(0xFF3D3028))),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ApiCell(
width: 150,
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: row.prop,
style: base.copyWith(color: const Color(0xFFE8C87A)),
),
if (row.required)
TextSpan(
text: ' *',
style: base.copyWith(color: const Color(0xFFF0A870)),
),
],
),
),
),
_ApiCell(
width: 270,
child: Text(row.desc, style: base),
),
_ApiCell(
width: 280,
child: Text(
row.type,
style: base.copyWith(color: const Color(0xFFD4A0E0)),
),
),
_ApiCell(
width: 120,
child: Text(
row.defaultVal,
style: base.copyWith(color: const Color(0xFFA8D4A0)),
),
),
],
),
);
}
}
class _ApiTableRow extends StatelessWidget {
const _ApiTableRow({
required this.cells,
required this.styles,
this.background,
});
final List<String> cells;
final List<TextStyle> styles;
final Color? background;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: background ?? Colors.transparent,
child: Row(
children: [
_ApiCell(width: 150, child: Text(cells[0], style: styles[0])),
_ApiCell(width: 270, child: Text(cells[1], style: styles[1])),
_ApiCell(width: 280, child: Text(cells[2], style: styles[2])),
_ApiCell(width: 120, child: Text(cells[3], style: styles[3])),
],
),
);
}
}
class _ApiCell extends StatelessWidget {
const _ApiCell({required this.width, required this.child});
final double width;
final Widget child;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: child,
),
);
}
}
TextStyle _apiHeaderStyle(AnimalThemeData theme) {
return theme.textStyle(
size: 13,
weight: FontWeight.w600,
color: const Color(0xFFE8D5BC),
);
}
class _DarkLabel extends StatelessWidget {
const _DarkLabel(this.text);
final String text;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: const BoxDecoration(
color: Color(0xFF3D3028),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
child: Text(
text,
style: theme.textStyle(
size: 14,
weight: FontWeight.w600,
color: const Color(0xFFE7E4E0),
),
),
);
}
}
class _DemoRow extends StatelessWidget {
const _DemoRow({
required this.children,
this.spacing = 16,
});
final List<Widget> children;
final double spacing;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: spacing,
runSpacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: children,
);
}
}
class _DemoColumn extends StatelessWidget {
const _DemoColumn({required this.children});
final List<Widget> children;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final indexed in children.indexed) ...[
if (indexed.$1 > 0) const SizedBox(height: 12),
indexed.$2,
],
],
),
);
}
}
class _ButtonDoc extends StatelessWidget {
const _ButtonDoc();
@override
Widget build(BuildContext context) {
return _ComponentDoc(
title: 'Button',
tags: const ['6 types'],
sections: [
_DocSection(
label: 'type 按钮类型',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
onPressed: () {},
child: const Text('Primary'),
),
AnimalButton(onPressed: () {}, child: const Text('Default')),
AnimalButton(
type: AnimalButtonType.dashed,
onPressed: () {},
child: const Text('Dashed'),
),
AnimalButton(
type: AnimalButtonType.text,
onPressed: () {},
child: const Text('Text'),
),
AnimalButton(
type: AnimalButtonType.link,
onPressed: () {},
child: const Text('Link'),
),
],
),
),
_DocSection(
label: 'danger / ghost / loading / disabled 状态',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
danger: true,
onPressed: () {},
child: const Text('Danger'),
),
AnimalButton(
type: AnimalButtonType.primary,
ghost: true,
onPressed: () {},
child: const Text('Ghost'),
),
const AnimalButton(
type: AnimalButtonType.primary,
loading: true,
child: Text('Loading'),
),
AnimalButton(
type: AnimalButtonType.primary,
disabled: true,
onPressed: () {},
child: const Text('Disabled'),
),
],
),
),
_DocSection(
label: 'size 尺寸',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
size: AnimalButtonSize.small,
onPressed: () {},
child: const Text('Small'),
),
AnimalButton(
type: AnimalButtonType.primary,
size: AnimalButtonSize.middle,
onPressed: () {},
child: const Text('Middle'),
),
AnimalButton(
type: AnimalButtonType.primary,
size: AnimalButtonSize.large,
onPressed: () {},
child: const Text('Large'),
),
],
),
),
_DocSection(
label: 'icon 图标按钮',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
icon: const Icon(Icons.search_rounded),
onPressed: () {},
child: const Text('搜索'),
),
AnimalButton(
icon: const Icon(Icons.star_rounded, size: 16),
onPressed: () {},
child: const Text('收藏'),
),
AnimalButton(
type: AnimalButtonType.dashed,
icon: const Text('+'),
onPressed: () {},
child: const Text('新增'),
),
],
),
),
_DocSection(
label: 'block 块级按钮',
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: AnimalButton(
type: AnimalButtonType.primary,
block: true,
onPressed: () {},
child: const Text('Block Button'),
),
),
),
_DocSection(
label: 'danger 组合',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
danger: true,
onPressed: () {},
child: const Text('Primary Danger'),
),
AnimalButton(
danger: true,
onPressed: () {},
child: const Text('Default Danger'),
),
AnimalButton(
type: AnimalButtonType.dashed,
danger: true,
onPressed: () {},
child: const Text('Dashed Danger'),
),
AnimalButton(
type: AnimalButtonType.text,
danger: true,
onPressed: () {},
child: const Text('Text Danger'),
),
AnimalButton(
type: AnimalButtonType.link,
danger: true,
onPressed: () {},
child: const Text('Link Danger'),
),
],
),
),
],
code: _buttonCode,
api: _buttonApi,
);
}
}
class _InputDoc extends StatelessWidget {
const _InputDoc();
@override
Widget build(BuildContext context) {
return _ComponentDoc(
title: 'Input',
tags: const ['3 sizes'],
sections: const [
_DocSection(
label: 'shadow 阴影控制',
child: _DemoColumn(
children: [
AnimalInput(hintText: 'No shadow (default)'),
AnimalInput(hintText: 'With shadow', shadow: true),
],
),
),
_DocSection(
label: '基础用法',
child: _DemoColumn(
children: [
AnimalInput(hintText: 'Basic input'),
AnimalInput(
hintText: 'With clear',
allowClear: true,
initialValue: 'Island',
),
AnimalInput(
hintText: 'Prefix & Suffix',
prefix: Text('🔍'),
suffix: Text('⏎'),
),
],
),
),
_DocSection(
label: 'size 尺寸',
child: _DemoColumn(
children: [
AnimalInput(hintText: 'Small', size: AnimalInputSize.small),
AnimalInput(hintText: 'Middle (default)'),
AnimalInput(hintText: 'Large', size: AnimalInputSize.large),
],
),
),
_DocSection(
label: 'status 校验状态',
child: _DemoColumn(
children: [
AnimalInput(
hintText: 'Error status',
status: AnimalInputStatus.error,
),
AnimalInput(
hintText: 'Warning status',
status: AnimalInputStatus.warning,
),
],
),
),
_DocSection(
label: 'disabled 禁用',
child: _DemoColumn(
children: [
AnimalInput(hintText: 'Disabled', enabled: false),
],
),
),
],
code: _inputCode,
api: _inputApi,
);
}
}
class _SwitchDoc extends StatelessWidget {
const _SwitchDoc({required this.checked, required this.onChanged});
final bool checked;
final ValueChanged<bool> onChanged;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return _ComponentDoc(
title: 'Switch',
tags: const ['2 sizes'],
sections: [
_DocSection(
label: '基础用法',
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimalSwitch(value: checked, onChanged: onChanged),
const SizedBox(width: 16),
Text(
checked ? 'ON' : 'OFF',
style: theme.textStyle(size: 13),
),
],
),
),
const _DocSection(
label: 'checkedChildren / unCheckedChildren 自定义文案',
child: _DemoRow(
children: [
AnimalSwitch(
defaultValue: true,
checkedChild: Text('开'),
uncheckedChild: Text('关'),
),
],
),
),
const _DocSection(
label: 'size 尺寸',
child: _DemoRow(
children: [
AnimalSwitch(defaultValue: true),
AnimalSwitch(size: AnimalSwitchSize.small, defaultValue: true),
],
),
),
const _DocSection(
label: 'disabled / loading 状态',
child: _DemoRow(
children: [
AnimalSwitch(disabled: true),
AnimalSwitch(loading: true, defaultValue: true),
],
),
),
],
code: _switchCode,
api: _switchApi,
);
}
}
class _CardDoc extends StatelessWidget {
const _CardDoc();
@override
Widget build(BuildContext context) {
return _ComponentDoc(
title: 'Card',
tags: const ['3 types', '13 colors'],
sections: [
const _DocSection(
label: 'type="default"',
child: _DemoRow(
children: [
AnimalCard(child: Text('基础卡片')),
SizedBox(
width: 560,
child: AnimalCard(
child: Text(
'在Nintendo 3DS《Animal Island: New Leaf》和《Animal Island: Happy Home Designer》中製作的「我的設計」QR Code,以智慧型裝置讀取就能通過狸端機入口站下載至《集合啦!動物森友會》。',
),
),
),
],
),
),
const _DocSection(
label: 'type="title"',
child: _DemoRow(
children: [
AnimalCard(type: AnimalCardType.title, child: Text('Title标题卡片')),
SizedBox(
width: 360,
child: AnimalCard(
type: AnimalCardType.title,
child: Text(
'欢迎来到无人岛!在Nintendo 3DS《Animal Island: New Leaf》和《Animal Island: Happy Home Designer》中製作的「我的設計」QR Code,以智慧型裝置讀取就能通過狸端機入口站下載至《集合啦!動物森友會》。',
),
),
),
],
),
),
const _DocSection(
label: 'type="dashed"',
child: _DemoRow(
children: [
AnimalCard(type: AnimalCardType.dashed, child: Text('虚线边框卡片')),
SizedBox(
width: 360,
child: AnimalCard(
type: AnimalCardType.dashed,
child: Text('欢迎来到无人岛!虚线边框适合用于轻量提示或次要信息展示。'),
),
),
],
),
),
_DocSection(
label: 'color — NookPhone 颜色',
child: LayoutBuilder(
builder: (context, constraints) {
return Wrap(
spacing: 16,
runSpacing: 16,
children: [
for (final item in _cardColors)
SizedBox(
width: 150,
child: AnimalCard(
color: item.color,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.en,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
const SizedBox(height: 4),
Opacity(
opacity: 0.85,
child: Text(
item.cn,
style: const TextStyle(fontSize: 12),
),
),
],
),
),
),
],
);
},
),
),
const _DocSection(
label: 'color + type="title"',
child: _DemoRow(
children: [
SizedBox(
width: 240,
child: AnimalCard(
type: AnimalCardType.title,
color: AnimalCardColor.appBlue,
child: _CardTitleSample(
title: '蓝色标题卡片',
subtitle: 'type="title" + color="app-blue"',
),
),
),
SizedBox(
width: 250,
child: AnimalCard(
type: AnimalCardType.title,
color: AnimalCardColor.appGreen,
child: _CardTitleSample(
title: '绿色标题卡片',
subtitle: 'type="title" + color="app-green"',
),
),
),
SizedBox(
width: 240,
child: AnimalCard(
type: AnimalCardType.title,
color: AnimalCardColor.purple,
child: _CardTitleSample(
title: '紫色标题卡片',
subtitle: 'type="title" + color="purple"',
),
),
),
],
),
),
],
code: _cardCode,
api: _cardApi,
);
}
}
class _CardTitleSample extends StatelessWidget {
const _CardTitleSample({required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
const SizedBox(height: 6),
Opacity(
opacity: 0.85,
child: Text(subtitle, style: const TextStyle(fontSize: 12)),
),
],
);
}
}
class _CollapseDoc extends StatelessWidget {
const _CollapseDoc();
@override
Widget build(BuildContext context) {
return const _ComponentDoc(
title: 'Collapse',
tags: ['FAQ'],
sections: [
_DocSection(
label: '基础用法',
child: SizedBox(
width: 720,
child: Column(
children: [
AnimalCollapse(
question: Text('1個島嶼可以登錄多少名用戶?'),
answer: Text('1座島嶼最多可以容納8位居民(用戶)。'),
),
AnimalCollapse(
question: Text('可以多少人一起玩?'),
answer: Text('同住1個島的居民可以最多4人一起遊玩。透過通訊最多8人一起遊玩。'),
),
],
),
),
),
_DocSection(
label: 'defaultExpanded 默认展开',
child: SizedBox(
width: 720,
child: AnimalCollapse(
question: Text('这个问题默认展开'),
answer: Text('答案已经展示出来了!可以点击收起。'),
defaultExpanded: true,
),
),
),
_DocSection(
label: 'disabled 禁用状态',
child: SizedBox(
width: 720,
child: AnimalCollapse(
question: Text('这个问题已被禁用(无法展开)'),
answer: Text('这段文字不应该被看到。'),
disabled: true,
),
),
),
],
code: _collapseCode,
api: _collapseApi,
);
}
}
class _CursorDoc extends StatelessWidget {
const _CursorDoc();
@override
Widget build(BuildContext context) {
return const _ComponentDoc(
title: 'Cursor',
tags: ['光标'],
sections: [
_DocSection(
label: 'Cursor 组件通过 CSS cursor 属性将子元素的鼠标光标替换为自定义手指图标,当前 Demo 全局已应用。',
child: AnimalCursor(
child: AnimalCard(child: Text('鼠标移入此区域将显示自定义光标')),
),
),
],
code: _cursorCode,
api: _cursorApi,
);
}
}
class _ModalDoc extends StatelessWidget {
const _ModalDoc({
required this.onBasic,
required this.onTitle,
required this.onFooter,
required this.onNoTypewriter,
});
final VoidCallback onBasic;
final VoidCallback onTitle;
final VoidCallback onFooter;
final VoidCallback onNoTypewriter;
@override
Widget build(BuildContext context) {
return _ComponentDoc(
title: 'Modal',
tags: const ['弹窗'],
sections: [
_DocSection(
label: '基础弹窗',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
onPressed: onBasic,
child: const Text('基础 Modal'),
),
AnimalButton(
onPressed: onTitle,
child: const Text('带标题 Modal'),
),
AnimalButton(
type: AnimalButtonType.dashed,
onPressed: onFooter,
child: const Text('自定义 Footer'),
),
],
),
),
_DocSection(
label: '关闭打字机效果',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
onPressed: onNoTypewriter,
child: const Text('关闭打字机效果'),
),
],
),
),
],
code: _modalCode,
api: _modalApi,
);
}
}
class _TypewriterDoc extends StatelessWidget {
const _TypewriterDoc({required this.replayKey, required this.onReplay});
final int replayKey;
final VoidCallback onReplay;
@override
Widget build(BuildContext context) {
return _ComponentDoc(
title: 'Typewriter',
tags: const ['打字机'],
sections: [
_DocSection(
label: '基础用法',
box: _DemoBoxStyle.dashed,
child: AnimalTypewriter(
trigger: replayKey,
text: '你好,欢迎来到动物岛!今天的天气真不错呢~',
),
),
_DocSection(
label: '保留多行与富内容 (速度 40ms)',
box: _DemoBoxStyle.dashed,
child: AnimalTypewriter(
trigger: replayKey,
speed: const Duration(milliseconds: 40),
text: '第一行:钓到石头了!\n第二行:竟然连这种都能钓起来...\n第三行:继续加油吧!',
style: const TextStyle(height: 1.8),
),
),
_DocSection(
label: '',
child: _DemoRow(
children: [
AnimalButton(
type: AnimalButtonType.primary,
onPressed: onReplay,
child: const Text('重新播放'),
),
],
),
),
],
code: _typewriterCode,
api: _typewriterApi,
);
}
}
class _DividerDoc extends StatelessWidget {
const _DividerDoc();
@override
Widget build(BuildContext context) {
return const _ComponentDoc(
title: 'Divider',
tags: ['5 types'],
sections: [
_DocSection(
label: 'line-brown',
child: AnimalDivider(type: AnimalDividerType.lineBrown),
),
_DocSection(
label: 'line-teal',
child: AnimalDivider(type: AnimalDividerType.lineTeal),
),
_DocSection(
label: 'line-white',
child: ColoredBox(
color: Color(0xFF333333),
child: Padding(
padding: EdgeInsets.all(10),
child: AnimalDivider(type: AnimalDividerType.lineWhite),
),
),
),
_DocSection(
label: 'line-yellow',
child: AnimalDivider(type: AnimalDividerType.lineYellow),
),
_DocSection(
label: 'wave-yellow',
child: AnimalDivider(type: AnimalDividerType.waveYellow),
),
],
code: _dividerCode,
api: _dividerApi,
);
}
}
class _IconDoc extends StatelessWidget {
const _IconDoc();
@override
Widget build(BuildContext context) {
return _ComponentDoc(
title: 'Icon',
tags: const ['10 icons'],
sections: [
const _DocSection(
label: '基础用法',
child: _DemoRow(
spacing: 20,
children: [
AnimalIcon(name: AnimalIconName.miles, size: 32),
AnimalIcon(name: AnimalIconName.camera, size: 32),
AnimalIcon(name: AnimalIconName.chat, size: 32),
AnimalIcon(name: AnimalIconName.design, size: 32),
AnimalIcon(name: AnimalIconName.map, size: 32),
],
),
),
const _DocSection(
label: 'size 尺寸',
child: _DemoRow(
spacing: 20,
children: [
AnimalIcon(name: AnimalIconName.miles, size: 16),
AnimalIcon(name: AnimalIconName.miles, size: 24),
AnimalIcon(name: AnimalIconName.miles, size: 32),
AnimalIcon(name: AnimalIconName.miles, size: 48),
],
),
),
const _DocSection(
label: 'bounce 弹跳动画(鼠标悬停查看效果)',
child: _DemoRow(
spacing: 20,
children: [
AnimalIcon(name: AnimalIconName.miles, size: 32, bounce: true),
AnimalIcon(name: AnimalIconName.camera, size: 32, bounce: true),
AnimalIcon(name: AnimalIconName.chat, size: 32, bounce: true),
],
),
),
_DocSection(
label: '图标列表',
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE8E2D6)),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
for (final indexed in animalIconList.indexed)
_IconListRow(
icon: indexed.$2,
last: indexed.$1 == animalIconList.length - 1,
),
],
),
),
),
],
code: _iconCode,
api: _iconApi,
);
}
}
class _IconListRow extends StatelessWidget {
const _IconListRow({required this.icon, required this.last});
final AnimalIconInfo icon;
final bool last;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: last
? BorderSide.none
: const BorderSide(color: Color(0xFFF0E8D8), width: 1),
),
),
child: Row(
children: [
AnimalIcon(name: icon.name, size: 32),
const SizedBox(width: 20),
Text(
icon.label,
style: theme.textStyle(size: 14, color: const Color(0xFF725D42)),
),
const Spacer(),
Text(
_iconCodeName(icon.name),
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Color(0xFFA0936E),
),
),
],
),
);
}
}
class _SelectDoc extends StatelessWidget {
const _SelectDoc({
required this.fishValue,
required this.flowerValue,
required this.fruitValue,
required this.disabledValue,
required this.onFishChanged,
required this.onFlowerChanged,
required this.onFruitChanged,
required this.onDisabledChanged,
});
final String fishValue;
final String? flowerValue;
final String? fruitValue;
final String disabledValue;
final ValueChanged<String> onFishChanged;
final ValueChanged<String> onFlowerChanged;
final ValueChanged<String> onFruitChanged;
final ValueChanged<String> onDisabledChanged;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
final selectedFish =
_fishOptions.firstWhere((option) => option.key == fishValue).label;
return _ComponentDoc(
title: 'Select',
tags: const ['基础用法'],
sections: [
_DocSection(
label: '默认状态',
box: _DemoBoxStyle.soft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
const TextSpan(text: '当前选中: '),
TextSpan(
text: selectedFish,
style: const TextStyle(
color: Color(0xFF19C8B9),
fontWeight: FontWeight.w600,
),
),
],
),
style: theme.textStyle(
size: 13,
color: const Color(0xFFA08060),
),
),
const SizedBox(height: 8),
AnimalSelect<String>(
options: _fishOptions,
value: fishValue,
onChanged: onFishChanged,
),
],
),
),
_DocSection(
label: '自定义占位文本',
box: _DemoBoxStyle.dashed,
child: _DemoRow(
children: [
AnimalSelect<String>(
options: _flowerOptions,
value: flowerValue,
onChanged: onFlowerChanged,
placeholder: '请选择花朵',
),
AnimalSelect<String>(
options: _fruitOptions,
value: fruitValue,
onChanged: onFruitChanged,
placeholder: '请选择水果',
),
],
),
),
_DocSection(
label: '禁用状态',
box: _DemoBoxStyle.soft,
child: AnimalSelect<String>(
options: _flowerOptions,
value: disabledValue,
onChanged: onDisabledChanged,
disabled: true,
),
),
],
code: _selectCode,
api: _selectApi,
);
}
}
class _CheckboxDoc extends StatelessWidget {
const _CheckboxDoc({
required this.selectedIslands,
required this.selectedCritters,
required this.onIslandsChanged,
required this.onCrittersChanged,
});
final List<String> selectedIslands;
final List<String> selectedCritters;
final ValueChanged<List<String>> onIslandsChanged;
final ValueChanged<List<String>> onCrittersChanged;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
final selectedText = selectedIslands.isEmpty
? '无'
: _islandOptions
.where((option) => selectedIslands.contains(option.value))
.map((option) => (option.label as Text).data ?? '')
.join('、');
return _ComponentDoc(
title: 'Checkbox',
tags: const ['基础用法'],
sections: [
_DocSection(
label: '默认水平排列(受控)',
box: _DemoBoxStyle.soft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
const TextSpan(text: '已选中: '),
TextSpan(
text: selectedText,
style: const TextStyle(
color: Color(0xFF19C8B9),
fontWeight: FontWeight.w600,
),
),
],
),
style: theme.textStyle(
size: 13,
color: const Color(0xFFA08060),
),
),
const SizedBox(height: 8),
AnimalCheckbox<String>(
options: _islandOptions,
value: selectedIslands,
onChanged: onIslandsChanged,
),
],
),
),
_DocSection(
label: '垂直排列 + 含禁用选项',
box: _DemoBoxStyle.soft,
child: AnimalCheckbox<String>(
options: _critterOptions,
value: selectedCritters,
direction: AnimalCheckboxDirection.vertical,
onChanged: onCrittersChanged,
),
),
const _DocSection(
label: '小尺寸',
box: _DemoBoxStyle.soft,
child: AnimalCheckbox<String>(
options: _islandOptions,
defaultValue: ['forest'],
size: AnimalCheckboxSize.small,
),
),
const _DocSection(
label: '中尺寸(默认)',
box: _DemoBoxStyle.soft,
child: AnimalCheckbox<String>(
options: _islandOptions,
defaultValue: ['beach'],
),
),
const _DocSection(
label: '大尺寸',
box: _DemoBoxStyle.soft,
child: AnimalCheckbox<String>(
options: _islandShortOptions,
defaultValue: ['beach'],
size: AnimalCheckboxSize.large,
),
),
const _DocSection(
label: '全部禁用',
box: _DemoBoxStyle.soft,
child: AnimalCheckbox<String>(
options: _islandOptions,
defaultValue: ['garden', 'village'],
disabled: true,
),
),
],
code: _checkboxCode,
api: _checkboxApi,
);
}
}
class _TabsDoc extends StatelessWidget {
const _TabsDoc({required this.activeKey, required this.onChanged});
final String activeKey;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
final selectedLabel =
_controlledTabs.firstWhere((item) => item.key == activeKey).label;
return _ComponentDoc(
title: 'Tab',
tags: const ['基础用法'],
sections: [
const _DocSection(
label: 'shadow 阴影控制',
child: _DemoRow(
children: [
SizedBox(width: 320, child: _FishTabs(shadow: true)),
SizedBox(width: 320, child: _FishTabs(shadow: false)),
],
),
),
const _DocSection(
label: '非受控模式',
box: _DemoBoxStyle.soft,
child: AnimalTabs(
defaultActiveKey: 'a',
items: [
AnimalTabItem(
key: 'a',
label: Text('鱼类'),
child: Text('鲈鱼、鲷鱼、河童...'),
),
AnimalTabItem(
key: 'b',
label: Text('昆虫'),
child: Text('蝴蝶、瓢虫、蜻蜓...'),
),
AnimalTabItem(
key: 'c',
label: Text('海洋生物'),
child: Text('海星、珊瑚、小丑鱼...'),
),
],
),
),
_DocSection(
label: '受控模式',
box: _DemoBoxStyle.soft,
child: AnimalTabs(
items: _controlledTabs,
activeKey: activeKey,
onChanged: onChanged,
),
),
_DocSection(
label: '当前选中: ${(selectedLabel as Text).data}',
child: const SizedBox.shrink(),
),
const _DocSection(
label: 'leafAnimation 叶子动画控制',
child: _DemoRow(
children: [
SizedBox(
width: 320,
child: _LeafTabs(
leafAnimation: true, caption: 'leafAnimation=true (默认)'),
),
SizedBox(
width: 320,
child: _LeafTabs(
leafAnimation: false, caption: 'leafAnimation=false'),
),
],
),
),
],
code: _tabsCode,
api: _tabsApi,
);
}
}
class _FishTabs extends StatelessWidget {
const _FishTabs({required this.shadow});
final bool shadow;
@override
Widget build(BuildContext context) {
return AnimalTabs(
shadow: shadow,
defaultActiveKey: 'a',
items: const [
AnimalTabItem(key: 'a', label: Text('鱼类'), child: Text('鲈鱼、鲷鱼...')),
AnimalTabItem(key: 'b', label: Text('昆虫'), child: Text('蝴蝶、瓢虫...')),
],
);
}
}
class _LeafTabs extends StatelessWidget {
const _LeafTabs({required this.leafAnimation, required this.caption});
final bool leafAnimation;
final String caption;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimalTabs(
leafAnimation: leafAnimation,
defaultActiveKey: 'a',
items: const [
AnimalTabItem(
key: 'a',
label: Text('鱼类'),
child: Text('鲈鱼、鲷鱼...'),
),
AnimalTabItem(
key: 'b',
label: Text('昆虫'),
child: Text('蝴蝶、瓢虫...'),
),
],
),
const SizedBox(height: 8),
Text(
caption,
style: theme.textStyle(size: 12, color: const Color(0xFFA0936E)),
),
],
);
}
}
class _FooterDoc extends StatelessWidget {
const _FooterDoc();
@override
Widget build(BuildContext context) {
return const _ComponentDoc(
title: 'Footer',
tags: ['底部装饰'],
sections: [
_DocSection(
label: 'Footer 组件 — 页面底部装饰图片,支持 sea(海)和 tree(树)两种类型。',
child: SizedBox.shrink(),
),
_DocSection(
label: 'tree 类型(默认)',
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: AnimalFooter(),
),
),
_DocSection(
label: 'sea 类型',
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: AnimalFooter(type: AnimalFooterType.sea),
),
),
],
code: _footerCode,
api: _footerApi,
);
}
}
class _CodeBlockDoc extends StatelessWidget {
const _CodeBlockDoc();
@override
Widget build(BuildContext context) {
return const _ComponentDoc(
title: 'CodeBlock',
tags: ['代码高亮'],
sections: [
_DocSection(
label: '基础用法',
box: _DemoBoxStyle.soft,
child: AnimalCodeBlock(
code:
"import 'package:flutter/material.dart';\n\nconst AnimalButton(\n type: AnimalButtonType.primary,\n child: Text('按钮'),\n);",
),
),
_DocSection(
label: '自定义样式',
box: _DemoBoxStyle.soft,
child: AnimalCodeBlock(
padding: EdgeInsets.symmetric(horizontal: 28, vertical: 22),
code:
"const AnimalCodeBlock(\n code: codeString,\n padding: EdgeInsets.all(28),\n);",
),
),
],
code: _codeBlockCode,
api: _codeBlockApi,
);
}
}
class _LoadingDoc extends StatelessWidget {
const _LoadingDoc({required this.active, required this.onToggle});
final bool active;
final VoidCallback onToggle;
@override
Widget build(BuildContext context) {
final theme = AnimalTheme.of(context);
return _ComponentDoc(
title: 'Loading',
tags: const ['加载动画'],
sections: [
_DocSection(
label: '动森风格小岛 Loading 动画组件,带有漂浮的小岛、摇曳的树叶和游动的鱼。关闭时会从中间圆形透明扩散,露出底层内容。',
child: AnimalButton(
type: active
? AnimalButtonType.defaultType
: AnimalButtonType.primary,
onPressed: onToggle,
child: Text(active ? '关闭 Loading' : '开启 Loading'),
),
),
_DocSection(
label: '',
child: SizedBox(
height: 800,
child: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFFFD6A5),
Color(0xFFFDFFB6),
Color(0xFFCAFFBF),
Color(0xFF9BF6FF),
Color(0xFFA0C4FF),
],
),
),
child: Center(
child: Text(
'底层内容 · Underlying Content',
style: theme.textStyle(
size: 28,
weight: FontWeight.w600,
color: const Color(0xFF333333),
),
),
),
),
),
Positioned.fill(child: AnimalLoading(active: active)),
],
),
),
),
],
code: _loadingCode,
api: _loadingApi,
);
}
}
class _TableDoc extends StatelessWidget {
const _TableDoc({
required this.striped,
required this.loading,
required this.onToggleStriped,
required this.onLoading,
});
final bool striped;
final bool loading;
final VoidCallback onToggleStriped;
final VoidCallback onLoading;
@override
Widget build(BuildContext context) {
return _ComponentDoc(
title: 'Table',
tags: const ['表格'],
sections: [
const _DocSection(
label: '数据表格组件,支持斑马纹、边框、加载状态等常用功能。',
child: SizedBox.shrink(),
),
_DocSection(
label: '',
child: _DemoRow(
children: [
AnimalButton(
type: striped
? AnimalButtonType.primary
: AnimalButtonType.defaultType,
onPressed: onToggleStriped,
child: Text('斑马纹 ${striped ? '✓' : '✗'}'),
),
AnimalButton(
type: AnimalButtonType.primary,
disabled: loading,
onPressed: loading ? null : onLoading,
child: Text(loading ? '加载中...' : '模拟加载'),
),
],
),
),
_DocSection(
label: '',
box: _DemoBoxStyle.soft,
child: _IslandTable(striped: striped, loading: loading),
),
],
code: _tableCode,
api: _tableApi,
);
}
}
class _IslandTable extends StatelessWidget {
const _IslandTable({required this.striped, required this.loading});
final bool striped;
final bool loading;
@override
Widget build(BuildContext context) {
return AnimalTable<Map<String, Object>>(
striped: striped,
loading: loading,
columns: [
AnimalTableColumn(
title: const Text('岛民'),
width: 120,
cellBuilder: (_, row, __) => Text(row['name'] as String),
),
AnimalTableColumn(
title: const Text('年龄'),
width: 80,
alignment: Alignment.center,
cellBuilder: (_, row, __) => Text('${row['age']}'),
),
AnimalTableColumn(
title: const Text('岛屿'),
cellBuilder: (_, row, __) => Text(row['island'] as String),
),
AnimalTableColumn(
title: const Text('喜欢的水果'),
cellBuilder: (_, row, __) => Text(row['fruit'] as String),
),
AnimalTableColumn(
title: const Text('爱好'),
cellBuilder: (_, row, __) => _HobbyTag(row['hobby'] as String),
),
],
rows: _tableRows,
);
}
}
class _HobbyTag extends StatelessWidget {
const _HobbyTag(this.text);
final String text;
@override
Widget build(BuildContext context) {
final style = _hobbyStyle[text] ??
const _TagStyle(
background: Color(0x2619C8B9),
foreground: Color(0xFF19C8B9),
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: style.background,
borderRadius: BorderRadius.circular(20),
),
child: Text(
text,
style: TextStyle(
color: style.foreground,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
);
}
}
class _TimeDoc extends StatelessWidget {
const _TimeDoc();
@override
Widget build(BuildContext context) {
return const _ComponentDoc(
title: 'Time',
tags: ['时间'],
sections: [
_DocSection(
label: 'Time 组件 — 动森经典 HUD 风格的时间显示组件,实时更新时间,支持星期、日期和时间显示。',
child: AnimalTime(),
),
],
code: _timeCode,
api: _timeApi,
);
}
}
class _PhoneDoc extends StatelessWidget {
const _PhoneDoc();
@override
Widget build(BuildContext context) {
return const _ComponentDoc(
title: 'Phone',
tags: ['手机'],
sections: [
_DocSection(
label: 'Phone 组件 — 手机界面组件。',
child: SizedBox(
width: 316,
height: 473,
child: AnimalPhone(),
),
),
],
code: _phoneCode,
api: _phoneApi,
);
}
}
class _DocPage {
const _DocPage({
required this.group,
required this.navTitle,
required this.title,
required this.summary,
required this.body,
});
final String group;
final String navTitle;
final String title;
final String summary;
final Widget body;
}
class _FeatureInfo {
const _FeatureInfo({
required this.icon,
required this.title,
required this.description,
});
final String icon;
final String title;
final String description;
}
class _ComponentInfo {
const _ComponentInfo({
required this.key,
required this.name,
required this.description,
});
final String key;
final String name;
final String description;
}
abstract final class _DemoAssets {
static const animalIcon = 'assets/demo/img/animal_icon.png';
static const contentBg = 'assets/demo/img/content_bg_pc.jpg';
static const guideBgLine = 'assets/demo/img/guide-bg-line.webp';
static const homeBg = 'assets/demo/img/home_bg.webp';
static const menuBg = 'assets/demo/img/menu_bg.svg';
static const nook1 = 'assets/demo/img/nook-phone/nook1.svg';
static const shopping = 'assets/demo/img/nook-phone/Property-Shopping.svg';
static const camera = 'assets/demo/img/nook-phone/Property-Camera.svg';
static const recipes = 'assets/demo/img/nook-phone/Property-Recipes.svg';
}
class _ApiRow {
const _ApiRow(
this.prop,
this.desc,
this.type,
this.defaultVal, {
this.required = false,
});
final String prop;
final String desc;
final String type;
final String defaultVal;
final bool required;
}
class _CardColorInfo {
const _CardColorInfo(this.color, this.en, this.cn);
final AnimalCardColor color;
final String en;
final String cn;
}
class _TagStyle {
const _TagStyle({required this.background, required this.foreground});
final Color background;
final Color foreground;
}
enum _DemoBoxStyle { none, soft, dashed }
const _homeFeatures = [
_FeatureInfo(
icon: _DemoAssets.nook1,
title: 'Animal风格',
description: 'SVG 有机形状裁切,3D 按压按钮,温暖质朴的自然 UI 质感',
),
_FeatureInfo(
icon: _DemoAssets.shopping,
title: '18 个组件',
description:
'Button / Input / Switch / Modal / Typewriter / Card / Collapse / Cursor / Divider / Time / Phone / Footer / Icon / Checkbox / Select / Tabs / CodeBlock / Loading 等',
),
_FeatureInfo(
icon: _DemoAssets.camera,
title: '主题定制',
description: '通过 AnimalThemeData 覆盖颜色、字体、圆角、阴影等 Flutter 设计令牌',
),
_FeatureInfo(
icon: _DemoAssets.recipes,
title: '开箱即用',
description: '纯 Dart Widget API,支持 Flutter Web / Desktop / Mobile 示例运行',
),
];
const _homeComponents = [
_ComponentInfo(
key: 'button',
name: 'Button',
description: '5 种类型、3 种尺寸、加载/危险/幽灵模式',
),
_ComponentInfo(key: 'input', name: 'Input', description: '前后缀、一键清空、校验状态'),
_ComponentInfo(
key: 'switch',
name: 'Switch',
description: '受控/非受控、自定义文案、加载状态',
),
_ComponentInfo(
key: 'checkbox',
name: 'Checkbox',
description: '多选框组件,支持水平/垂直排列',
),
_ComponentInfo(key: 'select', name: 'Select', description: '下拉选择器,支持搜索和禁用'),
_ComponentInfo(key: 'tabs', name: 'Tabs', description: '标签页组件,支持受控/非受控模式'),
_ComponentInfo(key: 'modal', name: 'Modal', description: 'SVG 有机形状弹窗、ESC 关闭'),
_ComponentInfo(
key: 'typewriter',
name: 'Typewriter',
description: '逐字打字机效果,支持多行与富内容',
),
_ComponentInfo(key: 'card', name: 'Card', description: '默认/标题两种卡片风格'),
_ComponentInfo(
key: 'collapse', name: 'Collapse', description: 'FAQ 折叠面板、平滑展开动画'),
_ComponentInfo(key: 'cursor', name: 'Cursor', description: '自定义手指光标,支持多种尺寸'),
_ComponentInfo(key: 'divider-comp', name: 'Divider', description: '装饰性水平分割线'),
_ComponentInfo(key: 'icon', name: 'Icon', description: 'SVG 图标库'),
_ComponentInfo(key: 'footer', name: 'Footer', description: '页脚组件'),
_ComponentInfo(key: 'time', name: 'Time', description: '可爱风格时间显示'),
_ComponentInfo(key: 'phone', name: 'Phone', description: 'Phone 模拟器'),
_ComponentInfo(key: 'codeblock', name: 'CodeBlock', description: '代码语法高亮组件'),
_ComponentInfo(key: 'loading', name: 'Loading', description: '动森风格小岛加载动画'),
];
const _cardColors = [
_CardColorInfo(AnimalCardColor.defaultColor, 'Default', '默认奶油色'),
_CardColorInfo(AnimalCardColor.appPink, 'App Pink', '应用粉'),
_CardColorInfo(AnimalCardColor.purple, 'Purple', '紫色'),
_CardColorInfo(AnimalCardColor.appBlue, 'App Blue', '应用蓝'),
_CardColorInfo(AnimalCardColor.appYellow, 'App Yellow', '应用黄'),
_CardColorInfo(AnimalCardColor.appOrange, 'App Orange', '应用橙'),
_CardColorInfo(AnimalCardColor.appTeal, 'App Teal', '应用青'),
_CardColorInfo(AnimalCardColor.appGreen, 'App Green', '应用绿'),
_CardColorInfo(AnimalCardColor.appRed, 'App Red', '应用红'),
_CardColorInfo(AnimalCardColor.limeGreen, 'Lime Green', '青柠绿'),
_CardColorInfo(AnimalCardColor.yellowGreen, 'Yellow-Green', '黄绿色'),
_CardColorInfo(AnimalCardColor.brown, 'Brown', '棕色'),
_CardColorInfo(AnimalCardColor.warmPeachPink, 'Warm Peach Pink', '暖桃粉'),
];
const _fishOptions = [
AnimalSelectOption(key: 'fish1', label: '鲈鱼'),
AnimalSelectOption(key: 'fish2', label: '鲷鱼'),
AnimalSelectOption(key: 'fish3', label: '草鱼'),
AnimalSelectOption(key: 'fish4', label: '龙睛鱼'),
AnimalSelectOption(key: 'fish5', label: '神仙鱼'),
];
const _flowerOptions = [
AnimalSelectOption(key: 'flower1', label: '樱花'),
AnimalSelectOption(key: 'flower2', label: '玫瑰'),
AnimalSelectOption(key: 'flower3', label: '向日葵'),
AnimalSelectOption(key: 'flower4', label: '薰衣草'),
AnimalSelectOption(key: 'flower5', label: '郁金香'),
];
const _fruitOptions = [
AnimalSelectOption(key: 'fruit1', label: '草莓'),
AnimalSelectOption(key: 'fruit2', label: '蓝莓'),
AnimalSelectOption(key: 'fruit3', label: '桃子'),
AnimalSelectOption(key: 'fruit4', label: '樱桃'),
AnimalSelectOption(key: 'fruit5', label: '猕猴桃'),
];
const _islandOptions = [
AnimalCheckboxOption(value: 'beach', label: Text('🌊 海滩')),
AnimalCheckboxOption(value: 'forest', label: Text('🌳 森林')),
AnimalCheckboxOption(value: 'garden', label: Text('🌸 花园')),
AnimalCheckboxOption(value: 'village', label: Text('🏡 村庄')),
];
const _islandShortOptions = [
AnimalCheckboxOption(value: 'beach', label: Text('🌊 海滩')),
AnimalCheckboxOption(value: 'forest', label: Text('🌳 森林')),
AnimalCheckboxOption(value: 'garden', label: Text('🌸 花园')),
];
const _critterOptions = [
AnimalCheckboxOption(value: 'butterfly', label: Text('🦋 蝴蝶')),
AnimalCheckboxOption(value: 'bass', label: Text('🐟 鲈鱼')),
AnimalCheckboxOption(value: 'crab', label: Text('🦀 螃蟹'), disabled: true),
AnimalCheckboxOption(value: 'caterpillar', label: Text('🐛 毛毛虫')),
AnimalCheckboxOption(value: 'jellyfish', label: Text('🌊 水母')),
];
const _controlledTabs = [
AnimalTabItem(
key: 'tab1',
label: Text('岛屿概况'),
child: Text('这里是一座无人岛,环境优美,气候宜人。\n可以钓鱼、捉虫、种植各种植物。'),
),
AnimalTabItem(
key: 'tab2',
label: Text('商店'),
child: Text('狸然超市营业中!\n各种商品应有尽有,价格实惠。'),
),
AnimalTabItem(
key: 'tab3',
label: Text('服务台'),
child: Text('欢迎来到服务台!\n可以办理各种服务业务。'),
),
];
const _tableRows = <Map<String, Object>>[
{
'key': '1',
'name': '豆狸',
'age': 26,
'island': '彩虹岛',
'fruit': '苹果',
'hobby': '音乐'
},
{
'key': '2',
'name': '粒狸',
'age': 24,
'island': '彩虹岛',
'fruit': '橘子',
'hobby': '运动'
},
{
'key': '3',
'name': '西施惠',
'age': 28,
'island': '好评岛',
'fruit': '樱桃',
'hobby': '唱歌'
},
{
'key': '4',
'name': '喻哥',
'age': 30,
'island': '无人岛',
'fruit': '梨',
'hobby': '钓鱼'
},
{
'key': '5',
'name': '小润',
'age': 22,
'island': '摸鱼岛',
'fruit': '桃子',
'hobby': '画画'
},
];
const _hobbyStyle = {
'音乐': _TagStyle(background: Color(0x269370DB), foreground: Color(0xFF9370DB)),
'运动': _TagStyle(background: Color(0x26FF8C00), foreground: Color(0xFFFF8C00)),
'唱歌': _TagStyle(background: Color(0x26FF6347), foreground: Color(0xFFFF6347)),
'钓鱼': _TagStyle(background: Color(0x261E90FF), foreground: Color(0xFF1E90FF)),
'画画': _TagStyle(background: Color(0x26FF69B4), foreground: Color(0xFFFF69B4)),
};
String _iconCodeName(AnimalIconName name) {
return switch (name) {
AnimalIconName.miles => 'icon-miles',
AnimalIconName.camera => 'icon-camera',
AnimalIconName.chat => 'icon-chat',
AnimalIconName.critterpedia => 'icon-critterpedia',
AnimalIconName.design => 'icon-design',
AnimalIconName.diy => 'icon-diy',
AnimalIconName.helicopter => 'icon-helicopter',
AnimalIconName.map => 'icon-map',
AnimalIconName.shopping => 'icon-shopping',
AnimalIconName.variant => 'icon-variant',
};
}
const _buttonApi = [
_ApiRow('type', '按钮类型', 'AnimalButtonType', 'defaultType'),
_ApiRow('size', '按钮尺寸', 'AnimalButtonSize', 'middle'),
_ApiRow('danger', '是否危险按钮', 'bool', 'false'),
_ApiRow('ghost', '是否幽灵按钮(透明背景)', 'bool', 'false'),
_ApiRow('block', '是否块级按钮', 'bool', 'false'),
_ApiRow('loading', '加载状态', 'bool', 'false'),
_ApiRow('disabled', '禁用状态', 'bool', 'false'),
_ApiRow('icon', '图标', 'Widget?', '-'),
_ApiRow('child', '按钮内容', 'Widget', '-', required: true),
_ApiRow('onPressed', '点击回调', 'VoidCallback?', '-'),
];
const _inputApi = [
_ApiRow('size', '输入框尺寸', 'AnimalInputSize', 'middle'),
_ApiRow('prefix', '前缀图标', 'Widget?', '-'),
_ApiRow('suffix', '后缀图标', 'Widget?', '-'),
_ApiRow('allowClear', '允许清除', 'bool', 'false'),
_ApiRow('status', '校验状态', 'AnimalInputStatus?', '-'),
_ApiRow('shadow', '是否显示阴影', 'bool', 'false'),
_ApiRow('onChanged', '值变化回调', 'ValueChanged<String>?', '-'),
_ApiRow('onClear', '清除回调', 'VoidCallback?', '-'),
_ApiRow('controller', '文本控制器', 'TextEditingController?', '-'),
_ApiRow('enabled', '是否启用', 'bool', 'true'),
];
const _switchApi = [
_ApiRow('value', '是否选中(受控)', 'bool?', '-'),
_ApiRow('defaultValue', '默认是否选中', 'bool', 'false'),
_ApiRow('size', '尺寸', 'AnimalSwitchSize', 'normal'),
_ApiRow('disabled', '禁用', 'bool', 'false'),
_ApiRow('loading', '加载状态', 'bool', 'false'),
_ApiRow('checkedChild', '选中时文案', 'Widget?', '-'),
_ApiRow('uncheckedChild', '未选中时文案', 'Widget?', '-'),
_ApiRow('onChanged', '变化回调', 'ValueChanged<bool>?', '-'),
];
const _cardApi = [
_ApiRow('type', '卡片类型', 'AnimalCardType', 'defaultType'),
_ApiRow('color', '背景颜色类型', 'AnimalCardColor', 'defaultColor'),
_ApiRow('child', '自定义内容', 'Widget', '-', required: true),
_ApiRow('padding', '自定义内边距', 'EdgeInsetsGeometry?', '-'),
_ApiRow('onTap', '点击回调', 'VoidCallback?', '-'),
];
const _collapseApi = [
_ApiRow('question', '问题标题', 'Widget', '-', required: true),
_ApiRow('answer', '答案内容', 'Widget', '-', required: true),
_ApiRow('defaultExpanded', '是否默认展开', 'bool', 'false'),
_ApiRow('disabled', '是否禁用', 'bool', 'false'),
];
const _cursorApi = [
_ApiRow('child', '子元素', 'Widget', '-', required: true),
_ApiRow('cursor', '鼠标光标', 'MouseCursor', 'SystemMouseCursors.none'),
_ApiRow('showImageCursor', '是否显示图片光标', 'bool', 'true'),
];
const _modalApi = [
_ApiRow('context', '弹窗上下文', 'BuildContext', '-', required: true),
_ApiRow('title', '标题', 'Widget?', '-'),
_ApiRow('width', '宽度', 'double', '520'),
_ApiRow('barrierDismissible', '点击遮罩关闭', 'bool', 'true'),
_ApiRow('footer', '底部按钮区域', 'Widget?', '默认按钮'),
_ApiRow('showFooter', '是否显示底部按钮', 'bool', 'true'),
_ApiRow('onOk', '确认回调', 'VoidCallback?', '-'),
_ApiRow('child', '自定义内容', 'Widget', '-', required: true),
_ApiRow('typeSpeed', '打字机每字间隔', 'Duration', '80ms'),
_ApiRow('typewriter', '是否启用打字机效果', 'bool', 'true'),
];
const _typewriterApi = [
_ApiRow('text', '需要逐字显示的内容', 'String', '-', required: true),
_ApiRow('speed', '每字间隔', 'Duration', '90ms'),
_ApiRow('trigger', '值变化时重新播放', 'Object?', '-'),
_ApiRow('autoPlay', '是否自动从头开始播放', 'bool', 'true'),
_ApiRow('style', '文本样式', 'TextStyle?', '-'),
_ApiRow('onDone', '播放完成回调', 'VoidCallback?', '-'),
];
const _dividerApi = [
_ApiRow('type', '分隔线类型', 'AnimalDividerType', 'lineBrown'),
_ApiRow('height', '分隔线高度', 'double', '12'),
];
const _iconApi = [
_ApiRow('name', '图标名称', 'AnimalIconName', '-', required: true),
_ApiRow('size', '图标尺寸', 'double', '24'),
_ApiRow('bounce', '弹跳动画', 'bool', 'false'),
];
const _selectApi = [
_ApiRow('options', '选项列表', 'List<AnimalSelectOption<T>>', '-',
required: true),
_ApiRow('value', '当前选中值', 'T?', '-', required: true),
_ApiRow('onChanged', '选中变化回调', 'ValueChanged<T>', '-', required: true),
_ApiRow('placeholder', '占位文本', 'String', '请选择'),
_ApiRow('disabled', '禁用状态', 'bool', 'false'),
_ApiRow('minWidth', '触发器最小宽度', 'double', '140'),
];
const _checkboxApi = [
_ApiRow('options', '选项列表', 'List<AnimalCheckboxOption<T>>', '-',
required: true),
_ApiRow('value', '受控选中值列表', 'List<T>?', '-'),
_ApiRow('defaultValue', '默认选中值列表', 'List<T>', '[]'),
_ApiRow('size', '尺寸', 'AnimalCheckboxSize', 'middle'),
_ApiRow('disabled', '禁用全部选项', 'bool', 'false'),
_ApiRow('direction', '排列方向', 'AnimalCheckboxDirection', 'horizontal'),
_ApiRow('onChanged', '选中值变化回调', 'ValueChanged<List<T>>?', '-'),
];
const _tabsApi = [
_ApiRow('items', '标签页配置列表', 'List<AnimalTabItem>', '-', required: true),
_ApiRow('defaultActiveKey', '默认激活的标签', 'String?', '第一个标签'),
_ApiRow('activeKey', '受控模式当前激活标签', 'String?', '-'),
_ApiRow('onChanged', '标签切换回调', 'ValueChanged<String>?', '-'),
_ApiRow('shadow', '是否显示选中状态阴影', 'bool', 'true'),
_ApiRow('leafAnimation', '是否启用叶子动画', 'bool', 'true'),
];
const _footerApi = [
_ApiRow('type', 'Footer 类型', 'AnimalFooterType', 'tree'),
_ApiRow('height', '自定义显示高度', 'double?', '80'),
];
const _codeBlockApi = [
_ApiRow('code', '代码字符串', 'String', '-', required: true),
_ApiRow('padding', '自定义内边距', 'EdgeInsetsGeometry', '24x20'),
];
const _loadingApi = [
_ApiRow('active', '是否显示加载动画', 'bool', 'true'),
_ApiRow('size', '辅助 loading 尺寸', 'double', '28'),
_ApiRow('strokeWidth', '辅助 loading 线宽', 'double', '3'),
_ApiRow('style', '加载样式', 'AnimalLoadingStyle', 'island'),
];
const _tableApi = [
_ApiRow('columns', '表格列配置', 'List<AnimalTableColumn<T>>', '[]'),
_ApiRow('rows', '表格数据源', 'List<T>', '[]'),
_ApiRow('striped', '是否显示斑马纹', 'bool', 'true'),
_ApiRow('showHeader', '是否显示表头', 'bool', 'true'),
_ApiRow('loading', '加载状态', 'bool', 'false'),
_ApiRow('emptyText', '空数据显示文本', 'String?', '暂无数据'),
_ApiRow('maxHeight', '表格最大高度', 'double?', '-'),
_ApiRow('onRowTap', '行点击回调', 'void Function(T row, int index)?', '-'),
];
const _timeApi = [
_ApiRow('now', '指定显示时间;为空时实时更新时间', 'DateTime?', '-'),
];
const _phoneApi = [
_ApiRow('width', '手机设计稿宽度', 'double', '527'),
_ApiRow('height', '手机设计稿高度', 'double', '788'),
];
const _buttonCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
children: [
// Primary
AnimalButton(type: AnimalButtonType.primary, onPressed: () {}, child: const Text('Primary')),
// Default
AnimalButton(onPressed: () {}, child: const Text('Default')),
// Dashed
AnimalButton(type: AnimalButtonType.dashed, onPressed: () {}, child: const Text('Dashed')),
// Text
AnimalButton(type: AnimalButtonType.text, onPressed: () {}, child: const Text('Text')),
// Link
AnimalButton(type: AnimalButtonType.link, onPressed: () {}, child: const Text('Link')),
// Danger
AnimalButton(type: AnimalButtonType.primary, danger: true, onPressed: () {}, child: const Text('Danger')),
// Ghost
AnimalButton(type: AnimalButtonType.primary, ghost: true, onPressed: () {}, child: const Text('Ghost')),
// Loading
const AnimalButton(type: AnimalButtonType.primary, loading: true, child: Text('Loading')),
// Large
AnimalButton(type: AnimalButtonType.primary, size: AnimalButtonSize.large, onPressed: () {}, child: const Text('Large')),
// Icon
AnimalButton(type: AnimalButtonType.primary, icon: const Text('🔍'), onPressed: () {}, child: const Text('搜索')),
// Block
AnimalButton(type: AnimalButtonType.primary, block: true, onPressed: () {}, child: const Text('Block')),
],
);
}
}''';
const _inputCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
// 基础输入框
AnimalInput(hintText: 'Basic input'),
// 带清除按钮
AnimalInput(hintText: 'With clear', allowClear: true),
// 前后缀
AnimalInput(hintText: 'Prefix', prefix: Text('🔍'), suffix: Text('⏎')),
// 小尺寸
AnimalInput(hintText: 'Small', size: AnimalInputSize.small),
// 大尺寸
AnimalInput(hintText: 'Large', size: AnimalInputSize.large),
// 错误状态
AnimalInput(hintText: 'Error', status: AnimalInputStatus.error),
// 警告状态
AnimalInput(hintText: 'Warning', status: AnimalInputStatus.warning),
// 有阴影
AnimalInput(hintText: 'With shadow', shadow: true),
],
);
}
}''';
const _switchCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
var checked = false;
@override
Widget build(BuildContext context) {
return Wrap(
children: [
// 受控模式
AnimalSwitch(value: checked, onChanged: (value) => setState(() => checked = value)),
// 自定义文案
const AnimalSwitch(defaultValue: true, checkedChild: Text('开'), uncheckedChild: Text('关')),
// 小尺寸
const AnimalSwitch(size: AnimalSwitchSize.small, defaultValue: true),
],
);
}
}''';
const _cardCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
// 基础卡片
SizedBox(width: 260, child: AnimalCard(child: Text('基础卡片'))),
// 标题卡片
SizedBox(
width: 260,
child: AnimalCard(type: AnimalCardType.title, child: Text('标题卡片')),
),
// 颜色变体
AnimalCard(color: AnimalCardColor.appBlue, child: Text('蓝色卡片')),
AnimalCard(color: AnimalCardColor.warmPeachPink, child: Text('暖桃粉卡片')),
// 颜色 + 标题 组合
AnimalCard(
type: AnimalCardType.title,
color: AnimalCardColor.purple,
child: Text('紫色标题卡片'),
),
],
);
}
}''';
const _collapseCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
// 基础用法
AnimalCollapse(question: Text('问题'), answer: Text('回答内容')),
// 默认展开
AnimalCollapse(question: Text('默认展开'), answer: Text('答案'), defaultExpanded: true),
// 禁用状态
AnimalCollapse(question: Text('禁用'), answer: Text('答案'), disabled: true),
],
);
}
}''';
const _cursorCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const AnimalCursor(
child: Text('鼠标移入此区域将显示自定义光标'),
);
}
}''';
const _modalCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
void openModal(BuildContext context) {
AnimalDialog.show<void>(
context: context,
child: const Text('Modal 内容'),
);
// 带标题
AnimalDialog.show<void>(
context: context,
title: const Text('标题'),
child: const Text('内容'),
);
// 自定义 Footer
AnimalDialog.show<void>(
context: context,
title: const Text('确认'),
footer: AnimalButton(onPressed: () {}, child: const Text('自定义按钮')),
child: const Text('内容'),
);
// 无 Footer
AnimalDialog.show<void>(
context: context,
showFooter: false,
child: const Text('无底部按钮'),
);
// 关闭打字机效果
AnimalDialog.show<void>(
context: context,
typewriter: false,
child: const Text('直接显示全部内容'),
);
}''';
const _typewriterCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
var replayKey = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimalTypewriter(
trigger: replayKey,
text: '你好,欢迎来到动物岛!',
),
// 支持多行文本
AnimalTypewriter(
speed: const Duration(milliseconds: 40),
trigger: replayKey,
text: '第一行\n第二行',
),
AnimalButton(
onPressed: () => setState(() => replayKey += 1),
child: const Text('重新播放'),
),
],
);
}
}''';
const _dividerCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
// line-brown
AnimalDivider(type: AnimalDividerType.lineBrown),
// line-teal
AnimalDivider(type: AnimalDividerType.lineTeal),
// line-white
AnimalDivider(type: AnimalDividerType.lineWhite),
// line-yellow
AnimalDivider(type: AnimalDividerType.lineYellow),
// wave-yellow
AnimalDivider(type: AnimalDividerType.waveYellow),
],
);
}
}''';
const _iconCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const Row(
children: [
// 基础用法
AnimalIcon(name: AnimalIconName.miles, size: 32),
// 弹跳动画
AnimalIcon(name: AnimalIconName.camera, size: 48, bounce: true),
],
);
}
}''';
const _selectCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
const options = [
AnimalSelectOption(key: 'option1', label: '选项一'),
AnimalSelectOption(key: 'option2', label: '选项二'),
];
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
String? value = 'option1';
@override
Widget build(BuildContext context) {
return Column(
children: [
// 受控模式
AnimalSelect<String>(options: options, value: value, onChanged: (next) => setState(() => value = next)),
// 占位文本
AnimalSelect<String>(options: options, value: null, onChanged: (_) {}, placeholder: '请选择'),
// 禁用状态
AnimalSelect<String>(options: options, value: value, onChanged: (_) {}, disabled: true),
],
);
}
}''';
const _checkboxCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
const options = [
AnimalCheckboxOption(value: 'beach', label: Text('🌊 海滩')),
AnimalCheckboxOption(value: 'forest', label: Text('🌳 森林')),
AnimalCheckboxOption(value: 'garden', label: Text('🌸 花园')),
];
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
// 非受控
AnimalCheckbox<String>(options: options, defaultValue: ['beach']),
// 垂直排列
AnimalCheckbox<String>(options: options, direction: AnimalCheckboxDirection.vertical),
],
);
}
}''';
const _tabsCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const AnimalTabs(
defaultActiveKey: 'tab1',
items: [
AnimalTabItem(key: 'tab1', label: Text('标签一'), child: Text('内容一')),
AnimalTabItem(key: 'tab2', label: Text('标签二'), child: Text('内容二')),
],
);
}
}''';
const _footerCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
AnimalFooter(),
AnimalFooter(type: AnimalFooterType.sea),
],
);
}
}''';
const _codeBlockCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const AnimalCodeBlock(
code: "AnimalButton(type: AnimalButtonType.primary, child: Text('按钮'))",
);
}
}''';
const _loadingCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
var active = true;
@override
Widget build(BuildContext context) {
return Stack(
children: [
const Positioned.fill(child: Text('Underlying Content')),
Positioned.fill(child: AnimalLoading(active: active)),
AnimalButton(
onPressed: () => setState(() => active = !active),
child: Text(active ? '关闭 Loading' : '开启 Loading'),
),
],
);
}
}''';
const _tableCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
const data = [
{'name': '豆狸', 'age': 26, 'island': '彩虹岛', 'fruit': '苹果', 'hobby': '音乐'},
{'name': '粒狸', 'age': 24, 'island': '彩虹岛', 'fruit': '橘子', 'hobby': '运动'},
{'name': '西施惠', 'age': 28, 'island': '好评岛', 'fruit': '樱桃', 'hobby': '唱歌'},
];
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return AnimalTable<Map<String, Object>>(
columns: [
AnimalTableColumn(title: const Text('岛民'), width: 120, cellBuilder: (_, row, __) => Text(row['name'] as String)),
AnimalTableColumn(title: const Text('年龄'), width: 80, alignment: Alignment.center, cellBuilder: (_, row, __) => Text('${row['age']}')),
AnimalTableColumn(title: const Text('岛屿'), cellBuilder: (_, row, __) => Text(row['island'] as String)),
],
rows: data,
striped: true,
);
}
}''';
const _timeCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const AnimalTime();
}
}''';
const _phoneCode =
r'''import 'package:animal_island_flutter/animal_island_flutter.dart';
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const AnimalPhone();
}
}''';