【 -Flutter自定义组件- 】Wrapper组件,包裹装饰你的一切

时间:2022-07-27
本文章向大家介绍【 -Flutter自定义组件- 】Wrapper组件,包裹装饰你的一切,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

零、前言

最近需要一个气泡框的需求,用图片,或现在三方组件一点都不灵活,倒不如自己写一个,分享来给大家一起用用。


1.看一下Wrapper组件整体的效果

主要就是一个包裹物,对于尖端的控制提供许多灵活的属性 包以及发布到pub,欢迎使用wrapper

dependencies:
  wrapper: ^$lastVersion

2. 应用于弹出的菜单框

通过Overlay可以显示弹框浮层,一般都会有个尖角指示,用Wrapper包裹就会非常方便。

3.在聊天界面中的使用:

效果还算不错,也顺便为我的《Flutter之旅》庆下生。


一、基础使用

1. 颜色和尖角方向

spineType是四种类型的枚举,上图依次是: SpineType.left、SpineType.right、SpineType.top、SpineType.bottom

属性名

类型

默认值

简介

color

Color

Colors.green

框框颜色

spineType

SpineType

SpineType.left

尖角边枚举

child

Widget

null

子组件

Wrapper(
    color: Color(0xff95EC69),
    spineType: SpineType.left,
    child: Text("张风捷特烈 " * 5),
),

2. 针尖属性控制

通过针尖的开角和高度能实现对尖角更细致的控制 通过offset进行位移,考虑到有可能从尾向前偏移,使用formEnd控制,如下[图四]

属性名

类型

默认值

简介

angle

double

75

针尖夹角

spineHeight

double

10

尖角高度

offset

double

15

偏移量

formEnd

bool

false

是否从尾部偏移

Wrapper(
  color: Color(0xff95EC69),
  spineType: SpineType.bottom,
  spineHeight: 20,
  angle: 45,
  offset: 15,
  fromEnd: false,
  child: Text("张风捷特烈 " * 5),
)

3. 框阴影

注意: 只有当elevation不为空的时候才能有阴影

属性名

类型

默认值

简介

elevation

double

null

影深

shadowColor

Color

Colors.grey

阴影颜色

Wrapper(
  color: Colors.white,
  spineType: SpineType.right,
  elevation: 1,
  shadowColor: Colors.grey.withAlpha(88),
  child: Text("张风捷特烈 " * 5),
)

4. 边线边距

注意: 当strokeWidth不为空时,会变为边线模式

属性名

类型

默认值

简介

strokeWidth

double

null

边线宽

padding

EdgeInsets

EdgeInsets.all(5)

内边距

Wrapper(
  formEnd: true,
  padding: EdgeInsets.all(10),
  color: Colors.yellow,
  offset: 60,
  strokeWidth: 2,
  spineType: SpineType.bottom,
  child: Text("张风捷特烈 " * 5),
)

5. Wrapper.just

提供无针尖的构造方法,实现类似包裹的效果,可以包裹任意组件。

Wrapper.just(
  padding: EdgeInsets.all(2),
  color: Color(0xff5A9DFF),
  child: Text(
    "Lv3",
    style: TextStyle(color: Colors.white),
  ),
)

6. 尖端路径构造器

为了让组件更灵活,我将尖端路径的构造提取出来,暴露接口,并提供默认路径 这样就可以自己定制尖端图形,提高拓展性。路径构造器,返回Path对象,回调尖端所在的矩形区域range,类型spineType,还回调了Canvas以供绘制。

Wrapper(
    spinePathBuilder: _spinePathBuilder,
    strokeWidth: 1.5,
    color: Color(0xff95EC69),
    spineType: SpineType.bottom,
    child: Text("张风捷特烈 " * 5)
),

Path _spinePathBuilder2(Canvas canvas, SpineType spineType, Rect range) {
  return Path()
    ..addOval(Rect.fromCenter(center: range.center, width: 10, height: 10));
}

7.属性一览

注意一点: Wrapper的区域是由父容器控制的,Wrapper本身并不承担定尺寸职责。

属性名

类型

默认值

简介

color

Color

Colors.green

框框颜色

spineType

SpineType

SpineType.left

尖角边枚举

child

Widget

null

子组件

angle

double

75

针尖夹角

spineHeight

double

10

尖角高度

offset

double

15

偏移量

formEnd

bool

false

是否从尾部偏移

elevation

double

null

影深

shadowColor

Color

Colors.grey

阴影颜色

strokeWidth

double

null

边线宽

padding

EdgeInsets

EdgeInsets.all(5)

内边距

radius

double

5

圆角半径

spinePathBuilder

SpinePathBuilder

null

尖端路径构造器


二、Wrapper在聊天界面中的使用

1. 实现思路

首先应该有一组数据,根据数据的类型觉得是左侧框,还是右侧框 这里简单演示一下,左侧是第偶数条数据,右侧是第奇数条数据 item的实现透过Row+Flexible进行布局控制,也正是因为Wrapper是填充父组件区域 这样就能实现一行短文字包裹住,当文字多行时,自动延伸。


2.具体代码实现
class ChatList extends StatelessWidget {
  //数据
  final data = [
    "经过十月怀胎,我的Flutter书总算出版了,是全彩色版的呢。",
    "编程书还搞彩色的,大佬就是有逼格,叫什么名字,我去捧捧场。",
    "书名是《Flutter之旅》,内容是偏向刚接触Flutter的小白,并没有讲的太深,像你这样的Lever,可能不是很需要。",
    "你想多了,我只是想买本书垫桌脚",
    "还有,书里的源码,你可以在FlutterUnit的GitHub主页看到下载链接。",
    "好的,话说FlutterUnit最近发展进度如何?",
    "FlutterUnit的绘制集录正在着手,不要心急。",
  ];

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListView.builder(
        itemCount: data.length,
        itemBuilder: (_, index) => index.isEven ? buildLeft(index) : buildRight(index),
      ),
    );
  }

  //左侧item组件
  Widget buildLeft(int index) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(right: 10),
            child: Image.asset( "assets/images/icon_head.png",  width: 50  ),
          ),
          Flexible(
              child: Padding(
                padding: const EdgeInsets.only(top:4.0),
                child: Wrapper(
                    elevation: 1,
                    shadowColor: Colors.grey.withAlpha(88),
                    offset: 8, color: Color(0xff95EC69), child: Text(data[index])),
              )),
          SizedBox(width: 50)
        ],
      ),
    );
  }
  
  //右测item组件
  Widget buildRight(int index) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        textDirection: TextDirection.rtl,
        children: [
          Padding(
            padding: const EdgeInsets.only(left: 10),
            child: Image.asset( "assets/images/icon_7.webp", width: 5 ),
          ),
          Flexible(
              child: Wrapper(
                spineType: SpineType.right,
                  elevation: 1,
                  shadowColor: Colors.grey.withAlpha(88),
                  offset: 8, color: Colors.white, child: Text(data[index]))),
          SizedBox(width: 50)
        ],
      ),
    );
  }
}
复制代码

三、Wrapper源码核心实现

1.定义属性

根据需求,进行属性定义

typedef SpinePathBuilder = Path Function(
    Canvas canvas, SpineType spineType, Rect range);

class Wrapper extends StatelessWidget {
  final double spineHeight;
  final double angle;

  final double radius;
  final double offset;
  final SpineType spineType;
  final Color color;
  final Widget child;
  final SpinePathBuilder spinePathBuilder;

  final double strokeWidth;

  final bool formEnd;
  final EdgeInsets padding;

  final double elevation;
  final Color shadowColor;

  Wrapper(
      {this.spineHeight = 8.0,
      this.angle = 75,
      this.radius = 5.0,
      this.offset = 15,
      this.strokeWidth,
      this.child,
      this.elevation,
      this.shadowColor = Colors.grey,
      this.formEnd = false,
      this.color = Colors.green,
      this.spinePathBuilder,
      this.padding = const EdgeInsets.all(8),
      this.spineType = SpineType.left});
复制代码

2.build方法使用画板

不同类型的尖端,由于高度会让边距出现问题,可以在内部处理一下,以方便外界的使用,这里自定义WrapperPainter,将绘制需要的所有属性全部传入。

  @override
  Widget build(BuildContext context) {
    var _padding = padding;
    switch (spineType) {
      case SpineType.top:
        _padding = padding + EdgeInsets.only(top: spineHeight);
        break;
      case SpineType.left:
        _padding = padding + EdgeInsets.only(left: spineHeight);
        break;
      case SpineType.right:
        _padding = padding + EdgeInsets.only(right: spineHeight);
        break;
      case SpineType.bottom:
        _padding = padding + EdgeInsets.only(bottom: spineHeight);
        break;
    }

    return CustomPaint(
      child: Padding(
        padding: _padding,
        child: child,
      ),
      painter: WrapperPainter(
          spineHeight: spineHeight,
          angle: angle,
          radius: radius,
          offset: offset,
          strokeWidth: strokeWidth,
          color: color,
          shadowColor: shadowColor,
          elevation: elevation,
          spineType: spineType,
          formBottom: formEnd,
          spinePathBuilder: spinePathBuilder),
    );
  }

3.WrapperPainter中的绘制

绘制主要分为两大块,一是外框盒子,二是尖端。由于尖端的存在,盒子需要根据类型进行处理。

  • 核心逻辑
@override
void paint(Canvas canvas, Size size) {
  // 绘制盒子
  path = buildBoxBySpineType(
    canvas,
    spineType,
    size.width,
    size.height,
  );
  
  // spinePathBuilder为null,使用buildDefaultSpinePath
  // 否则通过spinePathBuilder进行构造spinePath,比较复杂一丢丢的是区域的回调
  Path spinePath;
  if (spinePathBuilder == null) {
    spinePath = buildDefaultSpinePath(canvas, spineHeight, spineType, size);
  } else {
    Rect range ;
    switch(spineType){
      case SpineType.top:
        range = Rect.fromLTRB(0, -spineHeight, size.width, 0);
        break;
      case SpineType.left:
        range = Rect.fromLTRB(-spineHeight, 0, 0, size.height);
        break;
      case SpineType.right:
        range = Rect.fromLTRB(-spineHeight, 0, 0, size.height).translate(size.width, 0);
        break;
      case SpineType.bottom:
        range = Rect.fromLTRB(0, 0, size.width, spineHeight).translate(0, size.height-spineHeight);
        break;
    }
    spinePath = spinePathBuilder(canvas, spineType, range);
  }
  // 如果spinePath不为null,将两个路径结合,
  // 如果elevation存在,则绘制阴影
  if (spinePath != null) {
    path = Path.combine(PathOperation.union, spinePath, path);
    if (elevation != null) {
      canvas.drawShadow(path, shadowColor, elevation, true);
    }
    canvas.drawPath(path, mPaint);
  }
}

  • 绘制盒子
  Path buildBoxBySpineType(
   Canvas canvas,
    SpineType spineType,
    double width,
    double height,
  ) {
    double lineHeight, lineWidth;

    switch (spineType) {
      case SpineType.top:
        lineHeight = height - spineHeight;
        canvas.translate(0, spineHeight);
        lineWidth = width;
        break;
      case SpineType.left:
        lineWidth = width - spineHeight;
        lineHeight = height;
        canvas.translate(spineHeight, 0);
        break;
      case SpineType.right:
        lineWidth = width - spineHeight;
        lineHeight = height;
        break;
      case SpineType.bottom:
        lineHeight = height - spineHeight;
        lineWidth = width;
        break;
    }

    Rect box = Rect.fromCenter(
        center: Offset(lineWidth / 2, lineHeight / 2),
        width: lineWidth,
        height: lineHeight);

    return Path()..addRRect(RRect.fromRectXY(box, radius, radius));
  }

  • 绘制默认的线条
buildDefaultSpinePath(
    Canvas canvas, double spineHeight, SpineType spineType, Size size) {
  switch (spineType) {
    case SpineType.top: return _drawTop(size.width, size.height, canvas);
    case SpineType.left:
      return  _drawLeft(size.width, size.height, canvas);
    case SpineType.right:
      return  _drawRight(size.width, size.height, canvas);
    case SpineType.bottom:
      return _drawBottom(size.width, size.height, canvas);
  }
}

  Path _drawTop(double width, double height, Canvas canvas) {
    var angleRad = pi / 180 * angle;
    var spineMoveX = spineHeight * tan(angleRad / 2);
    var spineMoveY = spineHeight;
    if (spineHeight != 0) {
      return Path()
        ..moveTo(!formBottom ? offset : width - offset - spineHeight, 0)
        ..relativeLineTo(spineMoveX, -spineMoveY)
        ..relativeLineTo(spineMoveX, spineMoveY);
    }
    return Path();
  }

  Path _drawBottom(double width, double height, Canvas canvas) {
    var lineHeight = height - spineHeight;
    var angleRad = pi / 180 * angle;
    var spineMoveX = spineHeight * tan(angleRad / 2);
    var spineMoveY = spineHeight;
    if (spineHeight != 0) {
      return Path()
        ..moveTo(
            !formBottom ? offset : width - offset - spineHeight, lineHeight)
        ..relativeLineTo(spineMoveX, spineMoveY)
        ..relativeLineTo(spineMoveX, -spineMoveY);
    }
    return Path();
  }

  Path _drawLeft(double width, double height, Canvas canvas) {
    var angleRad = pi / 180 * angle;
    var spineMoveX = spineHeight;
    var spineMoveY = spineHeight * tan(angleRad / 2);
    if (spineHeight != 0) {
      return Path()
        ..moveTo(0, !formBottom ? offset : height - offset - spineHeight)
        ..relativeLineTo(-spineMoveX, spineMoveY)
        ..relativeLineTo(spineMoveX, spineMoveY);
    }
    return Path();
  }

  Path _drawRight(double width, double height, Canvas canvas) {
    var lineWidth = width - spineHeight;
    var angleRad = pi / 180 * angle;
    var spineMoveX = spineHeight;
    var spineMoveY = spineHeight * tan(angleRad / 2);
    if (spineHeight != 0) {
      return Path()
        ..moveTo(lineWidth, !formBottom ? offset : height - offset - spineHeight)
        ..relativeLineTo(spineMoveX, spineMoveY)
        ..relativeLineTo(-spineMoveX, spineMoveY);
    }
    return Path();
  }

本篇就到这里, 感谢大家关注FlutterUnit的发展~ , github地址: Star一下

End 2020-09-20 @张风捷特烈 未允禁转