Flutter 之 自定义控件

1,581 阅读4分钟

Flutter 之 自定义控件

通过前面的学习,了解了常用的布局方式和常用的子widget,可以完成大部分UI页面的编写,但有时候看到的UI控件不是这些基础控件就能实现的,这时候怎么办呢?

和Android 和iOS 原生开发一样,Flutter 也提供了两种方式来实现:组合和自绘。

组合控件

有时候,虽然基本控件不能完成UI需求,但是可以通过一些基础widget 组合成一个新的widget,来实现UI需求。

在开发中拿到一个UI页面,一般按照从上到下、从左到右对UI进行分析,然后使用合适的widget 去编写页面。下面就以一个华为应用市场,应用列表的item 为例进行简单的分析,通过组合的方式组合成一个新的widget。

image.png 以 今日头条 为例,进行分析: 首先确定item里面的数据有哪些,定义item 的数据结构

class UpdateItemModel {
  String appIcon; //App图标
  String appName; //App名称
  String appType; //App类别
  String appDecs; //App更新日期
  //构造函数语法糖,为属性赋值
  UpdateItemModel({this.appIcon, this.appName, this.appType, this.appDecs});
}

image.png

首先:分为左右两部分,可以使用Row,左边是一张图片使用Image,但图片是圆角的,但普通的 Image 并不支持圆角。这时,我们可以使用 ClipRRect 控件来解决这个问题;右边就比较复杂了,这里先定义为widget1,

widget1: 可以分上、下两个部分,可以使用Column,下面是一条分隔线,可以使用 Divider,但看到有边距,需要再包裹一层Padding;上面部分比较复杂,定义为widget2;

widget2: 又可以分为左右两部分,可以使用Row,右边是一个 FlatButton,左边部分比较复习,定义为widget3;

widget3: 竖直方向上放置的几个Text,可以使用Column;到这来就分析完成了。下面就看看代码和实际的运行效果吧。

image.png 分为 上下两个部分,下面部分是一个分隔线,上面又可以继续划分

image.png 水平排放,可以使用Row完成,左边是Column里面放置几个Text,右边是一个 FlatButton

下面看看具体代码:


class CustomDemo1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("自定义控件"),
      ),
      body: UpdateWidget(
        model: UpdateItemModel(
            appName: "今日头条",
            appIcon:
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Foss.huangye88.net%2Flive%2Fuser%2F0%2F1502268284008709600-0.png&refer=http%3A%2F%2Foss.huangye88.net&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1618804524&t=3b2eff4563a5421ed62be433277abe06",
            appType: "新闻",
            appDecs: "海量视频热点资讯高效搜索"),
        onPressed: () {
          print("安装今日头条");
        },
      ),
    );
  }
}

class UpdateWidget extends StatelessWidget {
  final UpdateItemModel model; //数据模型
  final VoidCallback onPressed;

  UpdateWidget({Key key, this.model, this.onPressed}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: EdgeInsets.all(10),
          child: ClipRect(
            child: Image.network(
              model.appIcon,
              width: 80,
              height: 80,
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.fromLTRB(0, 10, 10, 0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        model.appName,
                        style: TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 20,
                            color: Colors.black),
                      ),
                      Text(
                        model.appType,
                        style: TextStyle(fontSize: 14, color: Colors.black26),
                      ),
                      Text(
                        model.appDecs,
                        style: TextStyle(fontSize: 14, color: Colors.black26),
                      ),
                    ],
                  ),
                  Container(
                    padding: EdgeInsets.fromLTRB(30, 0, 0, 0),
                    alignment: Alignment.topRight,
                    child: MaterialButton(
                        onPressed: this.onPressed,
                        textColor: Colors.blue,
                        color: Colors.grey,
                        minWidth: 30,
                        height: 25,
                        child: Text(
                          "安装",
                        ),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8),
                        )),
                  )
                ],
              ),
            ],
          ),
        )
      ],
    );
  }
}

image.png

自绘控件

在原生 iOS 和 Android 开发中,我们可以继承 UIView/View,在 drawRect/onDraw 方法里进行绘制操作。其实,在 Flutter 中也有类似的方案,那就是 CustomPaint。

我们都知道在绘制的过程中有两个重要的东西--画布和画笔,画笔 Paint,我们可以配置它的各种属性,比如颜色、样式、粗细等;Canvas,则提供了各种常见的绘制方法,比如画线 drawLine、画矩形 drawRect、画点 DrawPoint、画路径 drawPath、画圆 drawCircle、画圆弧 drawArc 等。

一般绘制的流程分为三个部分:

  1. 自定义class 继承 CustomPainter
  2. 通过 CustomPaint,把自定义的控件放到widget中
  3. 像使用正常的widget 一样使用 新的widget 下面看看一个饼状图的例子:
// 1.继承 CustomPainter,在里面编写绘制逻辑
class CustomWidget extends CustomPainter {
  // 生成画笔
  Paint getPaintByColor(Color color) {
    Paint paint = Paint();
    paint.color = color;
    return paint;
  }

  @override
  void paint(Canvas canvas, Size size) {
    // 这里面是绘制的逻辑
    double wheelSize = min(size.width, size.height)/2;
    double nbElem = 6; // 分为6份
    // 绘制的圆弧
    double radius = (2 * pi) / nbElem;
    // 创建一个矩形
    Rect boundingRect = Rect.fromCircle(center: Offset(wheelSize, wheelSize),radius: wheelSize);
    // 绘制扇形
    canvas.drawArc(
        boundingRect, 0, radius, true, getPaintByColor(Colors.blueGrey));
    canvas.drawArc(
        boundingRect, radius * 1, radius, true, getPaintByColor(Colors.red));
    canvas.drawArc(
        boundingRect, radius * 2, radius, true, getPaintByColor(Colors.green));
    canvas.drawArc(
        boundingRect, radius * 3, radius, true, getPaintByColor(Colors.blue));
    canvas.drawArc(
        boundingRect, radius * 4, radius, true, getPaintByColor(Colors.brown));
    canvas.drawArc(
        boundingRect, radius * 5, radius, true, getPaintByColor(Colors.amber));
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate != this;
  }
}

//2. 将饼图包装成一个新的控件,通过 CustomPaint
class Cake extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(200, 200),
      painter: CustomWidget(),
    );
  }
}

// 就可以像使用普通控件一样使用新定义的控件
class CustomDemo2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("绘制控件"),
      ),
      body: Center(
        child: Cake(),
      ),
    );
  }
}

image.png

总结

在Flutter 中,自定义控件的方式有两种,组合和自绘。组合的方式是通过一些基本widget 元素的堆积,组合成一个新的控件;自绘则是会比较麻烦一点,通过 继承 CustomPainter 在 paint 方法中完成绘制逻辑;最后把 CustomPainter 放入到 CustomPaint成为一个新控件。

还记得刚开始学习 Android 的自定义控件,总是很排斥,但发现认真去学习之后,还是很简单的。Flutter 也是一样,通过学习是可以快速提高的。一起努力加油吧!