Flutter 动画(显式动画、隐式动画、Hero动画、页面转场动画、交错动画)

前言

当前案例 Flutter SDK版本:3.13.2

显式动画

Tween({this.begin,this.end}) 两个构造参数,分别是 开始值结束值,根据这两个值,提供了控制动画的方法,以下是常用的;

  • controller.forward() : 向前,执行 begin 到 end 的动画,执行结束后,处于end状态;
  • controller.reverse() : 反向,当动画已经完成,进行还原动画
  • controller.reset() : 重置,当动画已经完成,进行还原,注意这个是直接还原没有动画

使用方式一

使用 addListener()setState()

import 'package:flutter/material.dart';

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

  @override
  State<TweenAnimation> createState() => _TweenAnimationState();
}

/// 使用 addListener() 和 setState()
class _TweenAnimationState extends State<TweenAnimation>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = Tween<double>(begin: 50, end: 100).animate(controller)
      ..addListener(() {
        setState(() {}); // 更新UI
      })..addStatusListener((status) {
        debugPrint('status:$status'); // 监听动画执行状态
      });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        '显式动画',
        style: TextStyle(fontSize: 20),
      )),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                SizedBox(
                  width: animation.value,
                  height: animation.value,
                  child: const FlutterLogo(),
                ),
                ElevatedButton(
                  onPressed: () {
                    if (controller.isCompleted) {
                      controller.reverse();
                    } else {
                      controller.forward();
                    }
                    // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                    // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                    // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                  },
                  child: const Text('缩放'),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

使用方式二

AnimatedWidget,解决痛点:不需要再使用 addListener()setState()

import 'package:flutter/material.dart';

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

  @override
  State<TweenAnimation> createState() => _TweenAnimationState();
}

/// 测试 AnimatedWidget
class _TweenAnimationState extends State<TweenAnimation>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    animation = Tween<double>(begin: 50, end: 100).animate(controller)
      ..addStatusListener((status) {
        debugPrint('status:$status'); // 监听动画执行状态
      });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        '显式动画',
        style: TextStyle(fontSize: 20),
      )),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AnimatedLogo(animation: animation),
                ElevatedButton(
                  onPressed: () {
                    if (controller.isCompleted) {
                      controller.reverse();
                    } else {
                      controller.forward();
                    }
                    // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                    // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                    // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                  },
                  child: const Text('缩放'),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

/// 使用 AnimatedWidget,创建显式动画
/// 解决痛点:不需要再使用 addListener() 和 setState()
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
      : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        width: animation.value,
        height: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

使用方式三

使用 内置的显式动画 widget

后缀是 Transition 的组件,几乎都是 显式动画 widget

import 'package:flutter/material.dart';

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

  @override
  State<TweenAnimation> createState() => _TweenAnimationState();
}

/// 使用 内置的显式动画Widget
class _TweenAnimationState extends State<TweenAnimation>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);
    animation = Tween<double>(begin: 0.1, end: 1.0).animate(controller)
      ..addListener(() {
        setState(() {}); // 更新UI
      })
      ..addStatusListener((status) {
        debugPrint('status:$status'); // 监听动画执行状态
      });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        '显式动画',
        style: TextStyle(fontSize: 20),
      )),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                /// 单个显示动画
                FadeTransition(
                  opacity: animation,
                  child: const SizedBox(
                    width: 100,
                    height: 100,
                    child: FlutterLogo(),
                  ),
                ),

                /// 多个显示动画 配合使用
                // FadeTransition( // 淡入淡出
                //   opacity: animation,
                //   child: RotationTransition( // 旋转
                //     turns: animation,
                //     child: ScaleTransition( // 更替
                //       scale: animation,
                //       child: const SizedBox(
                //         width: 100,
                //         height: 100,
                //         child: FlutterLogo(),
                //       ),
                //     ),
                //   ),
                // ),
                ElevatedButton(
                  onPressed: () {
                    if (controller.isCompleted) {
                      controller.reverse();
                    } else {
                      controller.forward();
                    }
                    // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                    // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                    // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                  },
                  child: const Text('淡入淡出'),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

使用方式四

AnimatedBuilder,这种方式感觉是 通过逻辑 动态选择 Widget,比如 flag ? widgetA : widgetB

官方解释:

  • AnimatedBuilder 知道如何渲染过渡效果
  • 但 AnimatedBuilder 不会渲染 widget,也不会控制动画对象。
  • 使用 AnimatedBuilder 描述一个动画是其他 widget 构建方法的一部分。
  • 如果只是单纯需要用可重复使用的动画定义一个 widget,可参考文档:简单使用 AnimatedWidget。
import 'package:flutter/material.dart';

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

  @override
  State<TweenAnimation> createState() => _TweenAnimationState();
}

/// 测试 AnimatedBuilder
class _TweenAnimationState extends State<TweenAnimation>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = Tween<double>(begin: 50, end: 100).animate(controller)
      ..addStatusListener((status) {
        debugPrint('status:$status'); // 监听动画执行状态
      });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
            '显式动画',
            style: TextStyle(fontSize: 20),
          )),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                GrowTransition(
                    animation: animation,
                    child: const FlutterLogo()),
                ElevatedButton(
                  onPressed: () {
                    if (controller.isCompleted) {
                      controller.reverse();
                    } else {
                      controller.forward();
                    }
                    // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                    // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                    // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                  },
                  child: const Text('缩放'),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

class GrowTransition extends StatelessWidget {
  final Widget child;
  final Animation<double> animation;

  const GrowTransition(
      {required this.child, required this.animation, super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            width: animation.value,
            height: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

使用方式五

CurvedAnimation 曲线动画,一个Widget,同时使用多个动画;

import 'package:flutter/material.dart';

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

  @override
  State<TweenAnimation> createState() => _TweenAnimationState();
}

/// 测试 动画同步使用
class _TweenAnimationState extends State<TweenAnimation>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((status) {
        debugPrint('status:$status'); // 监听动画执行状态
      });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        '显式动画',
        style: TextStyle(fontSize: 20),
      )),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AnimatedLogoSync(animation: animation),
                ElevatedButton(
                  onPressed: () {
                    if (controller.isCompleted) {
                      controller.reverse();
                    } else {
                      controller.forward();
                    }
                    // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                    // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                    // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                  },
                  child: const Text('缩放 + 淡入淡出'),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

/// 动画同步使用
class AnimatedLogoSync extends AnimatedWidget {
  AnimatedLogoSync({super.key, required Animation<double> animation})
      : super(listenable: animation);

  final Tween<double> _opacityTween = Tween<double>(begin: 0.1, end: 1);
  final Tween<double> _sizeTween = Tween<double>(begin: 50, end: 100);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: SizedBox(
          width: _sizeTween.evaluate(animation),
          height: _sizeTween.evaluate(animation),
          child: const FlutterLogo(),
        ),
      ),
    );
  }
}

 隐式动画

  • 根据属性值变化,为 UI 中的 widget 添加动作并创造视觉效果,有些库包含各种各样可以帮你管理动画的widget这些 widgets 被统称为 隐式动画隐式动画 widget
  • 前缀是 Animated 的组件,几乎都是 隐式动画 widget;      

import 'dart:math';

import 'package:flutter/material.dart';

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

  @override
  State<ImplicitAnimation> createState() => _ImplicitAnimationState();
}

class _ImplicitAnimationState extends State<ImplicitAnimation> {

  double opacity = 0;

  late Color color;
  late double borderRadius;
  late double margin;

  double randomBorderRadius() {
    return Random().nextDouble() * 64;
  }

  double randomMargin() {
    return Random().nextDouble() * 32;
  }

  Color randomColor() {
    return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
  }

  @override
  void initState() {
    super.initState();
    color = randomColor();
    borderRadius = randomBorderRadius();
    margin = randomMargin();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        '隐式动画',
        style: TextStyle(fontSize: 20),
      )),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AnimatedOpacity(
                  opacity: opacity,
                  curve: Curves.easeInOutBack,
                  duration: const Duration(milliseconds: 1000),
                  child: Container(
                    width: 50,
                    height: 50,
                    margin: const EdgeInsets.only(right: 12),
                    color: Colors.primaries[2],
                  ),
                ),
                ElevatedButton(
                  onPressed: () {
                    if(opacity == 0) {
                      opacity = 1;
                    } else {
                      opacity = 0;
                    }
                    setState(() {});
                  },
                  child: const Text('淡入或淡出'),
                )
              ],
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AnimatedContainer(
                  width: 50,
                  height: 50,
                  margin: EdgeInsets.all(margin),
                  decoration: BoxDecoration(
                    color: color,
                    borderRadius: BorderRadius.circular(borderRadius)
                  ),
                  curve: Curves.easeInBack,
                  duration: const Duration(milliseconds: 1000),
                ),
                ElevatedButton(
                  onPressed: () {
                    color = randomColor();
                    borderRadius = randomBorderRadius();
                    margin = randomMargin();
                    setState(() {});
                  },
                  child: const Text('形状变化'),
                )
              ],
            )
          ],
        ),
      ),
    );
  }

}

显示和隐式的区别

看图,隐式动画 就是 显示动画 封装后的产物,是不是很蒙,这有什么意义?

应用场景不同:如果想 控制动画,使用 显示动画,controller.forward()controller.reverse()controller.reset(),反之只是在Widget属性值发生改变,进行UI过渡这种简单操作,使用 隐式动画;

误区

Flutter显式动画的关键对象 Tween,翻译过来 补间,联想到 Android原生的补间动画,就会有一个问题,Android原生的补间动画,只是视觉上的UI变化,对象属性并非真正改变,那么Flutter是否也是如此?

答案:非也,是真的改变了,和Android原生补间动画不同,看图:

以下偏移动画,在Flutter中的,点击偏移后的矩形位置,可以触发提示,反之Android原生不可以,只能在矩形原来的位置,才能触发;

Flutte 提示库 以及 封装相关 的代码

fluttertoast: ^8.2.4

toast_util.dart

import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

class ToastUtil {
  static FToast fToast = FToast();

  static void init(BuildContext context) {
    fToast.init(context);
  }

  static void showToast(String msg) {
    Widget toast = Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(25.0),
            color: Colors.greenAccent,
          ),
          alignment: Alignment.center,
          child: Text(msg),
        )
      ],
    );

    fToast.showToast(
      child: toast,
      gravity: ToastGravity.BOTTOM,
      toastDuration: const Duration(seconds: 2),
    );
  }
}

Flutter显示动画 代码

import 'package:flutter/material.dart';
import 'package:flutter_animation/util/toast_util.dart';

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

  @override
  State<TweenAnimation> createState() => _TweenAnimationState();
}

/// 测试显式动画,属性是否真的改变了
class _TweenAnimationState extends State<TweenAnimation>
    with SingleTickerProviderStateMixin {
  late Animation<Offset> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 500), vsync: this);
    animation =
        Tween<Offset>(begin: const Offset(0, 0), end: const Offset(1.5, 0))
            .animate(controller)
          ..addStatusListener((status) {
            debugPrint('status:$status'); // 监听动画执行状态
          });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        'Flutter 显式动画',
        style: TextStyle(fontSize: 20),
      )),
      body: Container(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        color: Colors.primaries[5],
        child: Stack(
          children: [
            Align(
              alignment: Alignment.center,
              child: Container(
                width: 80,
                height: 80,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.white, width: 1.0),
                ),
              ),
            ),
            Align(
              alignment: Alignment.center,
              child: SlideTransition(
                position: animation,
                child: InkWell(
                  onTap: () {
                    ToastUtil.showToast('点击了');
                  },
                  child: Container(
                    width: 80,
                    height: 80,
                    color: Colors.primaries[2],
                  ),
                ),
              ),
            ),
            Positioned(
              left: (MediaQuery.of(context).size.width / 2) - 35,
              top: 200,
              child: ElevatedButton(
                onPressed: () {
                  if (controller.isCompleted) {
                    controller.reverse();
                  } else {
                    controller.forward();
                  }
                  // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                  // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                  // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                },
                child: const Text('偏移'),
              ),
            )
          ],
        ),
      ),
    );
  }
}

Flutter隐式动画 代码

import 'package:flutter/material.dart';
import 'package:flutter_animation/util/toast_util.dart';

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

  @override
  State<ImplicitAnimation> createState() => _ImplicitAnimationState();
}

/// 测试隐式动画,属性是否真的改变了
class _ImplicitAnimationState extends State<ImplicitAnimation> {
  late double offsetX = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        'Flutter 隐式动画',
        style: TextStyle(fontSize: 20),
      )),
      body: Container(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        color: Colors.primaries[5],
        child: Stack(
          children: [
            Align(
              alignment: Alignment.center,
              child: Container(
                width: 80,
                height: 80,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.white, width: 1.0),
                ),
              ),
            ),
            Align(
              alignment: Alignment.center,
              child: AnimatedSlide(
                offset: Offset(offsetX, 0),
                duration: const Duration(milliseconds: 500),
                child: InkWell(
                  onTap: () {
                    ToastUtil.showToast('点击了');
                  },
                  child: Container(
                    width: 80,
                    height: 80,
                    color: Colors.primaries[2],
                  ),
                ),
              ),
            ),
            Positioned(
              left: (MediaQuery.of(context).size.width / 2) - 35,
              top: 200,
              child: ElevatedButton(
                onPressed: () {
                  if (offsetX == 0) {
                    offsetX = 1.5;
                  } else {
                    offsetX = 0;
                  }
                  setState(() {});
                },
                child: const Text('偏移'),
              ),
            )
          ],
        ),
      ),
    );
  }
}

Android原生补间动画 代码

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@android:color/holo_blue_light"
        android:gravity="center|left"
        android:text="Android原生 补间动画"
        android:paddingStart="16dp"
        android:textColor="@android:color/white"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/border"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:layout_marginBottom="50dp"
        android:background="@drawable/border" />

    <TextView
        android:id="@+id/offset_box"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:layout_marginBottom="50dp"
        android:background="@android:color/holo_orange_light" />

    <Button
        android:id="@+id/offset_x"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="12dp"
        android:text="偏移" />

</FrameLayout>
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.view.animation.TranslateAnimation
import android.widget.Toast
import com.example.flutter_animation.databinding.ActivityMainBinding

class MainActivity : Activity(), View.OnClickListener {

    private lateinit var bind: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        bind = ActivityMainBinding.inflate(layoutInflater)
        setContentView(bind.root)
        bind.offsetX.setOnClickListener(this)
        bind.offsetBox.setOnClickListener(this)
    }

    private fun offsetAnimation() {
        val translateAnimation = TranslateAnimation(0f, 200f, 0f, 0f)
        translateAnimation.duration = 800
        translateAnimation.fillAfter = true
        bind.offsetBox.startAnimation(translateAnimation)
    }

    override fun onClick(v: View?) {
        if (bind.offsetX == v) {
            offsetAnimation()
        } else if (bind.offsetBox == v) {
            Toast.makeText(this,"点击了",Toast.LENGTH_SHORT).show()
        }
    }

}

Hero动画

应用于 元素共享 的动画。

下面这三个图片详情案例的使用方式,将 Widget 从 A页面 共享到 B页面 后,改变Widget大小,被称为 标准 hero 动画

图片详情案例一:本地图片

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import 'package:flutter/scheduler.dart' show timeDilation;

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

  @override
  State<HeroAnimation> createState() => _HeroAnimationState();
}

/// 将 Widget 从 A页面 共享到 B页面 后,改变Widget大小
class _HeroAnimationState extends State<HeroAnimation> {
  /// 测试本地图片
  final List<String> images = [
    'assets/images/01.jpg',
    'assets/images/02.jpg',
    'assets/images/03.jpg',
    'assets/images/04.jpg',
  ];

  @override
  Widget build(BuildContext context) {
    // 减慢动画速度,可以通过此值帮助开发,
    // 注意这个值是针对所有动画,所以路由动画也会受影响
    // timeDilation = 10.0;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo List Page'),
      ),
      body: Container(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        alignment: Alignment.topLeft,
        child: GridView.count(
          padding: const EdgeInsets.all(10),
          crossAxisCount: 2,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          children: List.generate(
              images.length,
              (index) => PhotoHero(
                    photo: images[index],
                    size: 100,
                    onTap: () {
                      Navigator.of(context).push(CupertinoPageRoute<void>(
                        builder: (context) => PhotoDetail(
                            size: MediaQuery.of(context).size.width,
                            photo: images[index]),
                      ));
                    },
                  )),
        ),
      ),
    );
  }
}

class PhotoHero extends StatelessWidget {
  const PhotoHero({
    super.key,
    required this.photo,
    this.onTap,
    required this.size,
  });

  final String photo;
  final VoidCallback? onTap;
  final double size;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            /// 测试本地图片
            child: Image.asset(
                          photo,
                          fit: BoxFit.cover,
                        ),
          ),
        ),
      ),
    );
  }
}

class PhotoDetail extends StatelessWidget {
  const PhotoDetail({
    super.key,
    required this.photo,
    required this.size,
  });

  final String photo;
  final double size;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo Detail Page'),
      ),
      body: Column(
        children: [
          Container(
            color: Colors.lightBlueAccent,
            padding: const EdgeInsets.all(16),
            alignment: Alignment.topCenter,
            child: PhotoHero(
              photo: photo,
              size: size,
              onTap: () {
                Navigator.of(context).pop();
              },
            ),
          ),
          const Text(
            '详情xxx',
            style: TextStyle(fontSize: 20),
          )
        ],
      ),
    );
  }
}

图片详情案例二:网络图片

可以看出,在有延迟的情况下,效果没有本地图片好;

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import 'package:flutter/scheduler.dart' show timeDilation;

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

  @override
  State<HeroAnimation> createState() => _HeroAnimationState();
}

/// 将 Widget 从 A页面 共享到 B页面 后,改变Widget大小
class _HeroAnimationState extends State<HeroAnimation> {

  /// 测试网络图片
  final List<String> images = [
    'https://img1.baidu.com/it/u=1161835547,3275770506&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
    'https://p9.toutiaoimg.com/origin/pgc-image/6d817289d3b44d53bb6e55aa81e41bd2?from=pc',
    'https://img0.baidu.com/it/u=102503057,4196586556&fm=253&fmt=auto&app=138&f=BMP?w=500&h=724',
    'https://lmg.jj20.com/up/allimg/1114/041421115008/210414115008-3-1200.jpg',
  ];

  @override
  Widget build(BuildContext context) {
    // 减慢动画速度,可以通过此值帮助开发,
    // 注意这个值是针对所有动画,所以路由动画也会受影响
    // timeDilation = 10.0;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo List Page'),
      ),
      body: Container(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        alignment: Alignment.topLeft,
        child: GridView.count(
          padding: const EdgeInsets.all(10),
          crossAxisCount: 2,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          children: List.generate(
              images.length,
              (index) => PhotoHero(
                    photo: images[index],
                    size: 100,
                    onTap: () {
                      Navigator.of(context).push(CupertinoPageRoute<void>(
                        builder: (context) => PhotoDetail(
                            size: MediaQuery.of(context).size.width,
                            photo: images[index]),
                      ));
                    },
                  )),
        ),
      ),
    );
  }
}

class PhotoHero extends StatelessWidget {
  const PhotoHero({
    super.key,
    required this.photo,
    this.onTap,
    required this.size,
  });

  final String photo;
  final VoidCallback? onTap;
  final double size;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            /// 测试网络图片
            child: Image.network(
              photo,
              fit: BoxFit.cover,
            ),
          ),
        ),
      ),
    );
  }
}

class PhotoDetail extends StatelessWidget {
  const PhotoDetail({
    super.key,
    required this.photo,
    required this.size,
  });

  final String photo;
  final double size;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo Detail Page'),
      ),
      body: Column(
        children: [
          Container(
            color: Colors.lightBlueAccent,
            padding: const EdgeInsets.all(16),
            alignment: Alignment.topCenter,
            child: PhotoHero(
              photo: photo,
              size: size,
              onTap: () {
                Navigator.of(context).pop();
              },
            ),
          ),
          const Text(
            '详情xxx',
            style: TextStyle(fontSize: 20),
          )
        ],
      ),
    );
  }
}

图片详情案例三:背景透明

import 'package:flutter/material.dart';

import 'package:flutter/scheduler.dart' show timeDilation;

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

  @override
  State<HeroAnimation> createState() => _HeroAnimationState();
}

/// 测试 新页面背景透明色 的图片详情
class _HeroAnimationState extends State<HeroAnimation> {
  /// 测试本地图片
  final List<String> images = [
    'assets/images/01.jpg',
    'assets/images/02.jpg',
    'assets/images/03.jpg',
    'assets/images/04.jpg',
  ];

  @override
  Widget build(BuildContext context) {
    // 减慢动画速度,可以通过此值帮助开发,
    // 注意这个值是针对所有动画,所以路由动画也会受影响
    // timeDilation = 10.0;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo List Page'),
      ),
      body: Container(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        alignment: Alignment.topLeft,
        child: GridView.count(
          padding: const EdgeInsets.all(10),
          crossAxisCount: 2,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          children: List.generate(
              images.length,
                  (index) => PhotoHero(
                photo: images[index],
                size: 100,
                onTap: () {
                  Navigator.of(context).push(
                    PageRouteBuilder<void>(
                      opaque: false, // 新页面,背景色不透明度
                      pageBuilder: (context, animation, secondaryAnimation) {
                        return PhotoDetail(
                            size: MediaQuery.of(context).size.width,
                            photo: images[index]);
                      },
                    ),
                  );
                },
              )),
        ),
      ),
    );
  }
}

class PhotoHero extends StatelessWidget {
  const PhotoHero({
    super.key,
    required this.photo,
    this.onTap,
    required this.size,
  });

  final String photo;
  final VoidCallback? onTap;
  final double size;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            /// 测试本地图片
            child: Image.asset(
              photo,
              fit: BoxFit.cover,
            ),
          ),
        ),
      ),
    );
  }
}

class PhotoDetail extends StatelessWidget {
  const PhotoDetail({
    super.key,
    required this.photo,
    required this.size,
  });

  final String photo;
  final double size;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.transparent,
      // backgroundColor: const Color(0x66000000),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            padding: const EdgeInsets.all(16),
            alignment: Alignment.center,
            child: PhotoHero(
              photo: photo,
              size: size,
              onTap: () {
                Navigator.of(context).pop();
              },
            ),
          ),
          const Text(
            '详情xxx',
            style: TextStyle(fontSize: 20,color: Colors.white),
          )
        ],
      ),
    );
  }
}

图片形状转换案例:圆形 转 矩形

这个案例的使用方式,被称为 径向hero动画

  • 径向hero动画的 径 是半径距离,圆形状 向 矩形状转换,矩形状的对角半径距离 = 圆形状半径距离 * 2;
  • 这个是官方模版代码,我也没改什么;
  • 官方代码地址:https://github.com/cfug/flutter.cn/blob/main/examples/_animation/radial_hero_animation/lib/main.dart
  • 问题:这种官方代码是 初始化为 圆形 点击向 矩形改变的方式,我尝试反向操作:初始化为 矩形 点击向 圆形改变,但没有成功,如果有哪位同学找到实现方式,麻烦评论区留言;

我是这样修改的:

class RadialExpansion extends StatelessWidget {
  ... ... 

  @override
  Widget build(BuildContext context) {
    /// 原来的代码
    // 控制形状变化的核心代码
    // return ClipOval( // 圆形
    //   child: Center(
    //     child: SizedBox(
    //       width: clipRectSize,
    //       height: clipRectSize,
    //       child: ClipRect( // 矩形
    //         child: child,
    //       ),
    //     ),
    //   ),
    // );

    /// 尝试修改 形状顺序
    return ClipRect( // 矩形
      child: Center(
        child: SizedBox(
          width: clipRectSize,
          height: clipRectSize,
          child: ClipOval( // 圆形
            child: child,
          ),
        ),
      ),
    );

  }
}

官方代码演示 

import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:flutter/scheduler.dart' show timeDilation;

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

  @override
  State<HeroAnimation> createState() => _HeroAnimationState();
}

/// 将 Widget 从 A页面 共享到 B页面 后,改变Widget形状
class _HeroAnimationState extends State<HeroAnimation> {

  static double kMinRadius = 32.0;
  static double kMaxRadius = 128.0;
  static Interval opacityCurve =
  const Interval(0.0, 0.75, curve: Curves.fastOutSlowIn);

  static RectTween _createRectTween(Rect? begin, Rect? end) {
    return MaterialRectCenterArcTween(begin: begin, end: end);
  }

  static Widget _buildPage(
      BuildContext context, String imageName, String description) {
    return Container(
      color: Theme.of(context).canvasColor,
      child: Center(
        child: Card(
          elevation: 8,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              SizedBox(
                width: kMaxRadius * 2.0,
                height: kMaxRadius * 2.0,
                child: Hero(
                  createRectTween: _createRectTween,
                  tag: imageName,
                  child: RadialExpansion(
                    maxRadius: kMaxRadius,
                    child: Photo(
                      photo: imageName,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                ),
              ),
              Text(
                description,
                style: const TextStyle(fontWeight: FontWeight.bold),
                textScaleFactor: 3,
              ),
              const SizedBox(height: 16),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildHero(
      BuildContext context,
      String imageName,
      String description,
      ) {
    return SizedBox(
      width: kMinRadius * 2.0,
      height: kMinRadius * 2.0,
      child: Hero(
        createRectTween: _createRectTween,
        tag: imageName,
        child: RadialExpansion(
          maxRadius: kMaxRadius,
          child: Photo(
            photo: imageName,
            onTap: () {
              Navigator.of(context).push(
                PageRouteBuilder<void>(
                  pageBuilder: (context, animation, secondaryAnimation) {
                    return AnimatedBuilder(
                      animation: animation,
                      builder: (context, child) {
                        return Opacity(
                          opacity: opacityCurve.transform(animation.value),
                          child: _buildPage(context, imageName, description),
                        );
                      },
                    );
                  },
                ),
              );
            },
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    timeDilation = 5.0; // 1.0 is normal animation speed.

    return Scaffold(
      appBar: AppBar(
        title: const Text('Radial Transition Demo'),
      ),
      body: Container(
        padding: const EdgeInsets.all(32),
        alignment: FractionalOffset.bottomLeft,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            _buildHero(context, 'assets/images/01.jpg', 'Chair'),
            _buildHero(context, 'assets/images/02.jpg', 'Binoculars'),
            _buildHero(context, 'assets/images/03.jpg', 'Beach ball'),
            _buildHero(context, 'assets/images/04.jpg', 'Beach ball'),
          ],
        ),
      ),
    );
  }
}

class Photo extends StatelessWidget {
  const Photo({super.key, required this.photo, this.onTap});

  final String photo;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      // Slightly opaque color appears where the image has transparency.
      color: Theme.of(context).primaryColor.withOpacity(0.25),
      child: InkWell(
        onTap: onTap,
        child: LayoutBuilder(
          builder: (context, size) {
            return Image.asset(
              photo,
              fit: BoxFit.contain,
            );
          },
        ),
      ),
    );
  }
}

class RadialExpansion extends StatelessWidget {
  const RadialExpansion({
    super.key,
    required this.maxRadius,
    this.child,
  }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);

  final double maxRadius;
  final double clipRectSize;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    // 控制形状变化的核心代码
    return ClipOval( // 圆形
      child: Center(
        child: SizedBox(
          width: clipRectSize,
          height: clipRectSize,
          child: ClipRect( // 矩形
            child: child,
          ),
        ),
      ),
    );
  }
}

页面转场动画

自定义路由时,添加动画,自定义路由需要用到PageRouteBuilder<T>

import 'package:flutter/material.dart';

/// 为页面切换加入动画效果
class PageAnimation extends StatefulWidget {
  const PageAnimation({super.key});

  @override
  State<PageAnimation> createState() => _PageAnimationState();
}

class _PageAnimationState extends State<PageAnimation> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        '为页面切换加入动画效果',
        style: TextStyle(fontSize: 20),
      )),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(_createRouteX());
                },
                child: const Text(
                  'X轴偏移',
                  style: TextStyle(fontSize: 20),
                )),
            ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(_createRouteY());
                },
                child: const Text(
                  'Y轴偏移',
                  style: TextStyle(fontSize: 20),
                )),
            ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(_createRouteMix());
                },
                child: const Text(
                  '混合动画',
                  style: TextStyle(fontSize: 20),
                )),
          ],
        ),
      ),
    );
  }

  /// X轴 平移动画,切换页面
  Route _createRouteX() {
    return PageRouteBuilder(
        // opaque: false, // 新页面,背景色不透明度
        pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const begin = Offset(1.0, 0.0); // 将 dx 参数设为 1,这代表在水平方向左切换整个页面的宽度
          const end = Offset.zero;
          const curve = Curves.ease;

          var tween =
              Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

          return SlideTransition(
            position: animation.drive(tween),
            child: child,
          );
        });
  }

  /// Y轴 平移动画,切换页面
  Route _createRouteY() {
    return PageRouteBuilder(
        // opaque: false, // 新页面,背景色不透明度
        pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const begin = Offset(0.0, 1.0); // 将 dy 参数设为 1,这代表在竖直方向上切换整个页面的高度
          const end = Offset.zero;
          const curve = Curves.ease;

          var tween =
              Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

          return SlideTransition(
            position: animation.drive(tween),
            child: child,
          );
        });
  }

  /// 多个动画配合,切换页面
  Route _createRouteMix() {
    return PageRouteBuilder(
        // opaque: false, // 新页面,背景色不透明度
        pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          var tween = Tween<double>(begin: 0.1, end: 1.0)
              .chain(CurveTween(curve: Curves.ease));
          return FadeTransition(
            // 淡入淡出
            opacity: animation.drive(tween),
            child: RotationTransition(
              // 旋转
              turns: animation.drive(tween),
              child: ScaleTransition(
                // 更替
                scale: animation.drive(tween),
                child: child,
              ),
            ),
          );
        });
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.lightBlue,
      appBar: AppBar(
        title: const Text('TestPage01'),
      ),
    );
  }
}

交错动画

多个动画配合使用

这个案例是官方的,原汁原味;

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;


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

  @override
  State<IntertwinedAnimation> createState() => _IntertwinedAnimationState();
}

class _IntertwinedAnimationState extends State<IntertwinedAnimation>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
  }

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

  Future<void> _playAnimation() async {
    try {
      await _controller.forward().orCancel;
      await _controller.reverse().orCancel;
    } on TickerCanceled {}
  }

  @override
  Widget build(BuildContext context) {
    // timeDilation = 10.0;
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        '交错动画',
        style: TextStyle(fontSize: 20),
      )),
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: Center(
          child: Container(
            width: 300,
            height: 300,
            decoration: BoxDecoration(
                color: Colors.black.withOpacity(0.1),
                border: Border.all(
                  color: Colors.black.withOpacity(0.5),
                )),
            child: StaggerAnimation(controller: _controller),
          ),
        ),
      ),
    );
  }
}

class StaggerAnimation extends StatelessWidget {
  final Animation<double> controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius?> borderRadius;
  final Animation<Color?> color;

  StaggerAnimation({super.key, required this.controller})
      : opacity = Tween<double>(
          begin: 0.0,
          end: 1.0,
        ).animate(CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.0,
              0.100,
              curve: Curves.ease,
            ))),
        width = Tween<double>(
          begin: 50.0,
          end: 150.0,
        ).animate(CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.125,
              0.250,
              curve: Curves.ease,
            ))),
        height = Tween<double>(begin: 50.0, end: 150.0).animate(CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.250,
              0.375,
              curve: Curves.ease,
            ))),
        padding = EdgeInsetsTween(
          begin: const EdgeInsets.only(bottom: 16),
          end: const EdgeInsets.only(bottom: 75),
        ).animate(CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.250,
              0.375,
              curve: Curves.ease,
            ))),
        borderRadius = BorderRadiusTween(
          begin: BorderRadius.circular(4),
          end: BorderRadius.circular(75),
        ).animate(CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.375,
              0.500,
              curve: Curves.ease,
            ))),
        color = ColorTween(begin: Colors.indigo[100], end: Colors.orange[400])
            .animate(CurvedAnimation(
                parent: controller,
                curve: const Interval(
                  0.500,
                  0.750,
                  curve: Curves.ease,
                )));

  Widget _buildAnimation(BuildContext context, Widget? child) {
    return Container(
      padding: padding.value,
      alignment: Alignment.bottomCenter,
      child: Opacity(
        opacity: opacity.value,
        child: Container(
          width: width.value,
          height: height.value,
          decoration: BoxDecoration(
              color: color.value,
              border: Border.all(
                color: Colors.indigo[300]!,
                width: 3,
              ),
              borderRadius: borderRadius.value),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

依次执行动画

这个案例是根据官方demo改的,它那个太复杂了,不利于新手阅读(个人觉得);

官方文档:创建一个交错效果的侧边栏菜单 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter

import 'package:flutter/material.dart';

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

  @override
  State<Intertwined02Animation> createState() => _Intertwined02AnimationState();
}

class _Intertwined02AnimationState extends State<Intertwined02Animation> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: const TableList(),
        // child: const Column(
        //   crossAxisAlignment: CrossAxisAlignment.center,
        //   children: [
        //     TableList()
        //   ],
        // ),
      ),
    );
  }
}

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

  @override
  State<TableList> createState() => _TableListState();
}

class _TableListState extends State<TableList> with SingleTickerProviderStateMixin {

  /// 遍历循环写法
  late AnimationController _controller;

  final Duration _durationTime = const Duration(milliseconds: 3000);

  @override
  initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: _durationTime);
    _controller.forward();
  }

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

  /// 遍历Interval
  List<Interval> _createInterval() {
    List<Interval> intervals = [];

    // Interval(0.0,0.5);
    // Interval(0.5,0.75);
    // Interval(0.75,1.0);

    double begin = 0.0;
    double end = 0.5;
    for (int i = 0; i < 3; i++) {
      if (i == 0) {
        intervals.add(Interval(begin, end));
      } else {
        begin = end;
        end = begin + 0.25;
        intervals.add(Interval(begin, end));
      }
      // debugPrint('begin:$begin --- end:$end');
    }
    return intervals;
  }

  /// 遍历循环组件
  List<Widget> _createWidget() {
    var intervals = _createInterval();

    List<Widget> listItems = [];

    for (int i = 0; i < 3; i++) {
      listItems.add(AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          var animationPercent = Curves.easeOut.transform(intervals[i].transform(_controller.value));
          final opacity = animationPercent;
          final slideDistance = (1.0 - animationPercent) * 150;
          return Opacity(
              opacity: i == 2 ? opacity : 1,
              child: Transform.translate(
                offset: Offset(slideDistance, 100 + (i * 50)),
                child: child,
              ));
        },
        child: Container(
          width: 100,
          height: 50,
          color: Colors.lightBlue,
        ),
      ));
    }
    return listItems;
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width,
      height: MediaQuery.of(context).size.height,
      child: Column(
        children: _createWidget(),
      ),
    );
  }

  /// 非遍历循环写法
// late AnimationController _controller;
//
// final Interval _intervalA = const Interval(0.0, 0.5);
// final Interval _intervalB = const Interval(0.5, 0.8);
// final Interval _intervalC = const Interval(0.8, 1.0);
//
// final Duration _durationTime = const Duration(milliseconds: 3000);
//
// @override
// void initState() {
//   super.initState();
//   _controller = AnimationController(vsync: this, duration: _durationTime);
//   _controller.forward();
// }
//
// @override
// void dispose() {
//   _controller.dispose();
//   super.dispose();
// }
//
// @override
// Widget build(BuildContext context) {
//   return SizedBox(
//     width: MediaQuery.of(context).size.width,
//     height: MediaQuery.of(context).size.height,
//     child: Column(
//       children: [
//         AnimatedBuilder(
//           animation: _controller,
//           builder: (context,child) {
//             var animationPercent = Curves.easeOut.transform(_intervalA.transform(_controller.value));
//             final slideDistance = (1.0 - animationPercent) * 150;
//             return Transform.translate(
//               offset: Offset(slideDistance,100),
//               child: child
//             );
//           },
//           child: Container(
//             width: 100,
//             height: 50,
//             color: Colors.lightBlue,
//           ),
//         ),
//         AnimatedBuilder(
//           animation: _controller,
//           builder: (context,child) {
//             var animationPercent = Curves.easeOut.transform(_intervalB.transform(_controller.value));
//             final slideDistance = (1.0 - animationPercent) * 150;
//             return Transform.translate(
//                 offset: Offset(slideDistance,150),
//                 child: child
//             );
//           },
//           child: Container(
//             width: 100,
//             height: 50,
//             color: Colors.lightBlue,
//           ),
//         ),
//         AnimatedBuilder(
//           animation: _controller,
//           builder: (context,child) {
//             var animationPercent = Curves.easeOut.transform(_intervalC.transform(_controller.value));
//             final opacity = animationPercent;
//             final slideDistance = (1.0 - animationPercent) * 150;
//             return Opacity(
//               opacity: opacity,
//               child: Transform.translate(
//                   offset: Offset(slideDistance,200),
//                   child: child
//               ),
//             );
//           },
//           child: Container(
//             width: 100,
//             height: 50,
//             color: Colors.lightBlue,
//           ),
//         ),
//       ],
//     ),
//   );
// }

  /// 基础版本写法
// late AnimationController _controller;
// final Duration _durationTime = const Duration(milliseconds: 2000);
// // 0.0 - 1.0 / 0% - 100%
// final Interval _interval = const Interval(0.5, 1.0); // 延迟 50% 再开始 启动动画,执行到 100%
// // final Interval _interval = const Interval(0.5, 0.7); // 延迟 50% 再开始 启动动画,后期的执行速度,增加 30%
// // final Interval _interval = const Interval(0.0, 0.1); // 不延迟 动画执行速度,增加 90%
//
// @override
// void initState() {
//   super.initState();
//   _controller = AnimationController(vsync: this, duration: _durationTime);
//   _controller.forward();
// }
//
// @override
// void dispose() {
//   _controller.dispose();
//   super.dispose();
// }

// @override
// Widget build(BuildContext context) {
//   return AnimatedBuilder(
//       animation: _controller,
//       builder: (context,child) {
//         // var animationPercent = Curves.easeOut.transform(_controller.value); // 加动画曲线
//         // var animationPercent = _interval.transform(_controller.value); // 加动画间隔
//         var animationPercent = Curves.easeOut.transform(_interval.transform(_controller.value)); // 动画曲线 + 动画间隔
//
//         final slideDistance = (1.0 - animationPercent) * 150; // 就是对150 做递减
//         // debugPrint('animationPercent:$animationPercent --- slideDistance:$slideDistance');
//         debugPrint('slideDistance:$slideDistance');
//
//         return Transform.translate(
//           offset: Offset(0,slideDistance),
//           child: child
//         );
//       },
//     child: Container(
//       width: 100,
//       height: 50,
//       color: Colors.lightBlue,
//     ),
//   );
// }
}

官方文档

动画效果介绍 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/390262.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【开源】在线办公系统 JAVA+Vue.js+SpringBoot+MySQL

目录 1 功能模块1.1 员工管理模块1.2 邮件管理模块1.3 人事档案模块1.4 公告管理模块 2 系统展示3 核心代码3.1 查询用户3.2 导入用户3.3 新增公告 4 免责声明 本文项目编号&#xff1a; T 001 。 \color{red}{本文项目编号&#xff1a;T001。} 本文项目编号&#xff1a;T001。…

前端vue3实现本地及在线文件预览(含pdf/txt/mp3/mp4/docx/xlsx/pptx)

一、仅需实现在线预览&#xff0c;且文件地址公网可访问 &#xff08;一&#xff09;微软office免费预览&#xff08;推荐&#xff09; 支持doc/docx/xls/xlsx/ppt/pptx等多种office文件格式的免费预览 //示例代码//​在https://view.officeapps.live.com/op/view.aspx?src…

骨传导耳机是什么?如何选择骨传导耳机不会踩雷?

骨传导耳机是利用骨传导技术研发而成一种新型蓝牙耳机&#xff0c;其传声方式很独特&#xff0c;不通过空气传导&#xff0c;而是通过人体骨骼来传递声音。 详细传声原理请看下图&#xff1a; 随着骨传导耳机逐渐热门&#xff0c;如何选购耳机成为了问题&#xff0c;下面跟大家…

计算机设计大赛 深度学习YOLO图像视频足球和人体检测 - python opencv

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络4 Yolov5算法5 数据集6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习YOLO图像视频足球和人体检测 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非…

1_opencv3环境搭建与测试

之前2020年5月写过一次&#xff0c;时隔3年多&#xff0c;有机会再重新写一次。相比之前&#xff0c;应该是有一点儿进步的。之前是使用默认安装路径&#xff0c;所以无需配置共享库的搜索路径。这次是自定义安装路径&#xff0c;略有区别。随着写程序的时间增长&#xff0c;编…

Dockerfile 常用指令

1、FROM 指定base镜像。 2、Docker history 显示镜像的构建历史&#xff0c;也就是Dockerfile的执行过程。 Missing 表示无法获取IMAGE ID&#xff0c;通常从Docker Hub下载的镜像会有这个问题。 3、调试Dockerfile&#xff0c;使用sudo docker run -it XXXX&#xff0c;XXXX…

036-安全开发-JavaEE应用第三方组件Log4j日志FastJson序列化JNDI注入

036-安全开发-JavaEE应用&第三方组件&Log4j日志&FastJson序列化&JNDI注入 #知识点&#xff1a; 1、JavaEE-组件安全-Log4j 2、JavaEE-组件安全-Fastjson 3、JavaEE-基本了解-JNDI-API 演示案例&#xff1a; ➢Java-三方组件-Log4J&JNDI ➢Java-三方组件-Fa…

【开源】SpringBoot框架开发大学生相亲网站

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询会员4.2 查询相亲大会4.3 新增留言4.4 查询新闻4.5 新增新闻 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的大学生相亲网站&#xff0c;包含了会员管理模块、新闻管…

炬芯ATS2819 soundbar音响系统开发完全手册

加我微信hezkz17,可申请加入数字音频系统研究开发交流答疑群,赠送音频项目核心开发资料 ATS2819音响系统开发完全手册 1 硬件原理实现 图1 硬件原理框图 2 SOC ATS2819介绍 3 E800 板子项目实物…

黑群晖一键修复:root、AME、DTS、转码、CPU型号等

食用方法&#xff1a;SSH连接群晖使用临时root权限执行 AME3.x激活补丁 只适用于x86_64的&#xff1a;DSM7.x Advanced Media Extensions (AME)版本3.0.1-2004、3.1.0-3005 激活过程需要下载官方的解码包&#xff0c;过程较慢&#xff0c;耐心等待。。。 DSM7.1和7.2的AME版…

【前端设计】炫酷导航栏

欢迎来到前端设计专栏&#xff0c;本专栏收藏了一些好看且实用的前端作品&#xff0c;使用简单的html、css语法打造创意有趣的作品&#xff0c;为网站加入更多高级创意的元素。 html <!DOCTYPE html> <html lang"en"> <head><meta charset&quo…

Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(中)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

家人们,比赛打完了

啊&#xff0c;终于打完一场比赛了&#xff0c;但还有三场…… 先看看我的战绩&#xff1a; 共八题&#xff0c;AC6题&#xff0c;总共3902分&#xff0c;3.7k人参加&#xff0c;第980名 来看看第一&#xff1a; A8题&#xff0c;我只有2题没做出&#xff0c;相差4000多分&am…

wsl连接USB设备

参考教程&#xff1a;连接 USB 设备 | Microsoft Learn 1.安装usbipd-win WSL 本身并不支持连接 USB 设备&#xff0c;因此你需要安装开源usbipd-win项目【下载Releases dorssel/usbipd-win (github.com)】 2.共享USB设备 通过以管理员模式打开PowerShell或者CMD并输入以下…

吐血整理!操作系统【处理机调度】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;OS从基础到进阶 1 基本概念1.1 总览1.2 什么是调度1.2.1 调度1.2.2 处理机调度 1.3 调度的三个层次1.3.1 高级调度1.3.2 中级调度&#xff08;内存调度&#xff09;1.3.3 低级调度&#xf…

学生成绩管理系统|基于Springboot的学生成绩管理系统设计与实现(源码+数据库+文档)

学生成绩管理系统目录 目录 基于Springboot的学生成绩管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员功能模块 2、学生功能模块 3、教师功能模块 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源…

Medical Boundary Diffusion Modelfor Skin Lesion Segmentation

皮肤病灶分割的医学边界扩散模型 摘要 由于多尺度边界关注和特征增强模块的进步&#xff0c;皮肤镜图像中的皮肤病变分割最近取得了成功。然而&#xff0c;现有的方法依赖于端到端学习范式&#xff0c;直接输入图像和输出分割图&#xff0c;经常与极其困难的边界作斗争&#…

009集——vba实现内存中大小端序的转换(附不同进制转换代码)

小端序为很多系统默认的数据存储方式&#xff0c;但有些数据格式为大端序模式解读文件&#xff0c;因此我们需将小端序字节颠倒排序&#xff0c;这样用大端序模式解读此文件&#xff0c;最后即可读取我们想要的内容。方法如下&#xff1a; Function SwapEndian(ByVal value As…

问题:在额定电压500V以下的电路中,使用的各种用电设备,一般称为(_ _ _)用电设备 #媒体#媒体#媒体

问题&#xff1a;在额定电压500V以下的电路中,使用的各种用电设备,一般称为&#xff08;_ _ _)用电设备 参考答案如图所示

Go语言中的加密艺术:深入解析crypto/subtle库

Go语言中的加密艺术&#xff1a;深入解析crypto/subtle库 引言crypto/subtle库概览ConstantTimeCompare函数深入解析ConstantTimeSelect函数应用详解ConstantTimeLessOrEq函数实践指南安全编程实践性能优化与最佳实践与其他加密库的比较总结 引言 在当今快速发展的互联网时代&…