[Flutter]聊天氣泡組件

import 'dart:math';
import 'package:flutter/material.dart';

// 尖角方向枚舉
enum BubbleAngleDirection { left, right }

class BubbleWidget extends StatelessWidget {
 
  BubbleWidget(
    this.data, {
    Key key,
    this.textStyle = const TextStyle(color: Colors.black, fontSize: 13),
    this.maxWidth,
    this.color = Colors.lightGreen,
    this.radius = 10,
    this.padding = 10,
    //
    this.angle = 60,
    this.angleHeight = 8,
    this.anglePos = BubbleAngleDirection.left,
  }) : super(key: key);

  final String data; //文本內(nèi)容
  final TextStyle textStyle;//文本樣式

  final double maxWidth; //最大寬帶
  final Color color; //背景顏色
  final double radius; 
  final double padding; 

  // 尖角
  final double angle; //尖角角度
  final double angleHeight; //尖角高度
  final BubbleAngleDirection anglePos;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
        painter: _BubbleCanvas(
            context: context,
            data: data,
            textStyle: textStyle,
            // 默認(rèn)最大寬帶為屏幕寬度的3/4
            maxWidth: maxWidth == null ? MediaQuery.of(context).size.width * 0.75 : maxWidth,
            color: color,
            padding: padding,
            radius: radius,
            //
            angle: angle,
            angleHeight: angleHeight,
            anglePos: anglePos));
  }
}

class _BubbleCanvas extends CustomPainter {
  _BubbleCanvas({
    this.context,
    this.data,
    this.textStyle,
    //
    this.maxWidth,
    this.color,
    this.padding,
    this.radius,
    //
    this.angle,
    this.angleHeight,
    this.anglePos,
  });

  final BuildContext context;
  final String data;
  final TextStyle textStyle;

  final double maxWidth;
  final Color color;
  final double padding;
  final double radius;

  final double angle;
  final double angleHeight;
  final BubbleAngleDirection anglePos;

  double _angle(angle) => angle * pi / 180;

  @override
  void paint(Canvas canvas, Size size) {
    // 氣泡組件的實(shí)際寬度
    double width = 0;
    double maxHeight = 0;
    int lines = 1;
    // 計(jì)算文字內(nèi)容所需的寬和高
    data.runes.forEach((element) {
      String str = String.fromCharCode(element);
      TextPainter tp = TextPainter(text: TextSpan(style: textStyle, text: str), textDirection: TextDirection.rtl);
      tp.layout();
      if (width + tp.width > (maxWidth - padding)) {
        lines++;
        width = 0;
      }
      if (maxHeight < tp.height) maxHeight = tp.height;

      width += tp.width;
    });

    width = lines > 1 ? maxWidth : (width + padding * 2 + angleHeight);
    //氣泡組件的實(shí)際高度
    double height = maxHeight * lines + padding * 2;
    double angleLength = angleHeight * tan(_angle(angle * 0.5));
    // 重新計(jì)算坐標(biāo)原點(diǎn), 注意Row的mainAxisAlignment屬性會影響組件的坐標(biāo)原點(diǎn),需要重新計(jì)算
    Offset origin = Offset(anglePos == BubbleAngleDirection.left ? 0 : -width, -height / 2);

    Path path = Path();

    //左上角圓角
    Offset leftTop = Offset(anglePos == BubbleAngleDirection.left ? radius + angleHeight : radius, radius);
    path.arcTo(Rect.fromCircle(center: Offset(origin.dx + leftTop.dx, origin.dy + leftTop.dy), radius: radius), pi, pi * 0.5, false);

    // 右上角圓角
    Offset rightTop = Offset(anglePos == BubbleAngleDirection.right ? width - angleHeight - radius : width - radius, radius);
    path.arcTo(Rect.fromCircle(center: Offset(origin.dx + rightTop.dx, origin.dy + rightTop.dy), radius: radius), -pi * 0.5, pi * 0.5, false);

    if (anglePos == BubbleAngleDirection.right) {
      path.lineTo(origin.dx + width - angleHeight, origin.dy + padding + maxHeight / 2 - angleLength);
      path.lineTo(origin.dx + width, origin.dy + padding + maxHeight / 2);
      path.lineTo(origin.dx + width - angleHeight, origin.dy + padding + maxHeight / 2 + angleLength);
    }

    // 右下角圓角
    Offset rightBottom = Offset(anglePos == BubbleAngleDirection.right ? width - angleHeight - radius : width - radius, height - radius);
    path.arcTo(Rect.fromCircle(center: Offset(origin.dx + rightBottom.dx, origin.dy + rightBottom.dy), radius: radius), 0, pi * 0.5, false);

    // 左下角圓角
    Offset leftBottom = Offset((anglePos == BubbleAngleDirection.left) ? angleHeight + radius : radius, height - radius);
    path.arcTo(Rect.fromCircle(center: Offset(origin.dx + leftBottom.dx, origin.dy + leftBottom.dy), radius: radius), pi * 0.5, pi * 0.5, false);

    if (anglePos == BubbleAngleDirection.left) {
      path.lineTo(origin.dx + angleHeight, origin.dy + padding + maxHeight / 2 - angleLength);
      path.lineTo(origin.dx, origin.dy + padding + maxHeight / 2);
      path.lineTo(origin.dx + angleHeight, origin.dy + padding + maxHeight / 2 + angleLength);
    }

    path.close();
    canvas.drawPath(
        path,
        Paint()
          ..color = color
          ..style = PaintingStyle.fill
          ..strokeCap = StrokeCap.round
          ..isAntiAlias = true);
    canvas.save();

    // 計(jì)算文本內(nèi)容繪制的起始坐標(biāo)
    final defautX = anglePos == BubbleAngleDirection.left ? origin.dx + (angleHeight + padding) : origin.dx + padding;
    double offsetX = defautX;
    double offsetY = origin.dy + padding;

    data.runes.forEach((element) {
      String str = String.fromCharCode(element);
      TextPainter tp = TextPainter(text: TextSpan(style: textStyle, text: str), textDirection: TextDirection.rtl);
      tp.layout();
      //橫向達(dá)到氣泡組件的最大寬度時,換行
      if (offsetX + tp.width > (maxWidth - padding)) {
        offsetY += tp.height;
        offsetX = defautX;
      }
      //繪制文本
      //tp.height < maxHeight ? (maxHeight - tp.height) : 0 是因?yàn)樽帜负椭形牡母叨炔灰粯?這樣可以讓字母和文字底部對齊
      tp.paint(canvas, Offset(offsetX, offsetY + (tp.height < maxHeight ? (maxHeight - tp.height) : 0)));
      offsetX += tp.width;
    });
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容