Flutter移动应用开发 - 商品界面

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

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,
  );
}

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6