feat:增加轮播图组件
This commit is contained in:
@@ -85,3 +85,31 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget networkImage(String url) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
url,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
// 加载中显示占位符
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.image, color: Colors.grey, size: 30),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
127
lib/widgets/common/year_selector.dart
Normal file
127
lib/widgets/common/year_selector.dart
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
|
class YearSelector extends StatefulWidget {
|
||||||
|
final int initialYear;
|
||||||
|
final int? minYear;
|
||||||
|
final int? maxYear;
|
||||||
|
final Function(int) onYearChanged;
|
||||||
|
|
||||||
|
const YearSelector({
|
||||||
|
super.key,
|
||||||
|
required this.initialYear,
|
||||||
|
required this.onYearChanged,
|
||||||
|
this.minYear,
|
||||||
|
this.maxYear,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<YearSelector> createState() => _YearSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SingleTickerProviderStateMixin 动画控制器
|
||||||
|
class _YearSelectorState extends State<YearSelector>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late int _currentYear;
|
||||||
|
|
||||||
|
// 用于动画效果
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _scaleAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentYear = widget.initialYear;
|
||||||
|
|
||||||
|
// 初始化动画控制器
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 缩放动画
|
||||||
|
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
|
||||||
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换到上一年
|
||||||
|
void _previousYear() {
|
||||||
|
if (widget.minYear == null || _currentYear > widget.minYear!) {
|
||||||
|
_animateYearChange(() {
|
||||||
|
setState(() {
|
||||||
|
_currentYear--;
|
||||||
|
});
|
||||||
|
widget.onYearChanged(_currentYear);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换到下一年
|
||||||
|
void _nextYear() {
|
||||||
|
if (widget.maxYear == null || _currentYear < widget.maxYear!) {
|
||||||
|
_animateYearChange(() {
|
||||||
|
setState(() {
|
||||||
|
_currentYear++;
|
||||||
|
});
|
||||||
|
widget.onYearChanged(_currentYear);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 年份变化时的动画效果
|
||||||
|
void _animateYearChange(VoidCallback onComplete) {
|
||||||
|
_animationController.forward().then((_) {
|
||||||
|
onComplete();
|
||||||
|
_animationController.reverse();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TDButton(
|
||||||
|
icon: Icons.chevron_left,
|
||||||
|
size: TDButtonSize.small,
|
||||||
|
type: TDButtonType.fill,
|
||||||
|
shape: TDButtonShape.circle,
|
||||||
|
theme: TDButtonTheme.primary,
|
||||||
|
onTap: () => _previousYear(),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
// 年份显示
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _scaleAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.scale(scale: _scaleAnimation.value, child: child);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'$_currentYear',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
TDButton(
|
||||||
|
icon: Icons.chevron_right,
|
||||||
|
size: TDButtonSize.small,
|
||||||
|
type: TDButtonType.fill,
|
||||||
|
shape: TDButtonShape.circle,
|
||||||
|
theme: TDButtonTheme.primary,
|
||||||
|
onTap: () => _nextYear(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_hub_app/config/app_config.dart';
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
import 'package:food_hub_app/models/moment.dart';
|
import 'package:food_hub_app/models/moment.dart';
|
||||||
import 'package:food_hub_app/widgets/common/image_preview.dart';
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
class MomentCard extends StatelessWidget {
|
class MomentCard extends StatelessWidget {
|
||||||
@@ -54,13 +54,15 @@ class MomentCard extends StatelessWidget {
|
|||||||
type: TDAvatarType.customText,
|
type: TDAvatarType.customText,
|
||||||
shape: TDAvatarShape.circle,
|
shape: TDAvatarShape.circle,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
text: moment.username?[0]);
|
text: moment.username?[0],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return TDAvatar(
|
return TDAvatar(
|
||||||
size: TDAvatarSize.medium,
|
size: TDAvatarSize.medium,
|
||||||
type: TDAvatarType.normal,
|
type: TDAvatarType.normal,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
avatarUrl: '${AppConfig.baseApiUrl}/${moment.avatar}');
|
avatarUrl: '${AppConfig.baseApiUrl}/${moment.avatar}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +70,7 @@ class MomentCard extends StatelessWidget {
|
|||||||
Widget _buildNickname() {
|
Widget _buildNickname() {
|
||||||
return Text(
|
return Text(
|
||||||
moment.username ?? "",
|
moment.username ?? "",
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +103,22 @@ class MomentCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成图片URL列表(用于预览时切换)
|
// 生成图片URL列表(用于预览时切换)
|
||||||
final List<String> imageUrls = moment.imageList
|
final List<String> imageUrls =
|
||||||
|
moment.imageList
|
||||||
.map((path) => '${AppConfig.baseApiUrl}/$path')
|
.map((path) => '${AppConfig.baseApiUrl}/$path')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
void imageTapClick(int index) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
ImagePreviewPage(images: imageUrls, initialIndex: index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return GridView.count(
|
return GridView.count(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
@@ -119,32 +130,9 @@ class MomentCard extends StatelessWidget {
|
|||||||
// 单个图片项:添加点击事件
|
// 单个图片项:添加点击事件
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
// 点击图片时,跳转到预览页面
|
// 点击图片时,跳转到预览页面
|
||||||
onTap: () {
|
onTap: () => imageTapClick(index),
|
||||||
// 导航到全屏预览页面,传入所有图片URL和当前点击的索引
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ImagePreviewPage(
|
|
||||||
images: imageUrls,
|
|
||||||
initialIndex: index,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// 原图片组件
|
// 原图片组件
|
||||||
child: ClipRRect(
|
child: networkImage(imageUrls[index]),
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Image.network(
|
|
||||||
imageUrls[index],
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.grey[200],
|
|
||||||
child: const Icon(Icons.image, color: Colors.grey, size: 30),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:carousel_slider/carousel_slider.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
|
||||||
import 'package:food_hub_app/config/app_config.dart';
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
|
||||||
import 'package:food_hub_app/models/recipe.dart';
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
class RecipeCard extends StatelessWidget {
|
class RecipeCard extends StatelessWidget {
|
||||||
@@ -13,19 +13,51 @@ class RecipeCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final List<String> imageUrls =
|
||||||
|
recipe.recordList
|
||||||
|
.map((item) => '${AppConfig.baseApiUrl}/${item.imageUrl}')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Widget buildCarouselItem(String url) {
|
||||||
|
return Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AspectRatio(
|
||||||
|
aspectRatio: 2,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: networkImage(url),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(color: Theme.of(context).primaryColor, width: 1.0),
|
||||||
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Image.network(
|
FlutterCarousel(
|
||||||
'${AppConfig.baseApiUrl}/${recipe.recordList[0].imageUrl}',
|
// 轮播项
|
||||||
height: 200,
|
items:
|
||||||
width: double.infinity,
|
imageUrls.map((url) {
|
||||||
fit: BoxFit.contain,
|
return buildCarouselItem(url);
|
||||||
errorBuilder: (context, error, stackTrace) => errorImageContainer(200),
|
}).toList(),
|
||||||
|
// 轮播配置
|
||||||
|
options: FlutterCarouselOptions(
|
||||||
|
height: 300,
|
||||||
|
autoPlay: imageUrls.length > 1,
|
||||||
|
enableInfiniteScroll: imageUrls.length > 1,
|
||||||
|
autoPlayInterval: const Duration(seconds: 3),
|
||||||
|
viewportFraction: 0.9,
|
||||||
|
showIndicator: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Divider(
|
Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|||||||
class RecipeList extends StatefulWidget {
|
class RecipeList extends StatefulWidget {
|
||||||
const RecipeList({super.key});
|
const RecipeList({super.key});
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RecipeList> createState() => _RecipeListState();
|
State<RecipeList> createState() => _RecipeListState();
|
||||||
}
|
}
|
||||||
@@ -34,11 +33,14 @@ class _RecipeListState extends State<RecipeList> {
|
|||||||
if (recipeList.isEmpty) {
|
if (recipeList.isEmpty) {
|
||||||
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
||||||
} else {
|
} else {
|
||||||
return ListView.builder(
|
return ListView.separated(
|
||||||
itemCount: recipeList.length,
|
itemCount: recipeList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return RecipeCard(recipe: recipeList[index]);
|
return RecipeCard(recipe: recipeList[index]);
|
||||||
}
|
},
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return SizedBox(height: 10);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:food_hub_app/api/recipe.dart';
|
import 'package:food_hub_app/api/recipe.dart';
|
||||||
import 'package:food_hub_app/config/app_config.dart';
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
import 'package:food_hub_app/models/recipe.dart';
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/year_selector.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
import 'package:timelines_plus/timelines_plus.dart';
|
import 'package:timelines_plus/timelines_plus.dart';
|
||||||
|
|
||||||
@@ -31,16 +33,20 @@ class _RecipeTimeline extends State<RecipeTimeline> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
YearSelector(
|
YearSelector(
|
||||||
initialYear: DateTime.now().year,
|
initialYear: DateTime.now().year,
|
||||||
minYear: 2000,
|
minYear: 2000,
|
||||||
maxYear: 2100,
|
maxYear: 2100,
|
||||||
onYearChanged: (year) => refreshRecord(year)
|
onYearChanged: (year) => refreshRecord(year),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 10),
|
||||||
Expanded(child: timelineContainer(recordList)),
|
Expanded(child: timelineContainer(recordList)),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,9 +55,7 @@ Widget timelineContainer(List<Record> recordList) {
|
|||||||
if (recordList.isEmpty) {
|
if (recordList.isEmpty) {
|
||||||
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
||||||
} else {
|
} else {
|
||||||
return Padding(
|
return Timeline.tileBuilder(
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: Timeline.tileBuilder(
|
|
||||||
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
|
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
|
||||||
builder: TimelineTileBuilder.connected(
|
builder: TimelineTileBuilder.connected(
|
||||||
itemCount: recordList.length,
|
itemCount: recordList.length,
|
||||||
@@ -64,7 +68,6 @@ Widget timelineContainer(List<Record> recordList) {
|
|||||||
return TimelineCard(record: recordList[index]);
|
return TimelineCard(record: recordList[index]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +77,19 @@ class TimelineCard extends StatelessWidget {
|
|||||||
|
|
||||||
const TimelineCard({super.key, required this.record});
|
const TimelineCard({super.key, required this.record});
|
||||||
|
|
||||||
Widget cardContent() {
|
Widget cardContent(BuildContext context) {
|
||||||
|
final imageUrls = ['${AppConfig.baseApiUrl}/${record.imageUrl}'];
|
||||||
|
|
||||||
|
void imageTapClick() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder:
|
||||||
|
(context) => ImagePreviewPage(images: imageUrls, initialIndex: 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -84,13 +99,12 @@ class TimelineCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(record.name, style: TextStyle(fontSize: 16)),
|
Text(record.name, style: TextStyle(fontSize: 16)),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
Image.network(
|
GestureDetector(
|
||||||
'${AppConfig.baseApiUrl}/${record.imageUrl}',
|
onTap: () => imageTapClick(),
|
||||||
width: double.infinity,
|
child: AspectRatio(
|
||||||
height: 250,
|
aspectRatio: 1.5,
|
||||||
fit: BoxFit.contain,
|
child: networkImage(imageUrls[0]),
|
||||||
errorBuilder:
|
),
|
||||||
(context, error, stackTrace) => errorImageContainer(250),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -111,135 +125,10 @@ class TimelineCard extends StatelessWidget {
|
|||||||
record.date,
|
record.date,
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
cardContent(),
|
cardContent(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class YearSelector extends StatefulWidget {
|
|
||||||
final int initialYear;
|
|
||||||
final int? minYear;
|
|
||||||
final int? maxYear;
|
|
||||||
final Function(int) onYearChanged;
|
|
||||||
|
|
||||||
const YearSelector({
|
|
||||||
super.key,
|
|
||||||
required this.initialYear,
|
|
||||||
required this.onYearChanged,
|
|
||||||
this.minYear,
|
|
||||||
this.maxYear,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<YearSelector> createState() => _YearSelectorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// SingleTickerProviderStateMixin 动画控制器
|
|
||||||
class _YearSelectorState extends State<YearSelector>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late int _currentYear;
|
|
||||||
|
|
||||||
// 用于动画效果
|
|
||||||
late AnimationController _animationController;
|
|
||||||
late Animation<double> _scaleAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_currentYear = widget.initialYear;
|
|
||||||
|
|
||||||
// 初始化动画控制器
|
|
||||||
_animationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 缩放动画
|
|
||||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
|
|
||||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_animationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 切换到上一年
|
|
||||||
void _previousYear() {
|
|
||||||
if (widget.minYear == null || _currentYear > widget.minYear!) {
|
|
||||||
_animateYearChange(() {
|
|
||||||
setState(() {
|
|
||||||
_currentYear--;
|
|
||||||
});
|
|
||||||
widget.onYearChanged(_currentYear);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 切换到下一年
|
|
||||||
void _nextYear() {
|
|
||||||
if (widget.maxYear == null || _currentYear < widget.maxYear!) {
|
|
||||||
_animateYearChange(() {
|
|
||||||
setState(() {
|
|
||||||
_currentYear++;
|
|
||||||
});
|
|
||||||
widget.onYearChanged(_currentYear);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 年份变化时的动画效果
|
|
||||||
void _animateYearChange(VoidCallback onComplete) {
|
|
||||||
_animationController.forward().then((_) {
|
|
||||||
onComplete();
|
|
||||||
_animationController.reverse();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
TDButton(
|
|
||||||
icon: Icons.chevron_left,
|
|
||||||
size: TDButtonSize.small,
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
onTap: () => _previousYear(),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
// 年份显示
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: _scaleAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.scale(scale: _scaleAnimation.value, child: child);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'$_currentYear',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
TDButton(
|
|
||||||
icon: Icons.chevron_right,
|
|
||||||
size: TDButtonSize.small,
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
onTap: () => _nextYear(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -105,14 +105,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.10.1"
|
version: "8.10.1"
|
||||||
carousel_slider:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: carousel_slider
|
|
||||||
sha256: bcc61735345c9ab5cb81073896579e735f81e35fd588907a393143ea986be8ff
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "5.1.1"
|
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -286,6 +278,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_carousel_widget:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_carousel_widget
|
||||||
|
sha256: "6473e6df04bfafea70efd58251fe5945d5aa8d19461575c1b9d83643f08e0c77"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
flutter_form_builder:
|
flutter_form_builder:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ dependencies:
|
|||||||
shared_preferences: ^2.3.0
|
shared_preferences: ^2.3.0
|
||||||
logger: ^2.6.0
|
logger: ^2.6.0
|
||||||
photo_view: ^0.15.0
|
photo_view: ^0.15.0
|
||||||
carousel_slider: ^5.1.1
|
flutter_carousel_widget: ^3.1.0
|
||||||
easy_refresh: ^3.4.0
|
easy_refresh: ^3.4.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
|
|||||||
Reference in New Issue
Block a user