Flutter小说分页效果实现(一)

原创
05/20 08:00
阅读数 305

最近在考虑做小说分页,发现了这个神奇的动画切换控件,先在分页之前学习记录一下,同时也安利给各位大大,它就是 AnimatedSwitcher 控件。

构造函数

    
  
  
  1. const AnimatedSwitcher({

  2. Key key,

  3. this.child,

  4. @required this.duration,//新控件的动画展示时间

  5. this.reverseDuration,//老控件的动画展示时间

  6. this.switchInCurve = Curves.linear,//新控件的动画插值器

  7. this.switchOutCurve = Curves.linear,//老控件的动画插值器

  8. this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,//展示的动画效果构建器

  9. this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,//布局构建器

  10. })

AnimatedSwitcher 接收的 child 只有一个,当 child 或者传入的 Key 不同的时候,控件会根据 transitionBuilder 构建器展示新旧控件的替换动画,transitionBuilder 定义如下:

  • transitionBuilder

    
  
  
  1. typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);

该 builder 在 AnimatedSwitcher 的 child 切换时会分别对新、旧 child 绑定动画:

  • 对旧 child,绑定的动画会反向执行(reverse)

  • 对新 child,绑定的动画会正向指向(forward)

这样一下,便实现了对新、旧 child 的动画绑定。AnimatedSwitcher 的默认值是 AnimatedSwitcher.defaultTransitionBuilder:

    
  
  
  1. static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {

  2. return FadeTransition(

  3. opacity: animation,

  4. child: child,

  5. );

  6. }

如果你不指定动画构建器的话,默认的就是淡入淡出效果,可以先看个默认切换动画的效果。

例子

运行的主页面
  • AnimatedSwitcherPage

    
  
  
  1. class AnimatedSwitcherPage extends StatefulWidget {

  2. @override

  3. _AnimatedSwitcherPageState createState() => _AnimatedSwitcherPageState();

  4. }


  5. class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {

  6. int _count = 0;


  7. @override

  8. Widget build(BuildContext context) {

  9. return Scaffold(

  10. appBar: AppBar(

  11. title: Text(

  12. "AnimatedSwitcher",

  13. style: TextStyle(color: Colors.white),

  14. ),

  15. ),

  16. body: Container(

  17. height: double.infinity,

  18. width: double.infinity,

  19. alignment: Alignment.center,

  20. child: DefaultExample(

  21. keyCount: _count,

  22. ),//此处我随便抽了一下,后面还要写几个动画构建器,到时候直接替换这个控件就行

  23. color: Colors.amberAccent,

  24. ),

  25. floatingActionButton: FloatingActionButton(

  26. onPressed: () {

  27. setState(() {

  28. ++_count;

  29. });

  30. },

  31. child: Text("Start"),

  32. ),

  33. );

  34. }

  35. }

  • DefaultExample

    
  
  
  1. //默认的动画构造器

  2. class DefaultExample extends StatefulWidget {

  3. final int keyCount;


  4. const DefaultExample({Key key, this.keyCount}) : super(key: key);


  5. @override

  6. _DefaultExampleState createState() => _DefaultExampleState();

  7. }


  8. class _DefaultExampleState extends State<DefaultExample> {

  9. @override

  10. Widget build(BuildContext context) {

  11. return Container(

  12. //没有指定transitionBuilder

  13. child: AnimatedSwitcher(

  14. duration: Duration(milliseconds: 300),

  15. child: Text(

  16. "${widget.keyCount.toString()}",

  17. //显示指定key,不同的key会被认为是不同的Text,这样才能执行动画,这很重要

  18. key: ValueKey<int>(widget.keyCount),

  19. style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold),

  20. ),

  21. ),

  22. );

  23. }

  24. }

注意:AnimatedSwitcher 的新旧 child,如果 Key 的类型相同,则 Key 必须不相等。

默认的动画切换效果

可以看出,Text 在渐变中显示和隐藏。

我们再看看默认的动画构建器:

    
  
  
  1. static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {

  2. return FadeTransition(

  3. opacity: animation,

  4. child: child,

  5. );

  6. }

代码就很简单,它此处返回了渐变动画 FadeTransition:

    
  
  
  1. FadeTransition(opacity: animation,child: child,)

那我们可以直接给 transitionBuilder 赋值上它的兄弟,滑动(SlideTransition),缩放(ScaleTransition)动画效果:

    
  
  
  1. AnimatedSwitcher(

  2. duration: const Duration(milliseconds: 300),

  3. transitionBuilder: (Widget child, Animation<double> animation) {

  4. //执行缩放动画

  5. return ScaleTransition(child: child, scale: animation);

  6. },

  7. child: Text(

  8. '$_count',

  9. //显示指定key,不同的key会被认为是不同的Text,这样才能执行动画

  10. key: ValueKey<int>(_count),

  11. style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold)

  12. ),

  13. ),

这样的效果自己尝试,就是缩放切换的动画。

AnimatedSwitcher 的核心原理

AnimatedSwitcher 的动画切换核心伪代码

从 AnimatedSwitcher 的使用方式我们可以看到,当 child 发生变化时(子 widget 的 key 和类型不同时相等则认为发生变化),则重新会重新执行 build,然后动画开始执行。我们可以通过继承 StatefulWidget 来实现 AnimatedSwitcher,具体做法是在 didUpdateWidget 回调中判断其新旧 child 是否发生变化,如果发生变化,则对旧 child 执行反向退场(reverse)动画,对新 child 执行正向(forward)入场动画。此源码的版本是 Flutter 1.17。

    
  
  
  1. @override

  2. void didUpdateWidget(AnimatedSwitcher oldWidget) {

  3. super.didUpdateWidget(oldWidget);


  4. // If the transition builder changed, then update all of the previous

  5. // transitions.

  6. ...

  7. final bool hasNewChild = widget.child != null;

  8. final bool hasOldChild = _currentEntry != null;

  9. if (hasNewChild != hasOldChild ||

  10. hasNewChild && !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) {

  11. // 当child 控件有变化,

  12. _childNumber += 1;

  13. //添加一个入场新控件

  14. _addEntryForNewChild(animate: true);

  15. } else if (_currentEntry != null) {

  16. ...

  17. }

  18. }


  19. void _addEntryForNewChild({ @required bool animate }) {

  20. assert(animate || _currentEntry == null);

  21. if (_currentEntry != null) {

  22. ...

  23. //如果当前元素不为空,当前的控件相对来说已经是老控件了,

  24. // 给老child执行反向退场动画

  25. _currentEntry.controller.reverse();

  26. _markChildWidgetCacheAsDirty();

  27. _currentEntry = null;//当前元素设置为空

  28. }

  29. if (widget.child == null)

  30. return;

  31. //根据出入的动画构造动画控制器

  32. final AnimationController controller = AnimationController(

  33. duration: widget.duration,

  34. reverseDuration: widget.reverseDuration,

  35. vsync: this,

  36. );

  37. final Animation<double> animation = CurvedAnimation(

  38. parent: controller,

  39. curve: widget.switchInCurve,

  40. reverseCurve: widget.switchOutCurve,

  41. );

  42. //给_currentEntry赋值新的元素

  43. _currentEntry = _newEntry(

  44. child: widget.child,

  45. controller: controller,

  46. animation: animation,

  47. builder: widget.transitionBuilder,

  48. );

  49. if (animate) {

  50. //给新的_currentEntry执行入场动画。

  51. controller.forward();

  52. } else {

  53. assert(_outgoingEntries.isEmpty);

  54. controller.value = 1.0;

  55. }

  56. }

根据 AnimatedSwitcher 的原理来看,它执行的动画是对称的,我们用滑动的动画效果来看一下:

    
  
  
  1. transitionBuilder: (Widget child, Animation<double> animation) {

  2. var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));

  3. return SlideTransition(

  4. position: tween.animate(animation),

  5. child: child,

  6. );

  7. }

效果图

可以看到动画对称的,从右边进,从右边出,并不能像小说翻页一样,右边进,左边出,或者左边进,右边出。这就需要我们自定义一下 SlideTransition,如 SlideTransition 一样,继承自 AnimatedWidget。

思路是这样子的:当执行 reverse()动画时,我们改变一下它的滑动方向。

  • MySlideTransition

    
  
  
  1. class MySlideTransition extends AnimatedWidget {

  2. MySlideTransition({

  3. Key key,

  4. @required Animation<Offset> position,

  5. this.transformHitTests = true,

  6. this.child,

  7. })

  8. : assert(position != null),

  9. super(key: key, listenable: position) ;


  10. Animation<Offset> get position => listenable;

  11. final bool transformHitTests;

  12. final Widget child;


  13. @override

  14. Widget build(BuildContext context) {

  15. Offset offset=position.value;

  16. //动画反向执行时,调整x偏移,实现“从左边滑出隐藏”

  17. if (position.status == AnimationStatus.reverse) {

  18. offset = Offset(-offset.dx, offset.dy);

  19. }

  20. return FractionalTranslation(

  21. translation: offset,

  22. transformHitTests: transformHitTests,

  23. child: child,

  24. );

  25. }

  26. }

调用时,将 SlideTransition 替换成 MySlideTransition 即可。

    
  
  
  1. transitionBuilder: (Widget child, Animation<double> animation) {

  2. var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));

  3. // return SlideTransition(

  4. // position: tween.animate(animation),

  5. // child: child,

  6. // );

  7. return MySlideTransition(

  8. position: tween.animate(animation), child: child);

  9. }

效果图

一个封装的上下左右出入动画类

    
  
  
  1. //自定义上下左右出入动画

  2. class HSlideTransition extends AnimatedWidget {

  3. HSlideTransition({

  4. Key key,

  5. @required Animation<double> position,

  6. this.transformHitTests = true,

  7. this.direction = AxisDirection.down,

  8. this.child,

  9. }) : assert(position != null),

  10. super(key: key, listenable: position) {

  11. // 偏移在内部处理

  12. switch (direction) {

  13. case AxisDirection.up:

  14. _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));

  15. break;

  16. case AxisDirection.right:

  17. _tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));

  18. break;

  19. case AxisDirection.down:

  20. _tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));

  21. break;

  22. case AxisDirection.left:

  23. _tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));

  24. break;

  25. }

  26. }


  27. Animation<double> get position => listenable;


  28. final bool transformHitTests;


  29. final Widget child;


  30. //退场(出)方向

  31. final AxisDirection direction;


  32. Tween<Offset> _tween;


  33. @override

  34. Widget build(BuildContext context) {

  35. Offset offset = _tween.evaluate(position);

  36. if (position.status == AnimationStatus.reverse) {

  37. switch (direction) {

  38. case AxisDirection.up:

  39. offset = Offset(offset.dx, -offset.dy);

  40. break;

  41. case AxisDirection.right:

  42. offset = Offset(-offset.dx, offset.dy);

  43. break;

  44. case AxisDirection.down:

  45. offset = Offset(offset.dx, -offset.dy);

  46. break;

  47. case AxisDirection.left:

  48. offset = Offset(-offset.dx, offset.dy);

  49. break;

  50. }

  51. }

  52. return FractionalTranslation(

  53. translation: offset,

  54. transformHitTests: transformHitTests,

  55. child: child,

  56. );

  57. }

  58. }

如此,我们只用给 direction 传递不同的值,即可实现上下左右不同方向的出入效果。

    
  
  
  1. AnimatedSwitcher(

  2. duration: const Duration(milliseconds: 300),

  3. transitionBuilder: (Widget child, Animation<double> animation) {

  4. //执行缩放动画


  5. return HSlideTransition(

  6. child: child,

  7. direction: AxisDirection.right, //左入右出

  8. position: animation,

  9. );

  10. },

  11. child: Container(

  12. alignment: Alignment.center,

  13. key: ValueKey<int>(_count),

  14. child: Text(

  15. '$_count',

  16. //显示指定key,不同的key会被认为是不同的Text,这样才能执行动画


  17. style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold),

  18. ),

  19. ),

  20. )

效果图


本文分享自微信公众号 - Flutter学习簿(gh_d739155d3b2c)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部