[Перевод] Flutter: прокачиваем AppBar & SliverAppBar

Во Flutter для создания панели инструментов используется хорошо всем известный AppBar, ну, а когда нам нужна динамическая панель инструментов, которая покажет контент при свайпе, мы используем отличный виджет SliverAppBar.

Оба виджета позволяют сделать приложение чуточку красивее, что во Flutter, без сомнений, весьма просто.

Я видел много вопросов на StackOverflow и в группах Facebook о том, как можно изменить AppBar и SliverAppBar с точки зрения поведения или дизайна.

Давайте рассмотрим две задачи.


Задача 1

Мы хотим создать, не привинченный к верху экрана AppBar, но не так как делаем это обычно. Мы хотим добавить Drawer (боковое меню), на открытие которого AppBar будет реагировать. Вот и всё: наш собственный AppBarс нужными нам размерами.

Проблема в том что, как мы знаем, у AppBar есть размер по умолчанию, и изменить его мы не можем. Заглянув в исходный код, мы видим параметр appBar в Scaffold, видим, что он принимает виджет типа PreferredSizeWidget, теперь просматриваем исходный код AppBar и узнаём, что это только StatefulWidget, который реализует PreferredSizeWidget.


e3504c8e4b166c51a7258cd22e822bbe.jpg

Дело за малым: просто создать наш собственный виджет, который реализует PreferredSizeWidget.

Вот что мы хотим


dbb49e2bac7e343c68428b542a192ee3.png

Как бы так сделать, чтобы при нажатии кнопки меню нашего AppBar открывалось боковое меню.

Мы можем сделать это двумя способами:


Используя `AppBar`

Вот так AppBar сможет контролировать открытие бокового меню внутри Scaffold.

class Sample1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        drawer: Drawer(),
        appBar: MyCustomAppBar(
          height: 150,
        ),
        body: Center(
          child: FlutterLogo(
            size: MediaQuery.of(context).size.width / 2,
          ),
        ),
      ),
    );
  }
}

class MyCustomAppBar extends StatelessWidget implements PreferredSizeWidget {
  final double height;

  const MyCustomAppBar({
    Key key,
    @required this.height,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.grey[300],
          child: Padding(
            padding: EdgeInsets.all(30),
            child: AppBar(
                    title: Container(
                      color: Colors.white,
                      child: TextField(
                        decoration: InputDecoration(
                          hintText: "Search",
                          contentPadding: EdgeInsets.all(10),
                        ),
                      ),
                    ),
                    actions: [
                      IconButton(
                        icon: Icon(Icons.verified_user),
                        onPressed: () => null,
                      ),
                    ],
                  ) ,
          ),
        ),
      ],
    );
  }
  
 @override
  Size get preferredSize => Size.fromHeight(height);
}


Используя кастомный виджет

Здесь у нас больше гибкости и можно использовать GlobalKey типа ScaffoldState или InheritedWidget от Scaffold, получив таким образом доступ к методам состояния для открытия Drawer.

import 'package:flutter/material.dart';

class Sample1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        drawer: Drawer(),
        appBar: MyCustomAppBar(
          height: 150,
        ),
        body: Center(
          child: FlutterLogo(
            size: MediaQuery.of(context).size.width / 2,
          ),
        ),
      ),
    );
  }
}

class MyCustomAppBar extends StatelessWidget implements PreferredSizeWidget {
  final double height;

  const MyCustomAppBar({
    Key key,
    @required this.height,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.grey[300],
          child: Padding(
            padding: EdgeInsets.all(30),
            child: Container(
              color: Colors.red,
              padding: EdgeInsets.all(5),
              child: Row(children: [
                IconButton(
                  icon: Icon(Icons.menu),
                  onPressed: () {
                    Scaffold.of(context).openDrawer();
                  },
                ),
                Expanded(
                  child: Container(
                    color: Colors.white,
                    child: TextField(
                      decoration: InputDecoration(
                        hintText: "Search",
                        contentPadding: EdgeInsets.all(10),
                      ),
                    ),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.verified_user),
                  onPressed: () => null,
                ),
              ]),
            ),
          ),
        ),
      ],
    );
  }

  @override
  Size get preferredSize => Size.fromHeight(height);
}


Результат


8e3972730132b88a9d90f21f2a11af0a.gif

Просто, не так ли? Давайте рассмотрим вторую задачу для SliverAppBar.


Задача 2

Как мы знаем, SliverAppBar работает следующим образом:


5bd9caedd5e21c3b5d58835841cf1a66.gif

Что мы хотим, так это поместить Card, встроенную в наш SliverAppBar, как показано на следующем рисунке.


8d0f05d4d129599a2528b731a94b62c8.png

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


79fffe351f127ed40d6fa008a353c1f5.jpg

Без паники, давайте посмотрим исходный код SliverAppBar и, о сюрприз, это StatefulWidget, использующий внутри SliverPersistentHeader, вот и весь секрет.

Мы создадим свой собственный SliverPersistentHeaderDelegate, чтобы использовать SliverPersistentHeader.

class Sample2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Material(
        child: CustomScrollView(
          slivers: [
            SliverPersistentHeader(
              delegate: MySliverAppBar(expandedHeight: 200),
              pinned: true,
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (_, index) => ListTile(
                      title: Text("Index: $index"),
                    ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

class MySliverAppBar extends SliverPersistentHeaderDelegate {
  final double expandedHeight;

  MySliverAppBar({@required this.expandedHeight});

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Stack(
      fit: StackFit.expand,
      overflow: Overflow.visible,
      children: [
        Image.network(
          "https://images.pexels.com/photos/396547/pexels-photo-396547.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
          fit: BoxFit.cover,
        ),
        Center(
          child: Opacity(
            opacity: shrinkOffset / expandedHeight,
            child: Text(
              "MySliverAppBar",
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.w700,
                fontSize: 23,
              ),
            ),
          ),
        ),
        Positioned(
          top: expandedHeight / 2 - shrinkOffset,
          left: MediaQuery.of(context).size.width / 4,
          child: Opacity(
            opacity: (1 - shrinkOffset / expandedHeight),
            child: Card(
              elevation: 10,
              child: SizedBox(
                height: expandedHeight,
                width: MediaQuery.of(context).size.width / 2,
                child: FlutterLogo(),
              ),
            ),
          ),
        ),
      ],
    );
  }

  @override
  double get maxExtent => expandedHeight;

  @override
  double get minExtent => kToolbarHeight;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
}


Результат


92b895eef575b82b3fb02c0140691052.gif

Готово, обе задачи решены.

Примеры лежат в этом репозитории.


Вывод

Часто мы отчаиваемся, когда не находим каких-нибудь свойств у виджета, но надо лишь посмотреть его исходный код, чтобы понять как он реализован во Flutter, открыв для себя таким образом варианты реализации собственных виджетов.

© Habrahabr.ru