Сегодня мы узнаем, как добавить FloatingActionButton
(FAB) к BottomAppBar
в Flutter. В итоге должно получиться что-то такое:

Наша задача — сделать BottomAppBar
, который будет работать так же, как BottomNavigationBar
. В нём будет несколько вкладок и только одна из них активная.
Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter.
Вы можете удивиться, почему нельзя использовать сразу BottomNavigationBar
? Несмотря на то, что технически можно вставить FloatingActionButton
в BottomNavigationBar
, на практике это решение полно недостатков.
Добавляем отцентрированный FAB
После создания нового проекта Flutter, в нём уже есть FloatingActionButton
для увеличения счётчика.
Мы можем добавить BottomAppBar
в наш Scaffold.bottomNavigationBar
вот так:
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
onPressed: () { },
tooltip: 'Increment',
child: Icon(Icons.add),
elevation: 2.0,
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[],
),
notchedShape: CircularNotchedRectangle(),
color: Colors.blueGrey,
),
);
Обратите внимание, как мы установили положение Scaffold.floatingActionButtonLocation
в FloatingActionButtonLocation.centerDocked
для того, чтобы вставить FAB в центр BottomAppBar
.
Также мы добавили notchedShape: CircularNotchedRectangle()
для создания выемки в BottomAppBar
для FAB.
Если мы запустим приложение сейчас, вот что должно получиться:

Добавляем вкладки
Чтобы добавить вкладки с отдельными страницами в наше приложение, мы можем создать FABBottomAppBar
:
- он вмещает 2 или 4 вкладки (из-за симметрии).
- подсвечивает и отслеживает выбранную вкладку.
- делает callback при выборе вкладки, чтобы приложение могло обновить нужную страницу.
Сделаем набросок класса FABBottomAppBar
:
class FABBottomAppBarItem {
FABBottomAppBarItem({this.iconData, this.text});
IconData iconData;
String text;
}
class FABBottomAppBar extends StatefulWidget {
final List<FABBottomAppBarItem> items;
final ValueChanged<int> onTabSelected;
@override
State<StatefulWidget> createState() => FABBottomAppBarState();
}
class FABBottomAppBarState extends State<FABBottomAppBar> {
int _selectedIndex = 0;
_updateIndex(int index) {
widget.onTabSelected(index);
setState(() {
_selectedIndex = index;
});
}
// TODO: build method here
}
Вначале мы создали класс FABBottomAppBarItem
, содержащий переменные IconData
и String
. Так мы описали вкладку.
Затем мы создали виджет FABBottomAppBar
. Это необходимо для создания списка вкладок и задания callback (onTabSelected
).
FABBottomAppBar
это StatefulWidget
, так как он должен отслеживать выбранную вкладку и обновлять её.
В классе FABBottomAppBarState
мы задали _selectedIndex
, обновляемый при вызове _updateIndex
.
Создадим метод build
:
@override
Widget build(BuildContext context) {
List<Widget> items = List.generate(widget.items.length, (int index) {
return _buildTabItem(
item: widget.items[index],
index: index,
onPressed: _updateIndex,
);
});
return BottomAppBar(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items,
),
);
}
Здесь:
- Строки с 3 по 9: здесь мы используем генератор списка для создания вкладок, передавая нужный элемент, индекс и ширину.
- Строки с 11 по 17: создаём
BottomAppBar
, содержащийRow
с заданными элементами. Мы используемMainAxisSize.max
иMainAxisAlignment.spaceAround
для корректного размещения элементов в ряду в полную ширину.
Реализуем метод _buildTabItem
:
Widget _buildTabItem({
FABBottomAppBarItem item,
int index,
ValueChanged<int> onPressed,
}) {
Color color = _selectedIndex == index ? widget.selectedColor : widget.color;
return Expanded(
child: SizedBox(
height: widget.height,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => onPressed(index),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(item.iconData, color: color, size: widget.iconSize),
Text(
item.text,
style: TextStyle(color: color),
)
],
),
),
),
),
);
}
В строке 6 мы проверяет соответствие выбранного индекса индексу текущей вкладки, затем выбираем цвет вкладки в соответствии с этим.
В строках с 13 по 26 мы создаём InkWell
внутри виджета Material
. Таким образом мы получаем распознавание жеста onTap
и специальный эффект при нажатии.
Потомком InkWell
является Column
, содержащий Icon
и Text
, оба настроенные на приём данных из FABBottomAppBarItem
.
И всё это завёрнуто внутрь виджета Expanded
. Таким образом каждый элемент имеет одинаковую ширину внутри родительского Row
.
Протестируем FABBottomAppBar
, указав его как bottomNavigationBar
в Scaffold
, прописав четыре элемента:
bottomNavigationBar: FABBottomAppBar(
onTabSelected: _selectedTab,
items: [
FABBottomAppBarItem(iconData: Icons.menu, text: 'This'),
FABBottomAppBarItem(iconData: Icons.layers, text: 'Is'),
FABBottomAppBarItem(iconData: Icons.dashboard, text: 'Bottom'),
FABBottomAppBarItem(iconData: Icons.info, text: 'Bar'),
],
),
Результат:

И всё работает, вкладки переключаются, при выборе происходит вызов callback.
Подводя итоги
Итак, прогресс определённо есть, компонент работает как задумано. Осталось немного доработать FABBottomAppBar
.
В примере выше мы захардкодили такие вещи, как:
- Параметры
BottomAppBar
:height
(высота),backgroundColor
(цвет фона) иnotchedShape
(форма выемки) - Размер иконки
- Цвет активной/неактивной вкладок
Также иконки на вкладках посередине находятся слишком близко к FAB. Нужно добавить небольшой отступ для них.
Финальная версия FABBottomAppBar
с указанными доработками:
import 'package:flutter/material.dart';
class FABBottomAppBarItem {
FABBottomAppBarItem({this.iconData, this.text});
IconData iconData;
String text;
}
class FABBottomAppBar extends StatefulWidget {
FABBottomAppBar({
this.items,
this.centerItemText,
this.height: 60.0,
this.iconSize: 24.0,
this.backgroundColor,
this.color,
this.selectedColor,
this.notchedShape,
this.onTabSelected,
}) {
assert(this.items.length == 2 || this.items.length == 4);
}
final List<FABBottomAppBarItem> items;
final String centerItemText;
final double height;
final double iconSize;
final Color backgroundColor;
final Color color;
final Color selectedColor;
final NotchedShape notchedShape;
final ValueChanged<int> onTabSelected;
@override
State<StatefulWidget> createState() => FABBottomAppBarState();
}
class FABBottomAppBarState extends State<FABBottomAppBar> {
int _selectedIndex = 0;
_updateIndex(int index) {
widget.onTabSelected(index);
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
List<Widget> items = List.generate(widget.items.length, (int index) {
return _buildTabItem(
item: widget.items[index],
index: index,
onPressed: _updateIndex,
);
});
items.insert(items.length >> 1, _buildMiddleTabItem());
return BottomAppBar(
shape: widget.notchedShape,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items,
),
color: widget.backgroundColor,
);
}
Widget _buildMiddleTabItem() {
return Expanded(
child: SizedBox(
height: widget.height,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(height: widget.iconSize),
Text(
widget.centerItemText ?? '',
style: TextStyle(color: widget.color),
),
],
),
),
);
}
Widget _buildTabItem({
FABBottomAppBarItem item,
int index,
ValueChanged<int> onPressed,
}) {
Color color = _selectedIndex == index ? widget.selectedColor : widget.color;
return Expanded(
child: SizedBox(
height: widget.height,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => onPressed(index),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(item.iconData, color: color, size: widget.iconSize),
Text(
item.text,
style: TextStyle(color: color),
)
],
),
),
),
),
);
}
}
Обратите внимание, как мы задаём centerItemText
, который будет размещён прямо под FAB. Если он пустой или null
, мы отобразим пустой Text
.
Вот что получится в итоге:

Что же с BottomNavigationBar?
Я пробовал расположить FAB внутри BottomNavigationBar
, но у меня были такие проблемы:
- Невозможно добавить шаблонный или пустой текст под FAB, кроме как создать
BottomNavigationBarItem
. Но это нежелательно, так какBottomNavigationBarItem
это вкладка и может быть сама по себе задействована. BottomNavigationBar
не поддерживаетnotchedShape
.
В целом BottomAppBar
требует написания большего количества кода, но в результате получается лучше из-за использования более удобного в настройке Row
.
Исходный код
Полный исходный код приложения вы можете скачать на GitHub.
Хорошей разработки!
По материалам поста «Flutter: BottomAppBar Navigation with FAB» by Andrea Bizzotto.
Подписывайтесь на новости Flutter! https://t.me/flutterdaily
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).