我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!
每当下雨打雷,那么就能在iOS的天气预报app中: 欣赏到雨滴砸落在小花伞上,溅到手上; 欣赏到乌云变化成不同的形状,匆匆从东边赶往西边; 欣赏到闪电变为一把利剑贯穿整个手机,忽而凶猛忽而迅捷。
闪电
叹为观止的闪电,好想拥有。脑中闪过,万能的flutter能否实现我的梦想。构思中...
1. 夜空
为了凸显闪电的明亮,或因乌云密布,需要一个暗色的背景。最好是灰蒙蒙的群山,上方是急骤又瞬变的雷云,突然一条火蛇划开夜空。没有素材,再好的构思也没用,翻遍网图,没有心满意足的。退而求其次吧,来一张万家灯火的城镇,被黑压压的乌云覆盖的也可以,忽然夺人心魂的闪电,打在对面楼顶,映入你的眼帘。
2. 闪电
闪电从苍穹任意位置出发,以不可判断的方向击向地面.闪亮从暗暗的到明亮亮的,细细的到粗粗的,慢慢地到快快的。
3. 点亮夜空
当闪电划过夜空时,慢慢地点亮夜空,并且在最后释放所有能量的时候,把整个夜空染成白色。
大概就是这个意思了。
代码实现
采用三层构造,一、背景图,夜空。二、随闪电变化亮度层。三、闪电。
一、夜空
从网上找的素材darkSky.jpeg
,放入到images
文件夹中,并在pubspec.yaml
中的assets
添加- images/darkSky.jpeg
.最后点击一下Pub get
,如果是是在终端那么输入flutter pub get
。此时就可以引入素材了。
SizedBox(
width: windowSize.width,
height: windowSize.height,
child: Image.asset(
"images/darkSky.jpeg",
fit: BoxFit.fitHeight,
),
),
windowSize
为整个手机屏幕的大小。背景图铺满整个屏幕,使效果看起来真实一些。
二、 亮度层
随着闪电划开夜空,在伸向地面的过程中,夜空慢慢地被点亮。
Container(
color: backgroundColor(_animation.value),
width: windowSize.width,
height: windowSize.height,
),
_animation
为闪电动画的值,为了与闪电同步。backgroundColor
为不同的颜色。
Color backgroundColor(double value) {
var whiteColors = [
Colors.white10,
Colors.white10,
Colors.white10,
Colors.white10,
Colors.white10,
Colors.white12,
Colors.white12,
Colors.white12,
Colors.white12,
Colors.white12,
Colors.white24,
Colors.white24,
Colors.white24,
Colors.white24,
Colors.white24,
Colors.white24,
Colors.white24,
Colors.white24,
Colors.white70,
Colors.white70,
];
var index = (value * whiteColors.length).floor();
return whiteColors[index];
}
颜色值采用的是一个数组,根据不同的时间点获取不同的值。前前后后反反复复调整了很多次,希望能达到真实效果。不过似乎还是差亿点点。(自行调整调整)
三、绘制闪电
重点来了。全网搜了搜,尽然没有现成的。好吧,自己造一个。看到一篇绘制闪电的文章,这个绘制的思路可以用,但不能满足要求。 Let's begin.
1. 动画
闪电和亮度层,都需要一个动画过程。那么通过SingleTickerProviderStateMixin
创建一个动画,根据动画的值来绘制闪电的每一个时刻。
late AnimationController _controller;
late Animation<double> _animation;
static const int durationMilliseconds = 1000;
在initState
中初始化
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: durationMilliseconds));
_animation = Tween<double>(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic))
..addListener(() {
setState(() {
debugPrint("");
});
})
..addStatusListener((status) {
if (AnimationStatus.completed == status) {
setState(() {
pointsList.clear();
});
_controller.reset();
}
});
此时就能使用_animation.value
来控制动画的不同时刻了。
其中的curve
可以控制闪电出现方式,尝试了Curves
不同类型,发现Curves.easeOutCubic
比较合适,更贴近现实一些。
2. 生成闪电的轨迹
按绘制闪电的思路生成轨迹点,仅仅坐标点就可以,先不进行绘制。因为连接不同点的使用直线,效果不太自然。采用贝塞尔曲线更加的完美。
void drawLightning(
double x1, double y1, double x2, double y2, double displace) {
if (displace < curDetail) {
pointsList.add(Offset(x1, y1));
pointsList.add(Offset(x2, y2));
} else {
var midX = (x2 + x1) / 2;
var midY = (y2 + y1) / 2;
midX += (random.nextDouble() - 0.5) * displace;
midY += (random.nextDouble() - 0.5) * displace;
midX = midX.clamp(0, windowSize.width);
midY = midY.clamp(0, windowSize.height);
drawLightning(x1, y1, midX, midY, displace / 2);
drawLightning(midX, midY, x2, y2, displace / 2);
}
}
使用一个数组来保存闪电的所有坐标点。
var curDetail = 10.0;
var random = Random();
var pointsList = <Offset>[];
闪电的开始点,屏幕最上方开始:x轴上随机一点,Y轴上20内随机一点。
Offset _createStart() {
return Offset(
random.nextDouble() * windowSize.width, -random.nextDouble() * 20);
}
闪电的结束点,屏幕最下方结束:x轴上随机一点,Y轴上屏幕外20内随机一点。
Offset _createEnd() {
return Offset(random.nextDouble() * windowSize.width,
windowSize.height + random.nextDouble() * 20);
}
创建闪电坐标点,每一次调用的时候就会随机生成,每一次都不一样。
Future<List<Offset>> create() async {
Offset start = _createStart();
Offset end = _createEnd();
drawLightning(start.dx, start.dy, end.dx, end.dy, 500);
return pointsList;
}
3. 绘制闪电
自定义CustomPainter
,传入pointsList
和animationValue
就可以开始绘制了。
// 点集合
final List<Offset> points;
final double animationValue;
final Paint _mainPaint = Paint();
final Path _linePath = Path();
LightningPainter(this.points, this.animationValue);
使用贝塞尔曲线将pointsList
的点连接起来,并且生成_linePath
路径,此时就是闪电的路径了。
void addBezierPathWithPoints(Path path, List<Offset> points) {
for (int i = 0; i < points.length - 1; i++) {
Offset current = points[i];
Offset next = points[i + 1];
if (i == 0) {
path.moveTo(current.dx, current.dy);
double ctrlX = current.dx + (next.dx - current.dx) / 2;
double ctrlY = next.dy;
path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy);
} else if (i < points.length - 2) {
double ctrl1X = current.dx + (next.dx - current.dx) / 2;
double ctrl1Y = current.dy;
double ctrl2X = ctrl1X;
double ctrl2Y = next.dy;
path.cubicTo(ctrl1X, ctrl1Y, ctrl2X, ctrl2Y, next.dx, next.dy);
} else {
path.moveTo(current.dx, current.dy);
double ctrlX = current.dx + (next.dx - current.dx) / 2;
double ctrlY = current.dy;
path.quadraticBezierTo(ctrlX, ctrlY, next.dx, next.dy);
}
}
}
如果直接绘制上,那么就不能够实现闪电那种:从一个点出现闪电,然后沿着轨迹慢慢地展开。那么需要用到Path
的pathMetric
来实时获取当前的子path
了。刚从“岛上码农”那学的。
var pathMetric = _linePath.computeMetrics();
for (var metric in pathMetric) {
var subPath = metric.extractPath(0.0, metric.length * animationValue);
for (var i = 0; i < whiteColors.length; i++) {
canvas.drawPath(
subPath,
_mainPaint
..color = whiteColors[i]
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.round
..strokeWidth = strokeWidths[i] * animationValue);
}
break;
}
根据animationValue
不同,绘制不同的path,使得闪电更加贴近真实。
var whiteColors = [
const Color(0x05FFFFFF),
Colors.white12,
Colors.white30,
Colors.white,
];
var strokeWidths = [60.0, 40.0, 20.0, 10.0];
闪电的颜色和宽度,如果仅仅一条,那么就少了一些光晕,也就是被闪电点亮的空气。在闪电周围再加上一些浅色的闪电,一起展现出更完美的闪电。 还有一点闪电的宽度,开始时窄一点,到最后宽一点,也比较接近现实。
完整代码上传到lightning.dart了。
参考文章: