Flutter: створення свого Scaffold з випадним меню

16 хв. читання

Для своєї гри Дике Поле, написаної на Flutter, я спочатку використовував стандартний Material App Bar. Але він займав багато місця зверху і до нього неможливо було додати випадне меню. Тому я вирішив зробити свій Scaffold-віджет, в якому :

  • було всього декілька вхідних змінних для конструктора віджета: заголовок і список елементів для випадного меню.
  • панель програми після тапу на кнопку «Меню» розкривається і показує всі задані елементи меню.
  • панель програми закривається, якщо користувач натиснув десь поза межами меню.
  • частина дисплею, яку не закриває меню, має бути розмита і трошки затемнена, коли панель програми розкрита.

Ось який вигляд це має в дії:

custom_appbar.gif

І разом з Row в якості заголовка програми: scrollable_appbar.gif

А тут копія програми, яку ви можете спробувати онлайн: https://dartpad.dev/31c5f5035d02ece1c18433a4caf3444d

Почнемо з простого

Зазвичай я починаю проєктування своїх віджетів з погляду програміста-користувача. Я визначаю мінімальний набір вхідних для конструктора віджета даних, які хотів би надавати віджету. Далі вся реалізація віджета має бути захована в ньому самому.

Для нашого NarrowScaffold-віджета нам знадобляться лише такі параметри конструктора:

  • String: Widget title. Рядок, який буде рендеритися в Text-віджеті у заголовку програми.
  • Widget: titleView. Готовий Widget, який буде показуватися в заголовку програми. Опціональний. Якщо він є, то параметр String title ігнорується.
  • List actions[]. Список для меню. Він має показуватися, коли користувач натиснув кнопку «Меню». Кожен елемент списку повинен мати callback-функцію, яка буде викликана NarrowScaffold-віджетом, якщо користувач вибрав елемент меню.
  • Widget body. Тіло програми, яке, в принципі, і є програмою :)

Ось таким воно має бути з погляду користувача віджета:

NarrowScaffold(
          body: Center(
            child: Text(message),  
          ),
          title: 'My Custom App Bar Title',
          actions: [ ]),

Вся реалізація буде захована всередині.

Реалізація

Отож ми маємо розмістити на екрані мінімум два віджети: перший — це тіло програми, другий — заголовок, де і розташована кнопка меню.

Щоб розмістити ці віджети, ми використаємо віджет Stack. Він дозволяє через Positioned-віджет визначати місце, куди буде рендеритись конкретний дочірній віджет.

Метод build нашого віджета буде таким:

Scaffold(
      backgroundColor: Theme.of(context).backgroundColor,
      body: SafeArea(
        child: Stack(
          children: [
          // body,
          // app bar
           ]
      ),
  ),
)

Дочірні віджети рендеряться за таким правилом: перша дочка в списку буде рендериться під іншими дочками, які є в колекції. Тобто, щоб Widget A перекрив Widget B, треба, щоб колекція мала такий вигляд:

[Widget B, Widget A]

Також, знаючи бажану висоту панелі програми, ми можемо через Padding-віджет зробити відступ для тіла програми, аби панель не перекрила її. Або ж використати Positioned-віджет.

Scaffold(
      backgroundColor: Theme.of(context).backgroundColor,
      body: SafeArea(
        child: Stack(
          children: [
          Padding(
                padding: EdgeInsets.only(top: APP_BAR_HEIGHT + 8),
                child: BorderedContainer(
                  child: widget.body,
                )),
          // app bar
           ]
      ),
  ),
)

Додаємо згортання меню, якщо користувач натиснув поза його межами

Тепер покращимо наш віджет і додамо прозорий контейнер, який би перехоплював натискання користувача і закривав меню (якщо воно відкрите). Тепер NarrowScaffold має знати, чи відкрите меню, щоб передавати ці налаштування далі в глиб віджет-дерева.

Цю зміну додаємо в наш стан віджета:

  bool expanded = false;

А сам контейнер додаємо в колекцію children між тілом і заголовком. Також цей контейнер має займати всю висоту пристрою і розмивати віджет, який намальований під ним.

Scaffold(
      backgroundColor: Theme.of(context).backgroundColor,
      body: SafeArea(
        child: Stack(
          children: [
            Padding(
                padding: EdgeInsets.only(top: APP_BAR_HEIGHT + 8),
                child: BorderedContainer(
                  child: widget.body,
                )),
            if (expanded)
              SizedBox(
                height: MediaQuery.of(context).size.height,
                child: new BackdropFilter(
                  filter: new ui.ImageFilter.blur(sigmaX: 1.5, sigmaY: 1.5),
                  child: new Container(
                    decoration: new BoxDecoration(
                      color: Colors.black.withOpacity(0.2),
                    ),
                    child: Container(child: GestureDetector(onTap: () {
                      setState(() {
                        expanded = false;
                      });
                    })),
                  ),
                ),
              ),
            // app bar
          ],
        ),
      ),

Додаємо App Bar з двома режимами роботи: розкрите меню і мінімізоване

Ну а тепер головне: віджет панель програми.

Віджет має:

  • показувати заголовок;
  • показувати кнопку меню;
  • реагувати на натискання кнопки або на зміну expanded;
  • автоматично ховатися, якщо користувач натиснув на якийсь з елементів в меню.

З боку API це матиме такий вигляд:

 AppBarCustom(
              title: widget.title,
              titleView: widget.titleView,
              appBarButtons: widget.actions,
              expanded: expanded,
              onExpanded: (expand) {
                setState(() {
                  expanded = expand;
                });
              },
            ),

Для наших appBarButtons (елементів меню) ми не можемо просто додати якісь кнопки на кшталт IconButton, FlatButton. Необхідно слухати події натискання на них і реагувати відповідно: закриванням меню і викликом зворотної функції, яку програміст передав ще в NarrowScaffold-віджет.

Якби ми просто прокидували список віджет-кнопок, то наше меню не могло б перехоплювати події та реагувати на них. Ми б мали в NarrowScaffold додавати ще одну зміну стану: згорнути/розгорнути меню. Але це зайве навантаження на програміста, якому немає діла до внутрішнього стану такого віджета.

Тож для цих потреб ми створимо клас-контейнер:

class AppBarObject {
  final VoidCallback onTap;
  final String text;

  AppBarObject({@required this.text, @required this.onTap});
}

І тепер ми можемо передати список з його екземплярами в NarrowScaffold:

NarrowScaffold(
          body: Center(
            child: Text(message,
                style: TextStyle(
                    color: Colors.black,
                    fontSize: 30,
                    fontWeight: FontWeight.bold)),
          ),
          title: 'My Custom App Bar Title',
          actions: [
            AppBarObject(
              text: "The First Menu Item",
              onTap: () => {
                setState(() {
                  message = 'The First Menu';
                })
              },
            ),

Ця колекція буде прокинута в глиб віджета до AppBarCustom.

Рендер AppBarCustom доволі великий, тому тут я покажу лише важливі елементи:

  • висота панелі залежить від кількості елементів в меню. В мініфікованому режимі висота — це наша задана константа.
 height: widget.expanded
              ? (widget.appBarButtons.length * 50).toDouble() + APP_BAR_HEIGHT
              : APP_BAR_HEIGHT,
  • якщо віджет розкрито, тоді ми маємо показати меню з елементами колекції AppBarObjects:
                SizedBox(
                  height: (widget.appBarButtons.length * 50).toDouble(),
                  width: MediaQuery.of(context).size.width,
                  child: ListView(
                    scrollDirection: Axis.vertical,
                    children: widget.appBarButtons
                        .map((obj) => _appBarObjectToButton(obj, context))
                        .toList(),
                  ),
                ),

Доньки ListView — це наші специфічні проксі-кнопки:

class AppBarButton extends StatelessWidget {
  final VoidCallback onTap;
  final Color color;
  final String text;

  AppBarButton({this.onTap, this.color, this.text});

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      splashColor: color,
      onPressed: onTap,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Center(
          child: Text(
            text,
            style: TextStyle(
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
        ),
      ),
    );
  }
}

Використовуючи трошки приколів функціонального програмування, ми зв'язуємо AppBarObject зі звичайною кнопкою, яка показується в меню NarrowScaffold widget:

 Widget _appBarObjectToButton(AppBarObject object, BuildContext context) {
    return AppBarButton(
      onTap: _callbackerHandler(object.onTap),
      text: object.text,
      color: Theme.of(context).primaryColor,
    );
  }

  _callbackerHandler(VoidCallback callback) {
    return () {
      _toggleExpandedMenu();
      callback();
    };
  }

Ми перехоплюємо натискання на екран для того, щоб сховати меню, і тільки після цього викликаємо справжній оброблювач події, яку задав нам програміст в конструкторі NarrowScaffold.

Висота цього віджета гнучка, вона залежить від кількості елементі в меню.

Бонус

Ви також можете передати конкретний віджет, який буде показаний замість простого текстового віджета в панелі програми. Ось, наприклад, SingleChildScrollView в якості titleView:

scroll_view_in_title.gif

Підсумки

Наш кастомний Scaffold:

  • має меню з вибором дій для користувача;
  • може розкриватися/схлопуватися;
  • вміщає будь-яку кількість елементів меню;
  • створює прикольний ефект, коли меню розкрите і весь інший екран стає розмитим і трошки приглушеним;
  • надає надзвичайно простий вхідний інтерфейс для побудови такої складної структури віджетів.

Живий онлайн-приклад

Копія на гітхабі

Моя інтерактивна історія-гра, де ви можете помацати все живцем на iOS/Android/Web (там також почав робити гру в стилі city building): Loca Deserta

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація