Flutter中很多组件都有一个叫做shape的属性,类型是ShapeBorder,比如Button类、Card等组件,shape表示控件的形状,系统已经为我们提供了很多形状,对于没有此属性的组件,可以使用 Clip 类组件进行裁减。

shape

BeveledRectangleBorder 斜角矩形边框

image.png

  1. RaisedButton(
  2. child: Text('点击'),
  3. onPressed: () {},
  4. shape: BeveledRectangleBorder(
  5. side: BorderSide(width: 1, color: Colors.red),
  6. borderRadius: BorderRadius.circular(0), //如果半径设置为0,就是矩形。
  7. // borderRadius: BorderRadius.circular(10),
  8. // borderRadius: BorderRadius.circular(20),
  9. // borderRadius: BorderRadius.circular(1000), //如果设置的半径比控件还大,就会变成菱形:
  10. ),
  11. ),

CircleBorder 圆形边框

image.png

  1. RaisedButton(
  2. child: Text('点击'),
  3. onPressed: () {},
  4. shape: CircleBorder(
  5. side: BorderSide(color: Colors.red),
  6. ),
  7. ),

RoundedRectangleBorder、ContinuousRectangleBorder

  • RoundedRectangleBorder 圆角矩形
  • ContinuousRectangleBorder 连续的圆角矩形,直线和圆角平滑连续的过渡,和RoundedRectangleBorder相比,圆角效果会小一些。

image.png

  1. RaisedButton(
  2. child: Text('点击'),
  3. onPressed: () {},
  4. shape: RoundedRectangleBorder(
  5. side: BorderSide(width: 1, color: Colors.red), //设置边框
  6. borderRadius: BorderRadius.circular(10), //设置圆角
  7. ),
  8. ),
  9. RaisedButton(
  10. child: Text('点击'),
  11. onPressed: () {},
  12. shape: ContinuousRectangleBorder(
  13. side: BorderSide(width: 1, color: Colors.red),
  14. borderRadius: BorderRadius.circular(20),
  15. ),
  16. ),

StadiumBorder 两边圆形,中间矩形

类似足球场的形状,两边圆形,中间矩形。
image.png

  1. shape: StadiumBorder(
  2. side: BorderSide(color: Colors.red),
  3. ),


OutlineInputBorder

带外边框。
image.png

  1. shape: OutlineInputBorder(
  2. borderSide: BorderSide(color: Colors.red),
  3. borderRadius: BorderRadius.circular(10),
  4. ),

UnderlineInputBorder

下划线边框。
image.png

  1. shape: UnderlineInputBorder(
  2. borderSide: BorderSide(color: Colors.red),
  3. ),

Border

image.png

  1. RaisedButton(
  2. child: Text('点击'),
  3. onPressed: () {},
  4. // shape: Border.all(color: Colors.red),
  5. // shape: Border.symmetric(
  6. // vertical: BorderSide(),
  7. // horizontal: BorderSide(color: Colors.red, width: 4),
  8. // ),
  9. shape: Border(
  10. top: BorderSide(color: Colors.red, width: 10),
  11. right: BorderSide(color: Colors.blue, width: 10),
  12. bottom: BorderSide(color: Colors.yellow, width: 10),
  13. left: BorderSide(color: Colors.green, width: 10),
  14. ),
  15. ),

BorderDirectional

BorderDirectionalBorder基本一样,区别就是BorderDirectional带有阅读方向,大部分国家阅读是从左到右,但有的国家是从右到左的,比如阿拉伯等。
image.png

  1. RaisedButton(
  2. child: Text('点击'),
  3. onPressed: () {},
  4. // shape: Border.all(color: Colors.red),
  5. // shape: Border.symmetric(
  6. // vertical: BorderSide(),
  7. // horizontal: BorderSide(color: Colors.red, width: 4),
  8. // ),
  9. shape: BorderDirectional(
  10. top: BorderSide(color: Colors.red, width: 10),
  11. end: BorderSide(color: Colors.blue, width: 10),
  12. bottom: BorderSide(color: Colors.yellow, width: 10),
  13. start: BorderSide(color: Colors.green, width: 10),
  14. ),
  15. ),

Clip 裁剪

Flutter中提供了一些剪裁函数,用于对组件进行剪裁。

Rect.fromLTRB()

比如 截取一张图片,原图的宽高分别为w和h,
Rect.fromLTRB(0, 0, w/2, h/2) :从原图片中截取一个矩形,矩形的坐标从(0.0)到(w/2, h/2)。

ClipOval 裁剪为圆

子组件为正方形时剪裁为内贴圆形,为矩形时,剪裁为内贴椭圆。
image.png

  1. ClipOval(
  2. child: Image.network('https://www.bugclose.com/oss/27/42/b9/001e190a04c6.jpg', width: 100),
  3. ),
  4. ClipOval(
  5. child: Image.network(
  6. 'https://images.h128.com/upload/202012/20/202012201302083641.jpg',
  7. width: 200,
  8. ),
  9. ),

ClipRRect 裁剪为圆角矩形

将子组件剪裁为圆角矩形。
image.png

  1. ClipRRect(
  2. borderRadius: BorderRadius.circular(15),
  3. child: Image.network('https://www.bugclose.com/oss/27/42/b9/001e190a04c6.jpg', width: 100),
  4. ),

ClipRect 裁剪溢出部分

剪裁子组件到实际占用的矩形大小(溢出部分剪裁)。

ClipRect组件使用矩形裁剪子组件,通常情况下,ClipRect作用于CustomPaintCustomSingleChildLayoutCustomMultiChildLayoutAlignCenterOverflowBoxSizedOverflowBox组件。定义如下:

  1. ClipRect({
  2. Key key,
  3. this.clipper,
  4. this.clipBehavior = Clip.hardEdge,
  5. Widget child,
  6. })

clipBehavior参数定义了裁剪的方式,只有子控件超出父控件的范围才有裁剪的说法,各个方式说明如下:

  • none:不裁剪,系统默认值,如果子组件不超出边界,此值没有任何性能消耗。
  • hardEdge:裁剪但不应用抗锯齿,速度比none慢一点,但比其他方式快。
  • antiAlias:裁剪而且抗锯齿,此方式看起来更平滑,比antiAliasWithSaveLayer快,比hardEdge慢,通常用于处理圆形和弧形裁剪。
  • antiAliasWithSaveLayer:裁剪、抗锯齿而且有一个缓冲区,此方式很慢,用到的情况比较少。

示例

image.png

  1. Widget avatar = Image.network(
  2. 'https://www.bugclose.com/oss/27/42/b9/001e190a04c6.jpg',
  3. width: 100,
  4. );
  5. Widget text = Text("你好世界", textScaleFactor: 2);
  6. ...
  7. Column(
  8. crossAxisAlignment: CrossAxisAlignment.start,
  9. children: [
  10. // 原图效果
  11. Row(children: [
  12. Align(alignment: Alignment.topLeft, child: avatar),
  13. text,
  14. ]),
  15. //宽度设为原来宽度一半,另一半会溢出
  16. Row(children: [
  17. Align(alignment: Alignment.topLeft, widthFactor: 0.5, child: avatar),
  18. text,
  19. ]),
  20. //宽度设为原来宽度一半,将溢出部分剪裁
  21. Row(children: [
  22. ClipRect(
  23. child: Align(alignment: Alignment.topLeft, widthFactor: 0.5, child: avatar),
  24. ),
  25. text,
  26. ]),
  27. ],
  28. ),

ClipPath 按路径裁剪

根据路径进行裁剪,我们自定义裁剪路径也可以使用系统提供的。
shape参数是ShapeBorder类型,系统已经定义了很多形状,介绍如下:

  • RoundedRectangleBorder:圆角矩形
  • ContinuousRectangleBorder:直线和圆角平滑连续的过渡,和RoundedRectangleBorder相比,圆角效果会小一些。
  • StadiumBorder:类似于足球场的形状,两端半圆。
  • BeveledRectangleBorder:斜角矩形。
  • CircleBorder:圆形。

image.png

  1. ClipPath.shape(
  2. shape: StadiumBorder(),
  3. child: Container(
  4. height: 150,
  5. width: 250,
  6. child: Image.network(
  7. 'https://www.bugclose.com/oss/27/42/b9/001e190a04c6.jpg',
  8. fit: BoxFit.cover,
  9. ),
  10. ),
  11. ),

CustomClipper

CustomClipper并不是一个组件,而是一个abstract(抽象)类,使用CustomClipper可以绘制出任何我们想要的形状。

示例1

如果我们想剪裁子组件的特定区域,比如,在上面示例的图片中,如果我们只想截取图片中部40×30像素的范围应该怎么做?这时我们可以使用CustomClipper来自定义剪裁区域,实现代码如下:
首先,自定义一个CustomClipper

  1. class MyClipper extends CustomClipper<Rect> {
  2. @override
  3. Rect getClip(Size size) => Rect.fromLTWH(5, 15, 40, 30);
  4. // 以 距左5像素,距顶15像素位置为原点,裁剪出40*30的矩形。
  5. @override
  6. bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
  7. }
  • getClip()是用于获取剪裁区域的接口,由于图片大小是60×60,我们返回剪裁区域为Rect.fromLTWH(5.0, 15.0, 40.0, 30.0),即图片中部40×30像素的范围。
  • shouldReclip() 接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回true来重新执行剪裁。

然后,我们通过ClipRect来执行剪裁,为了看清图片实际所占用的位置,我们设置一个红色背景:
image.png

  1. Widget avatar = Image.network(
  2. 'https://www.bugclose.com/oss/27/42/b9/001e190a04c6.jpg',
  3. width: 100,
  4. );
  5. ...
  6. avatar,
  7. Container(
  8. color: Colors.red,
  9. child: ClipRect(
  10. clipper: MyClipper(),
  11. child: avatar,
  12. ),
  13. ),

示例2:三角形

image.png

  1. Container(
  2. color: Colors.red,
  3. child: ClipPath(
  4. clipper: TrianglePath(),
  5. child: avatar,
  6. ),
  7. ),
  8. class TrianglePath extends CustomClipper<Path>{
  9. @override
  10. Path getClip(Size size) {
  11. var path = Path();
  12. path.moveTo(size.width/2, 0);
  13. path.lineTo(0, size.height);
  14. path.lineTo(size.width, size.height);
  15. return path;
  16. }
  17. @override
  18. bool shouldReclip(CustomClipper<Path> oldClipper) {
  19. return true;
  20. }
  21. }

示例3:五角星

image.png

  1. Container(
  2. color: Colors.red,
  3. child: ClipPath(
  4. clipper: StarPath(),
  5. child: avatar,
  6. ),
  7. ),
  8. class StarPath extends CustomClipper<Path> {
  9. StarPath({this.scale = 2.5});
  10. final double scale; //scale参数表示间隔的点到圆心的缩放比例
  11. double perDegree = 36;
  12. /// 角度转弧度公式
  13. double degree2Radian(double degree) {
  14. return (pi * degree / 180);
  15. }
  16. @override
  17. Path getClip(Size size) {
  18. var R = min(size.width / 2, size.height / 2);
  19. var r = R / scale;
  20. var x = size.width / 2;
  21. var y = size.height / 2;
  22. var path = Path();
  23. path.moveTo(x, y - R);
  24. path.lineTo(x - sin(degree2Radian(perDegree)) * r, y - cos(degree2Radian(perDegree)) * r);
  25. path.lineTo(x - sin(degree2Radian(perDegree * 2)) * R, y - cos(degree2Radian(perDegree * 2)) * R);
  26. path.lineTo(x - sin(degree2Radian(perDegree * 3)) * r, y - cos(degree2Radian(perDegree * 3)) * r);
  27. path.lineTo(x - sin(degree2Radian(perDegree * 4)) * R, y - cos(degree2Radian(perDegree * 4)) * R);
  28. path.lineTo(x - sin(degree2Radian(perDegree * 5)) * r, y - cos(degree2Radian(perDegree * 5)) * r);
  29. path.lineTo(x - sin(degree2Radian(perDegree * 6)) * R, y - cos(degree2Radian(perDegree * 6)) * R);
  30. path.lineTo(x - sin(degree2Radian(perDegree * 7)) * r, y - cos(degree2Radian(perDegree * 7)) * r);
  31. path.lineTo(x - sin(degree2Radian(perDegree * 8)) * R, y - cos(degree2Radian(perDegree * 8)) * R);
  32. path.lineTo(x - sin(degree2Radian(perDegree * 9)) * r, y - cos(degree2Radian(perDegree * 9)) * r);
  33. path.lineTo(x - sin(degree2Radian(perDegree * 10)) * R, y - cos(degree2Radian(perDegree * 10)) * R);
  34. return path;
  35. }
  36. @override
  37. bool shouldReclip(StarPath oldClipper) {
  38. return oldClipper.scale != this.scale;
  39. }
  40. }

动画动态设置scale

改变形状组件 shape - 图16

  1. class StartClip extends StatefulWidget {
  2. @override
  3. State<StatefulWidget> createState() => _StartClipState();
  4. }
  5. class _StartClipState extends State<StartClip>
  6. with SingleTickerProviderStateMixin {
  7. AnimationController _controller;
  8. Animation _animation;
  9. @override
  10. void initState() {
  11. _controller =
  12. AnimationController(duration: Duration(seconds: 2), vsync: this)
  13. ..addStatusListener((status) {
  14. if (status == AnimationStatus.completed) {
  15. _controller.reverse();
  16. } else if (status == AnimationStatus.dismissed) {
  17. _controller.forward();
  18. }
  19. });
  20. _animation = Tween(begin: 1.0, end: 4.0).animate(_controller);
  21. _controller.forward();
  22. super.initState();
  23. }
  24. @override
  25. Widget build(BuildContext context) {
  26. return Center(
  27. child: AnimatedBuilder(
  28. animation: _animation,
  29. builder: (context, child) {
  30. return ClipPath(
  31. clipper: StarPath(scale: _animation.value),
  32. child: Container(
  33. height: 150,
  34. width: 150,
  35. color: Colors.red,
  36. ),
  37. );
  38. }),
  39. );
  40. }
  41. }