Flutter 104: 图解自定义 ACEDropdownButton 下拉框

简介: 0 基础学习 Flutter,第一百零四步:自定义一个简单的下拉框按钮!

      小菜之前尝试过 Flutter 自带的 DropdownButton 下拉框,简单方便;但仅单纯的原生效果不足以满足各类个性化设计;于是小菜以 DropdownButton 为基础,调整部分源码,扩展为 ACEDropdownButton 自定义下拉框组件;

  1. 添加 backgroundColor 设置下拉框背景色;
  2. 添加 menuRadius 设置下拉框边框效果;
  3. 添加 isChecked 设置下拉框中默认选中状态及 iconChecked 选中图标;
  4. 下拉框在展示时不会遮挡 DropdownButton 按钮,默认在按钮顶部或底部展示;
  5. 下拉框展示效果调整为默认由上而下;

      对于 DropdownButton 整体的功能是非常完整的,包括路由管理,已经动画效果等;小菜仅站在巨人的肩膀上进行一点小扩展,学习源码真的对我们自己的编码很有帮助;

DropdownButton 源码

      DropdownButton 源码整合在一个文件中,文件中有很多私有类,不会影响其它组件;

      以小菜的理解,整个下拉框包括三个核心组件,分别是 DropdownButton_DropdownMenu_DropdownRoute

      DropdownButton 是开发人员最直接面对的 StatefulWidget 有状态的组件,包含众多属性,基本框架是一个方便于视力障碍人员的 Semantics 组件,而其核心组件是一个层级遮罩 IndexedStack;其中在进行背景图标等各种样式绘制;

Widget innerItemsWidget;
if (items.isEmpty) {
  innerItemsWidget = Container();
} else {
  innerItemsWidget = IndexedStack(
      index: index, alignment: AlignmentDirectional.centerStart,
      children: widget.isDense ? items : items.map((Widget item) {
              return widget.itemHeight != null ? SizedBox(height: widget.itemHeight, child: item) : Column(mainAxisSize: MainAxisSize.min, children: <Widget>[item]);
            }).toList());
}

      在 DropdownButton 点击 _handleTap() 操作中,主要通过 _DropdownRoute 来完成的,_DropdownRoute 是一个 PopupRoute 路由;小菜认为最核心的是 getMenuLimits 对于下拉框的尺寸位置,各子 item 位置等一系列位置计算;在这里可以确定下拉框展示的起始位置以及与屏幕两端距离判断,指定具体的约束条件;DropdownButton 同时还起到了衔接 _DropdownMenu 展示作用;

      在 _DropdownMenuRouteLayout 中还有一点需要注意,通过计算 Menu 最大高度与屏幕差距,设置 Menu 最大高度比屏幕高度最少差一个 item 容器空间,用来用户点击时关闭下拉框;

_MenuLimits getMenuLimits(Rect buttonRect, double availableHeight, int index) {
  final double maxMenuHeight = availableHeight - 2.0 * _kMenuItemHeight;
  final double buttonTop = buttonRect.top;
  final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
  final double selectedItemOffset = getItemOffset(index);
  final double topLimit = math.min(_kMenuItemHeight, buttonTop);
  final double bottomLimit = math.max(availableHeight - _kMenuItemHeight, buttonBottom);
  double menuTop = (buttonTop - selectedItemOffset) - (itemHeights[selectedIndex] - buttonRect.height) / 2.0;
  double preferredMenuHeight = kMaterialListPadding.vertical;
  if (items.isNotEmpty)  preferredMenuHeight += itemHeights.reduce((double total, double height) => total + height);
  final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
  double menuBottom = menuTop + menuHeight;
  if (menuTop < topLimit) menuTop = math.min(buttonTop, topLimit);

  if (menuBottom > bottomLimit) {
    menuBottom = math.max(buttonBottom, bottomLimit);
    menuTop = menuBottom - menuHeight;
  }

  final double scrollOffset = preferredMenuHeight <= maxMenuHeight ? 0 : math.max(0.0, selectedItemOffset - (buttonTop - menuTop));
  return _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset);
}

      _DropdownMenu 也是一个 StatefulWidget 有状态组件,在下拉框展示的同时设置了一系列的动画,展示动画分为三个阶段,[0-0.25s] 先淡入选中 item 所在的矩形容器,[0.25-0.5s] 以选中 item 为中心向两端扩容直到容纳所有的 item[0.5-1.0s] 由上而下淡入展示 item 内容;

      _DropdownMenu 通过 _DropdownMenuPainter_DropdownMenuItemContainer 分别对下拉框以及子 item 的绘制,小菜主要是在此进行下拉框样式的扩展;

CustomPaint(
  painter: _DropdownMenuPainter(
      color: route.backgroundColor ?? Theme.of(context).canvasColor,
      menuRadius: route.menuRadius,
      elevation: route.elevation,
      selectedIndex: route.selectedIndex,
      resize: _resize,
      getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex))

      源码有太多需要学习的地方,小菜强烈建议多阅读源码;

ACEDropdownButton 扩展

1. backgroundColor 下拉框背景色

      根据 DropdownButton 源码可得,下拉框的背景色可以通过 _DropdownMenu 中绘制 _DropdownMenuPainter 时处理,默认的背景色为 Theme.of(context).canvasColor;当然我们也可以手动设置主题中的 canvasColor 来更新下拉框背景色;

      小菜添加 backgroundColor 属性,并通过 ACEDropdownButton -> _DropdownRoute -> _DropdownMenu 中转设置下拉框背景色;

class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
    ...
    @override
    Widget build(BuildContext context) {
    return FadeTransition(
        opacity: _fadeOpacity,
        child: CustomPaint(
            painter: _DropdownMenuPainter(
                color: route.backgroundColor ?? Theme.of(context).canvasColor,
                elevation: route.elevation,
                selectedIndex: route.selectedIndex,
                resize: _resize,
                getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex)),
        ...
    }
    ...
}

return ACEDropdownButton<String>(
    value: dropdownValue,
    backgroundColor: Colors.green.withOpacity(0.8),
    onChanged: (String newValue) => setState(() => dropdownValue = newValue),
    items: <String>['北京市', '天津市', '河北省', '其它'].map<ACEDropdownMenuItem<String>>((String value) {
      return ACEDropdownMenuItem<String>(value: value, child: Text(value));
    }).toList());

2. menuRadius 下拉框边框效果

      下拉框的边框需要在 _DropdownMenuPainter 中绘制,跟 backgroundColor 相同,设置 menuRadius 下拉框属性,并通过 _DropdownRoute 中转一下,其中需要在 _DropdownMenuPainter 中添加 menuRadius

class _DropdownMenuPainter extends CustomPainter {
  _DropdownMenuPainter(
      {this.color, this.elevation,
      this.selectedIndex, this.resize,
      this.getSelectedItemOffset,
      this.menuRadius})
      : _painter = BoxDecoration(
          color: color,
          borderRadius: menuRadius ?? BorderRadius.circular(2.0),
          boxShadow: kElevationToShadow[elevation],
        ).createBoxPainter(),
        super(repaint: resize);
}

return ACEDropdownButton<String>(
    value: dropdownValue,
    backgroundColor: Colors.green.withOpacity(0.8),
    menuRadius: const BorderRadius.all(Radius.circular(15.0)),
    onChanged: (String newValue) => setState(() => dropdownValue = newValue),
    items: <String>['北京市', '天津市', '河北省', '其它'].map<ACEDropdownMenuItem<String>>((String value) {
      return ACEDropdownMenuItem<String>(value: value, child: Text(value));
    }).toList());

3. isChecked & iconChecked 下拉框选中状态及图标

      小菜想实现在下拉框展示时,突显出选中状态 item,于是在对应 item 位置添加一个 iconChecked 图标,其中 isCheckedtrue 时,会展示选中图标,否则正常不展示;

      item 的绘制是在 _DropdownMenuItemButton 中加载的,可以通过 _DropdownMenuItemButton 添加属性设置,小菜为了统一管理,依旧通过 _DropdownRoute 进行中转;

class _DropdownMenuItemButtonState<T> extends State<_DropdownMenuItemButton<T>> {
    @override
    Widget build(BuildContext context) {
        ...
        Widget child = FadeTransition(
        opacity: opacity,
        child: InkWell(
            autofocus: widget.itemIndex == widget.route.selectedIndex,
            child: Container(
                padding: widget.padding,
                child: Row(children: <Widget>[
                  Expanded(child: widget.route.items[widget.itemIndex]),
                  widget.route.isChecked == true && widget.itemIndex == widget.route.selectedIndex
                      ? (widget.route.iconChecked ?? Icon(Icons.check, size: _kIconCheckedSize))
                      : Container()
                ])),
        ...
    }
}

return ACEDropdownButton<String>(
    value: dropdownValue,
    backgroundColor: Colors.green.withOpacity(0.8),
    menuRadius: const BorderRadius.all(Radius.circular(15.0)),
    isChecked: true,
    iconChecked: Icon(Icons.tag_faces),
    onChanged: (String newValue) => setState(() => dropdownValue = newValue),
    items: <String>['北京市', '天津市', '河北省', '其它'].map<ACEDropdownMenuItem<String>>((String value) {
      return ACEDropdownMenuItem<String>(value: value, child: Text(value));
    }).toList());

4. 避免遮挡

      小菜选择自定义 ACEDropdownButton 下拉框最重要的原因是,Flutter 自带的 DropdownButton 在下拉框展示时会默认遮挡按钮,小菜预期的效果是:

  1. 若按钮下部分屏幕空间足够展示所有下拉 items,则在按钮下部分展示,且不遮挡按钮;
  2. 若按钮下部分高度不足以展示下拉 items,查看按钮上半部分屏幕空间是否足以展示所有下拉 items,若足够则展示,且不遮挡按钮;
  3. 若按钮上半部分和下半部分屏幕空间均不足以展示所有下拉 items 时,此时以屏幕顶部或底部为边界,展示可滑动 items 下拉框;

      分析源码,下拉框展示位置是通过 _MenuLimits getMenuLimits 计算的,默认的 menuTop 是通过按钮顶部与选中 item 所在位置以及下拉框整体高度等综合计算获得的,因此展示的位置优先以选中 item 覆盖按钮位置,再向上向下延展;

      小菜简化计算方式,仅判断屏幕剩余空间与按钮高度差是否能容纳下拉框高度;从而确定 menuTop 起始位置,在按钮上半部分或按钮下半部分展示;

final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
if (bottomLimit - buttonRect.bottom < menuHeight) {
    menuTop = buttonRect.top - menuHeight;
} else {
    menuTop = buttonRect.bottom;
}
double menuBottom = menuTop + menuHeight;

5. Animate 下拉框展示动画

      DropdownButton 下拉框展示动画默认是以选中 item 为起点,分别向上下两端延展;

      小菜修改了下拉框展示位置,因为动画会显得很突兀,于是小菜调整动画起始位置,在 getSelectedItemOffset 设为 route.getItemOffset(0) 第一个 item 位即可;小菜同时也测试过若在按钮上半部分展示下拉框时,由末尾 item 向首位 item 动画,修改了很多方法,结果的效果却很奇怪,不符合日常动画展示效果,因此无论从何处展示下拉框,均是从第一个 item 位置开始展示动画;

getSelectedItemOffset: () => route.getItemOffset(0)),


      ACEDropdownButton 案例源码


      小菜对于源码的理解还不够深入,仅对需要的效果修改了部分源码,对于所有测试场景可能不够全面;如有错误,请多多指导!

来源: 阿策小和尚

目录
相关文章
|
13天前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
74 1
|
15天前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
111 1
|
15天前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
64 1
|
15天前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
64 1
|
15天前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
65 1
|
2月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
82 3
|
3月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
91 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
3月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
60 0
|
3月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
18 0
|
3月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
80 0