35_Flutter之自定义下拉列表组件

Flutter之自定义下拉列表组件

一.实现思路

  • 通过自定义路由继承自PopupRoute,并结合Navigator.push使弹出的下拉列表能够覆盖在当前页面显示
  • 使用CustomSingleChildLayout组件,自定义SingleChildLayoutDelegate并结合RelativeRect来控制下拉列表显示的位置和高度
  • 使用SizeTransition实现下拉列表的下拉动画效果

二.具体实现

1.自定义SingleChildLayoutDelegate,并重写getPositionForChild、getConstraintsForChild和shouldRelayout方法,

  • getPositionForChild的返回值控制显示位置

  • getConstraintsForChild的返回值控制显示的宽高

  • 在shouldRelayout中根据需要重绘界面

    class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
    
      @override
      BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
        // TODO: implement getConstraintsForChild
        return BoxConstraints.loose(Size(constraints.biggest.width, 300));
      }
    
      @override
      Offset getPositionForChild(Size size, Size childSize) {
        // TODO: implement getPositionForChild
        return Offset(0, 0);
      }
    
      @override
      bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) {
        // TODO: implement shouldRelayout
        return true;
      }
    
    }
    ```
    
    

2.自定义路由继承自PopupRoute,并重写buildPage方法,在buildPage方法中返回一个CustomSingleChildLayout,结合上面自定义的SingleChildLayoutDelegate完成下拉列表组件的创建

class _DropDownMenuRoute extends PopupRoute {
  @override
  // TODO: implement barrierColor
  Color get barrierColor => null;

  @override
  // TODO: implement barrierDismissible
  bool get barrierDismissible => true;

  @override
  // TODO: implement barrierLabel
  String get barrierLabel => null;

  @override
  Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
    // TODO: implement buildPage
    return CustomSingleChildLayout(
      delegate: _DropDownMenuRouteLayout(),
      child: Container(
        color: Color(0xffff0000),
      ),
    );
  }

  @override
  // TODO: implement transitionDuration
  Duration get transitionDuration => Duration(milliseconds: 300);

}

3.当点击触发下拉的按钮时,通过Navigator.push(context, _DropDownMenuRoute())将下拉列表显示出来

35_Flutter之自定义下拉列表组件_第1张图片

可以看到,下拉列表的显示位置是从左上角位置开始显示的,高度被设置为了300,并把触发下拉列表的按钮覆盖了

4.让下拉列表显示在触发下拉的按钮的正下方,宽度和按钮宽度相同,高度通过可选参数设置

  • 在触发下拉的按钮的点击事件中,获取该按钮的显示区域,即左、上、右、下的值

    onTap: () {
      RenderBox renderBox = _globalKey.currentContext.findRenderObject();
      Rect box = renderBox.localToGlobal(Offset.zero) & renderBox.size;
      print(box);
    }
    

    结果为:

    Rect.fromLTRB(0.0, 80.0, 360.0, 124.0)
    

    要使下拉列表显示在触发下拉按钮的下方,且宽度相等,只需要将getPositionForChild的返回值修改为Offset(0, box.bottom),getConstraintsForChild的返回值修改为BoxConstraints.loose(Size(box.right - box.left, 传入的高度))即可

  • 在_DropDownMenuRoute和_DropDownMenuRouteLayout中新增如下两个可选参数,并修改getPositionForChild和getConstraintsForChild方法的返回值

    • Rect position 控制下拉列表的位置

    • double menuHeight 控制下拉列表的高度

      class _DropDownMenuRoute extends PopupRoute {
      
        final Rect position;
        final double menuHeight;
      
        _DropDownMenuRoute({this.position, this.menuHeight});
      
        ...
      
        @override
        Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
          // TODO: implement buildPage
          return CustomSingleChildLayout(
            delegate: _DropDownMenuRouteLayout(position: position, menuHeight: menuHeight),
            child: Container(
              color: Color(0xffff0000),
            ),
          );
        }
      
        ...
      
      }
      
      class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
      
        final Rect position;
        final double menuHeight;
      
        _DropDownMenuRouteLayout({this.position, this.menuHeight});
      
        @override
        BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
          // TODO: implement getConstraintsForChild
          return BoxConstraints.loose(Size(position.right - position.left, 300));
        }
      
        @override
        Offset getPositionForChild(Size size, Size childSize) {
          // TODO: implement getPositionForChild
          return Offset(0, position.bottom);
        }
      
        ...
      
      }
      
  • 在打开下拉菜单时,将按钮的显示区域和预设高度(例如300)传入即可

    onTap: () {
      RenderBox renderBox = _globalKey.currentContext.findRenderObject();
      Rect box = renderBox.localToGlobal(Offset.zero) & renderBox.size;
      print(box);
      Navigator.push(context, _DropDownMenuRoute(position: box, menuHeight: 300));
    }
    

    35_Flutter之自定义下拉列表组件_第2张图片

5.添加下拉动画

  • 使用SizeTransition控制高度缩放实现下拉动画
class _DropDownMenuRoute extends PopupRoute {

  ...
  
  @override
  Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
    // TODO: implement buildPage
    return CustomSingleChildLayout(
      delegate: _DropDownMenuRouteLayout(position: position, menuHeight: menuHeight),
      child: SizeTransition(
        sizeFactor: Tween(
          begin: 0.0,
          end: 1.0
        ).animate(animation),
        child: Container(
          color: Color(0xffff0000),
        ),
      ),
    );
  }

  ...

}

35_Flutter之自定义下拉列表组件_第3张图片

6.完整代码

import 'package:flutter/material.dart';

class DropDownMenu extends StatelessWidget {
  GlobalKey _globalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("DropDownMenu"),
      ),
      body: Center(
        heightFactor: 1.0,
        child: Container(
          key: _globalKey,
          height: 44,
          decoration: BoxDecoration(
            border: Border.all(
              color: Color(0xffe6e6e6),
              width: 1.0
            )
          ),
          child: Material(
            color: Colors.transparent,
            child: InkWell(
              onTap: () {
                RenderBox renderBox = _globalKey.currentContext.findRenderObject();
                Rect box = renderBox.localToGlobal(Offset.zero) & renderBox.size;
                print(box);
                Navigator.push(context, _DropDownMenuRoute(position: box, menuHeight: 300));
              },
              child: Row(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Text(
                    "空间类型",
                    style: TextStyle(
                      fontSize: 16,
                      color: Color(0xff333333)
                    ),
                  ),
                  Icon(Icons.arrow_drop_down, size: 24, color: Color(0xffe6e6e6))
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

}

class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {

  final Rect position;
  final double menuHeight;

  _DropDownMenuRouteLayout({this.position, this.menuHeight});

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    // TODO: implement getConstraintsForChild
    return BoxConstraints.loose(Size(position.right - position.left, 300));
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // TODO: implement getPositionForChild
    return Offset(0, position.bottom);
  }

  @override
  bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) {
    // TODO: implement shouldRelayout
    return true;
  }

}

class _DropDownMenuRoute extends PopupRoute {

  final Rect position;
  final double menuHeight;

  _DropDownMenuRoute({this.position, this.menuHeight});

  @override
  // TODO: implement barrierColor
  Color get barrierColor => null;

  @override
  // TODO: implement barrierDismissible
  bool get barrierDismissible => true;

  @override
  // TODO: implement barrierLabel
  String get barrierLabel => null;

  @override
  Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
    // TODO: implement buildPage
    return CustomSingleChildLayout(
      delegate: _DropDownMenuRouteLayout(position: position, menuHeight: menuHeight),
      child: SizeTransition(
        sizeFactor: Tween(
          begin: 0.0,
          end: 1.0
        ).animate(animation),
        child: Container(
          color: Color(0xffff0000),
        ),
      ),
    );
  }

  @override
  // TODO: implement transitionDuration
  Duration get transitionDuration => Duration(milliseconds: 300);

} 

你可能感兴趣的:(Flutter)