[译] Flutter 从 0 到 1, 第二部分

Flutter 从 0 到 1(第一部分)html

探索如何在跨平台移动应用程序的上下文中为复合图形对象设置动画。引入一个新的概念,如何将 tween 动画应用于结构化值中,例如条形图表。所有代码,按步骤实现。前端

修订:2018 年 8 月 8 日适配 Dart 2。GitHub repo 而且差别连接于 2018 年 10 月 17 日添加。android


如何进入一个新的编程领域 ?实践是相当重要的,由于那是学习和模仿更有经验同行的代码。我我的喜欢挖掘概念:试着从最基本的原则出发,识别各类概念,探索它们的优点,有意识地寻求它们的本质。这是一种理性主义的方法,它不能独立存在,而是一种智力刺激的方法,能够更快地引导你得到更深刻的看法。ios

这是 Flutter 及其 widget 和 tween 概念介绍的第二部分也是最后一部分。在 Flutter 从 0 到 1 第一部分 最后,在咱们这么多 widges 的选择中,这个 tree 包含了下面两个:git

  • 一个使用自定义动画绘制代码, 绘制单 一 Bar 的 widget
  • 初始化一个 Bar 的高度的 widget

高度动画 制做 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.lerplerpDouble(一般为 double.lerp)和 Color.lerp,咱们就必须经过为高度建立 Tween <double> 同时为颜色建立 Tween<Color> 来实现 BarTween。这些 tweens 将是 BarTween 的实例字段,由构造函数初始化,并在其 lerp 方法中使用。 咱们将在 Bar 类以外屡次重复访问 Bar 的属性。代码维护者可能会发现这并非一个好主意。

为条形的颜色和高度制做动画。

为了在应用程序中使用彩色条,咱们将更新 BarChartPainter 来从 Bar 得到条形图颜色。在 main.dart 中,咱们须要有能力来建立一个空的 Bar 和一个随机的 Bar。咱们将为前者使用彻底透明的颜色,为后者使用随机颜色。 颜色将从一个简单的 ColorPalette 类中获取,咱们会在它本身的文件中快速实现它。 咱们将在 Bar 类中建立 Bar.emptyBar.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.emptyBarChart.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 以外 lerpDoublenull 视为 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.lerpdiff)。


让咱们总结一下到目前为止咱们对 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 内容。咱们将使用 BarBarStackBarGroup 实例化泛型参数 T,而且实现 MergeTweenable <T>diff)。

stackeddiff)、groupeddiff)和 stacked+groupeddiff)已经完成实现。我建议你本身实践一下:

  • 更改 BarChart.random建立的 groups、stacks 和 bars 的数量。
  • 更改调色板。对于 stacked+grouped,我使用了单色调色板,由于我以为它看起来更好。你和你的 UX 设计师可能并不认同。
  • BarChart.random 和浮动操做按钮替换为年份选择器,并以实际数据集建立 BarChart 实例。
  • 实现水平条形图。
  • 实现其余图表类型(饼图,线条,堆积区域)。使用 MergeTweenable <T> 或相似方法为它们设置动画。
  • 添加图表图例,标签,坐标轴,而后为它们设置动画。

最后两个任务很是具备挑战性。不妨试试。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索