Flutter 自带的CheckBox组件不能自由指定大小,可以通过自定义通过RenderObject来实现的一个可以自由指定大小的CustomCheckBox组件。
功能:
1、有选中和未选中状态。
2、状态切换时需要执行动画
3、可以自定义外观
CustomCheckBox定义:
class SSLCustomCheckBox extends LeafRenderObjectWidget{
final double strokeWidth;//勾的线条宽度
final Color strokeColor;
final Color? fillColor;
final bool value;
final double radius;
final ValueChanged<bool>? onChanged;
const SSLCustomCheckBox({
Key? key,
this.strokeWidth = 2.0,
this.value = false,
this.strokeColor = Colors.white,
this.fillColor = Colors.blue,
this.onChanged,
this.radius = 2.0,
}):super(key: key);
@override
RenderObject createRenderObject(BuildContext context) {
// TODO: implement createRenderObject
return SSLRenderCustomCheckBox(
value: value,
strokeWidth: strokeWidth,
strokeColor: strokeColor,
fillColor: fillColor,
onChanged: onChanged,
radius: radius,
);
}
@override
void updateRenderObject(BuildContext context, covariant SSLRenderCustomCheckBox renderObject) {
// TODO: implement updateRenderObject
// super.updateRenderObject(context, renderObject);
debugPrint("ssl chick checkbox 3\n");
if (renderObject.value != value){
//执行动画
debugPrint("ssl chick checkbox 5\n");
renderObject.animationStatus = value ? AnimationStatus.forward : AnimationStatus.reverse;
}
renderObject
..value = value
..strokeWidth = strokeWidth
..strokeColor = strokeColor
..fillColor = fillColor
..radius = radius
..onChanged = onChanged;
}
}
注意:updateRenderObject方法中状态变化需要更新动画状态。
SSLRenderCustomCheckBox实现:
class SSLRenderCustomCheckBox extends RenderBox with SSLRenderObjectAnimationMixin{
bool value;
int? pointerId = -1;
double? strokeWidth;
Color? strokeColor;
Color? fillColor;
double radius;
ValueChanged<bool>? onChanged;
SSLRenderCustomCheckBox({
Key? key,
required this.value,
this.strokeWidth,
this.strokeColor,
this.fillColor,
required this.radius,
this.onChanged,
this.pointerId,
}){
progress = value ? 1 : 0;
}
@override
// TODO: implement isRepaintBoundary
bool get isRepaintBoundary => true;
// //动画相关
// double progress = 0;//动画当前进度
// int? lastTimeStamp;//上一次绘制时间
// //动画时长
// Duration get duration => const Duration(milliseconds: 150);
// //动画当前状态
// AnimationStatus animationStatus = AnimationStatus.completed;
//
// set animationStatusValue(AnimationStatus v){
// if (animationStatus != v){
// markNeedsPaint();
// }
// animationStatus = v;
// }
//背景动画时长占比,背景动画要在前40%时间执行完,之后执行打勾动画
final double bgAnimationInterval = .4;
//布局
@override
void performLayout() {
// TODO: implement performLayout
// super.performLayout();
//布局策略:如果父组件指定了宽高,则使用父组件宽高,否则宽高默认25。
size = constraints.constrain(
constraints.isTight ? Size.infinite : const Size(25, 25),
);
}
//绘制
@override
void doPaint(PaintingContext context, Offset offset) {
// TODO: implement paint
// super.paint(context, offset);
Rect rect = offset & size;
drawBackground(context, rect);
drawCheckMark(context, rect);
}
//绘制背景
void drawBackground(PaintingContext context, Rect rect){
Color color = value ? (fillColor??Colors.grey) : Colors.grey;
var paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = strokeWidth ?? 2.0
..color = color;
final outer = RRect.fromRectXY(rect, radius, radius);
var rects = [rect.inflate(-strokeWidth!), Rect.fromCenter(center: rect.center, width: 0, height: 0)];
//根据动画执行进度调整来确定里面矩形在每一帧的大小
var rectProgress = Rect.lerp(
rects[0],
rects[1],
min(progress, bgAnimationInterval)/bgAnimationInterval
);
final inner = RRect.fromRectXY(rectProgress!, 0, 0);
context.canvas.drawDRRect(outer, inner, paint);
}
//绘制打勾
void drawCheckMark(PaintingContext context, Rect rect){
debugPrint("ssl chick checkbox 6 progress $progress bgAnimationIntervae $bgAnimationInterval\n");
if (progress > bgAnimationInterval){
debugPrint("ssl chick checkbox 7\n");
//确定中间拐点位置
final secondOffset = Offset(
rect.left + rect.width / 2.5,
rect.bottom - rect.height / 4
);
final lastOffset = Offset(
rect.right - rect.width / 6,
rect.top + rect.height / 4,
);
// 我们只对第三个点的位置做插值
final lastOffset0 = Offset.lerp(
secondOffset,
lastOffset,
(progress - bgAnimationInterval) / (1 - bgAnimationInterval),
)!;
// 将三个点连起来
final path = Path()
..moveTo(rect.left + rect.width / 7, rect.top + rect.height / 2)
..lineTo(secondOffset.dx, secondOffset.dy)
..lineTo(lastOffset0.dx, lastOffset0.dy);
final paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.stroke
..color = strokeColor ?? Colors.red
..strokeWidth = strokeWidth ?? 2.0;
context.canvas.drawPath(path, paint);
}
}
// //调度动画
// void scheduleAnimation(){
// if (animationStatus != AnimationStatus.completed){
// SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
// if (lastTimeStamp != null){
// double delta = (timeStamp.inMicroseconds - lastTimeStamp!)/duration.inMicroseconds;
// if (animationStatus == AnimationStatus.reverse){
// delta = -delta;
// }
// progress = progress + delta;
//
// if (progress >= 1 || progress <= 0){
// animationStatus = AnimationStatus.completed;
// progress = progress.clamp(0, 1);
// }
// }
// //标记为需要绘制
// markNeedsPaint();
// lastTimeStamp = timeStamp.inMicroseconds;
// });
// }else{
// lastTimeStamp = null;
// }
// }
//命中事件保持为true,表示可以响应事件
@override
bool hitTestSelf(Offset position) {
// TODO: implement hitTestSelf
return true;
}
@override
void handleEvent(PointerEvent event, covariant BoxHitTestEntry entry) {
// TODO: implement handleEvent
// super.handleEvent(event, entry);
if (event.down){
debugPrint("ssl chick checkbox 1\n");
pointerId = event.pointer;
}else if (pointerId == event.pointer){
//手指抬起触发回调
onChanged?.call(!value);
debugPrint("ssl chick checkbox 2\n");
}
}
}
其中performLayout布局策略是:如果父组件指定了固定宽高,则使用父组件指定的,否则使用默认宽高.
绘制CustomBox主要:
- 先绘制背景drawBackground
- 再绘制打勾drawCheckMark
- 动画调度SSLRenderObjectAnimationMixin
- 事件回调hitTestSelf&handleEvent
动画复用抽离:
mixin SSLRenderObjectAnimationMixin on RenderObject{
double _progress = 0;
int? lastTimeStamp;
Duration get duration => const Duration(milliseconds: 200);
AnimationStatus _animationStatus = AnimationStatus.completed;
set animationStatus(AnimationStatus v){
if (_animationStatus != v){
markNeedsPaint();
}
_animationStatus = v;
}
double get progress => _progress;
set progress(double v) {
_progress = v.clamp(0, 1);
}
@override
void paint(PaintingContext context, Offset offset) {
// TODO: implement paint
// super.paint(context, offset);
doPaint(context, offset);//调用子类绘制逻辑
scheduleAnimation();
}
void scheduleAnimation(){
debugPrint("ssl current progress$progress, animationStatus$_animationStatus");
if (_animationStatus != AnimationStatus.completed){
debugPrint("ssl current 11 progress$progress,");
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
if (lastTimeStamp != null){
double delta = (timeStamp.inMicroseconds - lastTimeStamp!)/duration.inMicroseconds;
//在特定情况下,可能在一帧中连续的往frameCallback中添加了多次,导致两次回调时间间隔为0,
//这种情况下应该继续请求重绘
if (delta == 0){
markNeedsPaint();
return;
}
if (_animationStatus == AnimationStatus.reverse){
delta = -delta;
}
_progress = _progress + delta;
if (_progress >= 1 || _progress <= 0){
animationStatus = AnimationStatus.completed;
_progress = _progress.clamp(0, 1);
}
}
markNeedsPaint();
lastTimeStamp = timeStamp.inMicroseconds;
});
}else{
lastTimeStamp = null;
}
}
//抽象子类实现绘制逻辑函数
void doPaint(PaintingContext context, Offset offset);
}
测试示例:
class SSLCustomCheckBoxTest extends StatefulWidget{
const SSLCustomCheckBoxTest({Key? key}):super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return SSLCustomCheckBoxTestState();
}
}
class SSLCustomCheckBoxTestState extends State<SSLCustomCheckBoxTest>{
bool checked = false;
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("SSL Custom CheckBox"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SSLCustomCheckBox(
value: checked,
onChanged: onChangedAction,
),
Padding(
padding: const EdgeInsets.all(18.0),
child: SizedBox(
width: 16,
height: 16,
child: SSLCustomCheckBox(
strokeWidth: 1,
radius: 1,
value: checked,
onChanged: onChangedAction,
),
),
),
SizedBox(
width: 30,
height: 30,
child: SSLCustomCheckBox(
strokeWidth: 3,
radius: 3,
value: checked,
onChanged: onChangedAction,
),
),
],
),
),
);
}
void onChangedAction(value){
debugPrint("ssl chick checkbox 3\n");
setState(() {
checked = value;
});
}
}