Flutter--Path学习

1,015 阅读4分钟

 本片笔记完全仿照【Flutter高级玩法-shape】Path在手,天下我有来学习的,感谢博主的分享。

 平时在使用一些Widget的时候,有时候会碰到shape属性,但是完全没有用过,也只是在使用Card的时候看到过这个属性,今天偶然看到这个博客,想不到这个属性这么有用,所以就学习记录一下。

  1. 下面是一个完全没有使用shape属性绘制出来的的一个Material控件:
Material(
  color: Colors.orangeAccent,
  elevation: 8.0,
  child: Container(
    alignment: Alignment.center,
    padding: EdgeInsets.all(10.0),
    constraints: BoxConstraints.expand(height:50.0),
    child: Text("没有Shape的效果",style: TextStyle(color: Colors.white),),
  ),
),

 执行效果如下:

没有使用shape的效果
没有使用shape的效果

  1. BoxBorderBorderDirectionalBorder

BoxBorder主要掌管边线方面的事,自身是一个abstract,不能直接使用。
BorderDirectional通过top,bottom,start,end分别控制上下左右的边线对象BorderSide

 下面使用BorderDirectional控制一个Material的边线:

Material(
    color: Colors.pinkAccent,
    elevation: 10.0,
    shape: BorderDirectional(
      top: BorderSide(color:Colors.redAccent,width: 5.0),
      start: BorderSide(color: Colors.blueAccent,width: 10.0),
      bottom: BorderSide(color: Colors.redAccent,width: 5.0),
      end: BorderSide(color:Colors.blueAccent,width: 10.0),
    ),
    child: Container(
      alignment: Alignment.center,
      constraints: BoxConstraints.expand(height: 50.0),
      color: Colors.greenAccent,
      child: Text("使用BorderDirectional控制边线",style: TextStyle(color: Colors.white),),
    ),
 ),

 最终的运行效果如下:

使用BorderDirectional控制边线
使用BorderDirectional控制边线

 使用Border也可以达到上面的效果:

Material(
    color: Colors.pinkAccent,
    elevation: 10.0,
    shape: Border(
      top: BorderSide(color: Colors.redAccent, width: 5.0),
      left: BorderSide(color: Colors.blueAccent, width: 10.0),
      bottom: BorderSide(color: Colors.redAccent, width: 5.0),
      right: BorderSide(color: Colors.blueAccent, width: 10.0),
    ),
    child: Container(
      constraints: BoxConstraints.expand(height: 50.0),
      child: Text(
        "使用Border控制边线",
        style: TextStyle(color: Colors.white),
      ),
      alignment: Alignment.center,
    ),
),

 从代码上来看,不管是BorderDirectional还是Border,内部都是使用BorderSide来设置边线的颜色和宽度的。

 程序运行效果如下:

使用Border控制边线
使用Border控制边线

 可以看到,和BorderDirectional运行效果是一致的。

  1. CircleBorder

CircleBorder会以min(width,height)为直径,裁切出一个圆形

//使用CircleBorder
  Material(
    color: Colors.lightGreenAccent,
    elevation: 10.0,
    shape: CircleBorder(side: BorderSide(color: Colors.tealAccent,width: 10.0)),
    child: Container(
      color: Colors.black38,
      alignment: Alignment.center,
      height: 200,
      child: Text("CircleBorder",style: TextStyle(color: Colors.black),),
    ),
),

 在Container中的decoration属性里面也可以指定shape的类型,现在将它指定为BoxShape.circle对比一下效果:

 //对Container使用BorderShape.circle
Container(
    alignment: Alignment.center,
    margin: EdgeInsets.only(top: 20.0),
    constraints: BoxConstraints.tightForFinite(height: 100.0),
    child: Text("BoxShape.Circle",style: TextStyle(color: Colors.white),),
    decoration: BoxDecoration(
      shape: BoxShape.circle,
      color: Colors.redAccent,
    ),
 ),

使用CircleBorder和Container中的BoxShape.circle的效果对比
CircleBorder和Container中的BoxShape.circle的效果对比

  1. RoundedRectangleBorderContinuousRectangleBorder

 圆角矩形:

Material(
    color: Colors.limeAccent,
    elevation: 10.0,
    shape: RoundedRectangleBorder(
      side: BorderSide(color: Colors.yellowAccent,width: 5.0),
      borderRadius: BorderRadius.all(Radius.circular(5.0))
    ),
    child: Container(
      alignment: Alignment.center,
      constraints: BoxConstraints.expand(height: 50.0),
      color: Colors.black26,
      child: Text("RoundedRectangleBorder圆角矩形",style: TextStyle(color: Colors.white),),
    ),
),

RoundedRectangleBorder圆角矩形
RoundedRectangleBorder圆角矩形

 从上面的效果图也可以看出,最好是就别给Container中再设置颜色了,否则圆角的效果就没有了。

 ContinuousRectangleBorder的效果如下:

Material(
    color: Colors.pinkAccent,
    elevation: 10.0,
    shape: ContinuousRectangleBorder(
      side: BorderSide.none,
      borderRadius: BorderRadius.circular(20.0),
    ),
    child: Container(
      alignment: Alignment.center,
      constraints: BoxConstraints.expand(height: 50.0),
      child: Text("ContinuousRectangleBorder圆角矩形",style: TextStyle(color: Colors.white),),
    ),
    ),

ContinuousRectangleBorder圆角矩形
ContinuousRectangleBorder圆角矩形

  1. OutlineInputBorderUnderlineInputBorder

 这两个是常用的输入框的边线:

Material(
    color: Colors.orangeAccent,
    elevation: 10.0,
    shape: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.purpleAccent,width: 2.0),
      borderRadius: BorderRadius.circular(10.0),
      gapPadding: 10.0
    ),
    child: Container(
      alignment: Alignment.center,
      height: 50.0,
      child: Text("OutlineInputBorder",style: TextStyle(color: Colors.white),),
    ),
     ),

     Padding(padding: EdgeInsets.only(top: 20.0)),

     Material(
    color: Colors.orangeAccent,
    elevation: 2.0,
    shape: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.blueAccent,width: 3.0),
      borderRadius: BorderRadius.circular(5.0),
    ),
    child: Container(
      alignment: Alignment.center,
      height: 50.0,
      child: Text("UnderlineInputBorder",style: TextStyle(color: Colors.white),),
    ),
),

OutlineInputBorder和UnderlineInputBorder
OutlineInputBorder和UnderlineInputBorder

自定义ShapeBorder

 共有5个抽象方法:

class _MyShapeBorder extends ShapeBorder{
  @override
  // TODO: implement dimensions
  EdgeInsetsGeometry get dimensions => null;

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) {
    // TODO: implement getInnerPath
    return null;
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    // TODO: implement getOuterPath
    return null;
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
    // TODO: implement paint
  }

  @override
  ShapeBorder scale(double t) {
    // TODO: implement scale
    return null;
  }
  
}

 可以看到有一个paint方法,在里面提供了CanvasRect,也就是可以在这个区域里面绘制,首先可以看一下这个Rect的参数:

shape rect is Rect.fromLTRB(0.0, 0.0, 360.0, 50.0)

 可以看到直接拿到了可以绘制的区域。

 首先在左上角绘制一个圆:

class _MyShapeBorder extends ShapeBorder {
  //外部圆的paint
  Paint _outPaint;
  //内部圆的Paint
  Paint _innerPaint;

  _MyShapeBorder(){
    _outPaint =  Paint()
      ..color = Colors.deepOrangeAccent
      ..strokeWidth = 2.0
      ..style = PaintingStyle.stroke
      ..strokeJoin = StrokeJoin.round;

    _innerPaint = Paint()
      ..color = Colors.black
      ..strokeWidth = 2.0
      ..style = PaintingStyle.fill
      ..strokeJoin = StrokeJoin.round;
  }

  @override
  // TODO: implement dimensions
  EdgeInsetsGeometry get dimensions => null;

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) {
    // TODO: implement getInnerPath
    return null;
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    // TODO: implement getOuterPath
    return null;
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
    print("shape rect is${rect}");
    double width = rect.width;
    double height = rect.height;
    canvas.drawCircle(Offset(0.1 * width, 0.3 * height), 0.1 * height, _outPaint);
    canvas.drawCircle(Offset(0.1 * width, 0.3 * height), 0.05 * height, _innerPaint);
  }

  @override
  ShapeBorder scale(double t) {
    // TODO: implement scale
    return null;
  }
}

 在Material中使用:

Material(
    color: Colors.orangeAccent,
    elevation: 2.0,
    shape: _MyShapeBorder(),
    child: Container(
      alignment: Alignment.center,
      height: 50.0,
      child: Text(
        "自定义ShapeBorder",
        style: TextStyle(color: Colors.white),
      ),
    ),
),

 运行效果:

自定义ShapeBorder
自定义ShapeBorder

getOuterPath可以返回一个Path对象,也就是形状的裁剪,下面将上面的_MyShapeBorder裁剪为圆角矩形:

@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    Path outerPath = Path();
    outerPath.addRRect(RRect.fromRectAndRadius(rect, Radius.circular(10.0)));
    return outerPath;
}

向自定义ShapeBorder添加圆角
向自定义ShapeBorder添加圆角

 还可以做一些其它效果,比如在右边打个洞,具体实现就是将圆角矩形和圆形两个路径叠加,使用奇偶环绕来处理路径:

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    Path outerPath = Path();
    outerPath.addRRect(RRect.fromRectAndRadius(rect, Radius.circular(10.0)));

    var width = rect.width;
    var height = rect.height;

    //圆的直径
    var radius = 0.2 * (min(width, height));

    //计算圆所在的矩形的点
    var pl = 0.1 * width;
    var pt = 0.1 * height;

    var left = width - radius - pl;
    var top = pt;
    var right = left + radius;
    var bottom = top + radius;
    
    outerPath.addOval(Rect.fromLTRB(left, top, right, bottom));
    outerPath.fillType = PathFillType.evenOdd;

    return outerPath;
  }

 运行效果如下:

给自定义ShapeBorder打个洞
给自定义ShapeBorder打个洞

 上面已经做了在右上角添加一个洞,相应的,也可以抽象出一个类来在指定的位置添加一个洞:

//在指定位置打洞的ShapeBorder
class HoleShapeBorder extends ShapeBorder {
  //指定圆角的大小
  final Radius radius;
  //洞的直径
  final double diameter;
  //指定偏移量
  final Offset offset;
  //可以不指定偏移量,指定宽度和高度的位置百分比
  final double widthPercentage;
  final double heightPercentage;

  HoleShapeBorder(this.radius,
      {this.offset,
      this.widthPercentage = 0.0,
      this.heightPercentage = 0,
      this.diameter = 20})
      : assert(
            !(offset == null && (widthPercentage == 0 && heightPercentage == 0))),
        assert(widthPercentage <= 1 && heightPercentage <= 1);

  @override
  EdgeInsetsGeometry get dimensions => null;

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) {
    // TODO: implement getInnerPath
    return null;
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    //首先根据指定的Radius在外边绘制一个圆角
    Path path = Path();
    path.addRRect(RRect.fromRectAndRadius(rect, radius));

    var width = rect.width;
    var height = rect.height;
    //定义坐标
    var left;
    var top;
    var right;
    var bottom;
    //距离左边的距离
    var pl;
    //距离上边的距离
    var pt;
    //判断offset是否为空
    if (offset == null) {
      pl = width * widthPercentage - diameter / 2;
      pt = height * heightPercentage - diameter / 2;
    } else {
      //指定的偏移量大于可用的宽度
      if (offset.dx > width)
        pl = width - diameter;
      else
        pl = offset.dx;
      //指定的偏移量大于可用的高度
      if (offset.dy > height)
        pt = height - diameter;
      else
        pt = offset.dy;
    }
    left = pl;
    right = left + diameter;
    top = pt;
    bottom = top + diameter;
    path.addOval(Rect.fromLTRB(left, top, right, bottom));
    path.fillType = PathFillType.evenOdd;
    return path;
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
    // TODO: implement paint
  }

  @override
  ShapeBorder scale(double t) {
    // TODO: implement scale
    return null;
  }
}

 在上面的代码中,可以通过指定Offset(dx,dy)来设置打洞的位置,也可以通过设置洞的宽高比来设置所在的位置,如:

 //自定义ShapeBorder -- 打洞
            Padding(
              padding: EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0),
              child: Material(
                color: Colors.orangeAccent,
                elevation: 5.0,
                shape: HoleShapeBorder(
                  Radius.circular(10.0),
                  widthPercentage: 0.5,
                  heightPercentage: 0.5,
                ),
                child: Container(
                  constraints: BoxConstraints.expand(height: 60.0),
                  child: Text(
                    "自定义Shaper -- 打洞",
                    style: TextStyle(color: Colors.white, fontSize: 16.0),
                  ),
                  alignment: Alignment.center,
                ),
              ),
            ),

 这里将指定的位置设置中间,执行效果如下:

设置在中间位置打洞
设置在中间位置打洞

 根据指定偏移量打洞,下面根据Offset(20,20)打洞:

Padding(
              padding: EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0),
              child: Material(
                color: Colors.orangeAccent,
                elevation: 5.0,
                shape: HoleShapeBorder(
                  Radius.circular(10.0),
                  offset: Offset(20, 20),
                ),
                child: Container(
                  constraints: BoxConstraints.expand(height: 60.0),
                  child: Text(
                    "自定义Shaper -- 打洞",
                    style: TextStyle(color: Colors.white, fontSize: 16.0),
                  ),
                  alignment: Alignment.center,
                ),
              ),
            ),

 运行效果如下:

根据指定偏移量打洞
根据指定偏移量打洞

打多个洞

 在上面我们抽象出一些属性来做了一个简单继承的打洞的Shape,下面继续学习打多个洞:

//打多个洞的Shape
class MutilHoldShapeBorder extends ShapeBorder{
  @override
  // TODO: implement dimensions
  EdgeInsetsGeometry get dimensions => null;

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) {
    // TODO: implement getInnerPath
    return null;
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    Path path = Path();
    //首先仍然是绘制一个圆角
    path.addRRect(RRect.fromRectAndRadius(rect, Radius.circular(10.0)));
    //设置要绘制的洞的直径
    double diameter = 20;
    //洞之间的间距
    double holePadding = 10;
    //允许打洞的数量
    int holeNum = rect.width ~/ (diameter + holePadding);
    path.fillType = PathFillType.evenOdd;
    //for循环打出每一个洞
    int i = 0;
    //距离左边的距离
    var pl = holePadding;
    //距离上边的距离
    var pt = rect.height * 0.1;
    double left = pl;
    double top = pt;
    for(i; i < holeNum; i++){
      path.addOval(Rect.fromLTRB(left, top, left + diameter, top + diameter));
      left += (holePadding + diameter);
    }
    return path;
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
    // TODO: implement paint
  }

  @override
  ShapeBorder scale(double t) {
    // TODO: implement scale
    return null;
  }
  
}

 运行上面的代码,执行效果如下:

一次打多个洞的ShapeBorder
一次打多个洞的ShapeBorder

优惠券相关

 使用Path也可以绘制出优惠券的背景,如下:

 @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    Path path = Path();
    path.addRect(rect);
    path.fillType = PathFillType.evenOdd;
    //设置直径为30
    var diameter = 30.0;
    //上边缺口
    //距离左边为0.7 * width
    var gapTopPl = 0.7 * rect.width;
    var gapTopPt = 0.0;
    //首先在优惠券靠近右边的地方上下个绘制一个半圆
    path.addArc(Rect.fromLTRB(gapTopPl, -diameter / 2, gapTopPl + diameter,diameter / 2 ),0,pi);
    //下边缺口
    var gapBottomPt = rect.height - diameter / 2;
    path.addArc(Rect.fromLTRB(gapTopPl, gapBottomPt, gapTopPl + diameter, gapBottomPt + diameter),0,-pi);

    return path;
  }

 需要注意的是,此时由于我们需要绘制的是半圆,所以这里使用path.addArc(Rect,startAngle,sweepAngle)来设置半圆的弧度。运行上面的代码,效果如下:

切出优惠券上下缺口的效果
切出优惠券上下缺口的效果

 之后我们就需要切出左右两边的效果了,这个直径看起来有点大,可以小一点.

    //优惠券左右两边的缺口
    var gapPadding = 10.0;
    //距离上边的偏移量
    var gapLeftPt = gapPadding;
    //计算可以切出的个数
    var gapNum = (rect.height - gapPadding) ~/ (gapPadding + diameter);
    print("可绘制的个数$gapNum");
    for(int i = 0; i < gapNum; i++){
        //绘制左边的缺口
        path.addArc(Rect.fromLTRB(-diameter / 2 , gapLeftPt, diameter / 2 , gapLeftPt + diameter),-0.5 * pi, pi);
        //绘制右边的缺口
        path.addArc(Rect.fromLTRB(rect.width - (diameter / 2), gapLeftPt, rect.width + diameter / 2 , gapLeftPt + diameter), -0.5 * pi, -pi);
        gapLeftPt += (gapPadding + diameter);
    }

优惠券两边的缺口
优惠券两边的缺口

 接着就是在上下两个缺口的位置绘制一条虚线:

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
    //绘制一条白色的虚线
    var enableHeight = rect.height - diameter;
    //虚线的高度为5,间隔为5
    int dashNum = enableHeight ~/ 10;
    var dashTop = diameter / 2;
    var dashleft = rect.width * 0.7 + diameter / 2;
    for(int i = 0; i < dashNum; i++){
      if(dashTop + 5 > rect.height - diameter)
        dashTop = rect.height - diameter;
      canvas.drawLine(Offset(dashleft, dashTop), Offset(dashleft, dashTop + 5), _paint);
      dashTop += 15;
    }
  }

 运行效果如下:

绘制优惠券背景
绘制优惠券背景

ClipPathCard

 上面的示例都是在Material中使用,在ClipPath中也可以使用:

Padding(
                padding: EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0),
                child: ClipPath(
                  child: Image.asset(
                    "image/test.jpg",
                    fit: BoxFit.cover,
                  ),
                  clipper: ShapeBorderClipper(shape: CouponShapeBorder()),
                ),
              ),

在ClipPath中使用
在ClipPath中使用

Card中使用:

Padding(
                padding: EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0),
                child: Card(
                  shape: MutilHoldShapeBorder(),
                  color: Colors.orangeAccent,
                  child: _ContentWidget("在Card中使用自定义Path"),
                  elevation: 5.0,
                ),
              ),

 运行效果如下:

在Card中使用
在Card中使用

最后,再次感谢博主的分享,笔记中绘制的效果还有很多不尽如人意的地方,不满意可以查看原博主的文章:【Flutter高级玩法-shape】Path在手,天下我有