0. 项目简介
项目想法脱胎于2023年服务外包大赛A18题 随手买(详情)
整个APP思路如下:
这篇博客主要服务于乘客界面的商品界面,模仿京东的商品界面、淘宝的商品详情界面和相关商品界面、探探的翻牌界面、得物的购买记录界面。
1. 效果展示
商品展示(图片轮播)+ toast 下边提示框
模仿淘宝商品详情(长图)
模仿探探卡片的评论
模仿得物的最近购买
相关商品
2. 代码
依赖如下
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
# 获取设备大小
flutter_screenutil: ^3.1.0
# 配置轮播图插件
flutter_swiper: ^1.1.6
# 下边提示
fluttertoast: ^4.0.1
# 最近购买标签旋转动画
toggle_rotate: ^0.0.5
相关文件如下
// 商品界面
commodity.dart
// 卡片评论界面
CardTry.dart
// 最近购买界面
recentPurchase.dart
commodity.dart
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_swiper/flutter_swiper.dart';
import 'CardTry.dart';
import 'recentPurchase.dart';
class _Page {
_Page({
required this.label});
final String label;
String get id => label[0];
String toString() => '$runtimeType("$label")';
}
class _CardData {
const _CardData({
required this.title, required this.imageAsset, required this.price});
final String title;
final String imageAsset;
final String price;
}
class _CardInfo {
_CardInfo({
required this.images,
required this.who,
required this.saywhat,
required this.time,
});
final String images;
final String who;
final String saywhat;
final String time;
}
List <_CardInfo> cardinfo=[
_CardInfo(
images:'assets/images/feedback1.jpg',
who: "水之凝落",
saywhat: "内外包装无珀斯按!生产日期是2022年10月8日!",
time: "2022-10-30 18:32"
),
_CardInfo(
images:'assets/images/feedback2.jpg',
who: "咚咚呛喇",
saywhat: "还是那个喜欢的喂到",
time: "2022-08-10 15:13"
),
_CardInfo(
images:'assets/images/feedback3.jpg',
who: "匿名买家",
saywhat: "味道和超市卖的没什么差别,但是瓶子材质很软,偏细,给人一种廉价感",
time: "2022-09-22 16:24"
),
_CardInfo(
images:'assets/images/feedback4.jpg',
who: '6',
saywhat: "666666",
time: "2022-12-31 23:11"
),
_CardInfo(
images:'assets/images/feedback5.jpg',
who: "迪士尼在逃铖铖的公主",
saywhat: "感觉没有超市里买的好喝,口感像是太甜,像糖精水吧。买都买了凑合着喝吧",
time: "2022-08-20 13:30"
),
];
// 生成卡片数组
List<Widget> cards = List.generate(
cardinfo.length,
(int index) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.0),
boxShadow: [
BoxShadow(
offset: Offset(0, 17),
blurRadius: 23.0,
spreadRadius: -13.0,
color: Colors.black54,
)
],
),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Image.asset(
cardinfo[index].images,
fit: BoxFit.cover,
),
),
Align(
child: ClipRRect(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16.0)),
child: ListTile(
title: Text(cardinfo[index].who, style: TextStyle(fontSize: 18, color: Colors.black ,fontWeight: FontWeight.bold)),
subtitle: RichText(
textDirection: TextDirection.ltr,
text: TextSpan(
children: <TextSpan>[
TextSpan(
text: cardinfo[index].saywhat,
style: TextStyle(fontSize: 14, color: Colors.black),
),
TextSpan(
text: "\n"+cardinfo[index].time,
style: TextStyle(fontSize: 12, height: 2, color: Colors.grey),
),
TextSpan(
text: "\n ",
style: TextStyle(fontSize: 1, color: Colors.grey),
),
]
),
),
trailing: Icon(Icons.more_vert),
isThreeLine: true,
),
),
alignment: Alignment.bottomCenter,
),
]
)
);
},
);
final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{
new _Page(label: '详情'): <_CardData>[
// const _CardData(
// title: 'Old Binoculars',
// imageAsset: 'shrine/products/binoculars.png',
// ),
// const _CardData(
// title: 'Teapot',
// imageAsset: 'shrine/products/teapot.png',
// ),
// const _CardData(
// title: 'Blue suede shoes',
// imageAsset: 'shrine/products/chucks.png',
// ),
],
new _Page(label: '评论'): <_CardData>[
// const _CardData(
// title: 'Beachball',
// imageAsset: 'shrine/products/beachball.png',
// ),
// const _CardData(
// title: 'Dipped Brush',
// imageAsset: 'shrine/products/brush.png',
// ),
// const _CardData(
// title: 'Perfect Goldfish Bowl',
// imageAsset: 'shrine/products/fish_bowl.png',
// ),
],
new _Page(label: '最近购买'): <_CardData>[
// const _CardData(
// title: 'Beachball',
// imageAsset: 'shrine/products/beachball.png',
// ),
// const _CardData(
// title: 'Dipped Brush',
// imageAsset: 'shrine/products/brush.png',
// ),
// const _CardData(
// title: 'Perfect Goldfish Bowl',
// imageAsset: 'shrine/products/fish_bowl.png',
// ),
],
new _Page(label: '相关商品'): <_CardData>[
const _CardData(
title: '统一冰红茶',
imageAsset: 'assets/images/tongyi1.jpg',
price: '3.50'
),
const _CardData(
title: '统一阿萨姆奶茶500ml',
imageAsset: 'assets/images/tongyi2.jpg',
price: '49.49'
),
const _CardData(
title: '元气森林奶茶',
imageAsset: 'assets/images/tongyi3.jpg',
price: '11.99'
),
const _CardData(
title: '依然乳矿气泡水',
imageAsset: 'assets/images/tongyi4.png',
price: '6.46'
),
const _CardData(
title: '可口可乐',
imageAsset: 'assets/images/tongyi5.jpeg',
price: '3.20'
),
const _CardData(
title: '雪碧',
imageAsset: 'assets/images/tongyi6.jpg',
price: '3.19'
),
],
};
class _CardDataItem extends StatelessWidget {
const _CardDataItem({
required this.page, required this.data});
static const double cardheight = 272.0;
final _Page page;
final _CardData data;
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
// Align(
// alignment:
// page.id == 'L' ? Alignment.centerLeft : Alignment.centerRight,
// child: new CircleAvatar(child: new Text('${page.id}')),
// ),
SizedBox(
width: 144.0,
height: 160.0,
child: Image.asset(
data.imageAsset,
fit: BoxFit.contain,
),
),
Spacer(),
Column(
crossAxisAlignment:CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 10.0,top: 0.0),
child: Text("¥${
data.price}",
style: TextStyle(fontSize: 16.0,
color: Color(0xFFe9546b)),)),
Padding(
padding: EdgeInsets.only(left: 12.0,top: 0.0),
child: Text("${
data.title}",
style: TextStyle(fontSize: 16.0,
color: Color(0xFF333333)),)),
],
),
],
),
),
);
}
}
class commodity extends StatefulWidget {
const commodity({
Key? key}) : super(key: key);
State<commodity> createState() => _commodityState();
}
class _commodityState extends State<commodity> {
List bannerDatas = [
'assets/images/asm1.png',
'assets/images/asm2.jpg',
'assets/images/asm3.jpg',
];
late SwiperController _swiperController;
void initState() {
// TODO: implement initState
super.initState();
_swiperController = SwiperController();
_swiperController.startAutoplay();
}
void dispose(){
_swiperController.stopAutoplay();
_swiperController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
ScreenUtil.init(context, allowFontScaling: false);
return DefaultTabController(
length: _allPages.length,
child: Scaffold(
appBar: AppBar(
title: Text("识别结果", style: TextStyle(color: Colors.black),),
backgroundColor: Colors.white70,
elevation: 0,
actions: <Widget>[
IconButton(
onPressed: (){
Fluttertoast.showToast(
msg: "正在建设中11111111...",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
// timeInSecForIos:1
);
},
icon: Icon(Icons.more_horiz, color: Colors.black,),
),
],
),
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled){
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
backgroundColor: Colors.transparent,
elevation: 0,
pinned: true,
// 悬浮框初始高度
expandedHeight: 380.0,
forceElevated: innerBoxIsScrolled,
bottom: PreferredSize(
child: Container(
child: TabBar(
indicatorColor: Colors.white,//选中下划线颜色,如果使用了indicator这里设置无效
labelColor: Colors.white,
labelStyle: TextStyle(fontSize: 16),
unselectedLabelStyle:TextStyle(fontSize: 14) ,
indicatorWeight: 3,
tabs: _allPages.keys.map(
(_Page page) => Tab(
child: Tab(text: page.label),
),
).toList(),
),
color: Colors.redAccent[100],
),
// 悬浮框锁定高度
preferredSize: Size(double.infinity, 0.0)
),
flexibleSpace: FlexibleSpaceBar(
background:Column(
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width,
height: 240.0,
margin: EdgeInsets.only(bottom: 10.0),
child: Swiper(
itemBuilder: (BuildContext context,int index){
return Image.asset(bannerDatas[index],fit: BoxFit.fill);
},
itemCount: bannerDatas.length,
autoplayDisableOnInteraction: true,
pagination: SwiperPagination(
builder: DotSwiperPaginationBuilder(size: 8, activeSize: 12,activeColor:Color(0xFFe9546b)),
),
controller: _swiperController,
),
),
Container(
width: MediaQuery.of(context).size.width,
height:80.0,
decoration: BoxDecoration(
color: Colors.white,
),
child: Column(
crossAxisAlignment:CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 10.0,top: 0.0),
child: Text("¥4.50",
style: TextStyle(fontSize: 16.0,
color: Color(0xFFe9546b)),)),
Padding(
padding: EdgeInsets.only(left: 10.0,top: 0.0),
child: Text("¥5.00",
style: TextStyle(fontSize: 12.0,
color:Color(0xFFaaaaaa)),)),
Padding(
padding: EdgeInsets.only(left: 12.0,top: 0.0),
child: Text("统一阿萨姆奶茶",
style: TextStyle(fontSize: 16.0,
color: Color(0xFF333333)),)),
],
),
),
],
),
),
),
)
];
},
body: Stack(
children: [
TabBarView(
children: _allPages.keys.map((_Page page) {
if (page.label=="相关商品"){
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context){
return CustomScrollView(
key: PageStorageKey<_Page>(page),
slivers: <Widget>[
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverPadding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
sliver: SliverFixedExtentList(
itemExtent: _CardDataItem.cardheight,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index){
final _CardData data = _allPages[page]![index];
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
),
child: _CardDataItem(
page: page,
data: data,
),
);
},
childCount:_allPages[page]?.length,
),
),
),
],
);
},
),
);
}
else if(page.label=="评论"){
return Padding(
padding: EdgeInsets.fromLTRB(20, 70, 20, 20),
child: Container(
height: 400,
child: CardTry(cards: cards,),
),
);
}
else if(page.label=="详情"){
String imagename = "assets/images/asm.png";
print(Image.asset(imagename).height);
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context){
return CustomScrollView(
slivers: <Widget>[
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverFixedExtentList(
itemExtent: Image.asset(imagename).height == null? 3600 : Image.asset(imagename).height!,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index){
return Image.asset(imagename, width: MediaQuery.of(context).size.width,);
},
childCount: 1,
),
),
],
);
},
),
);
}
else if(page.label=="最近购买"){
return SafeArea(
top: false,
bottom: false,
child: TolyExpandTile(),
);
}
else{
return SafeArea(
top: false,
bottom: false,
child: Container(
height: 1000,
)
);
}
}).toList()
),
Positioned(
width: ScreenUtil().setWidth(1070),
height: ScreenUtil().setHeight(110),
bottom: 0,
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Color(0xFFe5e5e5), width: 1),
),
color: Colors.white,
),
child: Row(
children: [
Container(
padding: EdgeInsets.only(top: ScreenUtil().setHeight(10)),
width: 60,
height: ScreenUtil().setHeight(88),
child:
InkWell(
onTap: () {
Fluttertoast.showToast(
msg: "正在建设中22222222...",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
// timeInSecForIos:1
);
},
child:Column(
children: <Widget>[
Icon(
Icons.message,
size: 15,
),
Text('联系客服', style: new TextStyle(fontSize: 12.0,
color:const Color(0xFF666666)))
],
),
),
),
Container(
padding: EdgeInsets.only(top: ScreenUtil().setHeight(10)),
width: 60,
height: ScreenUtil().setHeight(88),
child:
InkWell(
onTap: () {
Fluttertoast.showToast(
msg: "正在建设中22222222...",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
// timeInSecForIos:1
);
},
child:Column(
children: <Widget>[
Icon(
Icons.message,
size: 15,
),
Text('联系客服', style: new TextStyle(fontSize: 12.0,
color:const Color(0xFF666666)))
],
),
),
),
Container(
padding: EdgeInsets.only(top: ScreenUtil().setHeight(10)),
width: 60,
height: ScreenUtil().setHeight(88),
child:
InkWell(
onTap: () {
Fluttertoast.showToast(
msg: "正在建设中22222222...",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
// timeInSecForIos:1
);
},
child:Column(
children: <Widget>[
Icon(
Icons.message,
size: 15,
),
Text('联系客服', style: new TextStyle(fontSize: 12.0,
color:const Color(0xFF666666)))
],
),
),
),
Expanded(
flex: 1,
child: ElevatedButton (
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Color.fromRGBO(253, 1, 0, 0.9)),
),
child: Text('加入购物车'),
onPressed: () {
Fluttertoast.showToast(
msg: "正在建设中...",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
// timeInSecForIos:1
);
},
),
),
],
),
),
),
],
),
),
),
);
}
}
CardTry.dart
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:flutter/physics.dart';
class _CardInfo {
_CardInfo({
required this.images,
required this.who,
required this.saywhat,
required this.time,
});
final String images;
final String who;
final String saywhat;
final String time;
}
List <_CardInfo> cardinfo=[
_CardInfo(
images:'assets/images/feedback1.jpg',
who: "yonghu1",
saywhat: "saywhat1",
time: "time1"
),
_CardInfo(
images:'assets/images/feedback2.jpg',
who: "yonghu1",
saywhat: "saywhat1",
time: "time1"
),
_CardInfo(
images:'assets/images/feedback3.jpg',
who: "yonghu1",
saywhat: "saywhat1",
time: "time1"
),
_CardInfo(
images:'assets/images/feedback4.jpg',
who: "yonghu1",
saywhat: "saywhat1",
time: "time1"
),
_CardInfo(
images:'assets/images/feedback5.jpg',
who: "yonghu1",
saywhat: "saywhat1",
time: "time1"
),
];
// 图片
// List<String> images = [
// 'assets/images/feedback1.jpg',
// 'assets/images/feedback2.jpg',
// 'assets/images/feedback3.jpg',
// 'assets/images/feedback4.jpg',
// 'assets/images/feedback5.jpg',
// ];
// 生成卡片数组
List<Widget> cards = List.generate(
cardinfo.length,
(int index) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.0),
boxShadow: [
BoxShadow(
offset: Offset(0, 17),
blurRadius: 23.0,
spreadRadius: -13.0,
color: Colors.black54,
)
],
),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Image.asset(
cardinfo[index].images,
fit: BoxFit.cover,
),
),
Align(
child: ClipRRect(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16.0)),
child: ListTile(
title: Text(cardinfo[index].who, style: TextStyle(fontSize: 18, color: Colors.black)),
subtitle: RichText(
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
text: TextSpan(
children: <TextSpan>[
TextSpan(
text: cardinfo[index].saywhat,
style: TextStyle(fontSize: 18, color: Colors.black),
),
TextSpan(
text: cardinfo[index].time,
style: TextStyle(fontSize: 16, height: 2),
),
]
),
),
trailing: Icon(Icons.more_vert),
isThreeLine: true,
),
),
alignment: Alignment.bottomCenter,
),
]
)
);
},
);
void main() {
// 使用生成的卡片数组
runApp(CardTry(cards: cards));
}
/// 卡片尺寸
class CardSizes {
static Size front(BoxConstraints constraints) {
return Size(constraints.maxWidth * 0.9, constraints.maxHeight * 0.9);
}
static Size middle(BoxConstraints constraints) {
return Size(constraints.maxWidth * 0.85, constraints.maxHeight * 0.9);
}
static Size back(BoxConstraints constraints) {
return Size(constraints.maxWidth * 0.8, constraints.maxHeight * .9);
}
}
/// 卡片位置
class CardAlignments {
static Alignment front = Alignment(0.0, -0.5);
static Alignment middle = Alignment(0.0, 0.0);
static Alignment back = Alignment(0.0, 0.5);
}
/// 卡片运动动画
class CardAnimations {
/// 最前面卡片的消失动画值
static Animation<Alignment> frontCardDisappearAnimation(
AnimationController parent,
Alignment beginAlignment,
) {
return AlignmentTween(
begin: beginAlignment,
end: Alignment(
beginAlignment.x > 0
? beginAlignment.x + 30.0
: beginAlignment.x - 30.0,
0.0,
),
).animate(
CurvedAnimation(
parent: parent,
curve: Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
}
/// 中间卡片位置变换动画值
static Animation<Alignment> middleCardAlignmentAnimation(
AnimationController parent,
) {
return AlignmentTween(
begin: CardAlignments.middle,
end: CardAlignments.front,
).animate(
CurvedAnimation(
parent: parent,
curve: Interval(0.2, 0.5, curve: Curves.easeIn),
),
);
}
/// 中间卡片尺寸变换动画值
static Animation middleCardSizeAnimation(
AnimationController parent,
BoxConstraints constraints,
) {
return SizeTween(
begin: CardSizes.middle(constraints),
end: CardSizes.front(constraints),
).animate(
CurvedAnimation(
parent: parent,
curve: Interval(0.2, 0.5, curve: Curves.easeIn),
),
);
}
/// 最后面卡片位置变换动画值
static Animation<Alignment> backCardAlignmentAnimation(
AnimationController parent,
) {
return AlignmentTween(
begin: CardAlignments.back,
end: CardAlignments.middle,
).animate(
CurvedAnimation(
parent: parent,
curve: Interval(0.4, 0.7, curve: Curves.easeIn),
),
);
}
/// 最后面卡片尺寸变换动画值
static Animation backCardSizeAnimation(
AnimationController parent,
BoxConstraints constraints,
) {
return SizeTween(
begin: CardSizes.back(constraints),
end: CardSizes.middle(constraints),
).animate(
CurvedAnimation(
parent: parent,
curve: Interval(0.4, 0.7, curve: Curves.easeIn),
),
);
}
}
class CardTry extends StatefulWidget {
final List<Widget> cards;
const CardTry({
required this.cards});
_CardTryState createState() => _CardTryState();
}
class _CardTryState extends State<CardTry> with TickerProviderStateMixin {
// 卡片列表
final List<Widget> _cards = [];
// 最前面卡片的索引
int _frontCardIndex = 0;
// 保存最前面卡片的定位
Alignment _frontCardAlignment = Alignment(0.0, -0.5);
// 保存最前面卡片的旋转角度
double _frontCardRotation = 0.0;
// 卡片回弹动画
late Animation<Alignment> _reboundAnimation;
// 卡片回弹动画控制器
late AnimationController _reboundController;
// 卡片位置变换动画控制器
late AnimationController _cardChangeController;
// 前面的卡片,使用 Align 定位
Widget _frontCard(BoxConstraints constraints) {
// 判断是否还有卡片
Widget card =
_frontCardIndex < _cards.length ? _cards[_frontCardIndex] : Container();
// 判断动画是否在运行
bool forward = _cardChangeController.status == AnimationStatus.forward;
// 使用 Transform.rotate 旋转卡片
Widget rotate = Transform.rotate(
angle: (pi / 180.0) * _frontCardRotation,
// 使用 SizedBox 确定卡片尺寸
child: SizedBox.fromSize(
size: CardSizes.front(constraints),
child: card,
),
);
// 在动画运行时使用动画值
if (forward) {
return Align(
alignment: CardAnimations.frontCardDisappearAnimation(
_cardChangeController,
_frontCardAlignment,
).value,
child: rotate,
);
}
// 否则使用默认值
return Align(
alignment: _frontCardAlignment,
child: rotate,
);
}
// 中间的卡片,使用 Align 定位
Widget _middleCard(BoxConstraints constraints) {
// 判断是否还有两张卡片
Widget card = _frontCardIndex < _cards.length - 1
? _cards[_frontCardIndex + 1]
: Container();
// 判断动画是否在运行
bool forward = _cardChangeController.status == AnimationStatus.forward;
// 在动画运行时使用动画值
if (forward) {
return Align(
alignment: CardAnimations.middleCardAlignmentAnimation(
_cardChangeController,
).value,
child: card
);
}
// 否则使用默认值
return Align(
alignment: CardAlignments.middle,
child: SizedBox.fromSize(
size: CardSizes.middle(constraints),
child: card,
),
);
}
// 后面的卡片,使用 Align 定位
Widget _backCard(BoxConstraints constraints) {
// 判断数组中是否还有三张卡片
Widget card = _frontCardIndex < _cards.length - 2
? _cards[_frontCardIndex + 2]
: Container();
// 判断动画是否在运行
bool forward = _cardChangeController.status == AnimationStatus.forward;
// 在动画运行时使用动画值
if (forward) {
return Align(
alignment: CardAnimations.backCardAlignmentAnimation(
_cardChangeController,
).value,
child: card
);
}
// 否则使用默认值
return Align(
alignment: CardAlignments.back,
child: SizedBox.fromSize(
size: CardSizes.back(constraints),
child: card,
),
);
}
// 改变位置的动画
void _runChangeOrderAnimation() {
_cardChangeController.reset();
_cardChangeController.forward();
}
// 卡片回弹的动画
void _runReboundAnimation(Offset pixelsPerSecond, Size size) {
// 创建动画值
_reboundAnimation = _reboundController.drive(
AlignmentTween(
// 起始值是卡片当前位置,最终值是卡片的默认位置
begin: _frontCardAlignment,
end: Alignment(0.0, -0.5),
),
);
// 计算卡片运动速度
final double unitsPerSecondX = pixelsPerSecond.dx / size.width;
final double unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
// 创建弹簧模拟的定义
const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1);
// 创建弹簧模拟
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
// 根据给定的模拟运行动画
_reboundController.animateWith(simulation);
// 重置旋转值
_frontCardRotation = 0.0;
setState(() {
});
}
void initState() {
super.initState();
// 初始化卡片数组
_cards.addAll(widget.cards);
// 初始化回弹的动画控制器
_reboundController = AnimationController(vsync: this)
..addListener(() {
setState(() {
// 动画运行时更新最前面卡片的 alignment 属性
_frontCardAlignment = _reboundAnimation.value;
});
});
// 初始化卡片换位动画控制器
_cardChangeController = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this,
)
..addListener(() => setState(() {
}))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 动画结束后将最前面卡片的索引向前移动一位
_frontCardIndex++;
// 动画运行结束后重置位置和旋转
_frontCardRotation = 0.0;
_frontCardAlignment = CardAlignments.front;
setState(() {
});
}
});
}
Widget build(BuildContext context) {
return MaterialApp(
title: 'TCards demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: SizedBox(
width: 350,
height: 590,
child: LayoutBuilder(
builder: (context, constraints) {
// 使用 LayoutBuilder 获取容器的尺寸,传个子项计算卡片尺寸
Size size = MediaQuery.of(context).size;
double speed = 10.0;
// 卡片横轴距离限制
final double limit = 5.0;
return Stack(
children: [
// 后面的子项会显示在上面,所以前面的卡片放在最后
_backCard(constraints),
_middleCard(constraints),
_frontCard(constraints),
// 使用一个占满父元素的 GestureDetector 监听手指移动
// 如果动画在运行中就不在响应手势
_cardChangeController.status != AnimationStatus.forward
?
SizedBox.expand(
child: GestureDetector(
onPanDown: (DragDownDetails details) {
},
onPanUpdate: (DragUpdateDetails details) {
// 手指移动就更新最前面卡片的 alignment 属性
_frontCardAlignment += Alignment(
details.delta.dx / (size.width / 4) * speed,
details.delta.dy / (size.height / 4) * speed,
);
// 设置最前面卡片的旋转角度
_frontCardRotation = _frontCardAlignment.x;
// setState 更新界面
setState(() {
});
},
onPanEnd: (DragEndDetails details) {
// 如果最前面的卡片横轴移动距离超过限制就运行换位动画,否则运行回弹动画
if (_frontCardAlignment.x > limit ||
_frontCardAlignment.x < -limit) {
_runChangeOrderAnimation();
} else {
_runReboundAnimation(
details.velocity.pixelsPerSecond,
size,
);
}
},
),
)
: IgnorePointer(),
],
);
},
),
),
),
),
);
}
}
recentPurchase.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:toggle_rotate/toggle_rotate.dart';
class _RecentBuy {
_RecentBuy({
required this.images,
required this.name,
required this.count,
required this.getprice,
required this.time,
});
final String images;
final String name;
final double getprice;
final int count;
final String time;
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({
super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TolyExpandTile(),
);
}
}
class TolyExpandTile extends StatefulWidget {
_TolyExpandTileState createState() => _TolyExpandTileState();
}
class _TolyExpandTileState extends State<TolyExpandTile> with SingleTickerProviderStateMixin {
var _crossFadeState = CrossFadeState.showFirst;
bool get isFirst => _crossFadeState == CrossFadeState.showFirst;
List<_RecentBuy> rec = [
_RecentBuy(
images: 'assets/images/bg1.png',
name: '爱*想',
getprice: 4.50,
count: 1,
time: '1分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '1分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '1分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '2分钟前',
),
_RecentBuy(
images: 'assets/images/hjh1.jpg',
name: 's*w',
getprice: 8.99,
count: 2,
time: '2分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '7*H',
getprice: 4.50,
count: 1,
time: '4分钟前',
),
_RecentBuy(
images: 'assets/images/hjh5.jpg',
name: '6*6',
getprice: 4.50,
count: 1,
time: '4分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '撑*铖',
getprice: 4.50,
count: 1,
time: '4分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '事*发',
getprice: 4.52,
count: 1,
time: '5分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '5分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '6分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '6分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 9.04,
count: 2,
time: '9分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '10分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '10分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '10分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '10分钟前',
),
_RecentBuy(
images: 'assets/images/tongyi4.png',
name: '爱*饿',
getprice: 4.50,
count: 1,
time: '12分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '鳄*虚',
getprice: 4.50,
count: 1,
time: '12分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '我*去',
getprice: 4.50,
count: 1,
time: '13分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 8.99,
count: 2,
time: '13分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '14分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '二*粉',
getprice: 4.50,
count: 1,
time: '16分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '20分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 8.89,
count: 2,
time: '20分钟前',
),
_RecentBuy(
images: 'assets/images/bg1.png',
name: '如*9',
getprice: 4.50,
count: 1,
time: '24分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 17.59,
count: 4,
time: '25分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '的*2',
getprice: 4.50,
count: 1,
time: '29分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '小*我',
getprice: 8.69,
count: 2,
time: '40分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.49,
count: 1,
time: '41分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 8.49,
count: 2,
time: '41分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.49,
count: 1,
time: '43分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '匿*户',
getprice: 4.50,
count: 1,
time: '46分钟前',
),
_RecentBuy(
images: 'assets/images/comm7.jpg',
name: '疯*疯',
getprice: 4.50,
count: 1,
time: '51分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '狂*狂',
getprice: 4.50,
count: 1,
time: '52分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '星*星',
getprice: 4.50,
count: 1,
time: '52分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '期*期',
getprice: 4.50,
count: 1,
time: '54分钟前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '四*四',
getprice: 4.50,
count: 1,
time: '1小时前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: 'V*V',
getprice: 4.49,
count: 1,
time: '1小时前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '我*我',
getprice: 4.49,
count: 1,
time: '1小时前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '5*5',
getprice: 4.49,
count: 1,
time: '1小时前',
),
_RecentBuy(
images: 'assets/images/none.png',
name: '0*0',
getprice: 4.49,
count: 1,
time: '1小时前',
),
];
List<Widget> smallline(){
List<Widget> ret = [];
for(var i=0; i<4;i++){
ret.add(
Padding(
padding: EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CircleAvatar(
backgroundImage: AssetImage(rec[i].images),
radius: 15,
),
Text(
" ${
rec[i].name}",
),
],
),
Text(
"¥${
rec[i].getprice}",
),
Text(
"${
rec[i].time}",
),
],
),
)
);
}
return ret;
}
Widget bigline(){
List<Widget> ret = [];
for(var i=0; i<rec.length;i++){
ret.add(
Padding(
padding: EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CircleAvatar(
backgroundImage: AssetImage(rec[i].images),
radius: 15,
),
Text(
" ${
rec[i].name}",
),
],
),
Text(
"¥${
rec[i].getprice}",
),
Text(
"${
rec[i].time}",
),
],
),
)
);
}
return Column(
children: ret,
);
}
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(20,70,20,90),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.circular(10)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(15,20,15,20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"最近购买(4.7万+)",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700
),
),
Row(
children: [
Text(
"全部 ",
style: TextStyle(
color: Colors.grey
),
),
ToggleRotate(
curve: Curves.decelerate,
rad: pi/2,
durationMs: 400,//动画时长
clockwise: false, //是否是顺时针
child: Icon(Icons.code,size: 30,color: Colors.grey),
onTap: _togglePanel,
),
],
),
],
),
),
_buildPanel()
],
),
),
);
}
void _togglePanel() {
setState(() {
_crossFadeState =
!isFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond;
});
}
Widget _buildPanel() => AnimatedCrossFade(
firstCurve: Curves.easeInCirc,
secondCurve: Curves.easeInToLinear,
firstChild: Container(
// color: Colors.cyan,
child: Column(
children: smallline(),
),
),
secondChild:
// Container(
// color: Colors.blue,
// height: 400,
// child: Column(
// children: smallline(),
// ),
// ),
Container(
height: 540,
child: CustomScrollView(
scrollDirection: Axis.vertical,
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[bigline()]
)
)
],
),
),
duration: Duration(milliseconds: 400),
crossFadeState: _crossFadeState,
);
}
转载:https://blog.csdn.net/Hjh1906008151/article/details/128764910
查看评论