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),
),
),
),
),
);
}
}
На этом все! Надеемся, было полезно! Пишите в комментариях, каким виджетами из перечисленных пользуетесь и как они облегчают жизнь.
ligor
Так и так вызывать setState для Offstage а значит пересоздавать виджет.