Во Flutter для создания панели инструментов используется хорошо всем известный AppBar, ну а когда нам нужна динамическая панель инструментов, которая покажет контент при свайпе, мы используем отличный виджет SliverAppBar.
Оба виджета позволяют сделать приложение чуточку красивее, что во Flutter, без сомнений, весьма просто.
Я видел много вопросов на StackOverflow и в группах Facebook о том, как можно изменить AppBar и SliverAppBar с точки зрения поведения или дизайна.
Давайте рассмотрим две задачи.
Задача 1
Мы хотим создать, не привинченный к верху экрана AppBar, но не так как делаем это обычно. Мы хотим добавить Drawer (боковое меню), на открытие которого AppBar будет реагировать. Вот и всё: наш собственный AppBar с нужными нам размерами.
Проблема в том что, как мы знаем, у AppBar есть размер по умолчанию, и изменить его мы не можем. Заглянув в исходный код, мы видим параметр appBar в Scaffold, видим, что он принимает виджет типа PreferredSizeWidget, теперь просматриваем исходный код AppBar и узнаём, что это только StatefulWidget, который реализует PreferredSizeWidget.
![](https://habrastorage.org/getpro/habr/post_images/e35/04c/8e4/e3504c8e4b166c51a7258cd22e822bbe.jpg)
Дело за малым: просто создать наш собственный виджет, который реализует PreferredSizeWidget.
Вот что мы хотим
![](https://habrastorage.org/getpro/habr/post_images/dbb/49e/2ba/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);
}
Результат
![](https://habrastorage.org/getpro/habr/post_images/8e3/972/730/8e3972730132b88a9d90f21f2a11af0a.gif)
Просто, не так ли? Давайте рассмотрим вторую задачу для SliverAppBar.
Задача 2
Как мы знаем, SliverAppBar работает следующим образом:
![](https://habrastorage.org/getpro/habr/post_images/5bd/9ca/edd/5bd9caedd5e21c3b5d58835841cf1a66.gif)
Что мы хотим, так это поместить Card, встроенную в наш SliverAppBar, как показано на следующем рисунке.
![](https://habrastorage.org/getpro/habr/post_images/8d0/f05/d4d/8d0f05d4d129599a2528b731a94b62c8.png)
Подождите, но содержимое внутри SliverAppBar обрезается, поэтому не может выйти за рамки, что же делать?
![](https://habrastorage.org/getpro/habr/post_images/79f/ffe/351/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;
}
Результат
![](https://habrastorage.org/getpro/habr/post_images/92b/895/eef/92b895eef575b82b3fb02c0140691052.gif)
Готово, обе задачи решены.
Примеры лежат в этом репозитории.
Вывод
Часто мы отчаиваемся, когда не находим каких-нибудь свойств у виджета, но надо лишь посмотреть его исходный код, чтобы понять как он реализован во Flutter, открыв для себя таким образом варианты реализации собственных виджетов.