preface

Product requirements are always fantastical, changing from one idea to the next. One particular interaction we encountered in this request was that the product wanted to have a global floating button entry in the application, which the user could click from anywhere in the application to go to a certain page, thereby increasing the use of this feature. It’s kind of like adding a hover ball to your phone, bringing some deeper functions up to a level 1 menu, and you can use them anywhere, anytime.

Although the requirement was cut later because UI designers felt that the entry experience was not friendly, I had already implemented the Demo function in the early stage, so I wanted to record the implementation scheme of this function.

Implementation scheme

Draggable way

Flutter provides components for Draggable to use for drag and drop. The main categories are Child (the component to be dragged), childWhenDragging (the component at the origin after being dragged), dragging (the component being dragged).

Stack( children: <Widget>[ Positioned( left: 100, top: 100, child: Draggable( child: Text(" I'm just demo using "), childWhenDragging: Text(" I'm being pulled out 😢"), feedback: Text(" I'm the dragging thing "), (onDragEnd: (detail) { print( "Draggable onDragEnd ${detail.velocity.toString()} ${detail.offset.toString()}"); }, onDragCompleted: () { print("Draggable onDragCompleted"); }, onDragStarted: () { print("Draggable onDragStarted"); }, onDraggableCanceled: (Velocity velocity, Offset offset) { print( "Draggable onDraggableCanceled ${velocity.toString()} ${offset.toString()}"); }, ), ], ),Copy the code

The dragging process is divided into: OnDragStarted, onDragCompleted, onDraggableCanceled, onDragEnd, The callback sequence of the drag process method is as follows:

The above differences in dragging results need to be reflected in combination with the DragTarget. For example, onDragCompleted callback is triggered when dragging into the DragTarget and lifting; onDraggableCanceled callback is triggered when dragging into the DragTarget and lifting. The result of a different callback tells you whether to drag to the DragTarget. Let’s not expand too much on the DragTarget for now.

See how Draggable can then use a combination of Stack and jam to achieve the effect of being dragged to any spot on the full screen.

PS: Note that the offset of onDraggableCanceled is globalPosition, so you need to subtract the full-screen TopPadding and the height of the ToolBar if you need it.

double statusBarHeight = MediaQuery.of(context).padding.top; double appBarHeight = kToolbarHeight; Stack( children: <Widget>[ Positioned( left: offset.dx, top: offset.dy, child: Draggable( child: Box(), childWhenDragging: Container(), feedback: Box(), onDraggableCanceled: (Velocity velocity, SetState (() {this.offset = Offset(offset.dx,) {this.offset = Offset(offset.dx,) offset.dy - appBarHeight - statusBarHeight); }); }, ), ), Positioned( bottom: 10, child: Text("${offset.toString()}"), ) ], ),Copy the code

But in the gesture operation, you will find that the component Text is being dragged is not the default style, there are two solutions: the first is to customize the TextStyle style; The second is to nest a layer of Material in the feedback.

Feedback: Material(child: Text(" I'm the thing that gets pulled out "),),Copy the code

GestureDetector way

GestureDetector implementation is more customized. The use of GestureDetector has been described in the Hand Gesture Basics section of the Flutter Combat.

The GestureDetector combines the Stack and the space of space, and calculates the Offset by listening to gestures to move and position the module. Mainly using the onPanUpdate method of GestureDetector, get the delta in the DragUpdateDetails to calculate the displacement dx and dy. The original offset plus the delta offset is equal to the current position x and y coordinates, and the maximum and minimum offsets are calculated by combining the size of the component itself and the screen boundary value to control the maximum and minimum distance that the component can move to prevent the suspended component from exceeding the screen. The detailed code for the drag and drop suspension window is as follows:

class AppFloatBox extends StatefulWidget { @override _AppFloatBoxState createState() => _AppFloatBoxState(); } class _AppFloatBoxState extends State<AppFloatBox> { Offset offset = Offset(10, kToolbarHeight + 100); Offset _calOffset(Size size, Offset offset, Offset nextOffset) { double dx = 0; If (offset. Dx + nextoffset.dx <= 0) {dx = 0; } else if (offset.dx + nextOffset.dx >= (size.width - 50)) { dx = size.width - 50; } else { dx = offset.dx + nextOffset.dx; } double dy = 0; If (offset. Dy + nextoffset. dy >= (size.height-100)) {dy = size.height-100; } else if (offset.dy + nextOffset.dy <= kToolbarHeight) { dy = kToolbarHeight; } else { dy = offset.dy + nextOffset.dy; } return Offset( dx, dy, ); } @override Widget build(BuildContext context) { return Positioned( left: offset.dx, top: offset.dy, child: GestureDetector( onPanUpdate: (detail) { setState(() { offset = _calOffset(MediaQuery.of(context).size, offset, detail.delta); }); }, onPanEnd: (detail) {}, child: Box() ), ), ); }}Copy the code

Add the AppFloatBox component to the Stack. AppFloatBox must be on top of the Stack otherwise it may be overwritten by other components.

Stack(
  fit: StackFit.expand,
  children: <Widget>[
    Container1(),
    Container2(),
    Container3(),
    AppFloatBox(), // Display at the top],)Copy the code

OverlayEntry mode (global)

This paper introduces the realization of the above two kinds of suspension window, but it also has disadvantages. The above two methods are not elegant if we want to implement the floating window function in the global application. Because the Stack depends on the floating window, the entire screen needs to be contained within the Stack in order to be able to pull through. It would be complicated and cumbersome to manage the hover window using a Stack layout for every page, or a project where every page is not rooted in the Stack (would you need a major change in the global layout?). .

So in the end, overlays were a more elegant and simple way to do it. In fact, OverlayEntry is similar to the Stack StatefulWidget in that it is a component that floats over all the other widgets and can overlay the desired view into the global window, so you just add the desired view to OverlayEntry and it will always appear in the global view.

Using the AppFloatBox from the previous section, create an AppFloatBox with OverlayEntry and add it to the Overlay. You can also remove the current component directly from the Overlay by calling the Remove method of OverlayEntry. The detailed code is as follows:

static OverlayEntry entry;
Column(
    children: <Widget>[
      RaisedButton(
        child: Text("add"),
        onPressed: () {
          entry?.remove();
          entry = null;
          entry = OverlayEntry(builder: (context) {
            return AppFloatBox();
          });
          Overlay.of(context).insert(entry);
        },
      ),
      RaisedButton(
        child: Text("delete"),
        onPressed: () {
          entry?.remove();
          entry = null;
        },
      ),
    ],
  ),
Copy the code

PS: if the manual can be used to add OverlayEntry SchedulerBinding. Instance. AddPostFrameCallback adding suspended window to view.

🚀 See here 🚀 for the full code

reference

  • www.youtube.com/watch?v=QzA…
  • API. Flutter. Dev/flutter/wid…