TweenAnimationBuilder


ImplicitlyAnimatedWidget 的文件 中,可以看到一些與非動畫元件相對應的隱式動畫元件,這些元件本質上是作為一個包裹器,藉由控制自身特性,來實現對子元件的動畫。

如果你想直接操作某個元件的特性來完成動畫,可以使用 TweenAnimationBuilder,Tween 的意思是 between,顧名思義,可以指定起始與終值來控制動畫,例如,可以將〈隱式動畫 Widget〉中的縮放動畫,改用 TweenAnimationBuilder 實現:

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Caterpillar(),
  )
);

class Caterpillar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _CaterpillarState();
}

class _CaterpillarState extends State<Caterpillar> {
  var zoom_in = false;
  // 一開始動畫持續時間為 0 ,相當於一開始不展現動畫
  var durationSeconds = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('openhome.cc'),
      ),
      body: Center(
        // 使用 TweenAnimationBuilder
        child: TweenAnimationBuilder(
          // 指定特性區間
          tween: zoom_in ? Tween<double>(begin: 100.0, end: 300.0) : 
                           Tween<double>(begin: 300.0, end: 100.0),
          // 持續時間
          duration: Duration(seconds: durationSeconds),
          // 依傳入的插值建立元件
          builder: (_, double width, __) {
            return Image.network('https://openhome.cc/Gossip/images/caterpillar.jpg',
              width: width,
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {
          zoom_in = !zoom_in;
          // 操作後會展現動畫
          durationSeconds = 1;
        }),
        tooltip: zoom_in ? 'Zoom in' : 'Zoom out',
        child: Icon(zoom_in ? Icons.zoom_in : Icons.zoom_out),
      ),
    );
  }
}

tween 特性的型態是 Tween<T>,這邊指定了 Tween<double> 的實例,重點在於 builder 的部份,第一個參數是 BuildContext,第二個是插值,第三個是 WidgetTweenAnimationBuilder 可以設定 child 特性,如果你想要重用這個 child,就可以在 builder 的第三個參數取得。

這個範例的效果與〈隱式動畫 Widget〉中的縮放動畫是相同的,TweenAnimationBuilder 可以指定 curve 特性,預設是 Curves.linearcurve 計算後的結果,會傳給 tweenlerp,這邊使用的 Tween<T> 會使用 lerp 方法來計算區間的插值:

class Tween<T extends dynamic> extends Animatable<T> {
  Tween({ this.begin, this.end });

  T begin;
  T end;

  @protected
  T lerp(double t) {
    assert(begin != null);
    assert(end != null);
    return begin + (end - begin) * t as T;
  }

  @override
  T transform(double t) {
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return lerp(t);
  }

  @override
  String toString() => '${objectRuntimeType(this, 'Animatable')}($begin \u2192 $end)';
}

通常會使用 TweenAnimationBuilder,是想要直接操作元件特性來完成動畫,而這通常是發生在 Flutter 內建的隱式動畫元件無法滿足你的需求時。

例如,以下的範例可以切換混色,還沒有加入動畫效果:

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Caterpillar(),
  )
);

class Caterpillar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _CaterpillarState();
}

class _CaterpillarState extends State<Caterpillar> {
  var blendRed = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('openhome.cc'),
      ),
      body: Center(
        child: Image(
          image: NetworkImage('https://openhome.cc/Gossip/images/caterpillar.jpg'),
          color:  blendRed ? Colors.red : Colors.white,
          colorBlendMode: BlendMode.colorBurn,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {
          blendRed = !blendRed;
        }),
        tooltip: blendRed ? 'Clear' : 'Blend',
        child: Icon(blendRed ? Icons.clear : Icons.style),
      ),
    );
  }
}

TweenAnimationBuilder

如果想要實現混色時的漸變效果呢?Flutter 內建的隱式動畫沒有這類元件,這時可以使用 TweenAnimationBuilder

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Caterpillar(),
  )
);

class Caterpillar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _CaterpillarState();
}

class _CaterpillarState extends State<Caterpillar> {
  var blendRed = true;
  var durationSeconds = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('openhome.cc'),
      ),
      body: Center(
          child: TweenAnimationBuilder(
            tween: blendRed ? ColorTween(begin: Colors.white, end: Colors.red) :
                              ColorTween(begin: Colors.red, end: Colors.white),
            duration: Duration(seconds: durationSeconds),
            builder: (_, Color color, __) {
              return Image(
                image: NetworkImage('https://openhome.cc/Gossip/images/caterpillar.jpg'),
                color: color,
                colorBlendMode: BlendMode.colorBurn,
              );
            },
          )
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {
          blendRed = !blendRed;
          durationSeconds = 2;
        }),
        tooltip: blendRed ? 'Clear' : 'Blend',
        child: Icon(blendRed ? Icons.clear : Icons.style),
      ),
    );
  }
}

ColorTweenTween<Color> 的子類,重新定義了 leap 方法,使用 Color.lerp 來插值:

class ColorTween extends Tween<Color> {
  ColorTween({ Color begin, Color end }) : super(begin: begin, end: end);

  @override
  Color lerp(double t) => Color.lerp(begin, end, t);
}

Color.lerp 的實作主要就是 Color 來進行計算,並傳回 Color 實例:

static Color? lerp(Color? a, Color? b, double t) {
  assert(t != null); // ignore: unnecessary_null_comparison
  if (b == null) {
    if (a == null) {
      return null;
    } else {
      return _scaleAlpha(a, 1.0 - t);
    }
  } else {
    if (a == null) {
      return _scaleAlpha(b, t);
    } else {
      return Color.fromARGB(
        _clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
        _clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
        _clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
        _clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
      );
    }
  }
}

來看一下效果:

TweenAnimationBuilder