【 -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 | 子组件

dart 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 | 是否从尾部偏移

dart 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 | 阴影颜色

dart 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) | 内边距

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

```

5. Wrapper.just

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

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


6. 尖端路径构造器

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

```dart 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.具体代码实现

```dart 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.定义属性

根据需求,进行属性定义

```dart 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,将绘制需要的所有属性全部传入。

```dart @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中的绘制

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

  • 核心逻辑

```dart @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); } } ```


  • 绘制盒子

```dart 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));

}

```

  • 绘制默认的线条

```dart 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 @张风捷特烈 未允禁转

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值