Flutter - 6 : 一个附带删除动画的列表
友情提示 : 这个仅仅只是做出来看的,用到的东西可能会对其他人有些许提示效果,然而并不能保证这个东西一定不会出现错误。
这年头应用当中的动画几乎是一个绕不开的问题,虽然能带来很好的交互体验,然而,写起来也经常是很坑爹的,Flutter当中也有很多的Animate控件,比如:AnimatedContainer
,AnimatedSize
, AnimatedPadding
等等,当然,也有AnimatedList
。
官网链接:https://flutterchina.club/catalog/samples/animated-list/
官方的例子中,删除item之后,实现列表中其他item的移动,起始是依靠了一个size的动画,改变item的大小,以引起动画效果。本身而言并不复杂,所以,其实也可以自己实现一个,下面为实现之后的效果图:
实现方法:
简而言之,其实就是让item
先跟随手势进行平移,达到触发的长度之后,松手item
就会自动平移出屏幕,然后触发这个item
的size
的动画,后面的就会自动跟上来,当然如果拖动的长度不够,item
还得回去。下面是代码:
第一步:
先写一个widget,参数的话,需要把listview
的那些全复制过来,state
中构建listview
的时候需要用到它们,另外还需要一个方法OnActionFinished
,是当删除行为完成之后,通知父控件来刷新数据,并返回当前列表的长度。
这里的ScrollPhysics
用的是BouncingScrollPhysics()
,效果就是会有像ios一样的回弹效果。AlwaysScrollableScrollPhysics()
可以使item的数量在不足一屏时,也能够进行拖动。
const double triggerLength = 80.0; // 横向滑动触发距离
typedef OnActionFinished = int Function(int index); // 进行数据清除工作,并返回当前list的length
typedef AnimateFinishedCallBack = void Function(int index); // 动画结束通知列表进行刷新操作 --- 定义的item当中使用
class CustomAnimateList extends StatefulWidget {
final IndexedWidgetBuilder itemBuilder;
final OnActionFinished onActionFinished;
int itemCount;
final Axis scrollDirection;
final bool reverse;
final ScrollController controller;
final bool primary;
final ScrollPhysics physics;
final bool shrinkWrap;
final EdgeInsetsGeometry padding;
CustomAnimateList({
Key key,
@required this.itemCount,
@required this.itemBuilder,
@required this.onActionFinished,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _CustomAnimateListState();
}
}
class _CustomAnimateListState<T> extends State<CustomAnimateList> {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.itemCount,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: (widget.physics != null
? widget.physics
: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())),
shrinkWrap: widget.shrinkWrap,
padding: widget.padding,
itemBuilder: (context, index) {
return _ListItem(
index, widget.itemBuilder(context, index), removeTargetItem);
});
}
// 刷新列表,替换数据
void removeTargetItem(int index) {
setState(() {
widget.itemCount = widget.onActionFinished(index);
});
}
}
第二步:
定义完widget之后,剩下的就是对item的定义了,这里需要做的就是在传入的itemBuilder
的外面再加一层动画控件,并对手势操作进行对应的反馈,由于需要再外面加上GestureDetector
,所以说,如果需要添加点击事件或者其他的交互事件,可以添加相应的回调到定义的widget当中,然后进行处理。
整个部分唯一相对麻烦一点的就是对横向滑动的操作进行反馈的部分,其他的都还算简单。
class _ListItem extends StatefulWidget {
final int index;
final Widget child;
final AnimateFinishedCallBack onAnimateFinished;
double dragStartPoint = 0.0;
double draglength = 0.0;
_ListItem(this.index, this.child, this.onAnimateFinished);
@override
State<StatefulWidget> createState() {
return _ListItemState();
}
}
class _ListItemState extends State<_ListItem> with TickerProviderStateMixin {
bool _slideEnd = false;
bool _sizeEnd = false;
Size _size;
AnimationController _slideController;
AnimationController _sizeController;
Animation<Offset> _slideAnimation;
Animation<double> _sizeAnimation;
@override
void initState() {
super.initState();
initSlideAnimation();
initSizeAnimation();
WidgetsBinding.instance.addPostFrameCallback(onAfterRender);
}
@override
void didUpdateWidget(_ListItem oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance.addPostFrameCallback(onAfterRender);
}
void initSlideAnimation() {
_slideController =
AnimationController(vsync: this, duration: Duration(milliseconds: 250));
_slideAnimation = Tween(begin: Offset(0.0, 0.0), end: Offset(-1.0, 0.0))
.animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOut));
}
void initSizeAnimation() {
_sizeController =
AnimationController(vsync: this, duration: Duration(milliseconds: 250));
_sizeAnimation = Tween(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _sizeController, curve: Curves.easeOut));
}
@override
void dispose() {
super.dispose();
_slideController.dispose();
_sizeController.dispose();
}
void onAfterRender(Duration timeStamp){
_size = context.size;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
child: buildItem(),
onHorizontalDragStart: (detail) {
widget.dragStartPoint = detail.globalPosition.dx;
},
onHorizontalDragUpdate: (detail) {
widget.draglength = widget.dragStartPoint - detail.globalPosition.dx;
// 向左滑动时触发平移动画
if (widget.draglength > 0.0) {
if (widget.draglength <= triggerLength) {
double value = widget.draglength / triggerLength;
_slideController.value = getSlideAnimatePhysicsValue(
_slideController.value, value, 0.15);
}
}
},
onHorizontalDragEnd: (detail) {
if (widget.draglength >= triggerLength) {
// 滑动距离大于触发距离,开始删除动画
_slideController.forward().whenComplete(() {
setState(() {
_slideEnd = true;
_sizeController.forward().whenComplete(() {
_sizeEnd = true;
// 通知list 进行数据刷新操作
widget.onAnimateFinished(widget.index);
});
});
});
} else if (widget.draglength > 0.0 &&
widget.draglength < triggerLength) {
// 滑动距离大于0.0且小于触发距离,复位
_slideController.reverse();
}
},
);
}
Widget buildItem() {
if (_slideEnd && _sizeEnd) {
_slideController.value = 0.0;
_sizeController.value = 0.0;
_slideEnd = false;
_sizeEnd = false;
}
return (_slideEnd
? SizeTransition(
axis: Axis.vertical,
sizeFactor: _sizeAnimation,
child: Material(
color: Colors.transparent,
child: SizedBox.fromSize(size: _size),
),
)
: SlideTransition(
position: _slideAnimation,
child: widget.child,
));
}
double getSlideAnimatePhysicsValue(
double oldValue, double newValue, double percent) {
double addValue = (percent - oldValue) / percent * (newValue - oldValue);
return (oldValue + addValue) * percent;
}
}
最后是基础布局的代码。
void main() {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((Null) {
runApp(ForFun());
});
}
class ForFun extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ForFun',
theme: ThemeData(
primaryColorDark: Colors.blueAccent,
primaryColor: Colors.blue,
primaryColorLight: Colors.lightBlue,
primarySwatch: Colors.blue,
),
home: TestListPage(),
);
}
}
class TestListPage extends StatelessWidget {
final List<String> list = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
];
@override
Widget build(BuildContext context) {
return Material(
color: Colors.blue,
child: CustomAnimateList(
itemCount: list.length,
itemBuilder: itemBuilder,
onActionFinished: (index) {
list.removeAt(index);
return list.length;
},
));
}
Widget itemBuilder(BuildContext context, int index) {
return Padding(
padding: EdgeInsets.only(left: 12.0, top: 4.0, right: 12.0, bottom: 4.0),
child: Material(
elevation: 2.0,
borderRadius: BorderRadius.all(Radius.circular(2.0)),
child: SizedBox(
height: 64.0,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: Text(list[index]),
),
),
),
),
);
}
}
到这里删除动画列表就结束了,下一部分时做一个带删除动画的gridview
。