Flutter 可拖拽Widget

一、背景
我们经常会看到管理手机软件那种悬浮小圆圈,或者微信公众号的悬浮按钮。它们不仅能悬浮在所有页面之上,还可以在界面任意拖动。我们使用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,
    ));
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容