Zero, preface,

The need for a bubble box recently, with pictures, or now the three-way component is not flexible, rather write one, share to give everyone to use together.


1. Look at the Wrapper component as a whole

It is primarily a wrapper that provides many flexible attributes for the control of the tip

Package and publish to pub, welcome wrapper

dependencies:
  wrapper: ^$lastVersion
Copy the code


2. Apply to the pop-up menu box

Using Overlay, you can display the float layer of the frame, usually with a sharp corner. It is very convenient to wrap it with a Wrapper.

3. Use in chat interface:

The effect is not bad, and I also celebrate the birth of my Book The Journey of Flutter.


First, basic use

1. Color and angular direction

SpineType is an enumeration of four types: spinetype. left, spinetype. right, spinetype. top, spinetype. bottom

The property name type The default value Introduction to the
color Color Colors.green The box of color
spineType SpineType SpineType.left Enumeration of sharp edges
child Widget null Child components
Wrapper(
    color: Color(0xff95EC69),
    spineType: SpineType.left,
    child: Text("Zhang Fengjie Trier" * 5),),Copy the code

2. Control of tip properties

The Angle can be controlled more carefully by the Angle and height of the tip

Offset is used for displacement, allowing for the possibility of moving forward from the tail, using formEnd control, as shown below [Figure 4]

The property name type The default value Introduction to the
angle double 75 Tip Angle
spineHeight double 10 Sharp point height
offset double 15 The offset
formEnd bool false Whether it’s offset from the tail
Wrapper(
  color: Color(0xff95EC69),
  spineType: SpineType.bottom,
  spineHeight: 20,
  angle: 45,
  offset: 15,
  fromEnd: false,
  child: Text("Zhang Fengjie Trier" * 5),Copy the code

3. The box shadows

Note: Shadows can only be created if an elevation is not empty

The property name type The default value Introduction to the
elevation double null Deep shadow
shadowColor Color Colors.grey Shadow color
Wrapper(
  color: Colors.white,
  spineType: SpineType.right,
  elevation: 1,
  shadowColor: Colors.grey.withAlpha(88),
  child: Text("Zhang Fengjie Trier" * 5),Copy the code

4. Edge margins

Note: When strokeWidth is not empty, it changes to edge mode

The property name type The default value Introduction to the
strokeWidth double null Line width
padding EdgeInsets EdgeInsets.all(5) padding
Wrapper(
  formEnd: true,
  padding: EdgeInsets.all(10),
  color: Colors.yellow,
  offset: 60,
  strokeWidth: 2,
  spineType: SpineType.bottom,
  child: Text("Zhang Fengjie Trier" * 5),Copy the code

5. Wrapper.just

Provides a needle-free construction method to achieve the effect of wrapping, can wrap any component.

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

6. Tip path constructor

To make the component more flexible, I extracted the construction of the tip path, exposed the interface, and provided the default path

In this way, you can customize cutting-edge graphics to improve scalability. The Path constructor returns the Path object, calls back to the range of the rectangle where the tip is, of type spineType, and calls back to the Canvas for drawing.

Wrapper(
    spinePathBuilder: _spinePathBuilder,
    strokeWidth: 1.5,
    color: Color(0xff95EC69),
    spineType: SpineType.bottom,
    child: Text("Zhang Fengjie Trier" * 5)
),

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

7. Attribute overview

Note that the Wrapper area is controlled by the parent container and the Wrapper itself does not have sizing responsibilities.

The property name type The default value Introduction to the
color Color Colors.green The box of color
spineType SpineType SpineType.left Enumeration of sharp edges
child Widget null Child components
angle double 75 Tip Angle
spineHeight double 10 Sharp point height
offset double 15 The offset
formEnd bool false Whether it’s offset from the tail
elevation double null Deep shadow
shadowColor Color Colors.grey Shadow color
strokeWidth double null Line width
padding EdgeInsets EdgeInsets.all(5) padding
radius double 5 radius
spinePathBuilder SpinePathBuilder null Tip path constructor

Use of Wrapper in chat interface

1. Implementation idea

First, there should be a set of data, depending on the type of data, whether it is the left box or the right box

As a simple demonstration, the implementation of item is controlled by Row+Flexible layout. Because the Wrapper is the parent component area, it is possible to wrap a short line of text around it and extend it automatically when there are multiple lines of text.


2. Specific code implementation
class ChatList extends StatelessWidget {
  / / data
  final data = [
    "After 10 months of pregnancy, my Book Flutter was finally published, in full color."."Programming book also make color, big guy is have force case, call what name, I go to support."."The title of the book is The Journey of the Flutter, which is biased towards those who have just touched the Flutter. It doesn't go too far. A Lever like yours may not be needed."."You're overthinking this. I just want to buy a book for my table.".Also, the source code in the book can be downloaded from The GitHub page of FlutterUnit.."Okay, so how's FlutterUnit?"."We're working on the Drawing catalog for FlutterUnit, don't worry.",];@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),
      ),
    );
  }

  // Left item component
  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)],),); }// Test the item component right
  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)],),); }}Copy the code

Core implementation of Wrapper source code

1. Define attributes

Define attributes according to requirements

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});
Copy the code

2. The build method uses artboard

Different types of tips, because the height of the edge will cause problems, can be handled internally, to facilitate the use of the outside world, here custom WrapperPainter, will draw all the attributes needed to pass in.

  @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),
    );
  }
Copy the code

3. Drawing in WrapperPainter

Drawing is mainly divided into two blocks, one is the outer box, the other is the tip. Because of the tip, boxes need to be treated according to type.

  • The core logic
@override
void paint(Canvas canvas, Size size) {
  // Draw the box
  path = buildBoxBySpineType(
    canvas,
    spineType,
    size.width,
    size.height,
  );
  
  // spinePathBuilder is null, use buildDefaultSpinePath
  // Otherwise, construct the spinePath with spinePathBuilder. The more complicated part is the region callback
  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);
  }
  // If spinePath is not null, combine the two paths,
  // If elevation exists, the shadow is drawn
  if(spinePath ! =null) {
    path = Path.combine(PathOperation.union, spinePath, path);
    if(elevation ! =null) {
      canvas.drawShadow(path, shadowColor, elevation, true); } canvas.drawPath(path, mPaint); }}Copy the code

  • Draw the box
  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);

    returnPath().. addRRect(RRect.fromRectXY(box, radius, radius)); }Copy the code

  • Draw the default line
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) {
      returnPath() .. 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) {
      returnPath() .. 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) {
      returnPath() .. 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) {
      returnPath() .. moveTo(lineWidth, ! formBottom ? offset : height - offset - spineHeight) .. relativeLineTo(spineMoveX, spineMoveY) .. relativeLineTo(-spineMoveX, spineMoveY); }return Path();
  }
Copy the code

Thank you for following the development of FlutterUnit

End 2020-09-20 @ Zhang Fengjie not allowed to transfer