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

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 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 {
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 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 {
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 = [];
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 {
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 = [];

@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 get empty;

Tween tweenTo(T other);

bool operator <(T other);
}

class MergeTween<T extends MergeTweenable> extends Tween<List> {
MergeTween(List begin, List 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>[];

@override
List 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 设计师可能并不认同。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
-Fma9eGAu-1715790063598)]

[外链图片转存中…(img-oXxIIuah-1715790063599)]

[外链图片转存中…(img-7v73fV28-1715790063600)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 29
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值