如何在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
相关产品推荐
相关产品推荐

