写在前头
记得很久以前曾经看到过一个交友App的选择好友功能,当时觉得特别炫酷。后来在探探、tinder、九九之恋上也看到了类似的效果。正好最近也在复习flutter,所以就写了一个flutter版本的SwipeCard效果。独乐乐不如众乐乐,写篇文章留个纪念,也送给有需要的同学。
实现效果
思路
要实现这个效果,需要按照步骤进行如下操作:
- 按照效果布局。
- 调整布局参数,实现布局可根据一个或者几个参数动态变化。
- 添加手势动作,通过手势动作修改参数,驱动页面展现。
- 为了效果流畅,添加动画效果。手势执行完成之后根据状态执行动画。
实现(完整代码在最后)
1、按照效果布局
如果要实现这个布局效果,第一步就需要知道当前Widget尺寸,故我们选择使用LayoutBuilder来做根布局。使用LayoutBuild的好处是,在创建布局之前我们就能获得控件尺寸。知道尺寸之后就可以动态计算出每个Page组件尺寸和摆放位置。
2、布局参数化?
如下:
Opacity(
opacity: 0.8 + 0.2 * _scale,
child: Transform.scale(
scale: layer1Scale + (1 - layer1Scale) * _scale,
alignment: Alignment.topLeft,
child: Transform.translate(
offset: Offset(layer1X + (0 - layer1X) * _scale,
layer1Y + (0 - layer1Y) * _scale),
child: SizedBox(
child: widget.children[1],
width: constraints.maxWidth,
height: constraints.maxHeight - 2 * bottom,
),
),
),
)
例如:scale属性就需要根据_scale来动态改变。后续只需要驱动_scale改变即可改变UI效果。
3、添加手势
手势实现起来就相对比较简单,通过GestureDetector来监控手势变化,故根布局是要使用GestureDetector,然后监听onPanUpdate、onPanEnd、onPanCancel事件。通过这几个事件来改变参数。
如下:
onPanUpdate: _onPanUpdate,
onPanEnd: (DragEndDetails? details) {
setState(() {
_upX = _moveX;
_upY = _moveY;
_upScale = _scale;
if (_isToRight()) {
_direction = Direction.right;
} else if (_isToLeft()) {
_direction = Direction.left;
} else {
if (details != null) {
if (details.velocity.pixelsPerSecond.dx > 1000) {
_direction = Direction.right;
} else if (details.velocity.pixelsPerSecond.dx < -1000) {
_direction = Direction.left;
} else {
_direction = Direction.stay;
}
} else {
_direction = Direction.stay;
}
}
if(widget.children.length == 1) {
_direction = Direction.stay;
}
_startAnim();
});
},
onPanCancel: () {
setState(() {
_moveX = 0;
_moveY = 0;
_scale = 0;
});
}
?代码中改变的就是_scale、_moveX、moveY参数。
4、添加动画
动画实现是基于AnimationController来实现,通过这个动画改变的仍然是参数化的数据_scale、_moveX、moveY。
如下:
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
lowerBound: 0,
upperBound: 1.0);
_controller.addListener(() {
setState(() {
double width = context.size!.width;
if (_direction == Direction.right) {
_scale = _upScale + (1 - _upScale) * _controller.value;
_moveX = _upX + (width - _upX) * _controller.value;
} else if (_direction == Direction.left) {
_scale = _upScale + (1 - _upScale) * _controller.value;
_moveX = _upX + (-width - _upX) * _controller.value;
} else {
_scale = _upScale + (0 - _upScale) * _controller.value;
_moveX = _upX + (0 - _upX) * _controller.value;
_moveY = _upY + (0 - _upY) * _controller.value;
}
});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
if (_direction == Direction.left) {
widget.cardSelected(false);
} else if (_direction == Direction.right) {
widget.cardSelected(true);
}
setState(() {
_moveX = 0;
_moveY = 0;
_scale = 0;
});
}
});
决定动画如何执行,是根据手势结果来区分的。代码中是通过_direction来区别如何执行动画效果。
完成上述步骤基本上就完成了一个自定义SwipeCard效果。
完整代码
// Copyright (C) 2021 The swipe_card Project.
// @auth: yangzc
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Examples:
// class _UIHomeState extends State<UIHome> {
//
// List<Widget> list = [];
// int _index = 0;
//
// @override
// void initState() {
// super.initState();
// list.add(Card(
// child: Container(
// child: Center(
// child: Text(
// "Card 3 - $_index",
// style: TextStyle(fontSize: 30),
// ),
// ),
// height: double.infinity,
// width: double.infinity,
// color: Colors.blue,
// ),
// ));
// list.add(Card(
// child: Container(
// child: Center(
// child: Text(
// "Card 2 - $_index",
// style: TextStyle(fontSize: 30),
// ),
// ),
// height: double.infinity,
// width: double.infinity,
// color: Colors.yellow,
// ),
// ));
// list.add(Card(
// child: Container(
// child: Center(
// child: Text(
// "Card 1 - $_index",
// style: TextStyle(fontSize: 30),
// ),
// ),
// height: double.infinity,
// width: double.infinity,
// color: Colors.red,
// ),
// ));
// }
//
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// body: Container(
// child: SwipeCard((bool right) {
// setState(() {
// list.removeAt(0);
// });
// }, children: list),
// width: double.infinity,
// height: double.infinity,
// ),
// appBar: AppBar(
// title: Text("推荐"),
// centerTitle: true,
// ),
// );
//
// }
// }
/// Card左右选择回调
typedef SwipeCardSelected = Function(bool right);
/// 实现SwipeCard效果
class SwipeCard extends StatefulWidget {
final List<Widget> children;
final SwipeCardSelected cardSelected;
SwipeCard(this.cardSelected, {this.children = const <Widget>[]});
@override
State<StatefulWidget> createState() {
return _SwipeCardState();
}
}
enum Direction { left,//向左滑动
right,//向右滑动
stay //无需滑动
}
class _SwipeCardState extends State<SwipeCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _moveX = 0, _moveY = 0;
double _scale = 0;
double _upX = 0, _upY = 0, _upScale = 0;
Direction _direction = Direction.stay;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
lowerBound: 0,
upperBound: 1.0);
_controller.addListener(() {
setState(() {
double width = context.size!.width;
if (_direction == Direction.right) {
_scale = _upScale + (1 - _upScale) * _controller.value;
_moveX = _upX + (width - _upX) * _controller.value;
} else if (_direction == Direction.left) {
_scale = _upScale + (1 - _upScale) * _controller.value;
_moveX = _upX + (-width - _upX) * _controller.value;
} else {
_scale = _upScale + (0 - _upScale) * _controller.value;
_moveX = _upX + (0 - _upX) * _controller.value;
_moveY = _upY + (0 - _upY) * _controller.value;
}
});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
if (_direction == Direction.left) {
widget.cardSelected(false);
} else if (_direction == Direction.right) {
widget.cardSelected(true);
}
setState(() {
_moveX = 0;
_moveY = 0;
_scale = 0;
});
}
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double bottom = constraints.maxHeight * 0.025;
final layer1X = constraints.maxWidth * 0.05 / 2;
final layer1Y = (constraints.maxHeight * 0.05) + bottom;
final layer1Scale = 0.95;
final layer2X = constraints.maxWidth * (1 - 0.95 * 0.95) / 2;
final layer2Y = (constraints.maxHeight * (1 - 0.95 * 0.95)) + bottom * 2;
final layer2Scale = layer1Scale * layer1Scale;
if (_scale > 1) {
_scale = 1;
}
List<Widget> children = [];
if (widget.children.length > 2) {
children.add(Opacity(
opacity: 0.8 * 0.8 + 0.8 * 0.2 * _scale,
child: Transform.scale(
scale: layer2Scale + (layer1Scale - layer2Scale) * _scale,
alignment: Alignment.topLeft,
child: Transform.translate(
offset: Offset(layer2X + (layer1X - layer2X) * _scale,
layer2Y + (layer1Y - layer2Y) * _scale),
child: SizedBox(
child: widget.children[2],
width: constraints.maxWidth,
height: constraints.maxHeight - 2 * bottom,
),
),
),
));
}
if (widget.children.length > 1) {
children.add(Opacity(
opacity: 0.8 + 0.2 * _scale,
child: Transform.scale(
scale: layer1Scale + (1 - layer1Scale) * _scale,
alignment: Alignment.topLeft,
child: Transform.translate(
offset: Offset(layer1X + (0 - layer1X) * _scale,
layer1Y + (0 - layer1Y) * _scale),
child: SizedBox(
child: widget.children[1],
width: constraints.maxWidth,
height: constraints.maxHeight - 2 * bottom,
),
),
),
));
}
if (widget.children.length > 0) {
children.add(Opacity(
opacity: 1,
child: Transform.scale(
scale: 1,
child: Transform.translate(
offset: Offset(_moveX, _moveY),
child: SizedBox(
child: widget.children[0],
width: constraints.maxWidth,
height: constraints.maxHeight - 2 * bottom,
),
),
),
));
}
return GestureDetector(
child: Stack(
children: children,
),
onPanUpdate: _onPanUpdate,
onPanEnd: (DragEndDetails? details) {
setState(() {
_upX = _moveX;
_upY = _moveY;
_upScale = _scale;
if (_isToRight()) {
_direction = Direction.right;
} else if (_isToLeft()) {
_direction = Direction.left;
} else {
if (details != null) {
if (details.velocity.pixelsPerSecond.dx > 1000) {
_direction = Direction.right;
} else if (details.velocity.pixelsPerSecond.dx < -1000) {
_direction = Direction.left;
} else {
_direction = Direction.stay;
}
} else {
_direction = Direction.stay;
}
}
if(widget.children.length == 1) {
_direction = Direction.stay;
}
_startAnim();
});
},
onPanCancel: () {
setState(() {
_moveX = 0;
_moveY = 0;
_scale = 0;
});
});
});
}
void _onPanUpdate(DragUpdateDetails details) {
_moveX += details.delta.dx;
_moveY += details.delta.dy;
var size = context.size;
// var offset = details.localPosition;
var distance = _moveX / size!.width;
setState(() {
_scale = distance < 0 ? -distance : distance;
});
}
bool _isToRight() {
double width = context.size!.width;
return _upX > width / 4;
}
bool _isToLeft() {
double width = context.size!.width;
return _upX < -width / 4;
}
void _startAnim() {
_controller.reset();
_controller.forward(from: 0);
}
}
欢迎留言
|