Flutter進階—實現動畫效果(七)
我們假設一種情況,如果應用程式使用條形圖顯示給定年份的產品類別的銷售額,使用者可以選擇另一年,然後該應用程式將動畫到該年的條形圖。如果產品類別在兩年內是相同的,或者恰好是相同的,除了在其中一個圖表中右側顯示的其他類別,我們可以使用我們現有的程式碼。但如果公司在2016年有A,B,C和X類產品,但是在2017年中斷了B並推出了D?
動畫效果可以做得非常好看,但仍然會讓使用者感到困惑。為什麼?因為它不保留語義。它將表示產品類別B的圖形元素轉換為代表類別C的一個圖形元素,而將C的圖形元素轉換到其他地方。正因為2016 B恰好是在2017 C後來出現的同一位置,並不意味著前者應該變成後者。相反,2016年B應該消失,2016年C應該向左移動到2017年,而2017年D應該出現在右邊。我們可以使用傳統的合併排序列表實現這種混合。
我們可以分別給每一個條形以不同的顏色,然後使用顏色來區分銷售的產品,一種顏色代表一個產品,當一種顏色消失時,說明該產品已經下架,反之,則說明新產品已經上架。
通過語義對應的元件進行復合值之間的線性插值(lerp),當元件形成排序列表時,合併演算法可以將這些元件放在側面上,使用不可見元件來處理單面合併。我們所需要的是使Bar例項以線性順序相互比較。然後我們可以將它們合併。
// ...
class BarChart {
// ...
factory BarChart.random(Size size, Random random) {
const barWidthFraction = 0.75 ;
final ranks = selectRanks(random, ColorPalette.primary.length);
final barCount = ranks.length;
final barDistance = size.width / (1+barCount);
final barWidth = barDistance * barWidthFraction;
final startX = barDistance - barWidth/2;
final bars = new List.generate(
barCount,
(i)=> new Bar(
ranks[i],
startX + i * barDistance,
barWidth,
random.nextDouble() * size.height,
ColorPalette.primary[ranks[i]],
),
);
return new BarChart(bars);
}
static List<int> selectRanks(Random random, int cap) {
final ranks = <int>[];
var rank = 0;
while(true) {
// 模擬產品的上架下架
if(random.nextDouble() < 0.2) rank++;
if(cap <= rank) break;
ranks.add(rank);
rank++;
}
return ranks;
}
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) {
/*
這裡的條件判斷中包含兩種情況
b < bMax && e == eMax:
當新圖表條形數減少時
b < bMax && begin.bars[b] < end.bars[e]:
當新圖表不包含舊圖表的顏色條形時
滿足一種情況,處理舊圖表中多餘的條形,即向左側方清除舊條形
*/
if(b < bMax && (e == eMax || begin.bars[b] < end.bars[e])) {
bars.add(Bar.lerp(begin.bars[b], begin.bars[b].collapsed, t));
b++;
/*
這裡的條件判斷中包含兩種情況
e < eMax && b == bMax:
當新圖表條形數增加時
e < eMax && end.bars[e] < begin.bars[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 new BarChart(bars);
}
}
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.rank, this.x, this.width, this.height, this.color);
final int rank;
final double x;
final double width;
final double height;
final Color color;
Bar get collapsed => new Bar(rank, x, 0.0, 0.0, color);
/*
bool operator <(
Duration other
)
如果此Duration的值小於other值,則返回true
bool operator <(Duration other) => this._duration < other._duration;
*/
bool operator <(Bar other) => rank < other.rank;
static Bar lerp(Bar begin, Bar end, double t) {
assert(begin.rank == end.rank);
return new Bar(
begin.rank,
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)
);
}
}
class BarTween extends Tween<Bar> {
BarTween(Bar begin, Bar end) : super(begin: begin, end: end) {
assert(begin.rank == end.rank);
}
@override
Bar lerp(double t) => Bar.lerp(begin, end, t);
}
// ...
具體來說,我們將以整數rank屬性的形式為每個條形分配一個排序鍵。然後可以方便地使用rank來從調色盤中分配每個條形的顏色,從而使我們能夠跟蹤動畫演示中各個條形圖的移動。
隨機條形圖現在將基於隨機選擇的行列。
但這不是最有效的解決方案,我們正在BarChart.lerp中重複執行合併演算法,對於t的每個值都執行一次。為了解決這個問題,我們將實現前面一篇文章中提到的想法,以便在BarChartTween中儲存可重用的資訊。
// ...
class BarChartTween extends Tween<BarChart> {
final _tweens = <BarTween>[];
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(new BarTween(begin.bars[b], begin.bars[b].collapsed));
b++;
} else if(e < eMax && (b == bMax || end.bars[e] < begin.bars[b])) {
_tweens.add(new BarTween(end.bars[e].collapsed, end.bars[e]));
e++;
} else {
_tweens.add(new BarTween(begin.bars[b], end.bars[e]));
b++;
e++;
}
}
}
@override
BarChart lerp(double t) => new BarChart(
new List.generate(
_tweens.length,
(i) => _tweens[i].lerp(t))
);
}
// ...
然後我們就可以刪除靜態的BarChart.lerp方法了。
未完待續~~~