Flutter自定义View——仿高德三级联动Drawer

lazihuman
发布于 2020-9-4 18:00
浏览
0收藏

前言
我一直觉得高德地图主页上的Drawer滑动得非常漂亮,并且具有某种科技感。 我以前使用Android做过,最近不忙时又用Flutter做过一次。

示意图
为了方便区分布局结构,我使用了不同的颜色Flutter自定义View——仿高德三级联动Drawer-鸿蒙开发者社区

Drawer高度状态
可以看到drawer 高度有三种情况:

最大高度
距离顶部有一小段空间,这里空间高度定位70,

drawer的高度为:屏幕高度-70Flutter自定义View——仿高德三级联动Drawer-鸿蒙开发者社区

 

 

中等高度
这里我们将drawer的显示高度定位300

 

Flutter自定义View——仿高德三级联动Drawer-鸿蒙开发者社区

最小高度
这里drawer的显示高度定位150Flutter自定义View——仿高德三级联动Drawer-鸿蒙开发者社区

Drawer的ui 结构
 

Flutter自定义View——仿高德三级联动Drawer-鸿蒙开发者社区

 

可以看到drawer内部的ui分为三块:

搜索区域、多功能区域、扩展区域
同时drawer在最大高度和中等高度之间滚动时,多功能区域需要缩进/展开 到 扩展区域

代码实现
基本布局
因为窗口最底层需要显示地图,同时drawer要显示不同的高度,所以这里我采用stack作为跟布局:

 

size由mediaQuery.of(context)获得

 

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.white,
      child: Container(
        color: Colors.greenAccent,
        width: size.width,height: size.height,
        child: Stack(
          children: <Widget>[

            
            Positioned(
            top: initPositionTop,
            .......省去Drawer部分代码
            )


          ],
        ),
      ),
    );
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

我们通过positioned包裹drawer,然后通过top来控制drawer上下移动的高度,为了捕获触摸事件,我们需要用GestureDetector对我们的drawer进行包裹,代码:

            Positioned(
              top: initPositionTop,
              child: GestureDetector(
                onVerticalDragStart: verticalDragStart,
                onVerticalDragUpdate: verticalDragUpdate,
                onVerticalDragEnd: verticalDragEnd,
                ///Drawer
                child: Container(
                  width: size.width,height: drawerHeight,
                  color: Colors.white,
                  ///多功能区域需要实现缩进和站看,所以这里使用stack作为drawer的内部根布局
                  child: Stack(
                    children: <Widget>[
                      ///搜索区域
                      Container(
                        alignment: Alignment.center,
                        color: Colors.pink,
                        width: size.width,height: searchHeight - minHeight,
                        child: Text('我是搜索'),
                      ),
                      ///多功能区域
                      Positioned(
                        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('常去的地方',style: TextStyle(fontSize: 18,color: Colors.black),),
                              )
                            ],
                          ),
                        ),
                      ),
                      ///扩展区域
                      Positioned(
                        top: expandPosTop + topArea,
                        child: Container(
                          color: Colors.lightGreen,
                          alignment: Alignment.topCenter,
                          width: size.width,height: drawerHeight - searchHeight -rowH,///这里需要在滚动时向下滑动
                          child: Text('我是扩展区域'),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            )
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.

至此整个UI布局就搞定了,接下来处理手势滑动。

手势处理
首先我们只需要处理垂直滑动,因此在回调中,我们实现这三个方法:

              child: GestureDetector(
                onVerticalDragStart: verticalDragStart, ///第一次触摸屏幕时触发
                onVerticalDragUpdate: verticalDragUpdate,///滑动时会持续调用此方法
                onVerticalDragEnd: verticalDragEnd,///手指离屏时会调用此方法
  • 1.
  • 2.
  • 3.
  • 4.

dragStart
当手指触摸屏幕时,我们需要记录下点击位置:

Offset lastPos;

  void verticalDragStart(DragStartDetails details){
    lastPos = details.globalPosition;
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

dragUpdate
之后在用户滑动时,我们刷新drawer的position的top值(即initPositionTop),以此来达到drawer的滑动效果。

如果只是简单的滑动,我们可以直接将initPositionTop加上滑动差值即可,但是根据经验判断,后面肯定会需要滑动方向,所以我在这里顺便把滑动的方向也记录下来,这个可以根据滑动差值的正负来判断:

enum SlideDirection{
  Up,
  Down
}
  • 1.
  • 2.
  • 3.
  • 4.
  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;
    ///这里个方法暂时不用管
    refreshExpandWidgetTop();
    setState(() {

    });
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

dragEnd
这里我们什么都不需要做,代码如下:

  void verticalDragEnd(DragEndDetails details){
  }

这时我们运行发现,drawer可以跟着手指的滑动表现收起/展开的效果,但是我们的手指离屏后,drawer也就停在那了(原始版抽屉)。

参见高德,可以看到抽屉始终会停留在三级状态中的一级,如果手指滑动超出界限/未到界限,抽屉会自动滚动/滚回到最近的等级高度,现在我们要进行升级了。

升级
准备工作
首先我们要记录一下三个高度对应的position的top值(drawer的实时top值以后就叫initPositionTop了):

 ///stack 中 根container 的position 的top 值的三种情况

  double top1;// DrawerLvl   lvl 1

  double top2;// DrawerLvl   lvl 2

  double top3;// DrawerLvl   lvl 3

  

  double initPositionTop;

  ///初始化

    top1 = size.height - drawerHeight;

    top2 = size.height - searchHeight;

    top3 = size.height - minHeight;

    ///页面最初显示的是 top2等级

    initPositionTop = top2;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

  

然后我们需要记录一下drawer的状态:

enum DrawerLvl{

  LVL1,

  LVL2,

  LVL3

}




  ///抽屉层级

  DrawerLvl drawerLvl = DrawerLvl.LVL2;

  ///滑动方向

  SlideDirection direction;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

分别对应top1,top2,top3

当我们滑动时,如果从top1滑向top2,但是未到top2的高度,就松手了,这时我们需要完成剩下的操作,这就用到了

AnimationController

Animation

animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 300));

具体应该滑回top1,还是滑向top2呢?这里我们需要定两个阈值:

  ///层级之间的阈值

  double threshold1To2;

  double threshold2To3;

    ///构造函数

  DrawerDemoState(this.size){

    drawerHeight = size.height-paddingTop;

    threshold1To2 = size.height/3;

    threshold2To3 = size.height - 250;

  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

升级 dragStart
现在我们开始对原有的方法升级

  void verticalDragStart(DragStartDetails details){

    ///确定drawer 初始状态

    markDrawerLvl();

    ///将原有的动画置空

    animation = null;

    ///将控制器停止和复位

    if(animationController.isAnimating){

      animationController.stop();

    }

    animationController.reset();

    lastPos = details.globalPosition;

    log('start', '$initPositionTop');

  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

当用户触摸时,我们先要确定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;

    }




  }

升级 dragUpdate
  void verticalDragUpdate(DragUpdateDetails details){

    

    double dis = details.globalPosition.dy - lastPos.dy;

    if(dis<0){

      direction = SlideDirection.Up;

    }else{

      direction = SlideDirection.Down;

    }

    

    ///cacheDy 避免滑动过快溢出范围导致的判断失效

    

    if(direction == SlideDirection.Up){

    ///避免drawer滑出屏幕

      if(initPositionTop <= top1+cacheDy) return;

    }else if(direction == SlideDirection.Down){

      if(initPositionTop >= top3-cacheDy) return;

    }




    initPositionTop += dis;

    lastPos = details.globalPosition;

    ///暂时不用管

    refreshExpandWidgetTop();

    setState(() {




    });

  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.

升级dragEnd
在用户手指离开屏幕时,我们就要进行处理了,即:drawer是继续滚动,还是复位。

  void verticalDragEnd(DragEndDetails details){

    adjustPositionTop(details);

  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

这个方法较长,我将说明写在注释里

  void adjustPositionTop(DragEndDetails details){

    

    switch(direction){

      case SlideDirection.Up:

        if(details.velocity.pixelsPerSecond.dy.abs() > thresholdV){

          ///用户fling速度超过阈值后,直接判定为滑向下一级别

          switch(drawerLvl){

            case DrawerLvl.LVL1:

            ///处于顶部上滑时,不需要做处理

              // TODO: Handle this case.

              break;

            case DrawerLvl.LVL2:

              slideTo(begin: initPositionTop,end: top1);

              break;

            case DrawerLvl.LVL3:

              slideTo(begin: initPositionTop,end: top2);

              break;

          }

        }else{

            ///未超过阈值的话,我们则进行复位或者继续滑动

          if(initPositionTop >= top1 && initPositionTop <= top2){

            ///在1、2级之间

            

            这里根据手指离屏位置,进行复位或者滑向下一等级高度的处理

            if(initPositionTop <= threshold1To2){

              ///小于二分之一屏幕高度 滚向top1




              slideTo(begin:initPositionTop, end:top1);

            }else{

              ///滑向top2




              slideTo(begin: initPositionTop,end: top2);

            }

          }else if(initPositionTop >= top2 && initPositionTop <= top3){

            ///2-3之间

            if(initPositionTop <= threshold2To3){

              ///滑向2

              slideTo(begin: initPositionTop,end: top2);

            }else{

              ///滑向3

              slideTo(begin: initPositionTop,end: top3);

            }




          }

        }

        break;

      case SlideDirection.Down:

        ///原理同上

        if(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){

            ///在1、2级之间




            if(initPositionTop <= threshold1To2){

              ///小于二分之一屏幕高度 滚向top1




              slideTo(begin: initPositionTop,end:top1);

            }else{

              ///滑向top2




              slideTo(begin: initPositionTop,end: top2);

            }

          }else if(initPositionTop >= top2 && initPositionTop <= top3){

            ///2-3之间

            if(initPositionTop <= threshold2To3){

              ///滑向2

              slideTo(begin: initPositionTop,end: top2);

            }else{

              ///滑向3

              slideTo(begin: initPositionTop,end: top3);

            }




          }

        }

        break;

    }

  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.

在补全滑动这里,我们交给animationController来处理:

    ///begin基本是手指离屏的位置,end则是目标等级的top值

  slideTo({double begin,double end})async{

    animation = Tween<double>(begin: begin,end:end ).animate(animationController);

    await animationController.forward();

  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

在动画的listener中,我们刷新initPositionTop的值:

    animationController.addListener(() {

      if(animation == null) return;

      ///暂时不用管

      refreshExpandWidgetTop();

      setState(() {

        initPositionTop = animation.value;

      });




    });
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

至此我们就相对完善的完成了drawer的滑动功能。

多功能widget 显隐效果
继续观察drawer内部的widget,我们可以看到在top1和top2之间滚动时,内部的多功能区域也会进行相应的缩进和伸出,接下来我们实现这个。

UI布局
因为我们只需要移动扩展区域,就可以实现多功能区的滑出/收起 效果,所以我们可以用stack来完成基本的布局:

Stack(

                    children: <Widget>[

                      ///搜索

                      Container(

                        alignment: Alignment.center,

                        color: Colors.pink,

                        width: size.width,height: searchHeight - minHeight,

                        child: Text('我是搜索'),

                      ),

                      ///多功能区

                      Positioned(

                        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('常去的地方',style: TextStyle(fontSize: 18,color: Colors.black),),

                              )

                            ],

                          ),

                        ),

                      ),

                      ///扩展区

                      Positioned(

                        top: expandPosTop + topArea,

                        child: Container(

                          color: Colors.lightGreen,

                          alignment: Alignment.topCenter,

                          width: size.width,height: drawerHeight - searchHeight -rowH,///这里需要在滚动时向下滑动

                          child: Text('我是扩展区域'),

                        ),

                      ),

                    ],

                  ),
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.

搜索区和多功能区,只需要调整top,使他们顺序排列即可。

而扩展区,我们需要在页面初始是遮住一部分多功能区(只漏出一行圆)。

方便起见,将多功能的高度定位 rowH * 3;

那么扩展区的top初始值就是多功能的top + rowH,这里我们给扩展区的top值定义一个变量:

expandPosTop = 多功能区的top + rowH

进而,我们可以确定,expandPosTop的变化范围是:

我们给这个变化值定义一个变量:topArea

 

topArea = [0 - rowH * 2];

最终扩展区的代码如下:

                      ///扩展区域

                      Positioned(

                        top: expandPosTop + topArea,

                        child: Container(

                          color: Colors.lightGreen,

                          alignment: Alignment.topCenter,

                          width: size.width,height: drawerHeight - searchHeight -rowH,///这里需要在滚动时向下滑动

                          child: Text('我是扩展区域'),

                        ),

                      ),
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

整体UI布局就完成了,我们接着实现滚动功能。

扩展区滑动
我们在dragUpdate和动画的listener中见到过这个方法:

refreshExpandWidgetTop();//这里就是实现对应功能的

这里我把说明写在注释里,方便阅读

  ///刷新 扩展区域的 position top值

  ///这里的差值是 rowH * 2

  refreshExpandWidgetTop(){

    ///首先,我们根据initPositionTop,和top2 - top1 之间的差值,来计算滑动进度

    double progress = (initPositionTop-top2).abs() /(top2 - top1).abs();

    ///判断是从top1滑向top2 还是反着

    if(drawerLvl == DrawerLvl.LVL2){

      ///lvl2 滑向 lvl3时 不做处理

      if(initPositionTop > top2) return;

      ///之后我们根据进度,来刷新topArea的值

      ///这个值总是会在 0 到 rowh*2 这个范围内变化,具体由滑动方向来定

      topArea =   (progress * (rowH*2).clamp(0, rowH*2));

    }else if(drawerLvl == DrawerLvl.LVL1){

      ///lvl2 滑向 lvl3时 不做处理

      if(initPositionTop > top2) return;

      topArea = (progress) * (rowH*2).clamp(0, rowH*2);

    }
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

  

当我们在调用上述方法外面刷新时,就会看到多功能区域的收起/伸出的效果了(给加点阴影会更好看),至此我们整个功能就实现了,如果对你有帮助点歌赞或和star吧。 :)


作者:吉哈达
来源:掘金

 

分类
标签
已于2020-9-4 18:00:09修改
收藏
回复
举报
回复
    相关推荐