Flutter: реализация BottomAppBar с FAB

Flutter: BottomAppBar с FAB

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

Flutter: реализация BottomAppBar с FAB

Наша задача — сделать 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.

Если мы запустим приложение сейчас, вот что должно получиться:

Flutter: реализация 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'),
  ],
),

Результат:

Flutter: реализация BottomAppBar с FAB

И всё работает, вкладки переключаются, при выборе происходит вызов callback.

Подводя итоги

Итак, прогресс определённо есть, компонент работает как задумано. Осталось немного доработать FABBottomAppBar.

В примере выше мы захардкодили такие вещи, как:

  • ПараметрыBottomAppBarheight (высота), 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.

Вот что получится в итоге:

Flutter: реализация BottomAppBar с FAB

Что же с BottomNavigationBar?

Я пробовал расположить FAB внутри BottomNavigationBar, но у меня были такие проблемы:

  • Невозможно добавить шаблонный или пустой текст под FAB, кроме как создать BottomNavigationBarItem. Но это нежелательно, так как BottomNavigationBarItem это вкладка и может быть сама по себе задействована.
  • BottomNavigationBar не поддерживает notchedShape.

В целом BottomAppBar требует написания большего количества кода, но в результате получается лучше из-за использования более удобного в настройке Row.

Исходный код

Полный исходный код приложения вы можете скачать на GitHub

Хорошей разработки!

По материалам поста «Flutter: BottomAppBar Navigation with FAB» by Andrea Bizzotto.

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Flutter: реализация BottomAppBar с FAB

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).

Adblock detector