前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【 -Flutter自定义组件- 】Wrapper组件,包裹装饰你的一切

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

作者头像
张风捷特烈
发布2020-10-16 11:08:14
1.5K0
发布2020-10-16 11:08:14
举报
文章被收录于专栏:Android知识点总结
零、前言

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


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

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

代码语言:javascript
复制
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

子组件

代码语言:javascript
复制
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

是否从尾部偏移

代码语言:javascript
复制
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

阴影颜色

代码语言:javascript
复制
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)

内边距

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

5. Wrapper.just

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

代码语言:javascript
复制
Wrapper.just(
  padding: EdgeInsets.all(2),
  color: Color(0xff5A9DFF),
  child: Text(
    "Lv3",
    style: TextStyle(color: Colors.white),
  ),
)

6. 尖端路径构造器

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

代码语言:javascript
复制
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.具体代码实现
代码语言:javascript
复制
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.定义属性

根据需求,进行属性定义

代码语言:javascript
复制
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,将绘制需要的所有属性全部传入。

代码语言:javascript
复制
  @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中的绘制

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

  • 核心逻辑
代码语言:javascript
复制
@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);
  }
}

  • 绘制盒子
代码语言:javascript
复制
  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));
  }

  • 绘制默认的线条
代码语言:javascript
复制
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 @张风捷特烈 未允禁转

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 零、前言
    • 1.看一下Wrapper组件整体的效果
      • 2. 应用于弹出的菜单框
        • 3.在聊天界面中的使用:
        • 一、基础使用
          • 1. 颜色和尖角方向
            • 2. 针尖属性控制
              • 3. 框阴影
                • 4. 边线边距
                  • 5. Wrapper.just
                    • 6. 尖端路径构造器
                      • 7.属性一览
                      • 二、Wrapper在聊天界面中的使用
                        • 1. 实现思路
                          • 2.具体代码实现
                          • 三、Wrapper源码核心实现
                            • 1.定义属性
                              • 2.build方法使用画板
                                • 3.WrapperPainter中的绘制
                                相关产品与服务
                                容器服务
                                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档