A function similar to QQ side slide menu, support from the top, bottom, left, right four methods to open the menu bar. You can customize transform to achieve more cool dynamic effect! First, the effect picture:

Github address: github.com/yumi0629/Sl…

Usage:

SlideStack(
      child: SlideContainer(
        key: _slideKey,
        child: Container(
         /// widget mian.
        ),
        slideDirection: SlideDirection.top,
        onSlide: onSlide,
        drawerSize: maxSlideDistance,
        transform: transform,
      ),
      drawer: Container(
        /// widget drawer.
      ),
    );
Copy the code

The slideDirection property controls which method the menu opens from. Call key. The currentState. OpenOrClose () method can manually open or close menu; With the transform property and the listening values returned during the slide, you can add various sample transformations to the layout during the animation.

Implementation analysis

Implementing this effect with Flutter is actually quite simple, 300 lines of code is enough. The implementation of the slide menu is essentially the upper layout changes its position with the user’s gestures, allowing the lower level menu bar to be displayed. Once you understand this process, everything will be all right. Basic idea: the upper and lower layers of the layout with Stack combination, the upper layer of the layout needs to support gestures, the lower layer only need to be a normal layout. So the challenge is, how does the upper level layout support gestures? Learn more about Gestures in Flutter by reading this article: Explaining gesture control Gestures in Flutter and what gesturecognizer is. And, of course, we implement a simple slide function does not need so complicated, because there is no conflict involves sliding, we simply use the system own HorizontalDragGestureRecognizer class is ok. The progress of each frame of the upper layout is controlled by the AnimationController. The value in its callback allows us to easily obtain the progress value of the animation.

Implementation of the upper layer layout

Step 1 Sign up to the gesture listening Recognizer

First, we give our custom layout registration gesture Recognizer monitoring, _registerGestureRecognizer () method in the layout of initState () method:

final Map<Type, GestureRecognizerFactory> gestures =
      <Type, GestureRecognizerFactory>{};

void _registerGestureRecognizer() {
    if(isSlideVertical) { gestures[VerticalDragGestureRecognizer] = createGestureRecognizer<VerticalDragGestureRecognizer>( ()  => VerticalDragGestureRecognizer()); }else{ gestures[HorizontalDragGestureRecognizer] = createGestureRecognizer<HorizontalDragGestureRecognizer>( () => HorizontalDragGestureRecognizer()); } } GestureRecognizerFactoryWithHandlers<T> createGestureRecognizer<T extends DragGestureRecognizer>( GestureRecognizerFactoryConstructor<T> constructor) => GestureRecognizerFactoryWithHandlers<T>( constructor, (T instance) { instance .. onStart = handleDragStart .. onUpdate = handleDragUpdate .. onEnd = handleDragEnd; });Copy the code

Step 2 bind Ticker and AnimationController

With our Recognizer, how do we attach it to the user’s gestures? The AnimationController and Ticker classes are used.

AnimationController animationController;
Ticker fingerTicker;

@override
  void initState() { animationController = AnimationController(vsync: this, duration: widget.autoSlideDuration) .. AddListener (() {······ // Refresh the upper layout positionsetState(() {}); }); FingerTicker = createTicker ((_) {...... / / more user gestures move, update animationController. Value animationController. Value = · · · · · ·. }); _registerGestureRecognizer(); super.initState(); }Copy the code

It is obvious that the user’s gestures slip slide would produce a value, we will this slip value is calculated, then assigned to animationController. Value; At the same time, calculate the offset required by the upper layout by calling setState(() {}); Refresh the upper layout location.

Step 3 Builds the basic controls

So, the return value of the build function is well defined. Because of gestures, we wrap a RawGestureDetector around it and then pass in the Gestures we registered in Step 1 to indicate that the control will then receive gestures in the vertical/horizontal direction. Because the upper layout involves position movement, we chose to build it using Transform. Every time the user swipes, it generates a dragValue, which calculates what value the control should offset, and we save that as containerOffset, pass that containerOffset to Transform setState and that will create the movement on the page.

@override Widget build(BuildContext context) => RawGestureDetector( gestures: gestures, child: Transform.translate( offset: isSlideVertical ? Offset(0.0, containerOffset,) : Offset(containerOffset, 0.0,), child: _getContainer(),);Copy the code

Step 4 Calculate the offset distance

So far, the rough implementation framework is out, and the calculation part is next. So first of all, our containerOffset is really just a dragValue, which makes sense.

double get containerOffset =>  dragValue;
Copy the code

Then there’s the sliding (animation) progress, which is simply dragValue/maxDragDistance, which is the drag distance/total distance (width/height of the Drawer).

 fingerTicker = createTicker((_) {
        animationController.value = dragValue / maxDragDistance;
    });
Copy the code

So one of the things that might be a little bit confusing here is, I’m just going to get containerOffset based on the dragValue, and then I’m going to move the upper control, and it’s not going to take long to do that, okay? Why do I need an AnimationController? Indeed, the animationController serves only as a record. We use the animationController because we can use the animationController to return the drag to the outermost parent control, and because we can use the animationController to quickly complete/cancel the slide. AnimationController has the following benefits:

void openOrClose() { final AnimationStatus status = animationController.status; final bool isOpen = status == AnimationStatus.completed || status == AnimationStatus.forward; animationController.fling(velocity: isOpen ? 2.0, 2.0); } void _completeSlide() => animationController.forward().then((_) {if(widget.onSlideCompleted ! = null) widget.onSlideCompleted(); }); void _cancelSlide() => animationController.reverse().then((_) {if(widget.onSlideCanceled ! = null) widget.onSlideCanceled(); });Copy the code

Instead of manually refreshing containerOffset, we can easily use the API provided by the AnimationController to quickly open/close the containerOffset when the user is halfway through dragging it, or when the user clicks a button to open/close the menu. So, the AnimationController is a precautionary design, because it’s not a control where the layout simply follows the user’s gestures, we need a controller to control the placement of the layout.

Step 5 Automatically complete/cancel the operation when the user drags it halfway

In practice, we often encounter a problem that the user’s finger does not fully slide to the value of maxDragDistance and may stop halfway. So what should our upper controls do? Positioning the layout where the user’s gesture stops is clearly unfriendly. The solution of QQ sideslip menu is: the user finger exceeds a certain boundary value will automatically complete the opening operation; If the boundary value is not reached, cancel the open operation:

handleDragEnd
minAutoSlideDragVelocity

void handleDragUpdate(DragUpdateDetails details) {
    if (dragValue >
        widget.minAutoSlideDragVelocity) {
      _completeSlide();
    } else if (dragValue <
        widget.minAutoSlideDragVelocity) {
      _cancelSlide();
    } 
    fingerTicker.stop();
}
Copy the code

Merges upper and lower controls

This is easy, as mentioned earlier, the easiest way to use a Stack layout:

class SlideStack extends StatefulWidget {
  /// The main widget.
  final SlideContainer child;

  /// The drawer hidden below.
  final Widget drawer;

  const SlideStack({
    @required this.child,
    @required this.drawer,
  }) : super();

  @override
  State<StatefulWidget> createState() => _StackState();
}

class _StackState extends State<SlideStack> {
  @override
  Widget build(BuildContext context) {
    returnStack( children: <Widget>[ widget.drawer, widget.child, ], ); }}Copy the code

The details

At this point, we’re 90% done. Now it’s time to polish up some details, add some attributes to make the sideslip menu experience more user-friendly. See the source code for this part.

  • Add shadows to the upper layout: referenceshadowBlurRadiusandshadowSpreadRadiusProperties;
  • Add damping factordragDampeningThis parameter is very common when we do List sliding. The actual movement distance of the layout is often inconsistent with the movement distance of the user’s finger. We can control it by this damping coefficient.
  • Adding a Customtransform, our above implementation only translated the upper layout. If the translation + narrowing effect in the effect figure 1 is needed, a custom transform needs to be added. I didn’t wrap the shrink effect into the control because I wanted the control’s deformation to be flexible and controlled from the outside, rather than being written to death. And I’ve already exposed the animation progress through the AnimationController, which makes it easy to transform any way you want.
  • Add a progress callback listeneronSlideStarted,onSlideCompleted,onSlideCanceled,onSlide.