Flutter小说分页效果实现
Posted Flutter学习簿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter小说分页效果实现相关的知识,希望对你有一定的参考价值。
最近在考虑做小说分页,发现了这个神奇的动画切换控件,先在分页之前学习记录一下,同时也安利给各位大大,它就是 AnimatedSwitcher 控件。
构造函数
const AnimatedSwitcher({
Key key,
this.child,
@required this.duration,//新控件的动画展示时间
this.reverseDuration,//老控件的动画展示时间
this.switchInCurve = Curves.linear,//新控件的动画插值器
this.switchOutCurve = Curves.linear,//老控件的动画插值器
this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,//展示的动画效果构建器
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,//布局构建器
})
AnimatedSwitcher 接收的 child 只有一个,当 child 或者传入的 Key 不同的时候,控件会根据 transitionBuilder 构建器展示新旧控件的替换动画,transitionBuilder 定义如下:
transitionBuilder
typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);
该 builder 在 AnimatedSwitcher 的 child 切换时会分别对新、旧 child 绑定动画:
对旧 child,绑定的动画会反向执行(reverse)
对新 child,绑定的动画会正向指向(forward)
这样一下,便实现了对新、旧 child 的动画绑定。AnimatedSwitcher 的默认值是 AnimatedSwitcher.defaultTransitionBuilder:
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
}
如果你不指定动画构建器的话,默认的就是淡入淡出效果,可以先看个默认切换动画的效果。
例子
运行的主页面
AnimatedSwitcherPage
class AnimatedSwitcherPage extends StatefulWidget {
@override
_AnimatedSwitcherPageState createState() => _AnimatedSwitcherPageState();
}
class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"AnimatedSwitcher",
style: TextStyle(color: Colors.white),
),
),
body: Container(
height: double.infinity,
width: double.infinity,
alignment: Alignment.center,
child: DefaultExample(
keyCount: _count,
),//此处我随便抽了一下,后面还要写几个动画构建器,到时候直接替换这个控件就行
color: Colors.amberAccent,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
++_count;
});
},
child: Text("Start"),
),
);
}
}
DefaultExample
//默认的动画构造器
class DefaultExample extends StatefulWidget {
final int keyCount;
const DefaultExample({Key key, this.keyCount}) : super(key: key);
@override
_DefaultExampleState createState() => _DefaultExampleState();
}
class _DefaultExampleState extends State<DefaultExample> {
@override
Widget build(BuildContext context) {
return Container(
//没有指定transitionBuilder
child: AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: Text(
"${widget.keyCount.toString()}",
//显示指定key,不同的key会被认为是不同的Text,这样才能执行动画,这很重要
key: ValueKey<int>(widget.keyCount),
style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
);
}
}
注意:AnimatedSwitcher 的新旧 child,如果 Key 的类型相同,则 Key 必须不相等。
默认的动画切换效果
可以看出,Text 在渐变中显示和隐藏。
我们再看看默认的动画构建器:
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
}
代码就很简单,它此处返回了渐变动画 FadeTransition:
FadeTransition(opacity: animation,child: child,)
那我们可以直接给 transitionBuilder 赋值上它的兄弟,滑动(SlideTransition),缩放(ScaleTransition)动画效果:
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
//执行缩放动画
return ScaleTransition(child: child, scale: animation);
},
child: Text(
'$_count',
//显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
key: ValueKey<int>(_count),
style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold)
),
),
这样的效果自己尝试,就是缩放切换的动画。
AnimatedSwitcher 的核心原理
AnimatedSwitcher 的动画切换核心伪代码
从 AnimatedSwitcher 的使用方式我们可以看到,当 child 发生变化时(子 widget 的 key 和类型不同时相等则认为发生变化),则重新会重新执行 build,然后动画开始执行。我们可以通过继承 StatefulWidget 来实现 AnimatedSwitcher,具体做法是在 didUpdateWidget 回调中判断其新旧 child 是否发生变化,如果发生变化,则对旧 child 执行反向退场(reverse)动画,对新 child 执行正向(forward)入场动画。此源码的版本是 Flutter 1.17。
@override
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
// If the transition builder changed, then update all of the previous
// transitions.
...
final bool hasNewChild = widget.child != null;
final bool hasOldChild = _currentEntry != null;
if (hasNewChild != hasOldChild ||
hasNewChild && !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) {
// 当child 控件有变化,
_childNumber += 1;
//添加一个入场新控件
_addEntryForNewChild(animate: true);
} else if (_currentEntry != null) {
...
}
}
void _addEntryForNewChild({ @required bool animate }) {
assert(animate || _currentEntry == null);
if (_currentEntry != null) {
...
//如果当前元素不为空,当前的控件相对来说已经是老控件了,
// 给老child执行反向退场动画
_currentEntry.controller.reverse();
_markChildWidgetCacheAsDirty();
_currentEntry = null;//当前元素设置为空
}
if (widget.child == null)
return;
//根据出入的动画构造动画控制器
final AnimationController controller = AnimationController(
duration: widget.duration,
reverseDuration: widget.reverseDuration,
vsync: this,
);
final Animation<double> animation = CurvedAnimation(
parent: controller,
curve: widget.switchInCurve,
reverseCurve: widget.switchOutCurve,
);
//给_currentEntry赋值新的元素
_currentEntry = _newEntry(
child: widget.child,
controller: controller,
animation: animation,
builder: widget.transitionBuilder,
);
if (animate) {
//给新的_currentEntry执行入场动画。
controller.forward();
} else {
assert(_outgoingEntries.isEmpty);
controller.value = 1.0;
}
}
根据 AnimatedSwitcher 的原理来看,它执行的动画是对称的,我们用滑动的动画效果来看一下:
transitionBuilder: (Widget child, Animation<double> animation) {
var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));
return SlideTransition(
position: tween.animate(animation),
child: child,
);
}
效果图
可以看到动画对称的,从右边进,从右边出,并不能像小说翻页一样,右边进,左边出,或者左边进,右边出。这就需要我们自定义一下 SlideTransition,如 SlideTransition 一样,继承自 AnimatedWidget。
思路是这样子的:当执行 reverse()动画时,我们改变一下它的滑动方向。
MySlideTransition
class MySlideTransition extends AnimatedWidget {
MySlideTransition({
Key key,
@required Animation<Offset> position,
this.transformHitTests = true,
this.child,
})
: assert(position != null),
super(key: key, listenable: position) ;
Animation<Offset> get position => listenable;
final bool transformHitTests;
final Widget child;
@override
Widget build(BuildContext context) {
Offset offset=position.value;
//动画反向执行时,调整x偏移,实现“从左边滑出隐藏”
if (position.status == AnimationStatus.reverse) {
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
调用时,将 SlideTransition 替换成 MySlideTransition 即可。
transitionBuilder: (Widget child, Animation<double> animation) {
var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));
// return SlideTransition(
// position: tween.animate(animation),
// child: child,
// );
return MySlideTransition(
position: tween.animate(animation), child: child);
}
效果图
一个封装的上下左右出入动画类
//自定义上下左右出入动画
class HSlideTransition extends AnimatedWidget {
HSlideTransition({
Key key,
@required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
this.child,
}) : assert(position != null),
super(key: key, listenable: position) {
// 偏移在内部处理
switch (direction) {
case AxisDirection.up:
_tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
break;
case AxisDirection.right:
_tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
break;
case AxisDirection.down:
_tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
break;
case AxisDirection.left:
_tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
break;
}
}
Animation<double> get position => listenable;
final bool transformHitTests;
final Widget child;
//退场(出)方向
final AxisDirection direction;
Tween<Offset> _tween;
@override
Widget build(BuildContext context) {
Offset offset = _tween.evaluate(position);
if (position.status == AnimationStatus.reverse) {
switch (direction) {
case AxisDirection.up:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.right:
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.down:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.left:
offset = Offset(-offset.dx, offset.dy);
break;
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
如此,我们只用给 direction 传递不同的值,即可实现上下左右不同方向的出入效果。
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
//执行缩放动画
return HSlideTransition(
child: child,
direction: AxisDirection.right, //左入右出
position: animation,
);
},
child: Container(
alignment: Alignment.center,
key: ValueKey<int>(_count),
child: Text(
'$_count',
//显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
)
效果图
以上是关于Flutter小说分页效果实现的主要内容,如果未能解决你的问题,请参考以下文章
iOS播放器Flutter高仿书旗小说卡片动画二维码扫码菜单弹窗效果等源码
Android-利用Jetpack-Compose-+Paging3+swiperefresh实现分页加载,下拉上拉效果
flutter解决 dart:html 只支持 flutter_web 其他平台编译报错 Avoid using web-only libraries outside Flutter web(代码片段