Flutter custom drop-down selection box implementation process analysis

Get into the habit of writing together! This is the 5th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

  • In some list pages, we often have the need to filter items above. Click to display a drop-down menu, multiple selection, single selection, list selection, etc., but in Flutter, there is no such component ready-made, find a third-party one Extensions are sometimes limited, so it's best that we can make one ourselves, so that even if we extend, we will be handy.

First look at the renderings:

4d675755-289e-4497-9351-d5c057784e74.gif

key point:弹出、收回动画、状态改变、选项联动

  • Idea: We can see that a complete drop-down box consists of a header and specific drop-down options. The header is linked with the drop-down group. The header is regarded as an array, the lower options are regarded as an array, and the two Forming a complete drop-down selection box with the same number of arrays can better control the linkage effect.

First of all, we look at the pop-up and retraction. We can regard it as a component whose height gradually expands from 0 and then gradually shrinks to 0. As long as we use animation to control the height of this component, it can be achieved. There is a black transparency gradient below, Here we can change the change of the black shadow below according to the animation of the upper pop-up window.
Key code:

/// 下拉组件
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      _MenuBuilder(
        animation: animation,
        // 这里显示我们需要的具体下拉框选项内容
        child: widget.children[widget.menuController.index],
      ),
      isShowShadow // 是否显示下方黑色阴影 只有下拉弹出才显示 这个地方我们就可以根据UI设计来进行高度自定义
          ? Expanded(
              child: InkWell(
              child: AnimatedBuilder(
                  animation: animation,
                  builder: (context, child) {
                  // 这里是下拉框下方阴影 点击阴影隐藏下拉框
                    return Container(
                      width: double.infinity,
                      height: MediaQuery.of(context).size.height,
                      color: Colors.black
                          .withOpacity(animation.value / (widget.height * 3)),
                    );
                  }),
              onTap: () {
                widget.menuController.hide();
              },
            ))
          : const SizedBox(),
    ],
  );
}

class _MenuBuilder extends StatelessWidget {
  final Animation<double> animation;
  final Widget child;

  const _MenuBuilder({required this.animation, required this.child});

// 这里我们主要用动画来控制下拉内容组件的高度
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        child: child,
        animation: animation,
        builder: (context, child) {
          return Container(
            color: Colors.white,
            height: animation.value,
            child: child,
          );
        });
  }
复制代码

Next, let's look at the head design. The head design is a little more complicated. It is mainly about the change of state and the linkage between options. Here we create a new state controller: mainly to control some states of the head, such as clicking on the head. The change of the text or color after the button, the saving of the selected color, the restoration of the reset color and other states, the lower controller is mainly to control some states of the head.

/// 菜单控制器
class MenuController extends ChangeNotifier {
  // 当前组件是否显示 默认不显示 针对整个菜单数组
  bool isShow = false;

  // 显示当前组件title的文本 共用
  String title = "";

  // 显示哪个下拉框
  int index = 0;

  // 选择下拉框的的title 这个字段只有在真正选择的时候才会改变
  int titleIndex = 0;

  /// 更改Title
  changeTitle(int titleIndex, String? title) {
    this.titleIndex = titleIndex;
    this.title = title ?? "";
    hide();
  }

  // 显示下拉 index 为下拉哪一个菜单的index
  show(int index) {
    this.index = index;
    if (!isShow) {
      isShow = true;
    }
    notifyListeners();
  }

  // 隐藏 取消
  hide() {
    isShow = false;
    notifyListeners();
  }
}
复制代码

With the controller, we also need to process the header data. First, our header will have a default array when no options are selected. This will never change, so once this array is set, it cannot be changed. , and then we create a new dynamic array, which is the currently displayed array. The default value of this array is the default value of the option we did not select. Here we need to monitor the change of the header state to process the display array.
Key code: The key point is mainly for the processing of the change of the head state. After this code is clarified, it is basically OK.

@override
void initState() {
  super.initState();
  // changeTitles就是我们的显示数组
  changeTitles.addAll(widget.titles);
  for (var i = 0; i < changeTitles.length; i++) {
  //_chindren 是我们的头部组件数组
    _children.add(searchFilter(changeTitles[i], i));
  }
  widget.menuController.addListener(() {
   
    // 下拉 true 隐藏 false
    var isShow = widget.menuController.isShow;
    
    // 改变头部状态
    setState(() {
      if (widget.menuController.title != "") {
      // 说明当前选择了选项 赋值我选择的选项
        changeTitles[widget.menuController.titleIndex] =
            widget.menuController.title;
      } else {
      // 为空 说明当前的选项我清空了 赋值初始头部数组的数据
        changeTitles[widget.menuController.titleIndex] =
            widget.titles[widget.menuController.titleIndex];
      }
      // currentIndex 当前选择的index 默认-1 用来对比更新头部文字和颜色 
      // 如果下拉 更新当前选项inedx 如果隐藏说明没有选择任何一个下拉框 置为-1
      if (isShow && currentIndex < widget.titles.length) {
        currentIndex = widget.menuController.index;
      } else {
        currentIndex = -1;
      }
      // 每次下拉收回我们只需改变头部数据即可 changeTitles 永远都是显示的数组 直接全部更新到组件即可
      _children.clear();
      for (var i = 0; i < changeTitles.length; i++) {
        _children.add(searchFilter(changeTitles[i], i));
      }
    });
  });
}

// 这里就是一个简单的Row数组 按照百分比排列 也可以自定义不同宽度
@override
Widget build(BuildContext context) {
  return SizedBox(
    height: widget.headHeight ?? 45,
    child: Row(children: _children),
  );
}
复制代码

Mainly update the text content and color of the header. If the current option = option in the header || or the name of the option assignment is not equal to the initial value, we consider this menu to be selected and change the color. The basic logic is sorted out here, and the drop-down box style can be customized according to your own business.

Widget searchFilter(String name, int index) {
TextStyle(color: currentIndex == index || widget.titles[index] != name
                      ? widget.clickColor
                      : widget.defaultColor),
}
复制代码

Attached source address: github.com/lixp185/flu…

Summarize

The idea is to treat the drop-down components of the entire page as one. The purpose of this is to omit the cumbersome animation of retracting and opening when switching between different options, so that you can have an overall experience. In this drop-down box, animation and Skills related to state management can be said to be the biggest difference between state-based programming and native application-based programming in Flutter programming thinking. Only by truly mastering these two skills can we better understand the process of implementing the drop-down box.

Guess you like

Origin juejin.im/post/7084173826761687070