2019-04-04 用 flutter 教你自定义一个 checkbox

前言

最近在撸flutter代码的时候发现CheckBox不是那么好用,一直想是否可以跟Android的CheckBox一样可以设置文字在右边,看了网上的代码都是用组件嵌套在里面,所以决定写一个自定义个不用嵌套使用简单的CheckBox

思路

首先说一下实现一个CheckBox我们需要画一个空心圆、一个实心圆和一行文字,再增加一个点击事件即可实现简单的CheckeBox

编码

1、创建一个CheckRenderRenderBox

我这里创建RenderBox的时候就定义了一下组件的大小为14,先不去计算组件的大小。

import 'package:flutter/material.dart';
import 'dart:math' as Math;

class CheckRender extends LeafRenderObjectWidget {
  CheckRender({Key key}) : super(key: key);

  @override
  RenderObject createRenderObject(BuildContext context) => _CheckedRender();
}

class _CheckedRender extends RenderBox {
  final _paint = Paint();
  
  _CheckedRender() {
    _paint.color = Colors.white; //白色笔
    _paint.isAntiAlias = true; // 抗锯齿
  }

  @override
  void performLayout() {
    double width = Math.max(constraints.minWidth, 14.0);
    double height = Math.max(constraints.minHeight, 14.0);
   // 最终宽度
    size = Size(width, height);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    super.paint(context, offset);
    Canvas canvas = context.canvas;
  }
}
2、在paint函数中画一个空心圆和实心圆

(1)计算空心圆的中心点和半径出来,我们上面就给组价设置过高度和宽度

 double _center = size.height / 2;//中心点
 double _radius = _center - _center * 0.12 / 2;//半径

(2)设置paint的style为空心及线条宽度

_paint.style = PaintingStyle.stroke; // 设置空心笔
_paint.strokeWidth = height * 0.12;// 设置空心圆的宽度

(3)绘制空心圆(canvas在上面已经定义了)

Offset centerH = Offset(offset.dx + _center, offset.dy + _center); //中心位置
Rect arcRect = Rect.fromCircle(center: centerH, radius: _radius);//范围大小
canvas.drawArc(arcRect, 0.0, 360, false, _paint);//画出空心圆

(4)绘制实心圆

// 给paint的style设置为实心
_paint.style = PaintingStyle.fill;
// 画中间的圆圈
Rect arcRectCenter = Rect.fromCircle(center: centerH, radius: _radius / 1.8);
// 画实心圆
canvas.drawArc(arcRectCenter, 0.0, 360, false, _paint);
实现效果图
3、绘制文字

fluttercanvas中画文字的方法是drawParagraph,在ParagraphBuilder可以设置很多样式我们这次用不到,这次只是画个文字在上面显示就行。

1、创建一个Paragraph对象

var paragraph = ui.ParagraphBuilder(ui.ParagraphStyle(fontSize: 14))
    ..pushStyle(ui.TextStyle(color: Colors.white))
    ..addText('CheckBox');
    
Paragraph _paragraph = paragraph.build()
  ..layout(ui.ParagraphConstraints(width: double.infinity));

2、重新计算组件宽度performLayout

@override
void performLayout() {
    double width = Math.max(constraints.minWidth, 14.0);
    double height = Math.max(constraints.minHeight, 14.0);
    // 最终宽度
    size = Size(width + _paragraph.minIntrinsicWidth, height);
}

3、画出文字

canvas.drawParagraph(_paragraph, Offset(centerH.dx + 5.5 + _center, offset.dy + (size.height + _paragraph.height) / 2));
CheckBox图
4、增加状态切换

1、处理事件分发
_CheckedRender里面添加

@override
  bool hitTest(HitTestResult result, {ui.Offset position}) {
    if(size.contains(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    return false;
  }

2、创建一个CheckTextBox继承StatefulWidget
继承一个带状态的widget并且用GestureDetector包住CheckRender

class CheckTextBox extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _CheckTextBox();
}

class _CheckTextBox extends State<CheckTextBox> {
  // 是否是check状态
  bool _isChecked = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: CheckRender(),
      onTap: () => setState(() {_isChecked = !_isChecked;}),
    );
  }
}

不想写了............完整代码贴出来。

完整代码

import 'dart:math' as Math;
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

class CheckTextBox extends StatefulWidget {
  // 是否是check状态
  final bool isChecked;

  // check文本
  final String text;

  // check回传
  final ValueChanged<bool> onChecked;

  CheckTextBox(this.text, {Key key, this.isChecked: false, this.onChecked})
      : super(key: key);

  @override
  State<StatefulWidget> createState() => _CheckTextBox(isChecked: isChecked);
}

class _CheckTextBox extends State<CheckTextBox> {
  // 是否是check状态
  bool isChecked;

  _CheckTextBox({this.isChecked: false});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: CheckRender(isChecked: isChecked, text: widget.text),
      onTap: () => setState(() {
            isChecked = !isChecked;
            // 回传改变事件
            if (widget.onChecked != null) widget.onChecked(isChecked);
          }),
    );
  }
}

class CheckRender extends LeafRenderObjectWidget {
  final bool isChecked;
  final String text;

  CheckRender({Key key, this.text, this.isChecked}) : super(key: key);

  @override
  RenderObject createRenderObject(BuildContext context) =>
      _CheckedRender(checked: isChecked, text: text);

  @override
  void updateRenderObject(BuildContext context, _CheckedRender renderObject) {
    renderObject.isChecked = isChecked;
    renderObject.text = text;
  }
}

class _CheckedRender extends RenderBox {
  final _paint = Paint();
  ui.Paragraph _paragraph;
  bool _checked = false;
  String _text = '';

  _CheckedRender({checked: false, text}) {
    _checked = checked;
    _text = text;
    _paint.color = Color(0xffffffff); //白色笔
    _paint.isAntiAlias = true; // 抗锯齿
    resetText();
  }

  @override
  void performLayout() {
    double width = Math.max(constraints.minWidth, 14.0);
    double height = Math.max(constraints.minHeight, 14.0);
    if (_paragraph != null) {
      width += _paragraph.minIntrinsicWidth + 5.5;
    }
    // 最终宽度
    size = Size(width, height);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    super.paint(context, offset);
    Canvas canvas = context.canvas;

    double _center = size.height / 2; //中心点
    double _radius = _center - _center * 0.12 / 2; //半径
    _paint.style = PaintingStyle.stroke; // 设置空心笔
    _paint.strokeWidth = size.height * 0.12; // 设置空心圆的宽度

    Offset centerH = Offset(offset.dx + _center, offset.dy + _center); //中心位置
    Rect arcRect = Rect.fromCircle(center: centerH, radius: _radius); //范围大小
    canvas.drawArc(arcRect, 0.0, 360, false, _paint); //画出空心圆

    // 判断是否选中
    if (_checked) {
      // 设置实心圆
      _paint.style = PaintingStyle.fill;
      // 画中间的圆圈
      Rect arcRectCenter =
          Rect.fromCircle(center: centerH, radius: _radius / 1.8);
      canvas.drawArc(arcRectCenter, 0.0, 360, false, _paint);
    }

    if (_paragraph != null) {
      // 画出文字
      canvas.drawParagraph(
          _paragraph,
          Offset(centerH.dx + _center + 5.5,
              offset.dy + (size.height - _paragraph.height) / 2));
    }
  }

  @override
  bool hitTest(HitTestResult result, {ui.Offset position}) {
    if (size.contains(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    return false;
  }

  @override
  bool hitTestSelf(Offset position) => true;

  void resetText() {
    var paragraph = ui.ParagraphBuilder(ui.ParagraphStyle(fontSize: 14))
      ..pushStyle(ui.TextStyle(color: Color(0xffffffff)))
      ..addText(_text);

    _paragraph = paragraph.build()
      ..layout(ui.ParagraphConstraints(width: double.infinity));
  }

  set isChecked(bool newValue) {
    if (newValue != _checked) {
      _checked = newValue;
      markNeedsPaint();
    }
  }

  set text(String newValue) {
    if (newValue != _text) {
      _text = newValue;
      resetText();
      markNeedsPaint();
    }
  }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容