| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- import 'dart:math';
- import 'package:flutter/material.dart';
- class _BubbleBorderArrowProperties {
- /// 箭头宽度的一半
- final double halfWidth;
- /// 箭头斜边的长度
- final double hypotenuse;
- /// 该斜边在主轴上的投影(水平时为X轴)
- final double projectionOnMain;
- /// 该斜边在纵轴上的投影(水平时为Y轴)
- final double projectionOnCross;
- /// 计算箭头半径在主轴上的投影(水平时为X轴)
- final double arrowProjectionOnMain;
- /// 计算箭头半径尖尖的长度
- final double topLen;
- _BubbleBorderArrowProperties({
- required this.halfWidth,
- required this.hypotenuse,
- required this.projectionOnMain,
- required this.projectionOnCross,
- required this.arrowProjectionOnMain,
- required this.topLen,
- });
- }
- class BubbleShapeBorder extends OutlinedBorder {
- final BorderRadius borderRadius;
- final AxisDirection arrowDirection;
- final double arrowLength;
- final double arrowWidth;
- final double arrowRadius;
- final double? arrowOffset;
- final Color? fillColor;
- const BubbleShapeBorder({
- super.side,
- required this.arrowDirection,
- this.borderRadius = BorderRadius.zero,
- this.arrowLength = 12,
- this.arrowWidth = 18,
- this.arrowRadius = 3,
- this.arrowOffset,
- this.fillColor,
- });
- @override
- OutlinedBorder copyWith({
- AxisDirection? arrowDirection,
- BorderSide? side,
- BorderRadius? borderRadius,
- double? arrowLength,
- double? arrowWidth,
- double? arrowRadius,
- double? arrowOffset,
- Color? fillColor,
- }) {
- return BubbleShapeBorder(
- arrowDirection: arrowDirection ?? this.arrowDirection,
- side: side ?? this.side,
- borderRadius: borderRadius ?? this.borderRadius,
- arrowLength: arrowLength ?? this.arrowLength,
- arrowWidth: arrowWidth ?? this.arrowWidth,
- arrowRadius: arrowRadius ?? this.arrowRadius,
- arrowOffset: arrowOffset ?? this.arrowOffset,
- fillColor: fillColor ?? this.fillColor,
- );
- }
- @override
- EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
- @override
- Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
- return _buildPath(rect);
- }
- @override
- Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
- return _buildPath(rect);
- }
- _BubbleBorderArrowProperties _calculateArrowProperties() {
- final arrowHalfWidth = arrowWidth / 2;
- final double hypotenuse = sqrt(
- arrowLength * arrowLength + arrowHalfWidth * arrowHalfWidth,
- );
- final double projectionOnMain = arrowHalfWidth * arrowRadius / hypotenuse;
- final double projectionOnCross =
- projectionOnMain * arrowLength / arrowHalfWidth;
- final double arrowProjectionOnMain = arrowLength * arrowRadius / hypotenuse;
- final double pointArrowTopLen =
- arrowProjectionOnMain * arrowLength / arrowHalfWidth;
- return _BubbleBorderArrowProperties(
- halfWidth: arrowHalfWidth,
- hypotenuse: hypotenuse,
- projectionOnMain: projectionOnMain,
- projectionOnCross: projectionOnCross,
- arrowProjectionOnMain: arrowProjectionOnMain,
- topLen: pointArrowTopLen,
- );
- }
- /// 核心逻辑:构建路径
- /// 计算方向为:上、右、下、左
- ///
- /// 爱今天灵眸(ijtkj.cn),一款可根据天气改变系统显示模式的软件,期待各位有钱的码友支持
- Path _buildPath(Rect rect) {
- final path = Path();
- EdgeInsets padding = EdgeInsets.zero;
- if (arrowDirection == AxisDirection.up) {
- padding = EdgeInsets.only(top: arrowLength);
- } else if (arrowDirection == AxisDirection.right) {
- padding = EdgeInsets.only(right: arrowLength);
- } else if (arrowDirection == AxisDirection.down) {
- padding = EdgeInsets.only(bottom: arrowLength);
- } else if (arrowDirection == AxisDirection.left) {
- padding = EdgeInsets.only(left: arrowLength);
- }
- final nRect = Rect.fromLTRB(
- rect.left + padding.left,
- rect.top + padding.top,
- rect.right - padding.right,
- rect.bottom - padding.bottom,
- );
- final arrowProp = _calculateArrowProperties();
- final startPoint = Offset(nRect.left + borderRadius.topLeft.x, nRect.top);
- path.moveTo(startPoint.dx, startPoint.dy);
- // 箭头在上边
- if (arrowDirection == AxisDirection.up) {
- Offset pointCenter = Offset(
- nRect.left + (arrowOffset ?? nRect.width / 2),
- nRect.top,
- );
- Offset pointStart = Offset(
- pointCenter.dx - arrowProp.halfWidth,
- nRect.top,
- );
- Offset pointArrow = Offset(pointCenter.dx, rect.top);
- Offset pointEnd = Offset(pointCenter.dx + arrowProp.halfWidth, nRect.top);
- // 下面计算开始的圆弧
- {
- Offset pointStartArcBegin = Offset(
- pointStart.dx - arrowRadius,
- pointStart.dy,
- );
- Offset pointStartArcEnd = Offset(
- pointStart.dx + arrowProp.projectionOnMain,
- pointStart.dy - arrowProp.projectionOnCross,
- );
- path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
- path.quadraticBezierTo(
- pointStart.dx,
- pointStart.dy,
- pointStartArcEnd.dx,
- pointStartArcEnd.dy,
- );
- }
- // 计算中间箭头的圆弧
- {
- Offset pointArrowArcBegin = Offset(
- pointArrow.dx - arrowProp.arrowProjectionOnMain,
- pointArrow.dy + arrowProp.topLen,
- );
- Offset pointArrowArcEnd = Offset(
- pointArrow.dx + arrowProp.arrowProjectionOnMain,
- pointArrow.dy + arrowProp.topLen,
- );
- path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
- path.quadraticBezierTo(
- pointArrow.dx,
- pointArrow.dy,
- pointArrowArcEnd.dx,
- pointArrowArcEnd.dy,
- );
- }
- // 下面计算结束的圆弧
- {
- Offset pointEndArcBegin = Offset(
- pointEnd.dx - arrowProp.projectionOnMain,
- pointEnd.dy - arrowProp.projectionOnCross,
- );
- Offset pointEndArcEnd = Offset(pointEnd.dx + arrowRadius, pointEnd.dy);
- path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
- path.quadraticBezierTo(
- pointEnd.dx,
- pointEnd.dy,
- pointEndArcEnd.dx,
- pointEndArcEnd.dy,
- );
- }
- }
- path.lineTo(nRect.right - borderRadius.topRight.x, nRect.top);
- // topRight radius
- path.arcToPoint(
- Offset(nRect.right, nRect.top + borderRadius.topRight.y),
- radius: borderRadius.topRight,
- rotation: 90,
- );
- // 箭头在右边
- if (arrowDirection == AxisDirection.right) {
- Offset pointCenter = Offset(
- nRect.right,
- nRect.top + (arrowOffset ?? nRect.height / 2),
- );
- Offset pointStart = Offset(
- nRect.right,
- pointCenter.dy - arrowProp.halfWidth,
- );
- Offset pointArrow = Offset(rect.right, pointCenter.dy);
- Offset pointEnd = Offset(
- nRect.right,
- pointCenter.dy + arrowProp.halfWidth,
- );
- // 下面计算开始的圆弧
- {
- Offset pointStartArcBegin = Offset(
- pointStart.dx,
- pointStart.dy - arrowRadius,
- );
- Offset pointStartArcEnd = Offset(
- pointStart.dx + arrowProp.projectionOnCross,
- pointStart.dy + arrowProp.projectionOnMain,
- );
- path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
- path.quadraticBezierTo(
- pointStart.dx,
- pointStart.dy,
- pointStartArcEnd.dx,
- pointStartArcEnd.dy,
- );
- }
- // 计算中间箭头的圆弧
- {
- Offset pointArrowArcBegin = Offset(
- pointArrow.dx - arrowProp.topLen,
- pointArrow.dy - arrowProp.arrowProjectionOnMain,
- );
- Offset pointArrowArcEnd = Offset(
- pointArrow.dx - arrowProp.topLen,
- pointArrow.dy + arrowProp.arrowProjectionOnMain,
- );
- path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
- path.quadraticBezierTo(
- pointArrow.dx,
- pointArrow.dy,
- pointArrowArcEnd.dx,
- pointArrowArcEnd.dy,
- );
- }
- // 下面计算结束的圆弧
- {
- Offset pointEndArcBegin = Offset(
- pointEnd.dx + arrowProp.projectionOnCross,
- pointEnd.dy - arrowProp.projectionOnMain,
- );
- Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy + arrowRadius);
- path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
- path.quadraticBezierTo(
- pointEnd.dx,
- pointEnd.dy,
- pointEndArcEnd.dx,
- pointEndArcEnd.dy,
- );
- }
- }
- path.lineTo(nRect.right, nRect.bottom - borderRadius.bottomRight.y);
- // bottomRight radius
- path.arcToPoint(
- Offset(nRect.right - borderRadius.bottomRight.x, nRect.bottom),
- radius: borderRadius.bottomRight,
- rotation: 90,
- );
- // 箭头在下边
- if (arrowDirection == AxisDirection.down) {
- Offset pointCenter = Offset(
- nRect.left + (arrowOffset ?? nRect.width / 2),
- nRect.bottom,
- );
- Offset pointStart = Offset(
- pointCenter.dx + arrowProp.halfWidth,
- nRect.bottom,
- );
- Offset pointArrow = Offset(pointCenter.dx, rect.bottom);
- Offset pointEnd = Offset(
- pointCenter.dx - arrowProp.halfWidth,
- nRect.bottom,
- );
- // 下面计算开始的圆弧
- {
- Offset pointStartArcBegin = Offset(
- pointStart.dx + arrowRadius,
- pointStart.dy,
- );
- Offset pointStartArcEnd = Offset(
- pointStart.dx - arrowProp.projectionOnMain,
- pointStart.dy + arrowProp.projectionOnCross,
- );
- path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
- path.quadraticBezierTo(
- pointStart.dx,
- pointStart.dy,
- pointStartArcEnd.dx,
- pointStartArcEnd.dy,
- );
- }
- // 计算中间箭头的圆弧
- {
- Offset pointArrowArcBegin = Offset(
- pointArrow.dx + arrowProp.arrowProjectionOnMain,
- pointArrow.dy - arrowProp.topLen,
- );
- Offset pointArrowArcEnd = Offset(
- pointArrow.dx - arrowProp.arrowProjectionOnMain,
- pointArrow.dy - arrowProp.topLen,
- );
- path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
- path.quadraticBezierTo(
- pointArrow.dx,
- pointArrow.dy,
- pointArrowArcEnd.dx,
- pointArrowArcEnd.dy,
- );
- }
- // 下面计算结束的圆弧
- {
- Offset pointEndArcBegin = Offset(
- pointEnd.dx + arrowProp.projectionOnMain,
- pointEnd.dy + arrowProp.projectionOnCross,
- );
- Offset pointEndArcEnd = Offset(pointEnd.dx - arrowRadius, pointEnd.dy);
- path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
- path.quadraticBezierTo(
- pointEnd.dx,
- pointEnd.dy,
- pointEndArcEnd.dx,
- pointEndArcEnd.dy,
- );
- }
- }
- path.lineTo(nRect.left + borderRadius.bottomLeft.x, nRect.bottom);
- // bottomLeft radius
- path.arcToPoint(
- Offset(nRect.left, nRect.bottom - borderRadius.bottomRight.y),
- radius: borderRadius.bottomLeft,
- rotation: 90,
- );
- // 箭头在左边
- if (arrowDirection == AxisDirection.left) {
- Offset pointCenter = Offset(
- nRect.left,
- nRect.top + (arrowOffset ?? nRect.height / 2),
- );
- Offset pointStart = Offset(
- nRect.left,
- pointCenter.dy + arrowProp.halfWidth,
- );
- Offset pointArrow = Offset(rect.left, pointCenter.dy);
- Offset pointEnd = Offset(
- nRect.left,
- pointCenter.dy - arrowProp.halfWidth,
- );
- // 下面计算开始的圆弧
- {
- Offset pointStartArcBegin = Offset(
- pointStart.dx,
- pointStart.dy + arrowRadius,
- );
- Offset pointStartArcEnd = Offset(
- pointStart.dx - arrowProp.projectionOnCross,
- pointStart.dy - arrowProp.projectionOnMain,
- );
- path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
- path.quadraticBezierTo(
- pointStart.dx,
- pointStart.dy,
- pointStartArcEnd.dx,
- pointStartArcEnd.dy,
- );
- }
- // 计算中间箭头的圆弧
- {
- Offset pointArrowArcBegin = Offset(
- pointArrow.dx + arrowProp.topLen,
- pointArrow.dy + arrowProp.arrowProjectionOnMain,
- );
- Offset pointArrowArcEnd = Offset(
- pointArrow.dx + arrowProp.topLen,
- pointArrow.dy - arrowProp.arrowProjectionOnMain,
- );
- path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
- path.quadraticBezierTo(
- pointArrow.dx,
- pointArrow.dy,
- pointArrowArcEnd.dx,
- pointArrowArcEnd.dy,
- );
- }
- // 下面计算结束的圆弧
- {
- Offset pointEndArcBegin = Offset(
- pointEnd.dx - arrowProp.projectionOnCross,
- pointEnd.dy + arrowProp.projectionOnMain,
- );
- Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy - arrowRadius);
- path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
- path.quadraticBezierTo(
- pointEnd.dx,
- pointEnd.dy,
- pointEndArcEnd.dx,
- pointEndArcEnd.dy,
- );
- }
- }
- path.lineTo(nRect.left, nRect.top + borderRadius.topLeft.y);
- path.arcToPoint(startPoint, radius: borderRadius.topLeft, rotation: 90);
- return path;
- }
- @override
- void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
- if (fillColor == null && side == BorderSide.none) {
- return;
- }
- final path = _buildPath(rect);
- final Paint paint =
- Paint()
- ..color = side.color
- ..style = PaintingStyle.stroke;
- if (fillColor != null) {
- paint.color = fillColor!;
- paint.style = PaintingStyle.fill;
- canvas.drawPath(path, paint);
- }
- if (side != BorderSide.none) {
- paint.color = side.color;
- paint.strokeWidth = side.width;
- paint.style = PaintingStyle.stroke;
- canvas.drawPath(path, paint);
- }
- }
- @override
- ShapeBorder scale(double t) {
- return BubbleShapeBorder(
- arrowDirection: arrowDirection,
- side: side.scale(t),
- borderRadius: borderRadius * t,
- arrowLength: arrowLength * t,
- arrowWidth: arrowWidth * t,
- arrowRadius: arrowRadius * t,
- arrowOffset: (arrowOffset ?? 0) * t,
- );
- }
- @override
- bool operator ==(Object other) {
- if (other.runtimeType != runtimeType) {
- return false;
- }
- return other is BubbleShapeBorder &&
- other.side == side &&
- other.borderRadius == borderRadius &&
- other.arrowLength == arrowLength &&
- other.arrowWidth == arrowWidth &&
- other.arrowRadius == arrowRadius &&
- other.arrowDirection == arrowDirection &&
- other.arrowOffset == arrowOffset &&
- other.fillColor == fillColor;
- }
- @override
- int get hashCode => Object.hash(
- side,
- borderRadius,
- arrowLength,
- arrowWidth,
- arrowRadius,
- arrowDirection,
- arrowOffset,
- fillColor,
- );
- }
|