写点什么

Flutter 自定义 ACERaido 单选框 & ACECheckBox 复选框

作者:阿策小和尚
  • 2021 年 12 月 06 日
  • 本文字数:9695 字

    阅读完需:约 32 分钟

Flutter 自定义 ACERaido 单选框 & ACECheckBox 复选框

    Flutter 单选框和复选框在日常应用中很常见,而有的时候因为业务需求的不同,需要我们对我们对单选框和复选框进行自定义,今天和尚尝试一下自定义 ACERadio 单选框和 ACECheckBox 复选框;在自定义之前,和尚先学习一下基础的 Radio 单选框和 CheckBox 复选框;

Radio

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

1. 源码分析

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 回调;

2. 案例尝试

2.1 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: <Widget>[    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('不可选中')  ])]);
复制代码



2.2 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: <Widget>[    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')  ])]);
复制代码



2.3 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: <Widget>[    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))  ])]);
复制代码



2.4 focusColor & hoverColor

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

2.5 未选中颜色 & 不可选颜色

      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: <Widget>[        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 单选框,和尚准备在此基础上扩展如下几个方面:


  • 动态设置 未选中状态颜色

  • 动态设置 不可选中状态颜色

  • 动态设置 选中框按钮尺寸

  • 添加状态 取消按钮外边距

1. 源码扩展

      和尚自定义了三种 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 circlefinal 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 circleif (!position.isDismissed) { paint.style = PaintingStyle.fill; canvas.drawCircle(center, (radioSize != null ? radioSize * 4.5 / 8 : _kInnerRadius) * position.value, paint);}
复制代码

2. 案例尝试

2.1 取消按钮外边距

      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')  ])]);
复制代码



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

      未选中状态 & 不可选中状态 可以通过 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: <Widget>[    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('不可选中')  ])]);
复制代码



2.3 选中框按钮尺寸

      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')  ]),]);
复制代码



CheckBox

1. 源码分析

const Checkbox({    Key key,    @required this.value,       // 复选框状态 true/false/null    this.tristate = false,      // 是否为三态    @required this.onChanged,   // 状态变更回调    this.activeColor,           // 选中状态填充颜色    this.checkColor,            // 选中状态对号颜色    this.materialTapTargetSize, // 点击范围})
复制代码


      分析源码可知,tristatetrue 时复选框有三种状态;为 falsevalue 不可为 null

2. 案例尝试

return Checkbox( value: state, onChanged: (value) => setState(() => state = value));
return Checkbox(value: state, checkColor: Colors.purpleAccent.withOpacity(0.7), onChanged: (value) => setState(() => state = value));
return Checkbox(value: state, activeColor: Colors.teal.withOpacity(0.3), checkColor: Colors.purpleAccent.withOpacity(0.7), onChanged: (value) => setState(() => state = value));
return Checkbox(tristate: true, value: _triState == null ? _triState : state, activeColor: Colors.teal.withOpacity(0.3), checkColor: Colors.purpleAccent.withOpacity(0.7), onChanged: (value) => setState(() { if (value == null) { _triState = value; } else { _triState = ''; state = value; } }));}
复制代码



ACECheckBox

扩展一:变更未选中颜色

源码分析

// CheckBoxinactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,// ACECheckBoxinactiveColor: widget.onChanged != null    ? widget.unCheckColor ?? themeData.unselectedWidgetColor    : themeData.disabledColor,
复制代码


      分析 CheckBox 源码,其中复选框未选中颜色通过 ThemeData.unselectedWidgetColor 设置,修改颜色成本较大,和尚添加了 unCheckColor 属性,可自由设置未选中状态颜色,未设置时默认为 ThemeData.unselectedWidgetColor

案例尝试

return ACECheckbox(value: aceState, unCheckColor: Colors.amberAccent, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(value: aceState, checkColor: Colors.red.withOpacity(0.7), unCheckColor: Colors.amberAccent, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(value: aceState, activeColor: Colors.indigoAccent.withOpacity(0.3), checkColor: Colors.red.withOpacity(0.7), unCheckColor: Colors.amberAccent, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(tristate: true, value: _triAceState == null ? _triAceState : aceState, activeColor: Colors.indigoAccent.withOpacity(0.7), checkColor: Colors.red.withOpacity(0.4), unCheckColor: Colors.amberAccent, onChanged: (value) { setState(() { if (value == null) { _triAceState = value; } else { _triAceState = ''; aceState = value; } }); });
复制代码



扩展二:添加圆形样式

源码分析

// 绘制边框_drawBorder(canvas, outer, t, offset, type, paint) {  assert(t >= 0.0 && t <= 0.5);  final double size = outer.width;  if ((type ?? ACECheckBoxType.normal) == ACECheckBoxType.normal) {    canvas.drawDRRect(        outer, outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t)),        paint..strokeWidth = _kStrokeWidth / 2.0..style = PaintingStyle.fill);  } else {    canvas.drawCircle(        Offset(offset.dx + size / 2.0, offset.dy + size / 2.0), size / 2.0,        paint..strokeWidth = _kStrokeWidth..style = PaintingStyle.stroke);  }}// 绘制填充_drawInner(canvas, outer, offset, type, paint) {  if ((type ?? ACECheckBoxType.normal) == ACECheckBoxType.normal) {    canvas.drawRRect(outer, paint);  } else {    canvas.drawCircle(        Offset(offset.dx + outer.width / 2.0, offset.dy + outer.width / 2.0),        outer.width / 2.0, paint);  }}
复制代码


      分析源码可知,CheckBox 边框和内部填充以及对号全是通过 Canvas 进行绘制,其中绘制边框时,采用双层圆角矩形方式 drawDRRect,默认两层圆角矩形之间是填充方式;和尚添加 ACECheckBoxType 属性,允许用户设置圆角样式;


      绘制边框时画笔属性要与 drawDRRect 进行区分;其中复选框边框和内部填充两部分需要进行样式判断;

案例尝试

return ACECheckbox(value: aceState, unCheckColor: Colors.amberAccent, type: ACECheckBoxType.circle, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(value: aceState, checkColor: Colors.red.withOpacity(0.7), unCheckColor: Colors.amberAccent, type: ACECheckBoxType.circle, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox( value: aceState, activeColor: Colors.indigoAccent.withOpacity(0.3), checkColor: Colors.red.withOpacity(0.7), unCheckColor: Colors.amberAccent, type: ACECheckBoxType.circle, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(tristate: true, value: _triAceState == null ? _triAceState : aceState, activeColor: Colors.indigoAccent.withOpacity(0.7), checkColor: Colors.red.withOpacity(0.4), unCheckColor: Colors.amberAccent, type: ACECheckBoxType.circle, onChanged: (value) { setState(() { if (value == null) { _triAceState = value; } else { _triAceState = ''; aceState = value; } }); });
复制代码



扩展三:自定义尺寸

源码分析

@overridevoid paint(PaintingContext context, Offset offset) {  final Canvas canvas = context.canvas;  paintRadialReaction(canvas, offset, size.center(Offset.zero));    final Paint strokePaint = _createStrokePaint(checkColor);  final Offset origin = offset + (size / 2.0 - Size.square(width) / 2.0);  final AnimationStatus status = position.status;  final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed ? position.value : 1.0 - position.value;  if (_oldValue == false || value == false) {    final double t = value == false ? 1.0 - tNormalized : tNormalized;    final RRect outer = _outerRectAt(origin, t);    final Paint paint = Paint()..color = _colorAt(t);    if (t <= 0.5) {      _drawBorder(canvas, outer, t, origin, type, paint);    } else {      _drawInner(canvas, outer, origin, type, paint);      final double tShrink = (t - 0.5) * 2.0;      if (_oldValue == null || value == null)        _drawDash(canvas, origin, tShrink, width, strokePaint);      else        _drawCheck(canvas, origin, tShrink, width, strokePaint);    }  } else {    final RRect outer = _outerRectAt(origin, 1.0);    final Paint paint = Paint()..color = _colorAt(1.0);    _drawInner(canvas, outer, origin, type, paint);    if (tNormalized <= 0.5) {      final double tShrink = 1.0 - tNormalized * 2.0;      if (_oldValue == true)        _drawCheck(canvas, origin, tShrink, width, strokePaint);      else        _drawDash(canvas, origin, tShrink, width, strokePaint);    } else {      final double tExpand = (tNormalized - 0.5) * 2.0;      if (value == true)        _drawCheck(canvas, origin, tExpand, width, strokePaint);      else        _drawDash(canvas, origin, tExpand, width, strokePaint);    }  }}
复制代码


      分析源码 CheckBox 尺寸是固定的 Checkbox.width = 18.0,无法调整尺寸,和尚添加一个 width 参数,默认为 18.0 允许用户按需调整尺寸;如上是绘制复选框的三态情况;

案例尝试

return ACECheckbox(value: aceState, width: 10.0, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(value: aceState, checkColor: Colors.red.withOpacity(0.7), width: 18.0, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(value: aceState, activeColor: Colors.indigoAccent.withOpacity(0.3), checkColor: Colors.red.withOpacity(0.7), width: 28.0, onChanged: (value) => setState(() => aceState = value));
return ACECheckbox(tristate: true, value: _triAceState == null ? _triAceState : aceState, activeColor: Colors.indigoAccent.withOpacity(0.7), checkColor: Colors.red.withOpacity(0.4), type: ACECheckBoxType.normal, width: 38.0, onChanged: (value) { setState(() { if (value == null) { _triAceState = value; } else { _triAceState = ''; aceState = value; } }); });
复制代码




      ACERadio 案例源码 && ACECheckBox 源码


    和尚对 RadioCheckBox 的研究还不够深入,如有错误,请多指导!

【签约作者第二季】


来源: 阿策小和尚

发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2021.05.13 加入

Android / Flutter 小菜鸟~

评论

发布
暂无评论
Flutter 自定义 ACERaido 单选框 & ACECheckBox 复选框