Flutter 可拖拽Widget
2021-01-08 本文已影响0人
旺仔_100
一、背景
我们经常会看到管理手机软件那种悬浮小圆圈,或者微信公众号的悬浮按钮。它们不仅能悬浮在所有页面之上,还可以在界面任意拖动。我们使用flutter 改如何实现呢?
二、思路分析
1.全局弹窗。这个在flutter里面有一个 Overlay.of(context).insert(overlayEntry);
这个就是可以全局浮动的弹窗。
2.任意拖动。刚好flutter有个Draggable控件,它可以直接拖动一个widget。但是它一松手就会回到之前的位置。
3.为了Draggable控件停留在我们想要的位置,那么久引入了DragTarget。
三、每一个Widget介绍
1.Overlay:Overlay 之于 Flutter , 有点相当于 KeyWindow 之于 iOS 一样,可以将子 widget 置于其他 widget 的顶层,带来 “悬浮”的效果。
2.OverlayEntry:OverlayEntry 之于 Overlay,对于 iOS 开发而言,又有点 subView 之于 KeyWindow 的味道了。 OverlayEntry 是视图的实际的容器, 把其往 Overlay 那儿添加了,就可以成像了。
3.Draggable
const Draggable({
Key key,
@required this.child, // 初始化显示的 widget
@required this.feedback, // 拖拽过程中(活动中)显示的 widget
this.data, // widget 携带的数据,放手时可以将这个 data 数据传递出去
this.axis, // 限制 draggable 的移动范围
this.childWhenDragging, // 拖住动作发生过程中,初始化位置显示的 widget
this.feedbackOffset = Offset.zero, // 当 feedback 与 child 相比,有 transform 的时候,需要用到这个属性来调整 hittest 范围
this.dragAnchor = DragAnchor.child, //锚点
this.affinity, // 单词的意思是亲和力,当 Draggable 位于 另外一个 Scrollable 控件內时,来控制到底这个这个拖拽事件到底由 Draggable 响应,还是由 Scrollable 控件来响应
this.maxSimultaneousDrags, // 限制有多少个 Draggable 同时发生 拖拽动作
this.onDragStarted, // 拖拽动作开始回调
this.onDraggableCanceled, // 拖拽动作取消回调
this.onDragEnd, //拖拽动作结束回调
this.onDragCompleted, // 拖拽动作完成回调, 并被一个 DragTarget 接收
this.ignoringFeedbackSemantics = true, // 也是看了文档才知道,这个属性还是有点用的,当 feedback 跟 child 是同一个 widget A 对象时,就应该把这个属性设成 false, 配合赋值一个 GlobalKey,这样,这个 widget A 就不会在 feedback 跟 child 切换时,重新销毁后又创建了。这个在 widget A 带有播放动画是比较容易看出区别,每次手指拖放都伴随着动画的重新开始
})
4.DragTarget
const DragTarget({
Key key,
@required this.builder, //根据 Draggable 传过来的 data ,来显示想要的 widget
this.onWillAccept, // 根据传过来的 data ,选择是否接收这个 Draggable, 返回 true 则激活 onAccept
this.onAccept, // Draggable 被丢进了这个 DragTarget 区域后回调
this.onLeave, // Draggable 离开 DragTarget 区域后的回调
}) : super(key: key);
四、完整代码
import 'package:flutter/cupertino.dart';
class DragOverlay {
static Widget view;
static OverlayEntry _holder;
static void remove() {
if (_holder != null) {
_holder.remove();
_holder = null;
}
}
static void show({@required BuildContext context, @required Widget view}) {
DragOverlay.view = view;
remove();
OverlayEntry overlayEntry = OverlayEntry(builder: (context){
return Positioned(
top: MediaQuery.of(context).size.height *0.7,
child: _buildDraggable(context),
);
});
Overlay.of(context).insert(overlayEntry);
_holder = overlayEntry;
}
static _buildDraggable(context){
return Draggable(
child: view,
feedback: view,
onDragStarted: (){
},
onDragEnd: (detail){
print("onDraEnd:${detail.offset}");
//放手时候创建一个DragTarget
createDragTarget(offset:detail.offset,context:context);
},
//当拖拽的时候就展示空
childWhenDragging: Container(),
ignoringFeedbackSemantics: false,
);
}
static void createDragTarget({Offset offset,BuildContext context}){
if(_holder != null){
_holder.remove();
}
_holder = new OverlayEntry(builder: (context){
bool isLeft = true;
if(offset.dx + 100 > MediaQuery.of(context).size.width / 2){
isLeft = false;
}
double maxY = MediaQuery.of(context).size.height - 100;
return Positioned(
top: offset.dy < 50 ? 50 : offset.dy > maxY ? maxY : offset.dy,
left: isLeft ? 0:null,
right: isLeft ? null : 0,
child: DragTarget(
onWillAccept: (data){
print('onWillAccept:$data');
///返回true 会将data数据添加到candidateData列表中,false时会将data添加到rejectData
return true;
},
onAccept: (data){
print('onAccept : $data');
},
onLeave: (data){
print("onLeave");
},
builder: (BuildContext context,List incoming,List rejected){
return _buildDraggable(context);
},
),
);
});
Overlay.of(context).insert(_holder);
}
}
五、调用
DragOverlay.show(context: context, view: Container(
width: 100,
height: 20,
color: Colors.red,
));