preface
I always thought that Drawer on the home page of Amap slides beautifully and has some sense of technology. I implemented it on Android before, and then implemented it on Flutter when I am not busy recently.
Schematic diagram
In order to distinguish the layout structure, I used different colors
Status of Drawer height
You can see the height of the drawer in three cases:
Maximum height
There’s a little space above the top, where the space is positioned at 70,
The height of the drawer is -70
Medium height
Here we position the display height of the drawer at 300
The minimum height
The id of the drawer here is located at altitude 150
UI structure of a Drawer
You can see that the UI inside the drawer is divided into three parts:
Search area, multi-function area, extension areaCopy the code
While the drawer rolls between maximum and medium height, the multifunctional area needs to be indented/expanded into the extended area
Code implementation
Basic layout
Because the bottom of the window needs to display the map, and the drawer needs to display different heights, I use stack as the layout:
Size is obtained by mediaQuery.of(context)Copy the code
@override
Widget build(BuildContext context) {
returnMaterial( color: Colors.white, child: Container( color: Colors.greenAccent, width: size.width,height: Size. Height, child: Stack(children: <Widget>[toy (top: initPositionTop,....... omitted Drawer)],),);Copy the code
We control the distance of the toy by wrapping the drawer, and then by top. In order to capture the touch event, we need to wrap our drawer with the GestureDetector. The code is:
Positioned( top: initPositionTop, child: GestureDetector( onVerticalDragStart: verticalDragStart, onVerticalDragUpdate: verticalDragUpdate, onVerticalDragEnd: verticalDragEnd, ///Drawer child: Container( width: size.width,height: DrawerHeight, color: color. white, /// The multifunctional area needs indentation and standing, so we use stack as the internal root layout of the drawer child: stack (children: <Widget>[/// / Search area Container(alignment: alignment. Center, color: colors. pink, width: size.width,height: searchHeight - minHeight, child: Text('I am search'Toy (top: searchheight-minheight, child: Container(alignment: alignment. Center, color: Colors.white, width: size.width,height: rowH * 3+20, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ normalRow(), normalRow(), Container( color: Colors.grey[300], width: size.width,height: rowH, alignment: Alignment.topCenter, child: Text('常去的地方'Would offer tourists a small glimpse of space.), style: TextStyle(fontSize: 18,color: color.black),)],),), /// expand area of space (top: expandPosTop + topArea, child: Container( color: Colors.lightGreen, alignment: Alignment.topCenter, width: Font-size. Width,height: drawerheight-searchheight-rowh,//'I'm the extended area'() [() [() [() [() [()Copy the code
Now that you have your UI layout done, let’s do the gesture swiping.
Signal processing
First we only need to deal with vertical sliding, so in the callback we implement these three methods:
Child: GestureDetector(onVerticalDragStart: verticalDragStart, /// Trigger onVerticalDragUpdate when touching the screen for the first time: VerticalDragUpdate,/// this method is continuously called when sliding onVerticalDragEnd: verticalDragEnd,/// this method is called when the finger is off screenCopy the code
dragStart
When a finger touches the screen, we need to record where it is clicked:
Offset lastPos;
void verticalDragStart(DragStartDetails details){
lastPos = details.globalPosition;
}
Copy the code
dragUpdate
Then, as the user slides, we refresh the position top value of the drawer (that is, initPositionTop) to achieve the sliding effect of the drawer.
If it is a simple slide, we can simply add initPositionTop to the slide difference value, but based on our experience, we will definitely need the slide direction, so we can also record the slide direction here, which can be determined by the slide difference value:
enum SlideDirection{
Up,
Down
}
Copy the code
void verticalDragUpdate(DragUpdateDetails details){
double dis = details.globalPosition.dy - lastPos.dy;
if(dis<0){
direction = SlideDirection.Up;
}else{
direction = SlideDirection.Down;
}
if(direction == SlideDirection.Up){
if(initPositionTop <= top1+cacheDy) return;
}else if(direction == SlideDirection.Down){
if(initPositionTop >= top3-cacheDy) return; } initPositionTop += dis; LastPos = details. GlobalPosition; // Call the function refreshExpandWidgetTop();setState(() {
});
}
Copy the code
dragEnd
Here we don’t need to do anything, the code looks like this:
void verticalDragEnd(DragEndDetails details){
}
Copy the code
When we run the drawer, we see that the drawer can fold/expand with a finger slide, but when our finger goes off the screen, the drawer stays there.
As you can see from Gaud, the drawer will always stay at one level out of three, and if the finger slides beyond the limit, the drawer will automatically roll back to its nearest level. Now it’s time to upgrade.
upgrade
The preparatory work
InitPositionTop we need to record the top value of the three heights of positions in the drawer:
/// double top1 in the position of the root container; // DrawerLvl lvl 1 double top2; // DrawerLvl lvl 2 double top3; // DrawerLvl lvl 3 double initPositionTop; /// initialize top1 = size. height-drawerheight; top2 = size.height - searchHeight; top3 = size.height - minHeight; InitPositionTop = top2;Copy the code
Then we need to record the status of the drawer:
Enum DrawerLvl{LVL1, LVL2, LVL3} /// DrawerLvl = drawerlvl.lvl2; /// SlideDirection direction;Copy the code
They correspond to top1, top2 and top3 respectively
So when we slide, if we go from top1 to top2, but we don’t get to top2, we let go, and then we need to do the rest of the work, and that’s it
AnimationController
Animation
Copy the code
animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 300));
Copy the code
Should I slide back to top1 or to top2? Here we need to set two thresholds:
// Double threshold1To2; double threshold2To3; /// constructor DrawerDemoState(this.size){drawerHeight = size.height-paddingTop; threshold1To2 = size.height/3; threshold2To3 = size.height - 250; }Copy the code
Upgrade dragStart
Now we start to upgrade the old method
Void verticalDragStart(DragStartDetails details){// determine the initial state of the drawer markDrawerLvl(); Animation = null; /// Stop and reset the controllerif(animationController.isAnimating){
animationController.stop();
}
animationController.reset();
lastPos = details.globalPosition;
log('start'.'$initPositionTop');
}
Copy the code
When the user touches, we first determine the initial state of the drawer:
markDrawerLvl(){
double l1 = (top1-initPositionTop).abs();
double l2 = (top2-initPositionTop).abs();
double l3 = (top3-initPositionTop).abs();
if(l1 == (math.min(l1, math.min(l2, l3)))){
drawerLvl = DrawerLvl.LVL1;
}else if(l2 == (math.min(l1, math.min(l2, l3)))){
drawerLvl = DrawerLvl.LVL2;
}else{ drawerLvl = DrawerLvl.LVL3; }}Copy the code
Upgrade dragUpdate
void verticalDragUpdate(DragUpdateDetails details){
double dis = details.globalPosition.dy - lastPos.dy;
if(dis<0){
direction = SlideDirection.Up;
}else{ direction = SlideDirection.Down; } ///cacheDy prevents invalidation of judgments caused by sliding too fast out of rangeif(direction == slidedirection.up){/// avoid sliding drawer out of the screenif(initPositionTop <= top1+cacheDy) return;
}else if(direction == SlideDirection.Down){
if(initPositionTop >= top3-cacheDy) return; } initPositionTop += dis; lastPos = details.globalPosition; // refreshExpandWidgetTop();setState(() {
});
}
Copy the code
Upgrade dragEnd
When the user’s finger leaves the screen, we have to deal with whether the drawer continues to scroll or reset.
void verticalDragEnd(DragEndDetails details){
adjustPositionTop(details);
}
Copy the code
This method is longer, so I’ll explain it in the comments
void adjustPositionTop(DragEndDetails details){
switch(direction){
case SlideDirection.Up:
if(the details. Velocity. PixelsPerSecond. Dy. Abs () > thresholdV) {/ / / the user fling at a faster rate than threshold, to determine directly into the next level switch (drawerLvl) {caseDrawerLvl.LVL1: // TODO: Handle this casebreak;
case DrawerLvl.LVL2:
slideTo(begin: initPositionTop,end: top1);
break;
case DrawerLvl.LVL3:
slideTo(begin: initPositionTop,end: top2);
break; }}else{/// If the threshold is not exceeded, we reset or continue slidingif(initPositionTop >= top1 && initPositionTop <= top2){///if(initPositionTop <= threshold1To2){/// roll to top1 slideTo(begin:initPositionTop, end:top1); }else{/// slide top2 slideTo(begin: initPositionTop,end: top2); }}else if(initPositionTop >= top2 && initPositionTop <= top3){/// between 2 and 3if(initPositionTop <= threshold2To3){/// slideTo(begin: initPositionTop,end: top2); }else{/// slideTo 3 slideTo(begin: initPositionTop,end: top3); }}}break;
caseSlidedirection. Down: /// same as aboveif(details.velocity.pixelsPerSecond.dy.abs() > thresholdV){
switch(drawerLvl){
case DrawerLvl.LVL1:
slideTo(begin: initPositionTop,end: top2);
break;
case DrawerLvl.LVL2:
slideTo(begin: initPositionTop,end: top3);
break;
case DrawerLvl.LVL3:
//todo nothing
break; }}else{
if(initPositionTop >= top1 && initPositionTop <= top2){/// Between levels 1 and 2if(initPositionTop <= threshold1To2){/// roll to top1 slideTo(begin: initPositionTop,end:top1); }else{/// slide top2 slideTo(begin: initPositionTop,end: top2); }}else if(initPositionTop >= top2 && initPositionTop <= top3){/// between 2 and 3if(initPositionTop <= threshold2To3){/// slideTo(begin: initPositionTop,end: top2); }else{/// slideTo 3 slideTo(begin: initPositionTop,end: top3); }}}break; }}Copy the code
In the completion slide here, we hand it over to the animationController:
SlideTo ({double begin,double end})async{animation = Tween<double>(begin: begin,end:end ).animate(animationController); await animationController.forward(); }Copy the code
In the listener of the animation, we refresh initPositionTop:
animationController.addListener(() {
if(animation == null) return; // refreshExpandWidgetTop();setState(() {
initPositionTop = animation.value;
});
});
Copy the code
We now have a relatively complete sliding function for the drawer.
Multifunctional widgets display hidden effects
Moving on to the widget inside the drawer, we can see that as we scroll between top1 and top2, the internal multifunction area is indented and extended accordingly, and we implement this.
The UI layout
Since we only need to move the extension area to achieve the effect of sliding out/down the multifunction area, we can use the Stack to complete the basic layout:
Stack(children: <Widget>[/// / search for Container(alignment: align. center, color: color. pink, width: size.width,height: searchHeight - minHeight, child: Text('I am search'Toy (top: searchheight-minheight, child: Container(alignment: alignment. Center, color: Colors.white, width: size.width,height: rowH * 3+20, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ normalRow(), normalRow(), Container( color: Colors.grey[300], width: size.width,height: rowH, alignment: Alignment.topCenter, child: Text('常去的地方'Would offer tourists a small glimpse of space.), style: TextStyle(fontSize: 18,color: color.black),)],),), /// expandPosTop + topArea, child: Container( color: Colors.lightGreen, alignment: Alignment.topCenter, width: Font-size. Width,height: drawerheight-searchheight-rowh,//'I'm the extended area'(), ([], (Copy the code
Search area and multifunction area, just adjust the top to order them.
For the extension area, we need to cover part of the multifunction area at the beginning of the page (only one line of circles is missing).
For convenience, the multifunctional height positioning rowH * 3;Copy the code
So the initial value of top in the extension area is the multi-functional top + rowH. Here, we define a variable for the top value in the extension area:
ExpandPosTop = Top + rowH of the multi-function areaCopy the code
Further, we can determine that the change range of expandPosTop is:
We define a variable for this variable topArea topArea = [0 - rowH * 2];Copy the code
The code for the final extension area is as follows:
Toy (top: expandPosTop + topArea, child: Container(color: Colors. LightGreen, alignment: Alignment. TopCenter, width: size.width,height: drawerheight-searchheight-rowh,//'I'm the extended area'),),),Copy the code
Now that the overall UI layout is complete, we can implement the scroll function.
Spread zone slip
We’ve seen this method in dragUpdate and animation listener:
refreshExpandWidgetTop(); // This is where the corresponding function is implementedCopy the code
Here I put the instructions in the notes for easy reading
// refresh the position top value of the extended area /// where the difference is rowH * 2refreshExpandWidgetTop(){/// start with the difference between initPositionTop and top2-top1, Double progress = (initpositiontop-top2).abs() /(top2-top1).abs(); // determine whether to slide from top1 to top2 or vice versaif(drawerLvl == drawerlvl.lvl2){// LVL2 slides to lvl3 without processingif(initPositionTop > top2) return; TopArea = (progress * (rowh*2).clamp(0, rowh*2)); }else if(drawerLvl == drawerlvl.lvl1){//lvl2 slides to lvl3 without processingif(initPositionTop > top2) return; topArea = (progress) * (rowH*2).clamp(0, rowH*2); }}Copy the code
When we refresh outside of the above method, we will see the effect of the multifunction area collapse/extend (add some shadows to make it look better), so we have the whole function now, if it helps you to sing or star. 🙂
DEMO
Demo
recommended
Bedrock — A quick development framework for Flutter based on MVVM+Provider
Flutter custom View – a list of self-selected stocks that mimic a flush
Flutter — PageView PageController source code analysis note
Download and install implementation of Flutter – Android hybrid development