- 原文地址:Zero to One with Flutter, Part Two
- 原文做者:Mikkel Ravn
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:hongruqi
- 校对者:Fengziyin1234
探索如何在跨平台移动应用程序的上下文中为复合图形对象设置动画。引入一个新的概念,如何将 tween 动画应用于结构化值中,例如条形图表。所有代码,按步骤实现。前端
修订:2018 年 8 月 8 日适配 Dart 2。GitHub repo 而且差别连接于 2018 年 10 月 17 日添加。android
如何进入一个新的编程领域 ?实践是相当重要的,由于那是学习和模仿更有经验同行的代码。我我的喜欢挖掘概念:试着从最基本的原则出发,识别各类概念,探索它们的优点,有意识地寻求它们的本质。这是一种理性主义的方法,它不能独立存在,而是一种智力刺激的方法,能够更快地引导你得到更深刻的看法。ios
这是 Flutter 及其 widget 和 tween 概念介绍的第二部分也是最后一部分。在 Flutter 从 0 到 1 第一部分 最后,在咱们这么多 widges 的选择中,这个 tree 包含了下面两个:git
高度动画 制做 Bar 的高度的动画github
这个动画是经过 BarTween
来实现的,在第一部分中我曾经代表 tween
的概念能够扩展开去解决更加复杂的问题,这里咱们会将会经过为更多属性的和多种配置下的条形图做出设计来证实这一点。算法
首先咱们为单个条形图添加颜色属性。在 Bar
类的 height
字段旁边添加一个 color
字段,并更新 Bar.lerp
对它们进行线性插值。这种模式很典型:编程
经过线性插值对应的元件,生成 tween 的合成值。canvas
回想一下第一部分,lerp
是 线性插值
的缩写。后端
import 'dart:ui' show lerpDouble;
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class Bar {
Bar(this.height, this.color);
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, t),
);
}
}
class BarTween extends Tween<Bar> {
BarTween(Bar begin, Bar end) : super(begin: begin, end: end);
@override
Bar lerp(double t) => Bar.lerp(begin, end, t);
}
复制代码
注意这里对与 lerp
的使用。若是没有 Bar.lerp
,lerpDouble
(一般为 double.lerp
)和 Color.lerp
,咱们就必须经过为高度建立 Tween <double>
同时为颜色建立 Tween<Color>
来实现 BarTween
。这些 tweens 将是 BarTween
的实例字段,由构造函数初始化,并在其 lerp
方法中使用。 咱们将在 Bar
类以外屡次重复访问 Bar
的属性。代码维护者可能会发现这并非一个好主意。
为条形的颜色和高度制做动画。
为了在应用程序中使用彩色条,咱们将更新 BarChartPainter
来从 Bar
得到条形图颜色。在 main.dart
中,咱们须要有能力来建立一个空的 Bar
和一个随机的 Bar
。咱们将为前者使用彻底透明的颜色,为后者使用随机颜色。 颜色将从一个简单的 ColorPalette
类中获取,咱们会在它本身的文件中快速实现它。 咱们将在 Bar
类中建立 Bar.empty
和 Bar.random
两个工厂构造函数 (code listing, diff).
条形图涉及各类配置的多种形式。为了缓慢地引入复杂性,咱们的第一个实现将适用于显示固定类别数的值条形图。示例包括每一个工做日的访问者或每季度的销售额。对于此类图表,将数据集更改成另外一周或另外一年不会更改使用的类别,只会更改每一个类别显示的栏。
咱们首先更新 main.dart
,用 BarChart
替换 Bar
,用 BarChartTween
替换 BarTween
(代码列表,差分)。
为了更好体现 Dart 语言优点,咱们在 bar.dart
中建立 BarChart
类,并使用固定数目的 Bar
实例列表来实现它。咱们将使用五个条形图,表示一周中的工做日。而后,咱们须要将建立空条和随机条的函数从 Bar
类中转移到 BarChart
类中。对于固定类别,空条形图合理地被视为空条的集合。另外一方面,让随机条形图成为随机条形图的集合会使咱们的图表变得多种多样。相反,咱们将为图表选择一种随机颜色,让每一个仍然具备随机高度的条形继承该图形。
import 'dart:math';
import 'dart:ui' show lerpDouble;
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'color_palette.dart';
class BarChart {
static const int barCount = 5;
BarChart(this.bars) {
assert(bars.length == barCount);
}
factory BarChart.empty() {
return BarChart(List.filled(
barCount,
Bar(0.0, Colors.transparent),
));
}
factory BarChart.random(Random random) {
final Color color = ColorPalette.primary.random(random);
return BarChart(List.generate(
barCount,
(i) => Bar(random.nextDouble() * 100.0, color),
));
}
final List<Bar> bars;
static BarChart lerp(BarChart begin, BarChart end, double t) {
return BarChart(List.generate(
barCount,
(i) => Bar.lerp(begin.bars[i], end.bars[i], t),
));
}
}
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);
@override
BarChart lerp(double t) => BarChart.lerp(begin, end, t);
}
class Bar {
Bar(this.height, this.color);
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, t),
);
}
}
class BarTween extends Tween<Bar> {
BarTween(Bar begin, Bar end) : super(begin: begin, end: end);
@override
Bar lerp(double t) => Bar.lerp(begin, end, t);
}
class BarChartPainter extends CustomPainter {
static const barWidthFraction = 0.75;
BarChartPainter(Animation<BarChart> animation)
: animation = animation,
super(repaint: animation);
final Animation<BarChart> animation;
@override
void paint(Canvas canvas, Size size) {
void drawBar(Bar bar, double x, double width, Paint paint) {
paint.color = bar.color;
canvas.drawRect(
Rect.fromLTWH(x, size.height - bar.height, width, bar.height),
paint,
);
}
final paint = Paint()..style = PaintingStyle.fill;
final chart = animation.value;
final barDistance = size.width / (1 + chart.bars.length);
final barWidth = barDistance * barWidthFraction;
var x = barDistance - barWidth / 2;
for (final bar in chart.bars) {
drawBar(bar, x, barWidth, paint);
x += barDistance;
}
}
@override
bool shouldRepaint(BarChartPainter old) => false;
}
复制代码
BarChartPainter
在条形图中宽度均匀分布,使每一个条形占据可用宽度的 75%。
固定类别条形图。
注意 BarChart.lerp
是如何调用 Bar.lerp
实现的,使用 List.generate
生产列表结构。固定类别条形图是复合值,对于这些复合值,直接使用 lerp
进行有意义的组合,正如具备多个属性的单个条形图同样(diff)。
这里有一种模式。当 Dart 类的构造函数采用多个参数时,你一般能够线性插值单个参数或多个。你能够任意地嵌套这种模式:在 dashboard
中插入 bar charts
,在 bar charts
中插入 bars
,在 bars
中插入它们的高度和颜色。颜色 RGB 和 alpha 经过线性插值来组合。整个过程,就是递归叶节点上的值,进行线性插值。
在数学上倾向于用 _C_(_x_, _y_)
来表达复合的线性插值结构,而编程实践中咱们用 _lerp_(_C_(_x_1, _y_1), _C_(_x_2, _y_2), _t_) == _C_(_lerp_(_x_1, _x_2, _t_), _lerp_(_y_1, _y_2, _t_))
正如咱们所看到的,这很好地归纳了两个元件(条形图的高度和颜色)到任意多个元件(固定类别 n 条条形图)。
固然,(这个表示方法)也有一些这个解决不了的问题。咱们但愿在两个不以彻底相同的方式组成的值之间进行动画处理。举个简单的例子,考虑动画图表处理从包含工做日,到包括周末的状况。
你可能很容易想出这个问题的几种不一样的临时解决方案,而后可能会要求你的UX设计师在它们之间进行选择。这是一种有效的方法,但我认为在讨论过程当中要记住这些不一样解决方案共有的基本结构:tween
。回忆第一部分:
**动画值从 0 到 1 运动时,经过遍历空间路径中全部 _T_
的路径进行动画。用 Tween_ _<T>_
对路径建模。_
用户体验设计师要回答的核心问题是:图表有五个条形图和一个有七个条形图的中间值是多少? 显而易见的选择是六个条形图。 可是要使他的动画平滑,咱们须要比六个条形图更多中间值。咱们须要以不一样方式绘制条形图,跳出等宽,均匀间隔,适合的 200 像素设置 这些具体的设置。换句话说,T
的值必须是通用的。
经过将值嵌入到通用数据中,在具备不一样结构的值之间进行线性插值,包括动画端点和全部中间值所需的特殊状况。
咱们能够分两步完成。第一步,在 Bar
类中包含 x 坐标属性和宽度属性:
class Bar {
Bar(this.x, this.width, this.height, this.color);
final double x;
final double width;
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.x, end.x, t),
lerpDouble(begin.width, end.width, t),
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, t),
);
}
}
复制代码
第二步,咱们使 BarChart
支持具备不一样条形数的图表。咱们的新图表将适用于数据集,其中条形图 i 表明某些系列中的第 i 个值,例如产品发布后的第 i 天的销售额。Counting as programmers,任何这样的图表都涉及每一个整数值 0..n 的条形图,但条形图数 n 可能在各个图表中表示的意义不一样。
考虑两个图表分别有五个和七个条形图。五个常见类别的条形图 0..5 像上面咱们看到的那样进行动画。索引为5和6的条形在另外一个动画终点没有对应条,但因为咱们如今能够自由地给每一个条形图设置位置和宽度,咱们能够引入两个不可见的条形来扮演这个角色。视觉效果是当动画进行时,第 5 和第 6 条会减弱或淡化为隐形的。
经过线性插值对应的元件,生成 tween 的合成值。若是某个端点缺乏元件,在其位置使用不可见元件。
一般有几种方法能够选择隐形元件。假设咱们友好的用户体验设计师决定使用零宽度,零高度的条形图,其中 x 坐标和颜色从它们的可见元件继承而来。咱们将为 Bar
类添加一个方法,用于处理这样的实例。
class BarChart {
BarChart(this.bars);
final List<Bar> bars;
static BarChart lerp(BarChart begin, BarChart end, double t) {
final barCount = max(begin.bars.length, end.bars.length);
final bars = List.generate(
barCount,
(i) => Bar.lerp(
begin._barOrNull(i) ?? end.bars[i].collapsed,
end._barOrNull(i) ?? begin.bars[i].collapsed,
t,
),
);
return BarChart(bars);
}
Bar _barOrNull(int index) => (index < bars.length ? bars[index] : null);
}
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);
@override
BarChart lerp(double t) => BarChart.lerp(begin, end, t);
}
class Bar {
Bar(this.x, this.width, this.height, this.color);
final double x;
final double width;
final double height;
final Color color;
Bar get collapsed => Bar(x, 0.0, 0.0, color);
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.x, end.x, t),
lerpDouble(begin.width, end.width, t),
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, t),
);
}
}
复制代码
将上述代码集成到咱们的应用程序中,涉及从新定义 BarChart.empty
和 BarChart.random
。如今能够合理地将空条形图设置包含零条,而随机条形图能够包含随机数量的条,全部条都具备相同的随机选择颜色,而且每一个条具备随机选择的高度。但因为位置和宽度如今是 Bar
类定义的,咱们须要 BarChart.random
来指定这些属性。用图表 Size
做为BarChart.random
的参数彷佛是合理的,这样能够解除 BarChartPainter.paint
大部分计算(代码列表,差分)。
隐藏条形图线性插值。
大多数读者可能已经注意 BarChart.lerp
有潜在的效率问题。咱们建立 Bar
实例只是做为参数提供给 Bar.lerp
函数,而且对于每一个动画参数的 t
值都是重复调用。每秒 60 帧,即便是相对较短的动画,也意味着不少 Bar
实例被送到垃圾收集器。咱们还有其余选择:
Bar
实例能够经过在 Bar
类中建立一次而不是每次调用 collapsed
来从新生成。这种方法适用于此,但并不通用。
能够用 BarChartTween
来处理重用问题,方法是让 BarChartTween
的构造函数建立条形图列表时使用的 BarTween
实例的列表 _tween
:(i)=> _tweens [i] .lerp(t )
。这种方法打破了整个使用静态lerp
方法的惯例。静态BarChart.lerp
不会在动画持续时间内存储 tween 列表的对象。相比之下,BarChartTween
对象很是适合这种状况。
假设处理逻辑在 Bar.lerp
中,null
条可用于表示折叠条。这种方法既灵活又高效,但须要注意避免引用或误解 null
。在 Flutter SDK 中,静态 lerp
方法倾向于接受 null
做为动画终点,一般将其解释为某种不可见元件,如彻底透明的颜色或零大小的图形元件。做为最基本的例子,除非两个动画端点都是 null
以外 lerpDouble
将 null
视为 0。
下面的代码段显示了咱们如何处理 null
:
class BarChart {
BarChart(this.bars);
final List<Bar> bars;
static BarChart lerp(BarChart begin, BarChart end, double t) {
final barCount = max(begin.bars.length, end.bars.length);
final bars = List.generate(
barCount,
(i) => Bar.lerp(begin._barOrNull(i), end._barOrNull(i), t),
);
return BarChart(bars);
}
Bar _barOrNull(int index) => (index < bars.length ? bars[index] : null);
}
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);
@override
BarChart lerp(double t) => BarChart.lerp(begin, end, t);
}
class Bar {
Bar(this.x, this.width, this.height, this.color);
final double x;
final double width;
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
if (begin == null && end == null)
return null;
return Bar(
lerpDouble((begin ?? end).x, (end ?? begin).x, t),
lerpDouble(begin?.width, end?.width, t),
lerpDouble(begin?.height, end?.height, t),
Color.lerp((begin ?? end).color, (end ?? begin).color, t),
);
}
}
复制代码
我认为公正的说 Dart 的 ?
语法很是适合这项任务。但请注意,使用折叠(而不是透明)条形图做为不可见元件的决定如今隐藏在 Bar.lerp
中。这是我以前选择看似效率较低的解决方案的主要缘由。与性能与可维护性同样,你的选择应基于实践。
在完整地处理条形图动画以前,咱们还有一个步要作。考虑使用条形图的应用程序,按给定年份的产品类别显示销售额。用户能够选择另外一年,而后应用应该为该年的条形图设置动画。若是两年的产品类别相同,或者刚好相同,除了其中一个图表右侧显示的其余类别,咱们可使用上面的现有代码。可是,若是公司在 2016 年拥有 A、B、C 和 X 类产品,可是已经停产 B 并在 2017 年引入了 D,那该怎么办?咱们现有的代码动画以下:
2016 2017
A -> A
B -> C
C -> D
X -> X
复制代码
动画多是美丽而流畅的,但它仍然会让用户感到困惑。为何?由于它不保留语义。它将表示产品类别 B 的图形元件转换为表示类别 C 的图形元件,而将 C 表示元件转移到其余地方。仅仅由于 2016 B 刚好被绘制在 2017 C 后来出现的相同位置,并不意味着前者应该变成后者。相反,2016 B 应该消失,2016 C 应该向左移动并变为 2017 C,2017 D 应该出如今右边。咱们可使用书中最古老的算法之一来实现这种融合:合并排序列表。
经过线性插值对应的元件,生成 tween 的合成值。当元素造成排序列表时,合并算法可使这些元素处于同等水平,根据须要使用不可见元素来处理单侧合并。
咱们所须要的只是使 Bar
实例按线性顺序相互比较。而后咱们能够合并它们,以下:
static BarChart lerp(BarChart begin, BarChart end, double t) {
final bars = <Bar>[];
final bMax = begin.bars.length;
final eMax = end.bars.length;
var b = 0;
var e = 0;
while (b + e < bMax + eMax) {
if (b < bMax && (e == eMax || begin.bars[b] < end.bars[e])) {
bars.add(Bar.lerp(begin.bars[b], begin.bars[b].collapsed, t));
b++;
} else if (e < eMax && (b == bMax || end.bars[e] < begin.bars[b])) {
bars.add(Bar.lerp(end.bars[e].collapsed, end.bars[e], t));
e++;
} else {
bars.add(Bar.lerp(begin.bars[b], end.bars[e], t));
b++;
e++;
}
}
return BarChart(bars);
}
复制代码
具体地说,咱们将为 bar 添加 rank
属性做一个排序键。rank
也能够方便地用于为每一个栏分配调色板中的颜色,从而容许咱们跟踪动画演示中各个小节的移动。
随机条形图如今将基于随机选择的 rank
来包括(代码列表,diff)。
任意类别。合并基础,线性插值。
干的不错,但也许不是最有效的解决方案。 咱们在 BarChart.lerp
中重复执行合并算法,对于 t
的每一个值都执行一次。为了解决这个问题,咱们将实现前面提到的想法,将可重用信息存储在 BarChartTween
中。
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end) {
final bMax = begin.bars.length;
final eMax = end.bars.length;
var b = 0;
var e = 0;
while (b + e < bMax + eMax) {
if (b < bMax && (e == eMax || begin.bars[b] < end.bars[e])) {
_tweens.add(BarTween(begin.bars[b], begin.bars[b].collapsed));
b++;
} else if (e < eMax && (b == bMax || end.bars[e] < begin.bars[b])) {
_tweens.add(BarTween(end.bars[e].collapsed, end.bars[e]));
e++;
} else {
_tweens.add(BarTween(begin.bars[b], end.bars[e]));
b++;
e++;
}
}
}
final _tweens = <BarTween>[];
@override
BarChart lerp(double t) => BarChart(
List.generate(
_tweens.length,
(i) => _tweens[i].lerp(t),
),
);
}
复制代码
咱们如今能够删除静态方法 BarChart.lerp
(diff)。
让咱们总结一下到目前为止咱们对 tween
概念的理解:
动画 T 经过在全部 T 的空间中描绘出一条路径做为动画值,在 0 到 1 之间运行。使用 _Tween <T> _
路径建模。
先泛化 _T_
的概念,直到它包含全部动画端点和中间值。
经过线性插值对应的元件,生成 tween 的合成值。
考虑使用静态方法 _Xxx.lerp_
实现 tweens
,以便在实现复合 tween
实现时重用。对单个动画路径调用 _Xxx.lerp_
进行重要的从新计算,请考虑将计算移动到 _XxxTween_
类的构造函数,并让其实例承载计算结果。 。_
有了这些看法,咱们终于有了将更复杂的图表动画化的能力。咱们将快速连续地实现堆叠条形图,分组条形图和堆叠 + 分组条形图:
堆叠条形图。
分组条形图。
堆叠 + 分组条形图。
在全部三种变体中,动画可用于可视化数据集更改,从而引入额外的维度(一般是时间)而不会使图表混乱。
为了使动画有用而不只仅是漂亮,咱们须要确保咱们只在语义相应的元件之间进 lerp
。所以,用于表示 2016 年特定产品/地区/渠道收入的条形段,应变为 2017 年相同产品/区域/渠道(若是存在)的收入。
合并算法可用于确保这一点。 正如你在前面的讨论中所猜想的那样,合并将被用于多个层面,来反应类别的维度。咱们将在堆积图表中组合堆和条形图,在分组图表中合并组和条形图,以及堆叠 + 分组图表中组合上面三个。
为了减小重复代码,咱们将合并算法抽象为通用工具,并将其放在本身的文件 tween.dart
中:
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
abstract class MergeTweenable<T> {
T get empty;
Tween<T> tweenTo(T other);
bool operator <(T other);
}
class MergeTween<T extends MergeTweenable<T>> extends Tween<List<T>> {
MergeTween(List<T> begin, List<T> end) : super(begin: begin, end: end) {
final bMax = begin.length;
final eMax = end.length;
var b = 0;
var e = 0;
while (b + e < bMax + eMax) {
if (b < bMax && (e == eMax || begin[b] < end[e])) {
_tweens.add(begin[b].tweenTo(begin[b].empty));
b++;
} else if (e < eMax && (b == bMax || end[e] < begin[b])) {
_tweens.add(end[e].empty.tweenTo(end[e]));
e++;
} else {
_tweens.add(begin[b].tweenTo(end[e]));
b++;
e++;
}
}
}
final _tweens = <Tween<T>>[];
@override
List<T> lerp(double t) => List.generate(
_tweens.length,
(i) => _tweens[i].lerp(t),
);
}
复制代码
MergeTweenable <T>
接口精确得到合并两个有序的 T
列表的所需的 tween
内容。咱们将使用 Bar
,BarStack
和 BarGroup
实例化泛型参数 T
,而且实现 MergeTweenable <T>
(diff)。
stacked(diff)、grouped(diff)和 stacked+grouped(diff)已经完成实现。我建议你本身实践一下:
BarChart.random
建立的 groups、stacks 和 bars 的数量。stacked+grouped
,我使用了单色调色板,由于我以为它看起来更好。你和你的 UX 设计师可能并不认同。BarChart.random
和浮动操做按钮替换为年份选择器,并以实际数据集建立 BarChart
实例。MergeTweenable <T>
或相似方法为它们设置动画。最后两个任务很是具备挑战性。不妨试试。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。