Flutter предлагает различные виджеты для работы с определенным набором фигур, например, ClipRect, ClipRRect, ClipOval. Но также есть ClipPath, с помощью которого мы можем создавать любые типы фигур.


В данной статье мы сосредоточимся на том, что можно сделать, используя ClipPath и CustomClipper. Поехали!


Содержание:



ClipPath


Благодаря ClipPath мы можем создавать очень сложные и необычные фигуры. В этом нам поможет свойство clipper у ClipPath


@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.grey,
    body: Center(
      child: ClipPath(
        clipper: MyCustomClipper(), // <--
        child: Container(
          width: 200,
          height: 200,
          color: Colors.pink,
        ),
      ),
    ),
  );
}

В качестве значения для clipper необходимо указать экземпляр класса, который наследуют CustomClipper<Path> и переопределяет два метода.


class MyCustomClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    Path path = Path();
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false
}

Метод getClip вызывается каждый раз, когда требуется обновить нашу описанную фигуру. В качестве параметра метод получает Size, который содержит значения высоты и ширины виджета, переданного в сhild у ClipPath.


shouldReclip вызывается, когда в clipper передается новый экземпляр класса. Если новый экземпляр отличается от старого, то метод должен возвращать true, в противном случае – false.


Нам нужно описать фигуру внутри CustomClipper. Это не очень сложно, но сначала надо разобраться с основами графики.



Как показано на приведенном выше рисунке, каждая точка на графике описывается через координату (x,y). x представляет собой горизонтальную ось, а y – вертикальную ось. Построение фигуры начинается с верхнего левого угла, чья координата (0, 0).


Давайте рассмотрим доступные методы для построения фигур. С помощью этих методов вы можете создавать собственные фигуры.


lineTo


Данный метод используется для построения отрезка от текущей точки до заданной.



Как показано выше на рисунке (a), путь по умолчанию начинается с точки p1(0, 0). Теперь добавим новый отрезок к p2(0, h), а затем – p3(w, h). Нам не нужно определять линию от конечной точки p3 до начальной p1, она будет нарисована по умолчанию.


Результат можно увидеть на рисунке (b) с треугольником розового цвета.


@override
Path getClip(Size size) {

  Path path = Path()
    ..lineTo(0, size.height) // Добавить отрезок p1p2
    ..lineTo(size.width, size.height) // Добавить отрезок p2p3
    ..close();

  return path;
}

moveTo


Этот метод нужен для перемещения точки отрисовки.



Как показано на рисунке выше, начальная точка перемещена из (0, 0) в точку p1(w/2, 0).


@override
Path getClip(Size size) {

  Path path = Path() // Начальная точка в (0, 0)
    ..moveTo(size.width/2, 0) // передвигаем точку в (width/2, 0)
    ..lineTo(0, size.width)
    ..lineTo(size.width, size.height)
    ..close();

  return path;
}

quadraticBezierTo


Этот метод используется для построения квадратичной кривой Безье.



Источник: Wikipedia


Как показано на приведенном выше рисунке, мы можем нарисовать квадратичную кривую Безье, используя контрольную и конечную точки. P0 – это начальная точка, P1 – контрольная точка, а P2 – конечная точка.



Как показано выше на рисунке (а), кривая рисуется от точки p2(0, h) до p3(w, h) с использованием контрольной точки c(w/2, h/2).


@override
Path getClip(Size size) {
  // Эта переменная определена для лучшего понимания, какое значение указать в методе quadraticBezierTo
  var controlPoint = Offset(size.width / 2, size.height / 2);
  var endPoint = Offset(size.width, size.height);

  Path path = Path()
    ..moveTo(size.width / 2, 0)
    ..lineTo(0, size.height)
    ..quadraticBezierTo(
        controlPoint.dx, controlPoint.dy, endPoint.dx, endPoint.dy)
    ..close();

  return path;
}

cubicTo


Данный метод используется для построения кубической кривой путем указания 2 контрольных и конечной точек.



На рисунке выше представлена иллюстрация различных кубических кривых с разным расположением контрольных точек.



Как показано на рисунке (a), кубическая кривая рисуется между начальной точкой p2 и конечной точкой p3 с использованием контрольных точек c1 и c2.


@override
Path getClip(Size size) {

  var controlPoint1 = Offset(50, size.height - 100);
  var controlPoint2 = Offset(size.width - 50, size.height);
  var endPoint = Offset(size.width, size.height - 50);

  Path path = Path()
    ..moveTo(size.width / 2, 0)
    ..lineTo(0, size.height - 50)
    ..cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
        controlPoint2.dy, endPoint.dx, endPoint.dy)
    ..close();

  return path;
}

arcToPoint


Данный метод нужен для рисования дуги от начальной точки до указанной точки. Мы можем настроить дугу, установив радиус, указав направление (по часовой стрелке / против часовой стрелки).



Существуют эллиптический и круговой типы радиуса для построения дуги. Как показано на приведенном выше рисунке, эллиптический радиус рисуется с использованием значения (x, y), а круговой радиус – радиуса R.



Как показано на рисунке (a) выше, построение фигуры начинается с точки p1. Первая дуга рисуется от точки p2 до точки p3, при этом радиус не задан, поэтому по умолчанию равен нулю, соответственно наша дуга выглядит, как прямая. Вторая дуга тянется от начальной точки p4 до конечной точки p5 с использованием кругового радиуса и направления по часовой стрелке (прим. по часовой – направление по умолчанию). Третья дуга тянется от точки p6 до точки p7, используя круговой радиус и направление против часовой стрелки. Четвертая дуга проходит от начальной точки p8 до конечной точки p1, используя эллиптический радиус.


@override
Path getClip(Size size) {

  double radius = 20;

  Path path = Path()  
    ..moveTo(radius, 0)
    ..lineTo(size.width-radius, 0)
    ..arcToPoint(Offset(size.width, radius))
    ..lineTo(size.width, size.height - radius)
    ..arcToPoint(Offset(size.width - radius, size.height),radius: Radius.circular(radius))
    ..lineTo(radius, size.height)
    ..arcToPoint(Offset(0, size.height - radius), radius: Radius.circular(radius), clockwise: false)
    ..lineTo(0, radius)
    ..arcToPoint(Offset(radius, 0), radius: Radius.elliptical(40, 20))
    ..close();

  return path;
}

arcTo


Этот метод используется, чтобы нарисовать дугу, задав в качестве значения в радианах Rect, начальный угол (startAngle) и конечный угол (sweepAngle).



Приведенное выше изображение предназначено для предоставления основной информации об углах в радианах. Минимальный угол равен 0 PI (значение PI равно ~3.14), полный – 2 PI.



Существует несколько способов построения Rect, как с помощью точек, окружности, LTRB (Left, Top, Right, Bottom) и LTWH (Left, Top, Width, Height). На вышеприведенном рисунке (a) все типы дуг нарисованы с разным начальным углом.


@override
Path getClip(Size size) {

  double radius = 50;

  Path path = Path()
    ..lineTo(size.width - radius, 0)
    ..arcTo(
        Rect.fromPoints(
            Offset(size.width - radius, 0), Offset(size.width, radius)), // Rect
        1.5 * pi,   // начальный угол
        0.5 * pi,  // конечный угол
        true)  // направление по часовой стрелке
    ..lineTo(size.width, size.height - radius)
    ..arcTo(Rect.fromCircle(center: Offset(size.width - radius, size.height - radius), radius: radius), 0, 0.5 * pi, false)
    ..lineTo(radius, size.height)
    ..arcTo(Rect.fromLTRB(0, size.height - radius, radius, size.height), 0.5 * pi, 0.5 * pi, false)
    ..lineTo(0, radius)
    ..arcTo(Rect.fromLTWH(0, 0, 70, 100), 1 * pi, 0.5 * pi, false)
    ..close();

  return path;
}

addRect


Данный метод нужен для построения прямоугольников. Есть несколько различных методов для создания Rect: fromPoints, fromLTWH, fromCircle, fromLTRB и fromCircle.




@override
Path getClip(Size size) {

  Path path = Path()
    ..addRect(Rect.fromPoints(Offset(0, 0), Offset(60, 60)))
    ..addRect(Rect.fromLTWH(0, size.height - 50, 50, 50))
    ..addRect(Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: 20))
    ..close();

  return path;
}

addRRect


Этот метод используется для добавления прямоугольника с закругленными углами. Можно скруглить, как все углы сразу, так и один.



@override
Path getClip(Size size) {
    double radius = 10;

    Path path = Path()
      ..addRRect(RRect.fromLTRBR(0, 0, 60, 60, Radius.circular(radius)))
      ..addRRect(RRect.fromRectAndRadius(
          Rect.fromLTWH(0, size.height - 50, 50, 50), Radius.circular(radius)))
      ..addRRect(RRect.fromRectAndCorners(
          Rect.fromCircle(
              center: Offset(size.width / 2, size.height / 2), radius: 30
          ),
          topLeft: Radius.circular(radius)))
      ..close();

    return path;
}

addOval


Данный метод используется для описания овала. Как и для addRect, параметр типа Rect является обязательным.



@override
Path getClip(Size size) {

    Path path = Path()
      ..addOval(Rect.fromPoints(Offset(0, 0), Offset(60, 60)))
      ..addOval(Rect.fromLTWH(0, size.height - 50, 100, 50))
      ..addOval(Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: 20))
      ..close();
    return path;

}

addPolygon


Этот метод используется для добавления многоугольника путем определения нескольких точек.



@override
Path getClip(Size size) {
    var points = [
      Offset(size.width / 2, 0), // точка p1
      Offset(0, size.height / 2), // точка p2
      Offset(size.width / 2, size.height), // точка p3
      Offset(size.width, size.height / 2) // точка p4
    ]; 

    Path path = Path()
      ..addPolygon(points, false)
      ..close();

    return path;
}

addPath


Данный метод пригодится, если надо добавить ещё одну фигуру на отображение к уже имеющейся. Для этого необходимо указать описание фигуры и сдвиг относительно начальной позиции точки для вычислениям координат новой фигуры.



Как показано на рисунке (a), есть две фигуры (path 1 и path 2), path 1 является основной, а path 2 добавляется в path 1. path 2 строится в соответсвии со сдвигом (w/2, 0), поэтому начало координат (0, 0) и все остальные точки вычисляются с учетом указанного смещения.


@override
Path getClip(Size size) {

    Path path1 = Path()
      ..lineTo(0, size.height)
      ..lineTo(size.width/2, size.height)
      ..lineTo(0, 0);

    Path path2 = Path()
      ..lineTo(size.width/2, size.height)
      ..lineTo(size.width/2, 0)
      ..lineTo(0, 0);

    path1.addPath(path2, Offset(size.width/2,0));

    return path1;
}

relativeLineTo


Этот метод аналогичен методу lineTo, но конечная точка линии задается не точной координатой, а смещением из начальной.



На рисунке (a) линия p1p2 рисуется с помощью relativeLineTo, поэтому координата точки p2 вычисляется относительно p1. Можно записать в виде формулы p2(x, y) = (p1.x + 50, p1.y + h)


@override
Path getClip(Size size) {

    Path path = Path()
      ..moveTo(size.width/2, 0)
      ..relativeLineTo(50, size.height)
      ..lineTo(size.width , size.height)
      ..close();

    return path;
}

Примечание: relativeMoveTo, relativeQuadraticBezierTo, relativeArcToPoint, relativeCubicTo будут работать по сравнению с quadraticBezierTo, arcToPoint, cubicTo по тому же принципу, что и relativeLineTo по отношению к lineTo.



Надеюсь, что данный материал будет вам полезен и поможет создавать новые классные фигуры.