Flutter——实现网易音乐登录页的波纹效果

314 阅读3分钟

介绍

来自 仿网易云音乐App

预览图

分析

经过观察,可以发现,这是一个相对简单的动画,由两部分组成:

logo:自身没有任何动画效果
波纹:向外扩散的淡出波纹,最多有两条

这样,我们实现的话可以借助Stack来做。

实现

首先我们创建一个LogoWidget,它包含整个我们要做的功能

代码是基于Bedrock框架所写,widget的书写和创建你可能不太熟悉,可以点击下方链接了解

代码较多时,我会将说明写在注释里,方便联系阅读

LogoWidget

我们先创建3个变量

  final List<Widget> list = [];//用于存放widget,是stack的children
  Timer timer; //计时器,用于控制波纹 add/remove
  Image logo; //网易云的logo 

接下来我们对它们进行初始化

  @override
  void initState() {
    logo = Image.asset(ImageHelper.wrapAssetsIcon('logo'),width: getWidthPx(150),height: getWidthPx(150),);
    super.initState();

    list.add(logo);
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      initTimer();
    });
  }

  void initTimer(){
     timer = Timer.periodic(Duration(seconds: 2), (timer) {
       if(mounted){
         ///理论上讲 第二个总是先完成的
         list.add(WaveWidget((index){
           list.removeAt(1);//回调用于移除淡出的波纹
         }).generateWidget());
         setState(() {

         });
       }else{
         timer.cancel();
       }
     });
  }

initState里面的代码简单,不做赘述,我们看一下这个方法:

initTimer()

可以看到里面启动了一个timer,每2秒回执行以下第二个参数(Function)。

因为我们每2秒显示一个波纹,且波纹是会淡出的,所以理论上list内部应该只有三个widget,即要移除淡出完的widget

1,logo   2, 波纹1号   3,波纹2号

因为list是有序的,所以我们应该移除 index=1的元素,具体什么时候移除,我们交给了 waveWidget来处理。

WaveWidget

首先我们声明一个回调

typedef AnimationCallback = void Function(int index);//这个index貌似多余了

变量:

  final AnimationCallback animateDone;
  WaveWidget( this.animateDone);

  AnimationController controller;
  Animation first;//名字起2了...
	//波纹的透明度
  double opacity = 0.8;

很简单,不做赘述,我们初始化一下它们:

  @override
  void initState() {
    controller = AnimationController(duration: Duration(seconds: 3),vsync: this);
    //我们确定了动画的最大值和最小值 即 圆的最小半径和最大半径
    first = Tween<double>(begin: getWidthPx(180),end: getWidthPx(360)).animate(controller);
    super.initState();
	
    //对动画进行监听
    first.addListener(() {
    	//我们根据动画的执行进度,算出对应的 不透明度, 最终是0,即完全透明
      opacity = (1-first.value/getWidthPx(360)).clamp(0.0, 1.0);//这里要控制一下阈值,因为会超出的
      setState(() {

      });
    });
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
    ///开始动画
      controller.forward();
    });
  }

接下来我们看一下 布局

  @override
  Widget build(BuildContext context) {
    return Container(
      width: first.value,height: first.value,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(color: Colors.white.withOpacity(opacity),width: getWidthPx(2))
      ),
    );
  }

很简单,创建一个圆圈,半径与animation.value进行绑定,这样随着动画的执行,圆圈就会越变越大,颜色越来越淡。

至此整个动画就完成了,下面是完整代码

完整代码

class LogoWidget extends WidgetState with SingleTickerProviderStateMixin{

  final List<Widget> list = [];
  Timer timer;
  Image logo;
  @override
  void initState() {
    logo = Image.asset(ImageHelper.wrapAssetsIcon('logo'),width: getWidthPx(150),height: getWidthPx(150),);
    super.initState();

    list.add(logo);
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      initTimer();
    });
  }

  void initTimer(){
     timer = Timer.periodic(Duration(seconds: 2), (timer) {
       if(mounted){
         ///理论上讲 第二个总是先完成的
         list.add(WaveWidget((index){
           list.removeAt(1);
         }).generateWidget());
         setState(() {

         });
       }else{
         timer.cancel();
       }
     });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: getWidthPx(500),height: getWidthPx(500),
      child: Stack(
        alignment: Alignment.center,
        children: list,
      ),
    );
  }

}
typedef AnimationCallback = void Function(int index);

class WaveWidget extends WidgetState with SingleTickerProviderStateMixin{


  final AnimationCallback animateDone;
  WaveWidget( this.animateDone);

  AnimationController controller;
  Animation first;

  double opacity = 0.8;



  @override
  void initState() {
    controller = AnimationController(duration: Duration(seconds: 3),vsync: this);
    first = Tween<double>(begin: getWidthPx(180),end: getWidthPx(360)).animate(controller);
    super.initState();
    first.addListener(() {
      opacity = (1-first.value/getWidthPx(360)).clamp(0.0, 1.0);
      setState(() {

      });
    });
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      controller.forward();
    });
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Container(
      width: first.value,height: first.value,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(color: Colors.white.withOpacity(opacity),width: getWidthPx(2))
      ),
    );
  }

}

Demo

内部搜索即可

仿网易云音乐