Во Flutter для создания панели инструментов используется хорошо всем известный AppBar, ну а когда нам нужна динамическая панель инструментов, которая покажет контент при свайпе, мы используем отличный виджет SliverAppBar.
Оба виджета позволяют сделать приложение чуточку красивее, что во Flutter, без сомнений, весьма просто.
Я видел много вопросов на StackOverflow и в группах Facebook о том, как можно изменить AppBar и SliverAppBar с точки зрения поведения или дизайна.
Давайте рассмотрим две задачи.
Задача 1
Мы хотим создать, не привинченный к верху экрана AppBar, но не так как делаем это обычно. Мы хотим добавить Drawer (боковое меню), на открытие которого AppBar будет реагировать. Вот и всё: наш собственный AppBar с нужными нам размерами.
Проблема в том что, как мы знаем, у AppBar есть размер по умолчанию, и изменить его мы не можем. Заглянув в исходный код, мы видим параметр appBar в Scaffold, видим, что он принимает виджет типа PreferredSizeWidget, теперь просматриваем исходный код AppBar и узнаём, что это только StatefulWidget, который реализует PreferredSizeWidget.
Дело за малым: просто создать наш собственный виджет, который реализует PreferredSizeWidget.
Вот что мы хотим
Как бы так сделать, чтобы при нажатии кнопки меню нашего 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);
}
Результат
Просто, не так ли? Давайте рассмотрим вторую задачу для SliverAppBar.
Задача 2
Как мы знаем, SliverAppBar работает следующим образом:
Что мы хотим, так это поместить Card, встроенную в наш SliverAppBar, как показано на следующем рисунке.
Подождите, но содержимое внутри SliverAppBar обрезается, поэтому не может выйти за рамки, что же делать?
Без паники, давайте посмотрим исходный код 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;
}
Результат
Готово, обе задачи решены.
Примеры лежат в этом репозитории.
Вывод
Часто мы отчаиваемся, когда не находим каких-нибудь свойств у виджета, но надо лишь посмотреть его исходный код, чтобы понять как он реализован во Flutter, открыв для себя таким образом варианты реализации собственных виджетов.