阅读 806

【中秋征文活动】月亮阴晴圆缺动画Flutter版本来啦~

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

前几天在掘金看到一个安卓版本实现的月相变化,感觉挺有意思,值此中秋佳节,我也实现一个Flutter版本试一下吧~

先来介绍一下关于月相的知识

每当月球运行到太阳与地球之间,被太阳照亮的半球背对着地球时,人们在地球上就看不到月球,这一天称为“新月”,也叫“朔”,这时是农历初一。 过了新月,月球顺着地球自转方向运行,亮区逐渐转向地球,在地球上就可看到露出一丝纤细银钩似的月球,出现在西方天空,弓背朝向夕阳,这一月相叫“娥眉月”,这时是农历初三、四。 随后,月球在天空里逐日远离太阳,到了农历初七、八,半个亮区对着地球,人们可以看到半个月亮(凸面向西),这一月相叫“上弦月”。 上弦月过后,在农历初九左右-农历十四左右,便是“凸月”。我们能看到月球的大半部分。 当月球运行到地球的背日方向,即农历十五、十六,月球的亮区全部对着地球,我们能看到一轮圆月,这一月相称为“满月”,也叫“望”。 满月过后,亮区西侧开始亏缺,到农历二十二、二十三,又能看到半个月亮(凸面向东),这一月相叫做“下弦月”。在这一期间月球日渐向太阳靠拢,半夜时分才能从东方升起。 再过四五天,月球又变成一个娥眉形月牙,弓背朝向旭日,这一月相叫“残月”。 当月球再次运行到日地之间,月亮又回到“新月”

代码实现效果

月盈.gif

月亏.gif

思路

  • 首先,根据月相的变化图, 我们可以看出,在新月到峨眉月再到上弦月的过程中,可以看做是右半月被一个椭圆遮住,这个椭圆越来越窄,直到消失的过程;
  • 在上弦月到凸月再到满月的过程中,可以看做是一个右半月,左边有一个同色的椭圆越来越宽,直到成圆的过程;

知道这个,我们接下来就要动手编写代码了 示例如下图:

image.png

代码逻辑

此处关于画布 画笔的知识不在做过多介绍,有兴趣的同学可以参考

juejin.cn/post/684490…

1.我们首先使用画圆弧的方法,先画出一个半圆

double r = 100;
double centerX = size.width / 2;
double centerY = size.height / 2;
Rect rect2 = Rect.fromCircle(center: Offset(centerX, centerY), radius: r);
canvas.drawArc(
rect2,

math.pi * 3 / 2,

math.pi,

true,

_paint

..color = Colors.yellow

..style = PaintingStyle.fill);
复制代码
void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
  • 需要指定圆弧的位置rect(圆心,半径)
  • 设置startAngle,sweepAngle,这里是采用的弧度制,想必你可能还有些印象,见下图;
  • 还有一个useCenter的属性,如果“useCenter”为true,则弧为封闭回到中心,形成一个圆形扇区。否则,圆弧不会闭合,形成一个圆段。
  • 指定_paint

image.png

2.接下来,我们需要画椭圆了

我们知道,画椭圆是需要指定一个矩形区域的,椭圆是在这个矩形之中内切的,我们需要使用左上和右下角坐标来确定矩形的大小和位置. 分析我们想要实现的效果,可以看出,我们想要实现的椭圆,其上下顶点与圆的上下顶点是重合的,想要实现椭圆高度不变,宽度变窄的过程,其实就是外切矩形的左上角及右下角点在如图横线方向变化

Rect rect1 = Rect.fromPoints(Offset(centerX - changeR, centerY - r),

Offset(centerX + changeR, centerY + r));

_paint.color = Colors.black;

canvas.drawOval(rect1, _paint);
复制代码

image.png

3.接下来,加入动画

加入一个Tween动画,改变椭圆的外切矩形大小,从而实现椭圆+半圆的月相变化 这样,我们就实现了一个月盈的过程;同理,我们也可以实现月亏的过程了.

image.png

完整代码:

动画及逻辑

import 'dart:ui';

import 'package:flutter/material.dart';

import 'dart:math' as math;

class MoonPage extends StatefulWidget {
  MoonPage({Key? key}) : super(key: key);

  @override
  _MoonPageState createState() => _MoonPageState();
}

class _MoonPageState extends State<MoonPage>
    with SingleTickerProviderStateMixin {
  AnimationController? _controller; // 动画 controller

  Animation<double>? _animation; // 动画

  double changeR = 100.0;

  bool bigToSmall = true;

  int times = 0;

  double _screenWidth = 0, _screenHeight = 0;

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

    _controller =
        AnimationController(duration: Duration(seconds: 4), vsync: this);

    _animation = Tween(begin: 100.0, end: 0.0).animate(_controller!)
      ..addListener(() {
//times = 1; 月盈--月半 100->0

//times = 2; 月盈--满月 0->100

//times = 3; 月亏--月半 100->0

//times = 4; 月亏--无月 0->100

        if (_animation?.status == AnimationStatus.completed) {
          times = times + 1;
          _controller?.reverse();
        } else if (_animation?.status == AnimationStatus.dismissed) {
          times = times + 1;
          _controller?.forward();

          if (times == 4) {
            times = 0;
          }
        }

        if ((_animation?.value ?? 0.0) > changeR) {
          bigToSmall = false;
        } else {
          bigToSmall = true;
        }

        changeR = _animation?.value ?? 0.0;

        setState(() {});
      });

// 显示的时候动画就开始

    _controller?.forward();
  }

  @override
  Widget build(BuildContext context) {
    _screenHeight = MediaQuery.of(context).size.height;
    _screenWidth = MediaQuery.of(context).size.width;
    return Scaffold(
      appBar: AppBar(
        title: Text("月亮"),
      ),
      body: Container(
        color: Colors.black,
        width: _screenWidth,
        height: _screenHeight,
        child: Stack(children: [
          Offstage(
            offstage: !(times < 2),
            child: CustomPaint(
              painter: MoonFullShapedPainter(
                  _screenWidth, _screenWidth, changeR, bigToSmall),
              size: Size(_screenWidth, _screenWidth),
            ),
          ),
          Offstage(
            offstage: (times < 2),
            child: CustomPaint(
              painter: MoonLossShapedPainter(
                  _screenWidth, _screenWidth, changeR, bigToSmall),
              size: Size(_screenWidth, _screenWidth),
            ),
          ),
          Positioned(top:30,left:20,child: Text("人有悲欢离合,月有阴晴圆缺",style: TextStyle(color: Colors.yellow,fontSize: 18),))
        ]),
      ),
    );
  }

  @override
  void dispose() {
    _animation?.isDismissed;

    _controller?.dispose();

    super.dispose();
  }
}
复制代码

月盈:

class MoonFullShapedPainter extends CustomPainter {
  double width;

  double height;

  double changeR;

  bool bigToSmall;

  MoonFullShapedPainter(this.width, this.height, this.changeR, this.bigToSmall);

  Paint _paint = Paint()..strokeWidth = 2.0;

  @override
   void paint(Canvas canvas, Size size) {
    double r = 100;

    double centerX = size.width / 2;

    double centerY = size.height / 2;

    Rect rect2 = Rect.fromCircle(center: Offset(centerX, centerY), radius: r);

// 还需要开始的弧度、结束的弧度(0-2*pi)、是否使用中心点绘制、以及paint弧度

    canvas.drawArc(
        rect2,
        math.pi * 3 / 2,
        math.pi,
        true,
        _paint
          ..color = Colors.yellow
          ..style = PaintingStyle.fill);

    /// 椭圆

//使用左上和右下角坐标来确定矩形的大小和位置,椭圆是在这个矩形之中内切的

    Rect rect1 = Rect.fromPoints(Offset(centerX - changeR, centerY - r),
        Offset(centerX + changeR, centerY + r));

    if (bigToSmall) {
//右边黄色半圆+白色椭圆100->0

      _paint.color = Colors.black;

      canvas.drawOval(rect1, _paint);
    } else {
//右边黄色半圆,黄色椭圆0->100

      _paint.color = Colors.yellow;

      canvas.drawOval(rect1, _paint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}
复制代码

月亏:

class MoonLossShapedPainter extends CustomPainter {
  double width;

  double height;

  double changeR;

  bool bigToSmall;

  MoonLossShapedPainter(this.width, this.height, this.changeR, this.bigToSmall);

  Paint _paint = Paint()..strokeWidth = 2.0;

  @override
  void paint(Canvas canvas, Size size) {
    double r = 100;

    double centerX = size.width / 2;

    double centerY = size.height / 2;

    Rect rect2 = Rect.fromCircle(center: Offset(centerX, centerY), radius: r);

// 还需要开始的弧度、结束的弧度(0-2*pi)、是否使用中心点绘制、以及paint弧度

    canvas.drawArc(
        rect2,
        math.pi / 2,
        math.pi,
        true,
        _paint
          ..color = Colors.yellow
          ..style = PaintingStyle.fill);

    /// 椭圆

//使用左上和右下角坐标来确定矩形的大小和位置,椭圆是在这个矩形之中内切的

    Rect rect1 = Rect.fromPoints(Offset(centerX - changeR, centerY - r),
        Offset(centerX + changeR, centerY + r));

    if (bigToSmall) {
//左边边黄色半圆+白色椭圆100->0

      _paint.color = Colors.yellow;

      canvas.drawOval(rect1, _paint);
    } else {
//右边黄色半圆,黄色椭圆0->100

      _paint.color = Colors.black;

      canvas.drawOval(rect1, _paint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}
复制代码
文章分类
iOS