Flutter bubble box implementation

foreword

Encountered a component that needs to implement a bubble box, it looks like this:
Balloon Sketch
the content inside can be a single line of text, a menu, or other components;

I thought of 3 ways:

  • Use an existing bubble frame as the background
  • Make dot nine picture as background
  • draw the background yourself

It is always difficult to control the margins with the existing graphs . There is content in the rectangle;
the same problem exists with the dot-nine graphs. Only the graphs drawn by oneself can calculate the accurate margins.

So start customizing View.

draw

Because the ultimate goal is to place other components in the drawn components, CustomClipper< Path> , which is equivalent to cutting out a space that can accommodate the existence of sub-components;
theoretically, you can also use CustomPainter to draw the background and then operate, but that is not much different from using an existing picture as the background.

main point

As shown in the initial sketch, the bubble box is actually a combination of a rectangle and a triangle , but there are also some operations that can be extended, such as:

  1. The corners of rectangles and triangles can add a certain amount of radians
  2. The top corner of the triangle can be offset to become a bevel , which is close to the chat bubble box
  3. The margins of the rectangle containing the subcomponents can be calculated in advance
  4. Different orientations of triangles

In addition, it is to perform some simple geometric calculations , such as the change of the corresponding coordinate parameters when the triangle is facing different directions from left to right, up and down .

The idea is: first draw the triangle , then draw the rectangle , and finally calculate the margins .
The triangle needs to calculate the starting point of the drawing according to the orientation , and the rectangle determines the coordinates of the four corners according to the height of the triangle , and the margin is also determined according to the height of the triangle.

API

Rectangles are easier to implement, and so are rounded corners :

RRect.fromRectAndRadius(
            Rect.fromLTWH(l, t, r ,b), radius);

The triangle is also simple, just use the connection line:

path.lineTo(x,y);

But lineTo can only draw straight lines. If you want to make the corners have radians , you can consider using conic curves:

path.conicTo(peakX, peakY, endX, endY, weight);

When weight>1, the curve will become sharper and closer to the corner of the triangle, which can be adjusted by itself;

accomplish

First define the orientation :

enum ArrowDirection {
    
     left, right, top, bottom, none }

If the orientation is different, the corresponding coordinates need to be calculated;

Then determine the height and width of the triangle :

  @override
  Path getClip(Size size) {
    
    
    print("w=${size.width},h=${size.height}");

    final pathTriangle = Path();
    final pathRect = Path();

    //若有指定值,则宽高为指定值,
    //若无指定值,宽高以各自平行的矩形边作为基准
    final arrowW = arrowWidth == 0
        ? (direction == ArrowDirection.left || direction == ArrowDirection.right
                ? size.height
                : size.width) *
            widthWeight
        : arrowWidth;
    final arrowH = arrowHeight == 0
        ? (direction == ArrowDirection.left || direction == ArrowDirection.right
                ? size.width
                : size.height) *
            heightWeight
        : arrowHeight;

    print("arrowW=$arrowW,arrowH=$arrowH");
}

In order to make the width and height of the triangle change with the width and height of the parent layout, two options are set here, one is to directly pass in the exact value of the width and height, and the other is to pass in a proportional value proportional to the entire layout ;
The triangle always takes the side adjacent to the rectangle as the width (base). When the orientation is left and right (horizontal), the triangle is actually " turned down ", so it is necessary to judge the value in the horizontal direction ;

Then determine the starting point :

    //箭头为水平方向(左右)时,三角形底边中心的纵坐标
    final basisPointY = arrowBasisOffset < -1 || arrowBasisOffset > 1
        ? size.height / 2 + arrowBasisOffset
        : size.height / 2 * (1 + arrowBasisOffset);
    //箭头为水平方向(左右)时,三角形顶角顶点的纵坐标
    final peakPointY = basisPointY + arrowW * arrowPeakOffset;

    print("b=$arrowBasisOffset,p=$arrowPeakOffset");
    //箭头为垂直方向(上下)时,三角形底边中心的横坐标
    final basisPointX = arrowBasisOffset < -1 || arrowBasisOffset > 1
        ? size.width / 2 + arrowBasisOffset
        : size.width / 2 * (1 + arrowBasisOffset);
    //箭头为垂直方向(上下)时,三角形顶角顶点的横坐标
    final peakPointX = basisPointX + arrowW * arrowPeakOffset;
    print("peakX=$peakPointX,basisX=$basisPointX");

Here, the center point of the triangle base is used as the base point (basisPoint), and the starting point is an end point on the left or upper side of the base
; the peakPoint is used for offset. When the peakPoint and basisPoint are equal, it means equal Waist triangle; also divided into horizontal and vertical directions, the reference point is only the Y axis when it is horizontal, and the reference point is only the X axis when it is vertical; then start to draw a triangle ; according to the above figure, AB is the width , and the vertical height of c-basisPoint is Height , point C is the control point of the conic curve, draw a curve from A to B, if the weight (weight) exceeds 10, it is very close to a triangle;

triangle demo

  drawArrow(Path pathTriangle, double startX, double startY, double peakX,
      double peakY, double endX, double endY, double weight) {
    
    
    pathTriangle.moveTo(startX, startY);
    pathTriangle.conicTo(peakX, peakY, endX, endY, weight);
    pathTriangle.close();
  }

Point A (startX, startY);
Point B (endX, endY);
Point C (peakX, peakY);

Then draw a rectangle according to the orientation :

    switch (direction) {
    
    
      case ArrowDirection.left:
        //绘制位于左边的三角形箭头,即画一个顶角朝左的三角形
        drawArrow(pathTriangle, arrowH, basisPointY - arrowW / 2, 0, peakPointY,
            arrowH, basisPointY + arrowW / 2, conicWeight);
        //绘制位于右方的矩形
        pathRect.addRRect(RRect.fromRectAndRadius(
            Rect.fromLTWH(arrowH, 0, (size.width - arrowH), size.height),
            radius));
        break;
      case ArrowDirection.right:
        //绘制位于右边的三角形箭头,画一个顶角朝右的三角形
        drawArrow(
            pathTriangle,
            size.width - arrowH,
            basisPointY - arrowW / 2,
            size.width,
            peakPointY,
            size.width - arrowH,
            basisPointY + arrowW / 2,
            conicWeight);
        //绘制位于左边的矩形
        pathRect.addRRect(RRect.fromRectAndRadius(
            Rect.fromLTWH(0, 0, (size.width - arrowH), size.height), radius));
        break;
      case ArrowDirection.top:
        //绘制位于顶部的三角形箭头,画一个顶角朝上的三角形
        drawArrow(pathTriangle, basisPointX - arrowW / 2, arrowH, peakPointX, 0,
            basisPointX + arrowW / 2, arrowH, conicWeight);
        //绘制位于下边的矩形
        pathRect.addRRect(RRect.fromRectAndRadius(
            Rect.fromLTWH(0, arrowH, size.width, size.height - arrowH),
            radius));
        break;
      case ArrowDirection.bottom:
        //绘制位于底部的三角形箭头,画一个顶角朝下的三角形
        drawArrow(
            pathTriangle,
            basisPointX - arrowW / 2,
            size.height - arrowH,
            peakPointX,
            size.height,
            basisPointX + arrowW / 2,
            size.height - arrowH,
            conicWeight);
        // 绘制位于下边的矩形
        pathRect.addRRect(RRect.fromRectAndRadius(
            Rect.fromLTWH(0, 0, size.width, size.height - arrowH), radius));
        break;
      default:
        pathRect.addRRect(RRect.fromRectAndRadius(
            Rect.fromLTWH(0, 0, size.width, size.height), radius));
        break;
    }

Merge returns:

    pathTriangle.addPath(pathRect, const Offset(0, 0));
    return pathTriangle;

At this point , the work of CustomClipper is completed , and the calculation of margins needs to be implemented in a custom container; if an accurate triangle height
is specified , then the height can be directly used as padding, and the orientation can be considered:

  _padding() {
    
    
    switch (direction) {
    
    
      case ArrowDirection.bottom:
        return EdgeInsets.only(bottom: arrowHeight).add(padding!);
      case ArrowDirection.left:
        return EdgeInsets.only(left: arrowHeight).add(padding!);
      case ArrowDirection.right:
        return EdgeInsets.only(right: arrowHeight).add(padding!);
      case ArrowDirection.top:
        return EdgeInsets.only(top: arrowHeight).add(padding!);
      case ArrowDirection.none:
        return padding;
    }
  }

If the height is a specified ratio , use FractionallySizedBox to achieve padding in disguise :

  _box() {
    
    
    switch (direction) {
    
    
      case ArrowDirection.left:
      case ArrowDirection.right:
        return FractionallySizedBox(
          widthFactor: 1 - arrowHeightWeight,
          child: child,
        );
      case ArrowDirection.top:
      case ArrowDirection.bottom:
        return FractionallySizedBox(
          heightFactor: 1 - arrowHeightWeight,
          child: child,
        );
      case ArrowDirection.none:
        return FractionallySizedBox(
          child: child,
        );
    }
  }

Finally, just integrate the components, see the end of the source code .

achievement

Example 1, pass in exact width and height:

ChatBubble(
        direction: ArrowDirection.bottom,
        arrowWidth: 30,
        arrowHeight: 30,
        child: Container(
          alignment: Alignment.centerLeft,
          child: Text(
            "图来",
            style: TextStyle(color: Colors.black, inherit: false, fontSize: 18),
          ),
        ),
      )

Example 1
Example 2, add offset to become oblique triangle:

ChatBubble(
        direction: ArrowDirection.left,
        arrowWidth: 20,
        arrowHeight: 20,
        arrowPeakOffset: -0.8,
        child: Container(
          alignment: Alignment.centerLeft,
          child: Text(
            "图来",
            style: TextStyle(color: Colors.black, inherit: false, fontSize: 18),
          ),
        ),
      )

Example 2
Example 3, changing weights to make corners smoother:

ChatBubble(
        direction: ArrowDirection.top,
        arrowWidthWeight: 0.1,
        arrowHeightWeight: 0.2,
        conicWeight: 1.5,
        child: Container(
          alignment: Alignment.centerLeft,
          child: Text(
            "图来",
            style: TextStyle(color: Colors.black, inherit: false, fontSize: 18),
          ),
        ),
      )

Example 3

The source code is here.
above.

Guess you like

Origin blog.csdn.net/ifmylove2011/article/details/126228526