Flutter 109: 图解自定义 ACERadio 单选框

简介: 0 基础学习 Flutter,第一百零九步:扩展自定义 ACERadio 单选框!

      Radio 单选框在日常应用中很常见,Flutter 提供的单选框与 Android 提供的略有不同,小菜简单了解一下并对其进行部分扩展;

Radio

      Radio 单选框是在一组选项中,互斥的选择单个选项;

源码分析

class Radio<T> extends StatefulWidget {
  const Radio({
    Key key,
    @required this.value,       // 当前单选框设置的值
    @required this.groupValue,  // 当前单选框选定状态的值
    @required this.onChanged,   // 选中回调
    this.activeColor,           // 选中状态颜色
    this.focusColor,            // 获取焦点时颜色
    this.hoverColor,            // 高亮时颜色
    this.materialTapTargetSize, // 点击范围最小大小
    this.focusNode,
    this.autofocus = false,
  })
}

      简单分析源码可得,Radio 是一个有状态的 StatefulWidget 小组件;Radio 单选框本身不保持任何状态,通过 onChanged 回调,来判断当前 value 是否与 groupValue 选项组中对应的 item 是否一致,来判断选中状态;一般通过调用 State.setState() 更新单选按钮的 groupValue 从而响应 onChanged 回调;

案例尝试

onChanged

      Radio 单选框一般分为三个状态,分别为未选中状态、选中状态和不可选中状态;onChanged 为单选框选中的回调,根据 valuegroupValue 匹配是否为选中状态;当 onChangednull 时,单选框为不可选中状态;

return Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
  Row(children: <Widget>[
    Radio(value: GenderType.MALE, groupValue: _groupValue,
        onChanged: (val) { print('---onChanged---$val');
          setState(() => _groupValue = val);
        }),
    Text('男')
  ]),
  Row(children: [
    Radio(value: GenderType.FEMALE, groupValue: _groupValue,
        onChanged: (val) { print('---onChanged---$val');
          setState(() => _groupValue = val);
        }),
    Text('女')
  ]),
  Row(children: <Widget>[
    Radio(value: GenderType.FEMALE, groupValue: _groupValue, onChanged: null),
    Text('不可选中')
  ])
]);

materialTapTargetSize

      materialTapTargetSize 为默认 Radio 可选中点击的最小范围;主要分为 paddedshrinkWrap 两种状态,分析源码可以看到两者尺寸相差 8.0,因此 Radio 所在的范围是不可变更的,这也是小菜准备自定义 ACERadio 扩展方向之一;

switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) {
  case MaterialTapTargetSize.padded:
    size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0);
    break;
  case MaterialTapTargetSize.shrinkWrap:
    size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius);
    break;
}
return Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
  Row(children: <Widget>[
    Text('padded'),
    Container( color: Colors.grey.withOpacity(0.4),
        child: Radio( value: GenderType.MALE, groupValue: _groupValue,
            materialTapTargetSize: MaterialTapTargetSize.padded,
            onChanged: (val) { print('---onChanged---$val');
              setState(() => _groupValue = val);
            })),
  ]),
  SizedBox(width: 10),
  Row(children: [
    Container( color: Colors.grey.withOpacity(0.4),
        child: Radio( value: GenderType.FEMALE, groupValue: _groupValue,
            materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
            onChanged: (val) { print('---onChanged---$val');
              setState(() => _groupValue = val);
            })),
    Text('shrinkWrap')
  ])
]);

activeColor

      activeColor 为单选框选中状态时绘制的颜色;若未设置,默认为 ThemeData.toggleableActiveColor 对应颜色;

return Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
  Row(children: <Widget>[
    Radio( value: GenderType.MALE, groupValue: _groupValue,
        activeColor: Colors.green,
        onChanged: (val) { print('---onChanged---$val');
          setState(() => _groupValue = val);
        }),
    Text('男', style: TextStyle( color: _groupValue == GenderType.MALE ? Colors.green : Colors.black))
  ]),
  Row(children: [
    Radio( value: GenderType.FEMALE, groupValue: _groupValue,
        activeColor: Colors.red,
        onChanged: (val) { print('---onChanged---$val');
          setState(() => _groupValue = val);
        }),
    Text('女', style: TextStyle( color: _groupValue == GenderType.FEMALE ? Colors.red : Colors.black))
  ])
]);

focusColor & hoverColor

      focusColor / hoverColor 分别对应获取焦点时的颜色与点击高亮颜色;但小菜尝试了多次效果并不明显,因需求场景较少,暂不做处理;

未选中颜色 & 不可选颜色

      Radio 并未提供未选中状态和不可选中状态按钮颜色;小菜分析源码,发现 未选中状态ThemeData.unselectedWidgetColor 颜色对应,不可选中状态ThemeData.disabledColor 对应;若需要动态修改这两种颜色状态,可以在对应的 Radio 外设置 ThemeData 对应的颜色状态即可;

return Theme(
    data: ThemeData(unselectedWidgetColor: Colors.deepPurple, disabledColor: Colors.brown),
    child:
        Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
      Row(children: <Widget>[
        Radio( value: GenderType.MALE, groupValue: _groupValue,
            activeColor: Colors.green,
            onChanged: (val) { print('---onChanged---$val');
              setState(() => _groupValue = val);
            }),
        Text('男', style: TextStyle( color: _groupValue == GenderType.MALE ? Colors.green : Colors.black))
      ]),
      Row(children: [
        Radio( value: GenderType.FEMALE, groupValue: _groupValue,
            activeColor: Colors.red,
            onChanged: (val) { print('---onChanged---$val');
              setState(() => _groupValue = val);
            }),
        Text('女', style: TextStyle( color: _groupValue == GenderType.FEMALE ? Colors.red : Colors.black))
      ]),
      Row(children: <Widget>[
        Radio( value: GenderType.FEMALE, groupValue: _groupValue, onChanged: null),
        Text('不可选中')
      ])
    ]));

ACERadio

      为了更灵活的应用 Radio 单选框,小菜准备在此基础上扩展如下几个方面:

  • 动态设置 未选中状态颜色
  • 动态设置 不可选中状态颜色
  • 动态设置 选中框按钮尺寸
  • 添加状态 取消按钮外边距

源码扩展

      小菜自定义了三种 ACEMaterialTapTargetSize 尺寸,增加了 zero 类型取消按钮外边距;

enum ACEMaterialTapTargetSize { padded, shrinkWrap, zero }

double radius = widget.radioSize ?? kRadialReactionRadius;
switch (widget.materialTapTargetSize ?? ACEMaterialTapTargetSize.padded) {
  case ACEMaterialTapTargetSize.padded:
    size = Size(2 * radius + 8.0, 2 * radius + 8.0);
    break;
  case ACEMaterialTapTargetSize.shrinkWrap:
    size = Size(2 * radius, 2 * radius);
    break;
  case ACEMaterialTapTargetSize.zero:
    size = Size(radius, radius);
    break;
}

      小菜优先判断添加的未选中状态颜色和不可选中状态颜色;若未设置以 ThemeData 为准;

Color _getInactiveColor(ThemeData themeData) {
  return enabled ? widget.unCheckedColor ?? themeData.unselectedWidgetColor
      : widget.disabledColor ?? themeData.disabledColor;
}

      小菜添加一个 radioSize 属性,在绘制按钮时,按比例动态绘制按钮尺寸;

// Outer circle
final Paint paint = Paint()
  ..color = Color.lerp(inactiveColor, radioColor, position.value)..style = PaintingStyle.stroke
  ..strokeWidth = radioSize / 4 ?? 2.0;
canvas.drawCircle(center, radioSize ?? _kOuterRadius, paint);

// Inner circle
if (!position.isDismissed) {
  paint.style = PaintingStyle.fill;
  canvas.drawCircle(center,
      (radioSize != null ? radioSize * 4.5 / 8 : _kInnerRadius) * position.value, paint);
}

案例尝试

取消按钮外边距

      Radio 默认提供了两种最小可点击范围,但小菜想取消按钮整体外边距,于是添加一种 ACEMaterialTapTargetSize.zero 方式来仅设置按钮尺寸;

return Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
  Row(children: <Widget>[
    Text('padded'),
    Container(color: Colors.grey.withOpacity(0.4),
        child: ACERadio(value: GenderType.MALE, groupValue: _groupValue,
            materialTapTargetSize: ACEMaterialTapTargetSize.padded,
            onChanged: (val) => setState(() => _groupValue = val))),
  ]),
  Row(children: <Widget>[
    Container(color: Colors.grey.withOpacity(0.4),
        child: ACERadio(value: GenderType.FEMALE, groupValue: _groupValue,
            materialTapTargetSize: ACEMaterialTapTargetSize.shrinkWrap,
            onChanged: (val) => setState(() => _groupValue = val))),
    Text('shrinkWrap')
  ]),
  Row(children: <Widget>[
    Container(color: Colors.grey.withOpacity(0.4),
        child: ACERadio(value: GenderType.FEMALE, groupValue: _groupValue,
            materialTapTargetSize: ACEMaterialTapTargetSize.zero,
            onChanged: null)),
    Text('zero')
  ])
]);

未选中状态 & 不可选中状态

      未选中状态 & 不可选中状态 可以通过 ThemeData 来动态修改,小菜为了方便,添加了 unCheckedColor & disabledColor 可直接进行设置;

return Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
  Row(children: <Widget>[
    ACERadio(
        value: GenderType.MALE, groupValue: _groupValue,
        activeColor: Colors.green, unCheckedColor: Colors.deepPurple,
        onChanged: (val) { print('---onChanged---$val');
          setState(() => _groupValue = val);
        }),
    Text('男', style: TextStyle( color: _groupValue == GenderType.MALE ? Colors.green : Colors.black))
  ]),
  Row(children: [
    ACERadio(
        value: GenderType.FEMALE, groupValue: _groupValue,
        activeColor: Colors.red, unCheckedColor: Colors.deepPurple,
        onChanged: (val) { print('---onChanged---$val');
          setState(() => _groupValue = val);
        }),
    Text('女', style: TextStyle( color: _groupValue == GenderType.FEMALE ? Colors.red : Colors.black))
  ]),
  Row(children: <Widget>[
    ACERadio(
        value: GenderType.FEMALE, groupValue: _groupValue,
        disabledColor: Colors.brown, unCheckedColor: Colors.deepPurple,
        onChanged: null),
    Text('不可选中')
  ])
]);

选中框按钮尺寸

      Radio 单选框尺寸是固定的,小菜为了更方便的修改,添加了 radioSize 尺寸来动态修改按钮尺寸,且在动态设置按钮尺寸之后依旧支持最小点击范围的三种样式;

return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
  Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
    ACERadio(radioSize: 6.0, value: SizeType.SIZE_6, groupValue: _groupSizeValue, onChanged: (val) => setState(() => _groupSizeValue = val)),
    Text('Size:6.0*6.0')
  ]),
  Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
    ACERadio(radioSize: 8.0, value: SizeType.SIZE_8, groupValue: _groupSizeValue, onChanged: (val) => setState(() => _groupSizeValue = val)),
    Text('Size:8.0*8.0')
  ]),
  Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
    ACERadio(radioSize: 10.0, value: SizeType.SIZE_10, groupValue: _groupSizeValue, onChanged: (val) => setState(() => _groupSizeValue = val)),
    Text('Size:10.0*10.0')
  ]),
  Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
    ACERadio(radioSize: 14.0, value: SizeType.SIZE_14, groupValue: _groupSizeValue, onChanged: (val) => setState(() => _groupSizeValue = val)),
    Text('Size:14.0*14.0')
  ]),
  Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
    ACERadio(radioSize: 18.0, value: SizeType.SIZE_18, groupValue: _groupSizeValue, onChanged: (val) => setState(() => _groupSizeValue = val)),
    Text('Size:18.0*18.0')
  ]),
]);


      ACERadio 案例源码


      小菜对底层源码还不够深入,只是对 Radio 单选框的一点小扩展;如有错误,请多多指导!

来源: 阿策小和尚

目录
相关文章
|
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