Flutter 轮子之气泡登录页(1)

Flutter轮子之气泡登录页!

  • 渐变动画
  • 随机的透明,大小的圆(气泡)
  • 高斯模糊
  • 顶部Hellow Word文本
  • 底部TextField和登录按钮

先来看看效果:
效果图(1.1):
Flutter 轮子之气泡登录页(1)_第1张图片

黑屏时间这么长是因为我在输入密码的时候Flutter自动给我黑的,输完之后就好了

还是老套路,先不急着写代码,先来分析一波:

  • 背景为蓝色渐变色
  • 随机的透明,大小圆(气泡)
  • 还有一点点高斯模糊
  • 上面的Hellow Word布局
  • 下面的登录布局
  • 使用叠加布局把他们叠合到一起,就是今天要完成的最终效果
  • 使用动画,让圆动起来(气泡)

好了,分析完成了,接下来根据分析的来进行分阶段完成即可

先使用Stack()组件让他们叠加起来

 @override
  Widget build(BuildContext context) {
     
    return Scaffold(
        body: Container(
      //设置为屏幕为屏幕的宽 也可以使用MediaQuery.of(context).size.width
      width: double.infinity,
      //设置为屏幕为屏幕的高 也可以使用MediaQuery.of(context).size.height
      height: double.infinity,
      child: Stack(
        children: [
          //渐变背景
          buildGradientBackground(),
          //随机气泡
          buildRandomBubble(context),
          //高斯模糊
          buildGaussianBlur(),
          //Hellow Word
          buildTopText(),
          //底部输入框
          buildButtonTextField(),
        ],
      ),
    ));
  }

渐变动画

	//渐变背景
  buildGradientBackground() {
     
    return Container(
      decoration: BoxDecoration(
          gradient: LinearGradient(
              begin: Alignment.topRight,  //LinearGradient属性
              end: Alignment.bottomLeft,  //属性
              colors: [
                Colors.blue.withOpacity(0.3),
                Colors.blueAccent.withOpacity(0.3),
                Colors.lightBlueAccent.withOpacity(0.3),
                Colors.lightBlue.withOpacity(0.3),
              ]),
      ),
    );
  }

这段代码很简单,来看看效果吧:

效果图(1.2)

Flutter 轮子之气泡登录页(1)_第2张图片

随机的透明,大小的圆(气泡)

要想画出随机的透明的圆,咋们第一步得先知道怎么画一个随机的透明圆,然后再让他随机起来就好了

 ///随机气泡
  buildRandomBubble(BuildContext context) {
     
    return CustomPaint(
      painter: MyCustomPaint(),
      //设置屏幕大小  MediaQuery.of(context).size是屏幕的宽和高
      size: MediaQuery.of(context).size,
    );
  }

class MyCustomPaint extends CustomPainter {
     
  //设置锯齿
  Paint _paint = new Paint()
    ..isAntiAlias = true
  ..color = Colors.white.withOpacity(0.5)
  ..strokeWidth = 50;
  List<BubbleBean> _list;
  Random _random;

  MyCustomPaint(this._list, this._random);

  @override
  void paint(Canvas canvas, Size size) {
     
    canvas.drawCircle(Offset(100, 100), 50, _paint);
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
     
    return true;
  }
}

先开看看效果,在一步步解释:

效果图(1.3):
Flutter 轮子之气泡登录页(1)_第3张图片
可以从效果图并结合代码看出,这是一个100*100半透明的白色圆

在来分析一下代码吧:

在Flutter中,自定义Widget是使用的CustomPaint()组件,最重要的有2个参数

  • 参数一:painter自定义画布(需要自定义类,继承自画布的类(CustomPainter))
  • 参数二:size 设置的是画布的大小
class MyCustomPaint extends CustomPainter {
     
  @override
  void paint(Canvas canvas, Size size) {
     
  
	}

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

继承自CustomPainter需要重写2个方法:

  • 重写方法一:paint() 用来自定义画板,并绘制,canvas是绘制的方法,size是效果图(1.4)传过来的:

效果图(1.4)
Flutter 轮子之气泡登录页(1)_第4张图片

  • 重写方法二:shouldRepaint()是否刷新,true刷新 false不刷新,简单的理解就是调用setState(){}方法是否执行该操作

paint()画布举例:

绘制线:

//定义画笔
Paint _paint = new Paint()
  //设置锯齿
    ..isAntiAlias = true
    //透明颜色
  ..color = Colors.white.withOpacity(0.5)
    //粗细
  ..strokeWidth = 50;
  
//绘制线
 canvas.drawLine(Offset(100, 100), Offset(350, 350), _paint);

效果图(1.5):
Flutter 轮子之气泡登录页(1)_第5张图片

走到这里,咋们就可以完成自己画一个自定义半透明的白色圆了

因为咋们在效果图中并不是一个圆,而是20个随机的圆,所以需要定义一个类,吧这些属性都单独放到类里面执行;

定义圆的属性:

class BubbleBean {
     
  //位置
  Offset postion;

  //颜色
  Color color;

//运动的速度
  double speed;

  //运动的角度
  double theta;

  //半径
  double radio;
}

初始化圆的属性,并使用随机数:


 //随机数
  Random _random = new Random();

  //最大运行速度
  double _maxSpeed = 1.0;

  //最大的半径
  double _maxRadius = 100;

  //设置角度
  double _maxTheta = 2 * pi;


 @override
  void initState() {
     
    // TODO: implement initState
    super.initState();
    /**
     * 循环得到30个气泡
     */
    for (int i = 0; i < 30; i++) {
     
      BubbleBean bubbleBean = new BubbleBean();

      //获取随机透明度
      bubbleBean.color = getRandomColor(_random);

      //初始化设置位置
      bubbleBean.postion = Offset(-1, -1);

      /**
       *  _random.nextDouble() 获取的是0-1之间不为负数的小数
       */

      //设置随机运动
      bubbleBean.speed = _random.nextDouble() * _maxSpeed;

      //设置随机半径
      bubbleBean.radio = _random.nextDouble() * _maxRadius;

      //随机的角度
      bubbleBean.theta = _random.nextDouble() * _maxTheta;

      _list.add(bubbleBean);
    }
  }
  

Color getRandomColor(Random random) {
     
  //透明度为0-255
  int a = random.nextInt(200);
  //这里设置的是白色透明度为0-200的圆
  return Color.fromARGB(a, 255, 255, 255);
}

补充:

使用随机数Random()需要导入import ‘dart:math’;包

_random.nextDouble() 获取的是0-1之间不为负数的小数

_random.nextInt(200); 获取的是0-200之间的数

这样的话,30个随机大小,随机透明度的圆就创建好了~~

在来重新写MyCustomPaint()这个类

  ///随机气泡
  buildRandomBubble(BuildContext context) {
     
    return CustomPaint(
      painter: MyCustomPaint(_list, _random),
      //设置屏幕大小  MediaQuery.of(context).size是屏幕的宽和高
      size: MediaQuery.of(context).size,
    );
  }

class MyCustomPaint extends CustomPainter {
     

  Paint _paint = new Paint()
  //设置锯齿
    ..isAntiAlias = true;
  
  List<BubbleBean> _list;
  Random _random;

  MyCustomPaint(this._list, this._random);

  @override
  void paint(Canvas canvas, Size size) {
     

   _list.forEach((element) {
     
     //重新根据速度与运动角度计算Offset
      Offset newOffset = coordinates(element.speed, element.theta);
      
	  print("szjpaintdx${newOffset.dx}paintdy${newOffset.dy}");
      double dx = newOffset.dx + element.postion.dx;
      double dy = newOffset.dy + element.postion.dy;

     /**
       * 通过if判断  保证dx坐标在屏幕里面
       */
      if (dx < 0 || dx > size.width) {
     
        //_random.nextDouble() 生成的是0-1之间不为负数的小数    size.width是屏幕的宽
        dx = _random.nextDouble() * size.width;
      }


       /*通过if判断  保证dy坐标在屏幕里面*/
      if (dy < 0 || dy > size.height) {
     
        //_random.nextDouble() 生成的是0-1之间不为负数的小数    size.height是屏幕的高
        dy = _random.nextDouble() * size.height;
      }

      element.postion = Offset(dx, dy);
    });

    //绘制
    _list.forEach((element) {
     
      _paint.color = element.color;

      canvas.drawCircle(element.postion, element.radio, _paint);
    });
  }

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

  //重新计算Offset
  Offset coordinates(double speed, double theta) {
     
    return Offset(speed * sin(theta), speed * cos(theta));
  }
}


这里主要分析的就是paint()方法里面的代码:

  //重新计算Offset
  Offset coordinates(double speed, double theta) {
     
    return Offset(speed * sin(theta), speed * cos(theta));
  }

这个方法是通过速度和角度计算出圆是朝什么方向移动的,

先来看看Log:
Flutter 轮子之气泡登录页(1)_第6张图片
可以看到得到的dx和dy,是一些-1到1之间的数

再往下看代码:

	 //重新根据速度与运动角度计算Offset
      Offset newOffset = coordinates(element.speed, element.theta);

      print("szjpaintdx${newOffset.dx}paintdy${newOffset.dy}");
      double dx = newOffset.dx + element.postion.dx;
      double dy = newOffset.dy + element.postion.dy;

这段代码就是为了获取到一个最新的值,拿第一个气泡来举例,比如说,第一个气泡计算出新的dx == 0.1 dy == -0.3 那么计算第二次的时候,就会以dx == 0.1 dy == -0.3这个基础来计算,这段代码的意思就是如果第一个气泡是向上移动的,他就会一直向上移动,

在来看看最后的这个if判断:

 /**
       * 通过if判断  保证dx坐标在屏幕里面
       */
      if (dx < 0 || dx > size.width) {
     
        //_random.nextDouble() 生成的是0-1之间不为负数的小数    size.width是屏幕的宽
        dx = _random.nextDouble() * size.width;
      }


       /*通过if判断  保证dy坐标在屏幕里面*/
      if (dy < 0 || dy > size.height) {
     
        //_random.nextDouble() 生成的是0-1之间不为负数的小数    size.height是屏幕的高
        dy = _random.nextDouble() * size.height;
      }

      element.postion = Offset(dx, dy);


这个判断比较好理解,刚刚提到了,如果第一个气泡是向上的就会让他一直向上,然后如果他已经出了屏幕,则把它取消掉重新创建一个新的气泡来变化

最后这部分代码最简单,绘制圆

	//绘制
    _list.forEach((element) {
     
      canvas.drawCircle(element.postion, element.radio, _paint);
    });

来看看效果吧:

效果图(1.6)

Flutter 轮子之气泡登录页(1)_第7张图片
接下来咋们给气泡添加动画

 @override
  void initState() {
     
    super.initState();
    //创建动画,执行为1秒
    _animationController =
        new AnimationController(vsync: this, duration: Duration(seconds: 1));

    //动画监听器
    _animationController.addListener(() {
     
      setState(() {
     });
    });
    
    //重复执行
   _animationController.repeat();

在来看看添加动画之后的效果:

效果图(1.7):


是不是大家有迷惑的地方嘞~迷惑的地方就是代码能看懂,但是你设置了动画,并没有使用呀,是不是我忘记写了,哈哈哈哈哈当然不是啦,

注意看这里:Flutter 轮子之气泡登录页(1)_第8张图片
这里刷新的是这个地方:
Flutter 轮子之气泡登录页(1)_第9张图片
刚刚也说了,shouldRepaint()这个方法返回的是时候刷新,咋们这里返回true就代表了需要刷新,刷新的话就会调用paint方法,则一直会计算,一直会刷新,气泡就都动起来啦~

走到这里,气泡就可以动起来了!

高斯模糊


  /**
   * 高斯模糊
   */
  buildGaussianBlur() {
     
    return BackdropFilter(
      filter: ImageFilter.blur(sigmaX: 0.3, sigmaY: 0.3),
      child: Container(
        color: Colors.white.withOpacity(0.2),
      ),
    );
  }

顶部Hellow Word文本


  //顶部Hellow Word文本
  buildTopText() {
     
    return Container(
      width: MediaQuery.of(context).size.width,
      margin: EdgeInsets.only(top: 100),
      child: Text("Hellow Word",
          textAlign: TextAlign.center,
          style: TextStyle(
              fontSize: 40,
              color: Colors.lightBlue,
              fontWeight: FontWeight.bold)),
    );
  }

效果图(1.8):
Flutter 轮子之气泡登录页(1)_第10张图片

底部TextField和登录按钮

 //底部按钮
  buildButtonTextField() {
     
    return Positioned(
      left: 20,
      right: 20,
      bottom: 20,
      child: FadeTransition(
        //设置渐变动画
        opacity: _tweenAnimation,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            MyTextField("账号", Icon(Icons.format_list_numbered), false),
            SizedBox(
              height: 10,
            ),
            MyTextField("密码", Icon(Icons.qr_code), true),
            SizedBox(
              height: 10,
            ),
            Container(
              height: 50,
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
     
                  Navigator.push(context,
                      MaterialPageRoute(builder: (BuildContext context) {
     
                    return GalleryUnit();
                  }));
                },
                child: Text("注册", textAlign: TextAlign.center),
              ),
            ),
            SizedBox(
              height: 10,
            ),
            Container(
              height: 50,
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
     
                  //跳转页面并吧当前状态取消掉
                  Navigator.pushAndRemoveUntil(context,
                      new MaterialPageRoute(builder: (BuildContext context) {
     
                    return MainPage();
                  }), (route) => route == null);
                },
                child: Text("登录", textAlign: TextAlign.center),
              ),
            ),
          ],
        ),
      ),
    );
  }

class MyTextField extends StatelessWidget {
     
  String _lable;

  Icon _icon;

  bool isobscureText;

  MyTextField(this._lable, this._icon, this.isobscureText);

  @override
  Widget build(BuildContext context) {
     
    return Container(
      child: TextField(
        obscureText: isobscureText,
        decoration: InputDecoration(
          //透明填充
          // filled: true,
          icon: _icon,
          labelText: _lable,
          //选中
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(15),
          ),
          //未选中
          /* enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(15),
                //不显示边框
                borderSide: BorderSide(color: Colors.black38))*/
        ),
      ),
    );
  }
}

这里的布局非常简单,就不细说了

好了,到这里就结束了,来看看最终的效果吧:

效果图(1.9):

刚开始这个二维码是我的公众号哦,~

完整代码

猜你喜欢:

Flutter 轮子之下雪动画(2)

原创不易,您的点赞就是对我最大的支持,留下您的点赞吧~

Flutter 轮子之气泡登录页(1)_第11张图片

你可能感兴趣的:(Flutter,flutter,animation)