一、背景
我們經(jīng)常會看到管理手機軟件那種懸浮小圓圈,或者微信公眾號的懸浮按鈕。它們不僅能懸浮在所有頁面之上,還可以在界面任意拖動。我們使用flutter 改如何實現(xiàn)呢?
二、思路分析
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 開發(fā)而言,又有點 subView 之于 KeyWindow 的味道了。 OverlayEntry 是視圖的實際的容器, 把其往 Overlay 那兒添加了,就可以成像了。
3.Draggable
const Draggable({
Key key,
@required this.child, // 初始化顯示的 widget
@required this.feedback, // 拖拽過程中(活動中)顯示的 widget
this.data, // widget 攜帶的數(shù)據(jù),放手時可以將這個 data 數(shù)據(jù)傳遞出去
this.axis, // 限制 draggable 的移動范圍
this.childWhenDragging, // 拖住動作發(fā)生過程中,初始化位置顯示的 widget
this.feedbackOffset = Offset.zero, // 當 feedback 與 child 相比,有 transform 的時候,需要用到這個屬性來調(diào)整 hittest 范圍
this.dragAnchor = DragAnchor.child, //錨點
this.affinity, // 單詞的意思是親和力,當 Draggable 位于 另外一個 Scrollable 控件內(nèi)時,來控制到底這個這個拖拽事件到底由 Draggable 響應(yīng),還是由 Scrollable 控件來響應(yīng)
this.maxSimultaneousDrags, // 限制有多少個 Draggable 同時發(fā)生 拖拽動作
this.onDragStarted, // 拖拽動作開始回調(diào)
this.onDraggableCanceled, // 拖拽動作取消回調(diào)
this.onDragEnd, //拖拽動作結(jié)束回調(diào)
this.onDragCompleted, // 拖拽動作完成回調(diào), 并被一個 DragTarget 接收
this.ignoringFeedbackSemantics = true, // 也是看了文檔才知道,這個屬性還是有點用的,當 feedback 跟 child 是同一個 widget A 對象時,就應(yīng)該把這個屬性設(shè)成 false, 配合賦值一個 GlobalKey,這樣,這個 widget A 就不會在 feedback 跟 child 切換時,重新銷毀后又創(chuàng)建了。這個在 widget A 帶有播放動畫是比較容易看出區(qū)別,每次手指拖放都伴隨著動畫的重新開始
})
4.DragTarget
const DragTarget({
Key key,
@required this.builder, //根據(jù) Draggable 傳過來的 data ,來顯示想要的 widget
this.onWillAccept, // 根據(jù)傳過來的 data ,選擇是否接收這個 Draggable, 返回 true 則激活 onAccept
this.onAccept, // Draggable 被丟進了這個 DragTarget 區(qū)域后回調(diào)
this.onLeave, // Draggable 離開 DragTarget 區(qū)域后的回調(diào)
}) : 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}");
//放手時候創(chuàng)建一個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數(shù)據(jù)添加到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);
}
}
五、調(diào)用
DragOverlay.show(context: context, view: Container(
width: 100,
height: 20,
color: Colors.red,
));