效果圖
第一步:通過Stack實現層疊卡片
通過Stack實現層疊效果,它的子Widget的部署實現方式還是比較多的,比如使用Container,計算每個卡片的margin. 使用Positioned,計算left,top,right,bottom. 使用Transform,計算Offset.
@override
Widget build(BuildContext context) {
List<Widget> children = [];
double offset = 8.0;
for (int i = 0; i < 3; i++) {
Widget child = Transform.translate(
child: children[i],
offset: Offset(offset * i, offset * i),
);
children.add(child);
}
return Stack(children: children.reversed.toList());
}
複製程式碼
第二步:通過GestureDetector實現拖拽效果
第一層的卡片,需要識別使用者的拖拽手勢。並根據拖拽的距離更新第一層卡片的Offset.
關於手勢識別,可以檢視 [譯] 深入 Flutter 之手勢。
_onPanUpdate(DragUpdateDetails details) {
if (!_isDragging) {
_isDragging = true;
return;
}
_offsetDx += details.delta.dx;
_offsetDy += details.delta.dy;
setState(() {});
}
複製程式碼
第三步:實現移除動畫
當使用者抬起手勢的時候,啟動移除的動畫,也可以把第一層卡片移到最後一層。
切換List的兩個元素:可以使用
list.insert(a, list.removeAt(b));
複製程式碼
移除動畫的時候,需要計算移除的終止點Offset,比如使用者偏左上角拖拽,就從左上角移除。如果使用者拖拽的距離較時,可以選擇恢復原始位置,及Offset.zero。
啟動動畫,其實就是利用Tween,在規定時間內,由一個Offset到另一個Offset,並給Animation新增value監聽,重新整理UI. 新增status監聽,動畫完成時,移除卡片或切換卡片。
關於動畫的瞭解可以參考 Flutter 動畫及示例
DroppableWidget程式碼
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// Its child widget should have a fixed width and height
class DroppableWidget extends StatefulWidget {
/// Children cards
final List<Widget> children;
const DroppableWidget({Key key, this.children}) : super(key: key);
@override
_DroppableWidgetState createState() => _DroppableWidgetState();
}
class _DroppableWidgetState extends State<DroppableWidget>
with TickerProviderStateMixin {
/// Offset
double _offsetDx;
double _offsetDy;
/// Mark if dragging
bool _isDragging;
/// Animation Controller
AnimationController _animationController;
@override
void initState() {
super.initState();
// Init
_offsetDx = _offsetDy = 0;
_isDragging = false;
}
bool get _isAnimating => _animationController?.isAnimating ?? false;
/// Update offset value
_onPanUpdate(DragUpdateDetails details) {
if (_isAnimating) return;
if (!_isDragging) {
_isDragging = true;
return;
}
_offsetDx += details.delta.dx;
_offsetDy += details.delta.dy;
setState(() {});
}
/// Start animation when PanEnd
_onPanEnd(DragEndDetails details) {
if (_isAnimating) return;
_isDragging = false;
bool change = _offsetDx.abs() >= context.size.width * 0.1 ||
_offsetDy.abs() >= context.size.height * 0.1;
if (change) {
double endX, endY;
if (_offsetDx.abs() > _offsetDy.abs()) {
endX = context.size.width * _offsetDx.sign;
endY = _offsetDy.sign *
context.size.width *
_offsetDy.abs() /
_offsetDx.abs();
} else {
endY = context.size.height * _offsetDy.sign;
endX = _offsetDx.sign *
context.size.height *
_offsetDx.abs() /
_offsetDy.abs();
}
_startAnimation(Offset(_offsetDx, _offsetDy), Offset(endX, endY), true);
} else {
_startAnimation(Offset(_offsetDx, _offsetDy), Offset.zero, false);
}
}
/// Start animation
/// [change] if change child, when animation complete
_startAnimation(Offset begin, Offset end, bool change) {
_animationController = AnimationController(
duration: Duration(milliseconds: 200),
vsync: this,
);
var _animation = Tween(begin: begin, end: end).animate(
_animationController,
);
_animationController.addListener(() {
setState(() {
_offsetDx = _animation.value.dx;
_offsetDy = _animation.value.dy;
});
});
_animationController.addStatusListener((status) {
if (status != AnimationStatus.completed) return;
_offsetDx = 0;
_offsetDy = 0;
if (change) {
widget.children.insert(
widget.children.length - 1,
widget.children.removeAt(0),
);
}
setState(() {});
});
_animationController.forward();
}
@override
Widget build(BuildContext context) {
List<Widget> children = [];
int length = widget.children?.length ?? 0;
double offset = 8.0;
for (int i = 0; i < length; i++) {
double dx = i == 0 ? _offsetDx : 0.0;
double dy = i == 0 ? _offsetDy : 0.0;
Widget child = Transform.translate(
child: widget.children[i],
offset: Offset(dx + (offset * i), dy + (offset * i)),
);
if (i == 0) {
child = GestureDetector(
child: child,
onPanEnd: _onPanEnd,
onPanUpdate: _onPanUpdate,
);
}
children.add(child);
}
return Stack(children: children.reversed.toList());
}
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
}
複製程式碼