需求描述
这个需求有两个关键点:
- 顶级视图
- 可拖动
涉及 Widget 知识点
- Overlay,顶级视图解决方案
- Draggable,可拖动解决方案
Overlay
Overlay 之于 Flutter , 有点相当于 KeyWindow 之于 iOS 一样,可以将子 widget 置于其他 widget 的顶层,带来 “悬浮”的效果,具体可见注释:1
2
3
4
5
6
7
8/// A [Stack] of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
/// Rather than creating an overlay, consider using the overlay that is
/// created by the [WidgetsApp] or the [MaterialApp] for the application.
文档不建议我们重新初始化一个 overlay 对象 , 最好还是通过 Overlay.of(context)
,这样的方式去获取已经存在的 Overlay
对象。
这里就又引出了另外一个新概念 OverlayEntry
OverlayEntry
OverlayEntry
之于 Overlay
,对于 iOS 开发而言,又有点 subView 之于 KeyWindow 的味道了。 OverlayEntry
是视图的实际的容器, 把其往 Overlay
那儿添加了,就可以成像了。
1 | /// Creates an overlay entry. |
Draggable
1 | const Draggable({ |
一开始只留意到 feedback
, childWhenDragging
, onDragEnd
几个参数,实际上 ignoringFeedbackSemantics
也是挺重要的,这个放在后面再说。
把我们想要实现拖拽功能的 widget 传到 child 参数位置的时候,跑一下,可以发现,我们已经实现了拖拽功能了,但这个时候,当我们手指离开屏幕的话,child 又自动回到了初始化的位置了,并没有停留在我们想要他停留的位置,为了实现这个功能,我们又得用到另外一个 widget : DragTarget
DragTarget
1 | const DragTarget({ |
DragTarget 是用来作为 Draggable 被拖拽结束后接收他的区域, 当然 他可以通过 onWillAccept 的 data ,来选择 接不接收这个 Draggable 。
好了,前面搬文档说了一大堆废话,下面,我们来将这个几个 widget 组合运用起来,实现文章一开始的需求。
组合起来
关键代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19static void show({@required BuildContext context, @required Widget view}) {
TestOverLay.view = view;
//避免重复 show
remove();
//创建一个OverlayEntry对象
OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
//通过 Positioned 控制 位置
return new Positioned(
top: MediaQuery.of(context).size.height * 0.7,
child: _buildDraggable(context));
});
//往当前 Overlay 中插入 OverlayEntry
Overlay.of(context).insert(overlayEntry);
_holder = overlayEntry;
}
show 方法无非做了 2 件事:
- _buildDraggable
- 创建 OverlayEntry, 并插入到当前上下文的 Overlay
再看下 _buildDraggable1
2
3
4
5
6
7
8
9
10
11
12
13
14static _buildDraggable(context) {
return new Draggable(
child: view, // child 跟 feedback 用传入同一个 view,这样初始化跟拖拽过程都显示这个 view
feedback: view, //
onDragStarted: () {
print('onDragStarted:');
},
onDragEnd: (detail) {
print('onDragEnd:${detail.offset}');
createDragTarget(offset: detail.offset, context: context); // 放手的时候创建一个DragTarget
},
childWhenDragging: Container(), // 这里传个 Container,原来位置啥都不显示
);
}
放手的时候创建一个 DragTarget对象,用来接收 Draggable
1 | static void createDragTarget({Offset offset, BuildContext context}) { |
这里也是通过 Positioned 来给 DragTarget 指定位置的,需求对 Draggable 携带的 data 不关心,来者不拒,所以 onWillAccept 那儿直接 return true了;
当接收了 Draggable 后,在 builder 返回想要显示的内容,这里,我们直接返回之前那个 Draggable 对象好了,为下次的拖拽做好准备。
到此为止,整个流程就结束了。
这里看下初步实现效果:
优化
细心的同学可以很容易会发现,每次拖拽动作的开始,结束的时候,view 的旋转动画都会被重置,体验并不友好。看了下日志就知道,在这两个时刻, 都会触发 view 的重建和销毁:
ignoringFeedbackSemantics
文档提示我们,当 Draggable
的 child
跟feedback
相同时, ignoringFeedbackSemantics = false
,与 GlobalKey
配合使用,可以让 feedback
在 child
切换时,所对应 widget 不被 销毁 和 重新创建,这样设置后,再看下日志
onDragStated
,onDragEnd
,虽然也触发了 MiniRoomFloatingWidget
的 build
方法,但并没有销毁及重创建。
在来看下优化后的效果:
最后附上代码
1 | import 'package:flutter/material.dart'; |