【Flutter】交错动画&自定义动画&Hero动画

🔥 本文由 程序喵正在路上 原创,CSDN首发!
💖 系列专栏:Flutter学习
🌠 首发时间:2024年5月29日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾

交错动画

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));

    _controller.addListener(() {
      print(_controller.value);
    });
  }

  
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  void _toggleAnimation() {
    if (_controller.status == AnimationStatus.completed) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _toggleAnimation,
        child: const Icon(Icons.refresh),
      ),
      appBar: AppBar(
        title: const Text('AnimatedIcon'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            SlidingBox(
              controller: _controller,
              color: Colors.blue[200],
              curve: const Interval(0, 0.2),
            ),
            SlidingBox(
              controller: _controller,
              color: Colors.blue[400],
              curve: const Interval(0.2, 0.4),
            ),
            SlidingBox(
              controller: _controller,
              color: Colors.blue[600],
              curve: const Interval(0.4, 0.6),
            ),
            SlidingBox(
              controller: _controller,
              color: Colors.blue[800],
              curve: const Interval(0.6, 0.8),
            ),
            SlidingBox(
              controller: _controller,
              color: Colors.blue[900],
              curve: const Interval(0.8, 1),
            ),
          ],
        ),
      ),
    );
  }
}

class SlidingBox extends StatelessWidget {
  final AnimationController controller;
  final Color? color;
  final Curve curve;
  const SlidingBox(
      {super.key,
      required this.controller,
      required this.color,
      required this.curve});

  
  Widget build(BuildContext context) {
    return SlideTransition(
      position: Tween(begin: const Offset(-0.2, 1), end: const Offset(0.3, 0))
          .chain(CurveTween(curve: Curves.bounceInOut))
          .chain(CurveTween(curve: curve))
          .animate(controller),
      child: Container(
        width: 220,
        height: 60,
        color: color,
      ),
    );
  }
}

在这里插入图片描述

自定义动画

TweenAnimationBuilder自定义隐式动画

每当 Tweenend 发生变化的时候就会触发动画。

大小变化的动画:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool flag = true;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('大小变化'),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.refresh),
        onPressed: () {
          setState(() {
            flag = !flag;
          });
        },
      ),
      body: Center(
        child: TweenAnimationBuilder(
          tween: Tween(begin: 100.0, end: flag ? 100.0 : 200.0),
          duration: const Duration(seconds: 1),
          builder: ((context, value, child) {
            return Icon(
              Icons.star,
              color: Colors.red,
              size: value,
            );
          }),
        ),
      ),
    );
  }
}

在这里插入图片描述

效果:点击浮动按钮,五角星的大小会变化。

透明度变化的动画:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool flag = true;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('透明度变化'),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.refresh),
        onPressed: () {
          setState(() {
            flag = !flag;
          });
        },
      ),
      body: Center(
        child: TweenAnimationBuilder(
          tween: Tween(begin: 0.0, end: flag ? 0.2 : 1.0),
          duration: const Duration(seconds: 1),
          builder: ((context, value, child) {
            return Opacity(
              opacity: value,
              child: Container(color: Colors.blue, width: 200, height: 200),
            );
          }),
        ),
      ),
    );
  }
}

在这里插入图片描述

效果:点击浮动按钮,盒子的透明度会变化。

AnimatedBuilder自定义显式动画

透明度动画:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..repeat(reverse: true); //.. 连缀操作
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('透明度动画'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            return Opacity(
              opacity: _controller.value,	//从0到1变化	
              child: Container(
                width: 200,
                height: 200,
                color: Colors.blue,
                child: const Text('我是一个Text组件'),
              ),
            );
          },
        ),
      ),
    );
  }
}

在这里插入图片描述

效果:盒子的透明度会自动不停变化。

自定义变化范围:

上面代码中 opacity 的值我们也可以使用 Tween 来设置:

opacity: Tween(begin: 0.5, end: 1.0).animate(_controller).value, //从0.5到1变化

位置变化:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..repeat(reverse: true); //.. 连缀操作
  }

  
  Widget build(BuildContext context) {
    Animation y = Tween(begin: -120.0, end: 120.0)
        .chain(CurveTween(curve: Curves.easeIn))
        // .chain(CurveTween(curve: const Interval(0.2, 0.6)))
        .animate(_controller);

    return Scaffold(
      appBar: AppBar(
        title: const Text('位置变化'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            return Container(
              width: 200,
              height: 200,
              color: Colors.blue,
              transform: Matrix4.translationValues(0, y.value, 0),
              child: const Text('我是一个Text组件'),
            );
          },
        ),
      ),
    );
  }
}

在这里插入图片描述

效果:一个盒子在不停上下跳动。

child优化:

return Scaffold(
  appBar: AppBar(
    title: const Text('child优化'),
  ),
  body: Center(
    child: AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget? child) {
        return Container(
          width: 200,
          height: 200,
          color: Colors.blue,
          transform: Matrix4.translationValues(0, y.value, 0),
          child: child,
        );
      },
      child: const Text('我是一个Text组件'),
    ),
  ),
);

当我们将 Text 组件放在 builder 函数内部时,Text 组件会根据 _animation 的值进行重建。这意味着在每个动画帧上,Text 组件都会被重新构建,即使 Text 内容没有发生变化。这可能会导致不必要的重建和性能损失。

相比之下,将 Text 组件放在 builder 函数外部,则不会在每个动画帧上进行重建。Text 组件只会在初始渲染时创建一次,并且不会随着动画的进度而重建。这样可以减少重建次数,提高性能。

因此,将 Text 组件放在 builder 函数外部是一种更优化的做法,特别是当 Text 内容不会随动画进度而改变时。只有当动画进度对 Text 内容有影响时,才需要将 Text 组件放在 builder 函数内部,以确保 Text 能够根据动画的进度进行更新。

Hero动画

Hero动画的应用一

微信朋友圈点击小图片的时候会有一个动画效果到大图预览,这个动画效果就可以使用Hero 动画实现。

Hero 指的是可以在路由(页面)之间 “飞行” 的 widget,简单来说 Hero 动画就是在路由切换时,有一个共享的 widget 可以在新旧路由间切换。

我们回到之前写的自定义底部导航实现页面切换的代码:

在这里插入图片描述

我们将 home.dart 进行改进,并添加一个 hero.dart 用于演示 Hero 动画,具体代码如下:

main.dart

import 'package:flutter/material.dart';
import './routers/router.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      initialRoute: "/",
      onGenerateRoute: onGenerateRoute,
    );
  }
}

home.dart

import 'package:flutter/material.dart';
import '../../res/listData.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<Widget> _getListData() {
    var tempList = listData.map((value) {
      return GestureDetector(
        onTap: () {
          Navigator.pushNamed(context, "/hero", arguments: {
            "imageUrl": value['imageUrl'],
            "author": value['author'],
          });
        },
        child: Container(
          decoration: BoxDecoration(
            border: Border.all(
                color: const Color.fromRGBO(233, 233, 233, 0.9), width: 1),
          ),
          child: Column(
            children: [
              Hero(
                tag: value['imageUrl'],	//唯一值
                child: Image.network(value['imageUrl']),
              ),
              const SizedBox(height: 12),
              Text(
                value['title'],
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 17),
              ),
            ],
          ),
        ),
      );
    });

    return tempList.toList();
  }

  
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 2, //一行的Widget数量
      crossAxisSpacing: 10.0, //水平方向的子Widget之间的间距
      mainAxisSpacing: 10.0, //垂直方向的子Widget之间的间距
      padding: const EdgeInsets.all(10),
      children: _getListData(),
    );
  }
}

hero.dart

import 'package:flutter/material.dart';

class HeroPage extends StatefulWidget {
  final Map arguments;
  const HeroPage({super.key, required this.arguments});

  
  State<HeroPage> createState() => _HeroPageState();
}

class _HeroPageState extends State<HeroPage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('详情页面'),
      ),
      body: ListView(
        children: [
          Hero(
            tag: widget.arguments['imageUrl'],	//tag值要与home.dart中一致
            child: Image.network(widget.arguments['imageUrl']),
          ),
          const SizedBox(height: 20),
          Padding(
            padding: const EdgeInsets.fromLTRB(30, 5, 30, 0),
            child: Text(
              widget.arguments['author'],
              style: const TextStyle(fontSize: 22),
            ),
          ),
        ],
      ),
    );
  }
}

router.dart

import 'package:flutter/cupertino.dart';
import '../pages/hero.dart';
import '../pages/tabs/category.dart';
import '../pages/tabs/home.dart';
import '../pages/tabs/message.dart';
import '../pages/tabs/setting.dart';
import '../pages/tabs/user.dart';
import '../pages/tabs.dart';

//1. 定义路由
final Map routes = {
  "/": (context) => const Tabs(),
  "/home": (context) => const HomePage(),
  "/category": (context) => const CategoryPage(),
  "/setting": (context) => const SettingPage(),
  "/message": (context) => const MessagePage(),
  "/user": (context) => const UserPage(),
  "/hero": (context, {arguments}) => HeroPage(arguments: arguments)
};

//2. 配置onGenerateRoute,固定写法
var onGenerateRoute = (settings) {
  // 统一处理
  final String? name = settings.name;
  final Function? pageContentBuilder = routes[name];
  if (pageContentBuilder != null) {
    if (settings.arguments != null) {
      final Route route = CupertinoPageRoute(
          builder: (context) =>
              pageContentBuilder(context, arguments: settings.arguments));
      return route;
    } else {
      final Route route =
          CupertinoPageRoute(builder: (context) => pageContentBuilder(context));
      return route;
    }
  }
  return null;
};

在这里插入图片描述

点击图片会进入对应的详情页面:

在这里插入图片描述

在这里插入图片描述

Hero动画的应用二

hero.dart 换成这个:

import 'package:flutter/material.dart';

class HeroPage extends StatefulWidget {
  final Map arguments;
  const HeroPage({super.key, required this.arguments});

  
  State<HeroPage> createState() => _HeroPageState();
}

class _HeroPageState extends State<HeroPage> {
  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.pop(context);
      },
      child: Hero(
        tag: widget.arguments['imageUrl'],
        child: Scaffold(
          //加Scaffold是为了点击屏幕任意位置都可以返回
          backgroundColor: Colors.black,
          body: Center(
            child: AspectRatio(
              aspectRatio: 16 / 9,
              child: Image.network(
                widget.arguments['imageUrl'],
                fit: BoxFit.cover,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

配置Hero动画的执行时间

  1. 引入 scheduler.dart

    import 'package:flutter/scheduler.dart';
    
  2. 设置动画时间

    void initState() {
      super.initState();
      timeDilation = 1.0; //设置动画时间
    }
    

Hero+photo_view实现类似微信朋友圈图片预览

photo_view预览单张图片

  1. 配置依赖

    dependencies:
      photo_view: ^0.15.0
    
  2. 引入

    import 'package:photo_view/photo_view.dart';
    
  3. 单张图片的预览

    改进 hero.dart

    import 'package:flutter/material.dart';
    import 'package:photo_view/photo_view.dart';
    
    class HeroPage extends StatefulWidget {
      final Map arguments;
      const HeroPage({super.key, required this.arguments});
    
      
      State<HeroPage> createState() => _HeroPageState();
    }
    
    class _HeroPageState extends State<HeroPage> {
      
      Widget build(BuildContext context) {
        return GestureDetector(
          onTap: () {
            Navigator.pop(context);
          },
          child: Hero(
            tag: widget.arguments['imageUrl'],
            child: Scaffold(
              //加Scaffold是为了点击屏幕任意位置都可以返回
              backgroundColor: Colors.black,
              body: Center(
                child: AspectRatio(
                    aspectRatio: 16 / 9,
                    child: PhotoView(
                      imageProvider: NetworkImage(widget.arguments['imageUrl']),
                    )),
              ),
            ),
          ),
        );
      }
    }
    

photo_view预览多张图片

  1. 配置依赖

    dependencies:
      photo_view: ^0.15.0
    
  2. 引入

    import 'package:photo_view/photo_view_gallery.dart';
    
  3. 多张图片的预览

    改造 listData,加个属性:

    List listData = [
      {
        "id": 0,
        "title": 'Candy Shop',
        "author": 'Mohamed Chahin',
        "imageUrl": 'https://www.itying.com/images/flutter/1.png',
      },
      {
        "id": 1,
        "title": 'Childhood in a picture',
        "author": 'Google',
        "imageUrl": 'https://www.itying.com/images/flutter/2.png',
      },
      {
        "id": 2,
        "title": 'Alibaba Shop',
        "author": 'Alibaba',
        "imageUrl": 'https://www.itying.com/images/flutter/3.png',
      },
      {
        "id": 3,
        "title": 'Candy Shop',
        "author": 'Mohamed Chahin',
        "imageUrl": 'https://www.itying.com/images/flutter/4.png',
      },
      {
        "id": 4,
        "title": 'Tornado',
        "author": 'Mohamed Chahin',
        "imageUrl": 'https://www.itying.com/images/flutter/5.png',
      },
      {
        "id": 5,
        "title": 'Undo',
        "author": 'Mohamed Chahin',
        "imageUrl": 'https://www.itying.com/images/flutter/6.png',
      },
      {
        "id": 6,
        "title": 'white-dragon',
        "author": 'Mohamed Chahin',
        "imageUrl": 'https://www.itying.com/images/flutter/7.png',
      }
    ];
    

    home.dart 中需要传入 hero.dart 需要的参数:

    在这里插入图片描述
    hero.dart

    import 'package:flutter/material.dart';
    import 'package:photo_view/photo_view_gallery.dart';
    
    class HeroPage extends StatefulWidget {
      final Map arguments;
      const HeroPage({super.key, required this.arguments});
    
      
      State<HeroPage> createState() => _HeroPageState();
    }
    
    class _HeroPageState extends State<HeroPage> {
      late List listData = [];
      late int initialPage;
    
      
      void initState() {
        super.initState();
        listData = widget.arguments['listData'];
        initialPage = widget.arguments['initialPage'];
      }
    
      
      Widget build(BuildContext context) {
        return GestureDetector(
          onTap: () {
            Navigator.pop(context);
          },
          child: Hero(
            tag: widget.arguments['imageUrl'],
            child: Scaffold(
              //加Scaffold是为了点击屏幕任意位置都可以返回
              backgroundColor: Colors.black,
              body: Center(
                child: PhotoViewGallery.builder(
                  itemCount: listData.length,
                  pageController:
                      PageController(initialPage: initialPage), //点击后显示点击的图片
                  builder: ((context, index) {
                    return PhotoViewGalleryPageOptions(
                        imageProvider: NetworkImage(listData[index]["imageUrl"]));
                  }),
                ),
              ),
            ),
          ),
        );
      }
    }
    

    可以实现点击哪张图片就预览哪张图片,同时可以左右滑动切换图片。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序喵正在路上

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值