Flutter предлагает множество виджетов. Одни используются почти в каждом проекте, другие остаются без внимания из-за специфичности или редких сценариев применения. В этой статье расскажем о пяти малоизвестных виджетах: PhysicalShape, 
Offstage, Flow, UnconstrainedBox, SizedOverflowBox.

1. PhysicalShape

PhysicalShape — виджет, который позволяет создавать фигуры с произвольными границами и применять к ним физические эффекты, такие как тень или elevation. Он полезен, когда нужно создать сложную форму, отличную от стандартных прямоугольников. Пример:

PhysicalShape(
  clipper: CustomClipper<Path>(), // CustomClipper определяет форму
  color: Colors.blue,
  elevation: 5.0,
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
);

Пример ниже создает треугольник с тенью, используя CustomClipper для определения формы.

Треугольник с тенью
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: PhysicalShape(
          clipper: TriangleClipper(),
          color: Colors.green,
          elevation: 10,
          shadowColor: Colors.black,
          child: const SizedBox.square(
            dimension: 150,
          ),
        ),
      ),
    );
  }
}

class TriangleClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    Path path = Path();
    path.moveTo(size.width / 2, 0);
    path.lineTo(0, size.height);
    path.lineTo(size.width, size.height);
    path.close();
    return path;
  }

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

2. Offstage

Offstage позволяет управлять видимостью виджета, сохраняя его состояние. Он полезен, когда нужно полностью скрыть элемент, но при этом продолжать занимать его место в иерархии виджетов. Пример:

Offstage(
  offstage: true, // если true, виджет скрыт
  child: Text("Этот текст не отображается"),
);

Можно использовать Offstage для скрытия экранов загрузки или анимаций. Они могут понадобиться снова, чтобы избежать повторного создания виджета.

Индикатор загрузки
Column(
  children: <Widget>[
    Offstage(
      offstage: !isLoading,
      child: CircularProgressIndicator(),
    ),
    Offstage(
      offstage: isLoading,
      child: Text('Загрузка завершена'),
    ),
  ],
);

3. Flow

Flow — это мощный виджет для создания кастомных layout'ов. Он позволяет реализовывать нестандартное расположение дочерних виджетов и подходит для сложных, динамических компоновок, которые невозможно создать с помощью стандартных Row или Column. Пример:

Flow(
  delegate: MyFlowDelegate(),
  children: <Widget>[
    Container(width: 50, height: 50, color: Colors.red),
    Container(width: 50, height: 50, color: Colors.green),
    Container(width: 50, height: 50, color: Colors.blue),
  ],
);

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

Иконки с алгебраическим отступом
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Flow(
          delegate: MenuFlowDelegate(),
          children: const <Widget>[
            Icon(Icons.home, size: 48, color: Colors.red),
            Icon(Icons.search, size: 48, color: Colors.green),
            Icon(Icons.settings, size: 48, color: Colors.blue),
          ],
        ),
      ),
    );
  }
}

class MenuFlowDelegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    double x = 0.0;
    double height = context.size.height;
    double centerY = height / 2;

    for (int i = 0; i < context.childCount; i++) {
      x += i * 50.0;
      context.paintChild(i, transform: Matrix4.translationValues(x, centerY, 0));
    }
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) => false;
}

4. UnconstrainedBox

UnconstrainedBox используется для снятия constraints, наложенных "родителем", что позволяет дочернему виджету занимать больше пространства, чем разрешено. Пример:

UnconstrainedBox(
  child: Container(
    width: 150,
    height: 150,
    color: Colors.orange,
  ),
);

UnconstrainedBox позволяет создавать анимации увеличения виджетов без ограничений размера родительского контейнера, например, для зума изображения при нажатии. Пример ниже показывает, как это сделать.

Контейнер за 1000
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: AnimatedBox(),
      )
    );
  }
}

class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});

  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  double _size = 200.0;

  void _toggleSize() {
    setState(() {
      _size = _size == 200.0 ? 1000.0 : 200.0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _toggleSize,
      child: UnconstrainedBox(
        child: AnimatedContainer(
          duration: const Duration(seconds: 1),
          width: _size,
          height: _size,
          color: Colors.orange,
          curve: Curves.easeInOut,
        ),
      ),
    );
  }
}

5. SizedOverflowBox

SizedOverflowBox полезен для управления размером дочернего элемента, игнорируя ограничения, которые обычно действуют на виджеты. Этот вариант будет полезен, если вам нужно принудительно задать размер дочернему виджету, независимо от того, вписывается он в размеры "родителя" или нет. Пример:

SizedOverflowBox(
  size: const Size.square(100),
  child: Container(
    color: Colors.purple,
    width: 200,
    height: 200,
  ),
);

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

И contraints не предел
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: <Widget>[
            AnimatedOverflowBox(),
            AnimatedOverflowBox(),
            AnimatedOverflowBox(),
          ],
        ),
      ),
    );
  }
}

class AnimatedOverflowBox extends StatefulWidget {
  const AnimatedOverflowBox({super.key});

  @override
  State<AnimatedOverflowBox> createState() => _OverflowBoxExampleState();
}

class _OverflowBoxExampleState extends State<AnimatedOverflowBox> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  bool _expanded = false;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    _animation = Tween<double>(begin: 100, end: 500).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    
    super.dispose();
  }

  void _toggleSize() {
    if (_expanded) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
    _expanded = !_expanded;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _toggleSize,
      child: SizedOverflowBox(
        size: const Size.square(100),
        child: AnimatedBuilder(
          animation: _animation,
          builder: (BuildContext context, Widget? child) {
            return Container(
              width: _animation.value,
              height: _animation.value,
              decoration: BoxDecoration(
                color: Colors.blue.withOpacity(0.5),
                border: Border.all(color: Colors.blue, width: 2),
              ),
              child: child,
            );
          },
          child: const Center(
            child: Text(
              'Нажми меня',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

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

Комментарии (1)


  1. ligor
    13.12.2024 13:34

    Так и так вызывать setState для Offstage а значит пересоздавать виджет.