You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中复刻iOS Threads应用的Post按钮Squircle形状?

在Flutter中实现Threads风格的Squircle按钮

这个Squircle形状完全可以在Flutter中实现,并非iOS原生独有。iOS的Squircle本质是通过复杂的贝塞尔曲线定义的平滑圆角,而ContinuousRectangleBorder采用二次贝塞尔曲线实现,精度不足,因此会和目标形状产生差异。

自定义ShapeBorder实现精准匹配

通过自定义ShapeBorder,使用四次贝塞尔曲线可以模拟Threads按钮的圆角弧度,以下是可直接复用的代码:

class ThreadsSquircleBorder extends ShapeBorder {
  final double radius;
  final BorderSide side;

  const ThreadsSquircleBorder({
    this.radius = 16.0,
    this.side = BorderSide.none,
  });

  @override
  EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width);

  @override
  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
    return _getSquirclePath(rect.deflate(side.width), radius);
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    return _getSquirclePath(rect, radius);
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
    if (side.style == BorderStyle.none) return;
    final path = _getSquirclePath(rect.deflate(side.width / 2), radius);
    final paint = side.toPaint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = side.width;
    canvas.drawPath(path, paint);
  }

  @override
  ShapeBorder scale(double t) {
    return ThreadsSquircleBorder(
      radius: radius * t,
      side: side.scale(t),
    );
  }

  Path _getSquirclePath(Rect rect, double radius) {
    final controlPointFactor = 0.4; // 微调此值可匹配Threads的弧度

    final topLeft = rect.topLeft;
    final topRight = rect.topRight;
    final bottomRight = rect.bottomRight;
    final bottomLeft = rect.bottomLeft;

    final tlControl1 = Offset(topLeft.dx + radius * controlPointFactor, topLeft.dy);
    final tlControl2 = Offset(topLeft.dx, topLeft.dy + radius * controlPointFactor);

    final trControl1 = Offset(topRight.dx - radius * controlPointFactor, topRight.dy);
    final trControl2 = Offset(topRight.dx, topRight.dy + radius * controlPointFactor);

    final brControl1 = Offset(bottomRight.dx - radius * controlPointFactor, bottomRight.dy);
    final brControl2 = Offset(bottomRight.dx, bottomRight.dy - radius * controlPointFactor);

    final blControl1 = Offset(bottomLeft.dx + radius * controlPointFactor, bottomLeft.dy);
    final blControl2 = Offset(bottomLeft.dx, bottomLeft.dy - radius * controlPointFactor);

    final path = Path()
      ..moveTo(topLeft.dx + radius, topLeft.dy)
      ..cubicTo(tlControl1.dx, tlControl1.dy, tlControl2.dx, tlControl2.dy, topLeft.dx, topLeft.dy + radius)
      ..lineTo(bottomLeft.dx, bottomLeft.dy - radius)
      ..cubicTo(blControl2.dx, blControl2.dy, blControl1.dx, blControl1.dy, bottomLeft.dx + radius, bottomLeft.dy)
      ..lineTo(bottomRight.dx - radius, bottomRight.dy)
      ..cubicTo(brControl1.dx, brControl1.dy, brControl2.dx, brControl2.dy, bottomRight.dx, bottomRight.dy - radius)
      ..lineTo(topRight.dx, topRight.dy + radius)
      ..cubicTo(trControl2.dx, trControl2.dy, trControl1.dx, trControl1.dy, topRight.dx - radius, topRight.dy)
      ..close();

    return path;
  }
}

使用示例

将自定义边框应用到按钮上:

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    shape: const ThreadsSquircleBorder(radius: 16),
    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
  ),
  child: const Text("Post"),
)

细节调整

  • 微调controlPointFactor的值(建议在0.35-0.45区间),可让圆角平滑度完全匹配Threads按钮的效果。
  • 若需要极致精准,可对比Threads按钮截图,测量圆角曲率后再调整控制点位置。

为什么ContinuousRectangleBorder效果不佳?

ContinuousRectangleBorder采用二次贝塞尔曲线实现圆角过渡,而iOS的Squircle基于四次贝塞尔或更复杂的曲线算法,两者的曲线形态差异明显,因此只能得到近似效果,无法完全匹配。


内容的提问来源于stack exchange,提问作者Pedro Estevão

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 19:03:18