赞
踩
最近需要一个气泡框的需求,用图片,或现在三方组件一点都不灵活,倒不如自己写一个,分享来给大家一起用用。
主要就是一个包裹物,对于尖端的控制提供许多灵活的属性
包以及发布到pub,欢迎使用wrapper
dependencies: wrapper: ^$lastVersion
通过Overlay可以显示弹框浮层,一般都会有个尖角指示,用Wrapper包裹就会非常方便。
效果还算不错,也顺便为我的《Flutter之旅》庆下生。
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), ),
通过针尖的开角和高度能实现对尖角更细致的控制
通过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), )
注意: 只有当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), )
注意: 当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), )
Wrapper.just
提供无针尖的构造方法,实现类似包裹的效果,可以包裹任意组件。
dart Wrapper.just( padding: EdgeInsets.all(2), color: Color(0xff5A9DFF), child: Text( "Lv3", style: TextStyle(color: Colors.white), ), )
为了让组件更灵活,我将尖端路径的构造提取出来,暴露接口,并提供默认路径
这样就可以自己定制尖端图形,提高拓展性。路径构造器,返回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)); } ```
注意一点: 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 | 圆角半径
首先应该有一组数据,根据数据的类型觉得是左侧框,还是右侧框
这里简单演示一下,左侧是第偶数条数据,右侧是第奇数条数据
item的实现透过Row+Flexible进行布局控制,也正是因为Wrapper是填充父组件区域
这样就能实现一行短文字包裹住,当文字多行时,自动延伸。
```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) ], ), ); } }
根据需求,进行属性定义
```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});
不同类型的尖端,由于高度会让边距出现问题,可以在内部处理一下,以方便外界的使用,这里自定义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),
- );
} ```
绘制主要分为两大块,一是外框盒子,二是尖端。由于尖端的存在,盒子需要根据类型进行处理。
核心逻辑
```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 @张风捷特烈 未允禁转
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。