筆記:Flutter動畫記錄

一、帶動畫刷新的小部件

比如AnimatedContainer,這一類小部件都以Animated開頭,示例如下:

AnimatedContainer(
          duration: const Duration(seconds: 3),
          width: 300,
          height: 300,
          color: Colors.blue,
        )

將上面的顏色換成其他顏色然后熱更新一下,就會有顏色過渡動畫。這一類的動畫小部件只對自己的屬性起到動畫效果,他不能控制自己的child也執(zhí)行動畫。
差值器:curve
默認(rèn)是Curves.linear。他是水平勻速運(yùn)動。Curves里面還有很多,可以都看看。做一個(gè)勻速下落盒子的動畫,如下:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: AnimatedPadding(
          curve: Curves.linear,
          duration: Duration(
            seconds: 2
          ),
          padding: EdgeInsets.only(top: 0),
          child: Container(
            width: 300,
            height: 300,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }

二、兩種小部件來回切換動畫

上面的動畫提到了child切換時(shí),沒有動畫,那么我們要做到這樣的功能需要AnimatedSwitcher來實(shí)現(xiàn)。如下:

AnimatedSwitcher(
            duration: Duration(seconds: 2),
            child: Center(child: CircularProgressIndicator(),),//Image.network("https://www.baidu.com/img/flexible/logo/pc/result.png")
          )

當(dāng)吧AnimatedSwitcher的child從Center(child: CircularProgressIndicator(),)換成Image,就會有過渡動畫。
值得一提的是,如果是Text只是換掉里面的文案,那么不會有動畫。比如:

AnimatedSwitcher(
            duration: Duration(seconds: 2),
            child: Center(child: Text("HI",style: TextStyle(fontSize: 41),),),
          )
//把文案換成hello。
AnimatedSwitcher(
            duration: Duration(seconds: 2),
            child: Center(child: Text("HELLO",style: TextStyle(fontSize: 41),),),
          )

為什么沒有動畫?因?yàn)橹皇歉奈陌?,flutter不會認(rèn)為是換了小部件,所以AnimatedSwitcher也不會執(zhí)行動畫,那么要想執(zhí)行動畫就要讓flutter覺得小部件改變了。就要用到key。
在Flutter中,當(dāng)重新build時(shí),會更新widget和element。
Flutter的UI是通過構(gòu)建widget樹來實(shí)現(xiàn)的,widget是不可變的,當(dāng)需要更新UI時(shí),會創(chuàng)建一個(gè)新的widget,并使用diff算法比較新舊widget的差異,然后更新element樹。如果widget樹中的某個(gè)widget的屬性發(fā)生了變化,或者父級widget調(diào)用了setState方法,那么Flutter會重新build該widget及其子樹。
在重新build時(shí),F(xiàn)lutter會根據(jù)widget的差異更新element樹。如果新舊widget的類型相同,F(xiàn)lutter會復(fù)用element,只更新相關(guān)的屬性值。如果新舊widget的類型不同,則會銷毀舊的element,并創(chuàng)建一個(gè)新的element。如果當(dāng)類型相同,key不同,flutter也會重新構(gòu)建widget。
所以把上面的代碼改成下面的加個(gè)key就行:

AnimatedSwitcher(
            duration: const Duration(seconds: 2),
            child: Center(child: Text(key: UniqueKey(),"HI",style: const TextStyle(fontSize: 41),),),
          )
//把文案換成hello。
AnimatedSwitcher(
            duration: const Duration(seconds: 2),
            child: Center(child: Text(key: UniqueKey(),"HELLO",style: const TextStyle(fontSize: 41),),),
          )

當(dāng)然AnimatedSwitcher還可以選擇動畫類型,用transitionBuilder屬性即可。默認(rèn)用的是fade也就是透明度變化效果。如下:

AnimatedSwitcher(
            transitionBuilder: (child,animation){
              return FadeTransition(opacity: animation,child: child,)
            },
            duration:  Duration(seconds: 2),
            child: Center(child: Text(key: ValueKey("Hi"),"Hi",style:  TextStyle(fontSize: 41),),),
          )

我們也能改成放大縮小動畫或者選擇,如下:改成放大縮小

AnimatedSwitcher(
            transitionBuilder: (child,animation){
              return ScaleTransition(scale: animation,child: child,)
            },
            duration:  Duration(seconds: 2),
            child: Center(child: Text(key: ValueKey("Hi"),"Hi",style:  TextStyle(fontSize: 41),),),
          )

那么如果我們既需要透明動畫也需要放大縮小動畫怎么辦?那就套娃。如下:

AnimatedSwitcher(
            transitionBuilder: (child,animation){
              return ScaleTransition(scale: animation,
                child: FadeTransition(
                opacity: animation,
                  child: child,
              ),);
            },
            duration:  Duration(seconds: 2),
            child: Center(child: Text(key: ValueKey("Hi"),"Hi",style:  TextStyle(fontSize: 41),),),
          )

三、補(bǔ)間動畫

和安卓一樣,flutter也有補(bǔ)間動畫,其實(shí)可以理解成,一個(gè)屬性設(shè)置在一個(gè)返回變化,然后一變化就給小部件賦值,達(dá)到動畫效果。比如下面的透明度動畫。如下:

Scaffold(
      body: SafeArea(
        child: Center(
          child: TweenAnimationBuilder(
            duration: Duration(seconds: 2),
            builder: (BuildContext context,  value,  child) {
              return Opacity(
                opacity: value,
                child: Container(
                  width: 300,
                  height: 300,
                  color: Colors.blue,
                ),
              );
            },
            tween: Tween(begin: 1.0,end: 0.0),
          ),
        ),
      ),
    )

上面的代碼可以實(shí)現(xiàn)AnimatedOpacity的動畫效果,而且好掌握。就是代碼長了些。有沒有發(fā)現(xiàn)builder里面的child沒有用到?這個(gè)是優(yōu)化用的。還沒到時(shí)機(jī),暫時(shí)不記錄。
補(bǔ)間動畫經(jīng)常和Transform一起連用。下面來一個(gè)按按鈕讓小部件來回放大縮小的動畫。如下:

class Test1State extends State<Test1> {
  var _begin = true;
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: TweenAnimationBuilder(
            duration: Duration(seconds: 2),
            builder: (BuildContext context,  value,  child) {
              return Container(
                width: 300,
                height: 300,
                color: Colors.blue,
                child: Center(
                  child: Transform.scale(
                    scale: value,
                    child: Text("HI",style: TextStyle(fontSize: 50),),
                  ),
                ),
              );
            },
            tween: Tween(begin: 1.0,end: _begin ? 2.0 : 1.0),

          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        setState(() {
          _begin = !_begin;
        });
      },),
    );
  }
}

Transform還有旋轉(zhuǎn),平移等,都可以看看。

四、顯示動畫
顯示動畫帶控制器,能更好的方便我們停止開始等。下面做一個(gè)不停旋轉(zhuǎn)的圖標(biāo)動畫,如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
  AnimationController? _controller = null;
  bool _start = false;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Container(
            width: 300,
            height: 300,
            color: Colors.blue,
            child: Center(
              child: RotationTransition(
                turns: _controller!!,
                child: Icon(Icons.refresh,color: Colors.black, size: 50,),
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        if(_start){
          _controller?.stop();
        }else{
          _controller?.repeat();
        }
        setState(() {
          _start = !_start;
        });
      },),
    );
  }
}

AnimationController必傳的一個(gè)參數(shù)是vsync,這是個(gè)屏幕刷新時(shí)通知其他頁面的回調(diào),我們這里with了一個(gè)SingleTickerProviderStateMixin,單個(gè)的屏幕刷新回調(diào)。因?yàn)槲覀兙鸵粋€(gè)動畫需要。這樣屏幕每一次刷新都會通知我們的AnimationController,然后AnimationController根據(jù)我們傳的值和時(shí)間,計(jì)算當(dāng)前需要的旋轉(zhuǎn)角度。repeat代表0到1重復(fù)執(zhí)行。forward是0到1執(zhí)行一次。那我要是指定3到5執(zhí)行呢?那就設(shè)置區(qū)間

//設(shè)置lowerBound和upperBound
@override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1),lowerBound: 3.0,upperBound: 5.0);
    _controller?.addListener(() {
      print("value = ${_controller?.value}");
    });
  }
........
//forward用執(zhí)行一次
floatingActionButton: FloatingActionButton(onPressed: (){
        if(_start){
          _controller?.stop();
        }else{
          _controller?.forward();
        }
        setState(() {
          _start = !_start;
        });
},),

除了RotationTransition還有FadeTransition和ScaleTransition、SlideTransition等。下面用ScaleTransition做一個(gè)放大縮小發(fā)大縮小重復(fù)循環(huán)的動畫。

class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
  AnimationController? _controller = null;
  bool _start = false;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
    _controller?.addListener(() {
      print("value = ${_controller?.value}");
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: ScaleTransition(
            scale: _controller!!,
            child: Container(
              width: 300,
              height: 300,
              color: Colors.blue,
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        _controller?.repeat(reverse: true);
      },),
    );
  }
}

值得一提的是reverse,如果沒有設(shè)置,那么動畫只會放大,不會從大變小。設(shè)置了reverse就會有放大縮小再放大再縮小的動畫。

再來一個(gè)來回移動的動畫:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
  AnimationController? _controller = null;
  bool _start = false;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
    _controller?.addListener(() {
      print("value = ${_controller?.value}");
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SlideTransition(
            position: _controller!.drive(Tween(begin: Offset(0,0),end: Offset(1,0))),
            child: Container(
              width: 300,
              height: 300,
              color: Colors.blue,
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        _controller?.repeat(reverse: true);
      },),
    );
  }
}

SlideTransition需要position,而position是一系列的Offset。_controller.drive加上Tween就能生成一系列Offset。當(dāng)然Tween還有另外的寫法。如下:

position: Tween(begin: Offset(0,0),end: Offset(1,0)).animate(_controller!),

這樣寫有好處,他可以疊加其他的Tween。如下:

position: Tween(begin: Offset(0, 0), end: Offset(1, 0))!
                .chain(CurveTween(curve: Curves.elasticInOut))
                .animate(_controller!),

這樣我們就能做一個(gè)間隔動畫,比如一個(gè)小部件執(zhí)行完動畫再執(zhí)行其他小部件,一個(gè)個(gè)得來。如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
  AnimationController? _controller = null;
  bool _start = false;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 4));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SliverBox(_controller!,Colors.blue[100]!,0.0,0.2),
              SliverBox(_controller!,Colors.blue[200]!,0.2,0.4),
              SliverBox(_controller!,Colors.blue[300]!,0.4,0.6),
              SliverBox(_controller!,Colors.blue[400]!,0.6,0.8),
              SliverBox(_controller!,Colors.blue[500]!,0.8,1.0),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller?.repeat(reverse: true);
        },
      ),
    );
  }
}

class SliverBox extends StatelessWidget {
  AnimationController _controller;
  Color color;
  double startInterval;
  double endInterval;
  SliverBox(this._controller,this.color,this.startInterval,this.endInterval,{Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: Tween(begin: Offset(0, 0), end: Offset(0.1, 0))
          .chain(CurveTween(curve: Interval(startInterval, endInterval,curve: Curves.bounceOut)))
          .animate(_controller!),
      child: Container(
        width: 280,
        height: 100,
        color: color,
      ),
    );
  }
}

五、方便擴(kuò)展的AnimatedBuilder

我們做一個(gè)透明度加高度變化的動畫,如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
  AnimationController? _controller = null;
  bool _start = false;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 4));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: AnimatedBuilder(
            builder: (BuildContext context, Widget? child) {
              return Opacity(
                opacity: _controller!.value,
                child: Container(
                  width: 200,
                  height: 100 + (100 * _controller!.value),
                  color: Colors.blue,
                  child: Center(
                    child: Text(
                      "HI",
                      style: TextStyle(fontSize: 40),
                    ),
                  ),
                ),
              );
            }, animation: _controller!,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller?.repeat();
        },
      ),
    );
  }
}

可以看到我們的透明度增加,高度也隨之增加。那么上次我們提到了AnimatedBuilder里面的builder有一個(gè)child參數(shù),他是干嘛的?其實(shí)他是flutter為了增加效率做的參數(shù),你看啊,我們這代碼里面,Opacity和Container都有變化,一個(gè)跟著動畫改變透明度,提個(gè)改變高度。唯獨(dú)中間的文字沒有變。所以我們可以把這個(gè)文字放到AnimatedBuilder的child中,這樣的話,每一幀動畫回調(diào)的builder都會把這個(gè)child返回,我們有直接復(fù)用即可??梢钥闯鰂lutter對性能優(yōu)化的注重。優(yōu)化代碼如下:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: AnimatedBuilder(
            builder: (BuildContext context, Widget? child) {
              return Opacity(
                opacity: _controller!.value,
                child: Container(
                  width: 200,
                  height: 100 + (100 * _controller!.value),
                  color: Colors.blue,
                  child: child,
                ),
              );
            },
            animation: _controller!,
            child: Center(   //這個(gè)child將會在build回調(diào)中復(fù)用。
              child: Text(
                "HI",
                style: TextStyle(fontSize: 40),
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller?.repeat();
        },
      ),
    );
  }
}

六、hero動畫

android有一個(gè)兩個(gè)頁面的跳轉(zhuǎn)動畫,那flutter也有,它就是hero動畫。如下:

class HeroAnimationRoute extends StatelessWidget {
  const HeroAnimationRoute({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        padding: EdgeInsets.only(top: 100),
        alignment: Alignment.topCenter,
        child: Column(
          children: <Widget>[
            InkWell(
              child: Hero(
                tag: "avatar", //唯一標(biāo)記,前后兩個(gè)路由頁Hero的tag必須相同
                child: ClipOval(
                  child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F511%2F101611154647%2F111016154647-10-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1644588215&t=9a40338757751be9d2684f0d3c80ae31',
                    width: 100.0,
                  ),
                ),
              ),
              onTap: () {
                //打開B路由
                Navigator.push(context, PageRouteBuilder(
                  pageBuilder: (
                      BuildContext context,
                      animation,
                      secondaryAnimation,
                      ) {
                    return FadeTransition(
                      opacity: animation,
                      child: Scaffold(
                        appBar: AppBar(
                          title: Text("原圖"),
                        ),
                        body: HeroAnimationRouteB(),
                      ),
                    );
                  },
                ));
              },
            ),
            Padding(
              padding: const EdgeInsets.only(top: 8.0),
              child: Text("點(diǎn)擊頭像"),
            )
          ],
        ),
      ),
    );
  }
}
 
 
class HeroAnimationRouteB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Hero(
        tag: "avatar", //唯一標(biāo)記,前后兩個(gè)路由頁Hero的tag必須相同
        child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F511%2F101611154647%2F111016154647-10-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1644588215&t=9a40338757751be9d2684f0d3c80ae31'),
      ),
    );
  }
}

Hero中的tag必須保持一致。這樣,跳轉(zhuǎn)頁面就不會看起來生硬了。

七、Paint與動畫結(jié)合

像android一樣,flutter也有自己的畫布、畫筆。如果要在flutter中畫畫,需要用到

Container(
    width: double.infinity,
    height: double.infinity,
    child: CustomPaint(
        painter: MyPaint(),
    )
)
......
class MyPaint extends CustomPainter{
  MyPaint(this.whitePaint,this._snows);
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawCircle(Offset(0, 0), 50, Paint());
    });
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }

}

這樣就能在屏幕上畫一個(gè)圓。
那么我們把這個(gè)圓當(dāng)做是雪花,再在底部畫一個(gè)雪人,那么結(jié)合anmation的事實(shí)刷新,就能讓雪花動起來。如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
  late AnimationController _controller ;
  late Paint whitePaint;//雪人和雪花都是白色,所以定義白色畫筆
  late List<Snow> _snows = List.generate(100, (index) => Snow());//一共一百個(gè)雪花

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 4));
    whitePaint = Paint();
    whitePaint.color = Colors.white;
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(//寬高都最大
        width: double.infinity,
        height: double.infinity,
        decoration: const BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.blue,Colors.white],
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
            )
        ),
        child: AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            return CustomPaint(//設(shè)置自定義畫布
              painter: MyPaint(whitePaint,_snows),//傳入畫筆和雪花到自定義畫布
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller.repeat();//點(diǎn)擊循環(huán)播放動畫
        },
      ),
    );
  }
}

class MyPaint extends CustomPainter{
  Paint whitePaint;
  List<Snow> _snows;

  MyPaint(this.whitePaint,this._snows);

  @override
  void paint(Canvas canvas, Size size) {
    print("width = ${size.width}");
    canvas.drawCircle(Offset(size.width/2, size.height - 200), 50, whitePaint);//雪人頭
    canvas.drawOval(Rect.fromCenter(center: Offset(size.width/2,size.height - 75), width: 140, height: 200), whitePaint);//雪人身體
    _snows.forEach((element) {//循環(huán)畫出雪花
      canvas.drawCircle(Offset(element.x, element.y), element.radio, whitePaint);
      element.start();//開始下落,并且下落完成,重新設(shè)置隨機(jī)數(shù)。
    });
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;//這是判斷paint方法要不要執(zhí)行,如果返回false,paint方法不會執(zhí)行,返回true就會,我們這里必須讓他刷新。
  }

}

class Snow {
  double x = Random().nextDouble() * 360;//橫坐標(biāo)
  double y = Random().nextDouble() * 720;//縱坐標(biāo)
  double radio = Random().nextDouble() * 2 + 4;// 最小3最大5
  double speed = Random().nextDouble() * 2 + 2;// 最小2最大4

  start(){
    y = y + speed;
    if(y > 720){//如果雪花落地了,那么重新生成位置、大小、速度等
      x = Random().nextDouble() * 360;
      y = 0;
      radio = Random().nextDouble() * 2 + 4;// 最小3最大5
      speed = Random().nextDouble() * 2 + 2;// 最小2最大4
    }

  }
}

八、第三方動畫框架
和安卓一樣,flutter也支持lottie還支持Flare等。可以去插件庫看看。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容