[adaptive_scaffold] New and Improved Helper Widgets & Updated Examples and Their Tests (#2455)
Co-authored-by: Serena Behera <serenabehera@google.com>
diff --git a/packages/adaptive_scaffold/CHANGELOG.md b/packages/adaptive_scaffold/CHANGELOG.md
index c9d596f..b98b804 100644
--- a/packages/adaptive_scaffold/CHANGELOG.md
+++ b/packages/adaptive_scaffold/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.0.2
+
+* Adds some more examples.
+
## 0.0.1+1
* Updates text theme parameters to avoid deprecation issues.
diff --git a/packages/adaptive_scaffold/README.md b/packages/adaptive_scaffold/README.md
index ba3f7c1..1983704 100644
--- a/packages/adaptive_scaffold/README.md
+++ b/packages/adaptive_scaffold/README.md
@@ -1,3 +1,5 @@
+<?code-excerpt path-base="excerpts/packages/adaptive_scaffold_example"?>
+
# Helper Widgets for Making Adaptive Layouts in Flutter (AdaptiveScaffold)
This package contains some helper widgets that make the process of developing adaptive layouts easier, especially with navigational elements.
@@ -12,22 +14,55 @@
AdaptiveScaffold implements the basic visual layout structure for Material Design 3 that adapts to a variety of screens. It provides a preset of layout, including positions and animations, by handling macro changes in navigational elements and bodies based on the current features of the screen, namely screen width and platform. For example, the navigational elements would be a BottomNavigationBar on a small mobile device and a NavigationRail on larger devices. The body is the primary screen that takes up the space left by the navigational elements. The secondaryBody acts as an option to split the space between two panes for purposes such as having a detail view. There is some automatic functionality with foldables to handle the split between panels properly. AdaptiveScaffold is much simpler to use but is not the best if you would like high customizability. Apps that would like more refined layout and/or animation should use AdaptiveLayout.
### Example Usage:
-<?code-excerpt ...>
+<?code-excerpt "adaptive_scaffold_demo.dart (Example)"?>
```dart
- AdaptiveScaffold(
- destinations: const [
- NavigationDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
- NavigationDestination(icon: Icon(Icons.article), label: 'Articles'),
- NavigationDestination(icon: Icon(Icons.chat), label: 'Chat'),
- NavigationDestination(icon: Icon(Icons.video_call), label: 'Video'),
- ],
- smallBody: (_) => ListView.builder(
- itemCount: children.length,
- itemBuilder: (_, idx) => children[idx]
- ),
- body: (_) => GridView.count(crossAxisCount: 2, children: children),
- )
+ @override
+ Widget build(BuildContext context) {
+ // Define the children to display within the body at different breakpoints.
+ final List<Widget> children = <Widget>[
+ for (int i = 0; i < 10; i++)
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Container(
+ color: const Color.fromARGB(255, 255, 201, 197), height: 400))
+ ];
+
+ return BottomNavigationBarTheme(
+ data: const BottomNavigationBarThemeData(
+ unselectedItemColor: Colors.black,
+ selectedItemColor: Colors.black,
+ backgroundColor: Colors.white),
+ child: AdaptiveScaffold(
+ // An option to override the default breakpoints used for small, medium,
+ // and large.
+ smallBreakpoint: const WidthPlatformBreakpoint(end: 700),
+ mediumBreakpoint:
+ const WidthPlatformBreakpoint(begin: 700, end: 1000),
+ largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000),
+ useDrawer: false,
+ destinations: const <NavigationDestination>[
+ NavigationDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
+ NavigationDestination(
+ icon: Icon(Icons.article), label: 'Articles'),
+ NavigationDestination(icon: Icon(Icons.chat), label: 'Chat'),
+ NavigationDestination(
+ icon: Icon(Icons.video_call), label: 'Video')
+ ],
+ body: (_) => GridView.count(crossAxisCount: 2, children: children),
+ smallBody: (_) => ListView.builder(
+ itemCount: children.length,
+ itemBuilder: (_, int idx) => children[idx]),
+ // Define a default secondaryBody.
+ secondaryBody: (_) =>
+ Container(color: const Color.fromARGB(255, 234, 158, 192)),
+ // Override the default secondaryBody during the smallBreakpoint to be
+ // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
+ // overriden.
+ smallSecondaryBody: AdaptiveScaffold.emptyBuilder));
+ }
+}
```
+
## The Background Widget Suite
These are the set of widgets that are used on a lower level and offer more customizability at a cost of more lines of code.
#### AdaptiveLayout:
@@ -39,56 +74,88 @@
SlotLayout.from creates a SlotLayoutConfig holds the actual widget to be displayed and the entrance animation and exit animation.
### Example Usage:
-<?code-excerpt ...>
+<?code-excerpt "adaptive_layout_demo.dart (Example)"?>
```dart
-AdaptiveLayout(
- primaryNavigation: SlotLayout(
- config: {
- Breakpoints.small: SlotLayout.from(key: const Key('pnav'), builder: (_) => const SizedBox.shrink()),
- Breakpoints.medium: SlotLayout.from(
- inAnimation: leftOutIn,
- key: const Key('pnav1'),
- builder: (_) => AdaptiveScaffold.toNavigationRail(destinations: destinations),
- ),
- Breakpoints.large: SlotLayout.from(
- key: const Key('pnav2'),
- inAnimation: leftOutIn,
- builder: (_) => AdaptiveScaffold.toNavigationRail(extended: true, destinations: destinations),
- ),
- },
- ),
- body: SlotLayout(
- config: {
- Breakpoints.small: SlotLayout.from(
- key: const Key('body'),
- builder: (_) => ListView.builder(
- itemCount: children.length,
- itemBuilder: (_, idx) => children[idx]
- ),
- ),
- Breakpoints.medium: SlotLayout.from(
- key: const Key('body1'),
- builder: (_) => GridView.count(
- crossAxisCount: 2,
- children: children
- ),
- ),
- },
- ),
- bottomNavigation: SlotLayout(
- config: {
- Breakpoints.small: SlotLayout.from(
- key: const Key('botnav'),
- inAnimation: bottomToTop,
- builder: (_) => AdaptiveScaffold.toBottomNavigationBar(destinations: destinations),
- ),
- },
- ),
-)
+ // AdaptiveLayout has a number of slots that take SlotLayouts and these
+ // SlotLayouts' configs take maps of Breakpoints to SlotLayoutConfigs.
+ return AdaptiveLayout(
+ // Primary navigation config has nothing from 0 to 600 dp screen width,
+ // then an unextended NavigationRail with no labels and just icons then an
+ // extended NavigationRail with both icons and labels.
+ primaryNavigation: SlotLayout(
+ config: <Breakpoint, SlotLayoutConfig>{
+ Breakpoints.medium: SlotLayout.from(
+ inAnimation: AdaptiveScaffold.leftOutIn,
+ key: const Key('pnav1'),
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ leading: const Icon(Icons.menu),
+ destinations: destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList()),
+ ),
+ Breakpoints.large: SlotLayout.from(
+ key: const Key('pn1'),
+ inAnimation: AdaptiveScaffold.leftOutIn,
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ extended: true,
+ leading: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: const <Widget>[
+ Text('REPLY',
+ style:
+ TextStyle(color: Color.fromARGB(255, 255, 201, 197))),
+ Icon(Icons.menu_open)
+ ],
+ ),
+ destinations: destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList(),
+ trailing: trailingNavRail,
+ ),
+ ),
+ },
+ ),
+ // Body switches between a ListView and a GridView from small to medium
+ // breakpoints and onwards.
+ body: SlotLayout(
+ config: <Breakpoint, SlotLayoutConfig>{
+ Breakpoints.small: SlotLayout.from(
+ key: const Key('body'),
+ builder: (_) => ListView.builder(
+ itemCount: children.length,
+ itemBuilder: (BuildContext context, int index) => children[index],
+ ),
+ ),
+ Breakpoints.mediumAndUp: SlotLayout.from(
+ key: const Key('body1'),
+ builder: (_) =>
+ GridView.count(crossAxisCount: 2, children: children),
+ )
+ },
+ ),
+ // BottomNavigation is only active in small views defined as under 600 dp
+ // width.
+ bottomNavigation: SlotLayout(
+ config: <Breakpoint, SlotLayoutConfig>{
+ Breakpoints.small: SlotLayout.from(
+ key: const Key('bn'),
+ inAnimation: AdaptiveScaffold.bottomToTop,
+ outAnimation: AdaptiveScaffold.topToBottom,
+ builder: (_) => BottomNavigationBarTheme(
+ data: const BottomNavigationBarThemeData(
+ selectedItemColor: Colors.black),
+ child: AdaptiveScaffold.standardBottomNavigationBar(
+ destinations: destinations)),
+ )
+ },
+ ),
+ );
+ }
+}
```
##
Both of the examples shown here produce the same output:

## Additional information
-You can find more information on this package and its usage in the public [design doc](https://docs.google.com/document/d/1qhrpTWYs5f67X8v32NCCNTRMIjSrVHuaMEFAul-Q_Ms/edit?usp=sharing)
+You can find more information on this package and its usage in the public [design doc](https://docs.google.com/document/d/1qhrpTWYs5f67X8v32NCCNTRMIjSrVHuaMEFAul-Q_Ms/edit?usp=sharing).
diff --git a/packages/adaptive_scaffold/example/build.excerpt.yaml b/packages/adaptive_scaffold/example/build.excerpt.yaml
new file mode 100644
index 0000000..5af6123
--- /dev/null
+++ b/packages/adaptive_scaffold/example/build.excerpt.yaml
@@ -0,0 +1,16 @@
+targets:
+ $default:
+ sources:
+ include:
+ - lib/**
+ # Some default includes that aren't really used here but will prevent
+ # false-negative warnings:
+ - $package$
+ - lib/$lib$
+ exclude:
+ - '**/.*/**'
+ - '**/build/**'
+ builders:
+ code_excerpter|code_excerpter:
+ enabled: true
+
\ No newline at end of file
diff --git a/packages/adaptive_scaffold/example/images/avocado.png b/packages/adaptive_scaffold/example/images/avocado.png
new file mode 100644
index 0000000..16ec1e7
--- /dev/null
+++ b/packages/adaptive_scaffold/example/images/avocado.png
Binary files differ
diff --git a/packages/adaptive_scaffold/example/images/habanero.png b/packages/adaptive_scaffold/example/images/habanero.png
new file mode 100644
index 0000000..3d85abd
--- /dev/null
+++ b/packages/adaptive_scaffold/example/images/habanero.png
Binary files differ
diff --git a/packages/adaptive_scaffold/example/images/mushroom.png b/packages/adaptive_scaffold/example/images/mushroom.png
new file mode 100644
index 0000000..a12fc69
--- /dev/null
+++ b/packages/adaptive_scaffold/example/images/mushroom.png
Binary files differ
diff --git a/packages/adaptive_scaffold/example/images/plum.png b/packages/adaptive_scaffold/example/images/plum.png
new file mode 100644
index 0000000..743c7a4
--- /dev/null
+++ b/packages/adaptive_scaffold/example/images/plum.png
Binary files differ
diff --git a/packages/adaptive_scaffold/example/images/potato.png b/packages/adaptive_scaffold/example/images/potato.png
new file mode 100644
index 0000000..0f09e21
--- /dev/null
+++ b/packages/adaptive_scaffold/example/images/potato.png
Binary files differ
diff --git a/packages/adaptive_scaffold/example/images/strawberry.png b/packages/adaptive_scaffold/example/images/strawberry.png
new file mode 100644
index 0000000..feecd8a
--- /dev/null
+++ b/packages/adaptive_scaffold/example/images/strawberry.png
Binary files differ
diff --git a/packages/adaptive_scaffold/example/lib/adaptive_layout_demo.dart b/packages/adaptive_scaffold/example/lib/adaptive_layout_demo.dart
index efe3c5a..6c122bb 100644
--- a/packages/adaptive_scaffold/example/lib/adaptive_layout_demo.dart
+++ b/packages/adaptive_scaffold/example/lib/adaptive_layout_demo.dart
@@ -39,6 +39,71 @@
);
});
+ final Widget trailingNavRail = Column(
+ children: <Widget>[
+ const Divider(color: Colors.black),
+ const SizedBox(height: 10),
+ Row(
+ children: const <Widget>[
+ SizedBox(
+ width: 27,
+ ),
+ Text('Folders', style: TextStyle(fontSize: 16)),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Text('Freelance'),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Text('Mortage'),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Flexible(
+ child: Text('Taxes', overflow: TextOverflow.ellipsis)),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Flexible(
+ child: Text('Receipts', overflow: TextOverflow.ellipsis)),
+ ],
+ ),
+ ],
+ );
+
// Define the list of destinations to be used within the app.
const List<NavigationDestination> destinations = <NavigationDestination>[
NavigationDestination(
@@ -54,6 +119,7 @@
icon: Icon(Icons.video_call_outlined, color: Colors.black)),
];
+ // #docregion Example
// AdaptiveLayout has a number of slots that take SlotLayouts and these
// SlotLayouts' configs take maps of Breakpoints to SlotLayoutConfigs.
return AdaptiveLayout(
@@ -61,49 +127,73 @@
// then an unextended NavigationRail with no labels and just icons then an
// extended NavigationRail with both icons and labels.
primaryNavigation: SlotLayout(
- config: <Breakpoint, SlotLayoutConfig?>{
+ config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.medium: SlotLayout.from(
inAnimation: AdaptiveScaffold.leftOutIn,
- key: const Key('pn'),
- builder: (_) =>
- AdaptiveScaffold.toNavigationRail(destinations: destinations),
+ key: const Key('pnav1'),
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ leading: const Icon(Icons.menu),
+ destinations: destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList()),
),
Breakpoints.large: SlotLayout.from(
key: const Key('pn1'),
inAnimation: AdaptiveScaffold.leftOutIn,
- builder: (_) => AdaptiveScaffold.toNavigationRail(
- extended: true, destinations: destinations),
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ extended: true,
+ leading: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: const <Widget>[
+ Text('REPLY',
+ style:
+ TextStyle(color: Color.fromARGB(255, 255, 201, 197))),
+ Icon(Icons.menu_open)
+ ],
+ ),
+ destinations: destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList(),
+ trailing: trailingNavRail,
+ ),
),
},
),
// Body switches between a ListView and a GridView from small to medium
// breakpoints and onwards.
body: SlotLayout(
- config: <Breakpoint, SlotLayoutConfig?>{
+ config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.small: SlotLayout.from(
key: const Key('body'),
builder: (_) => ListView.builder(
- itemCount: 10, itemBuilder: (_, int idx) => children[idx]),
+ itemCount: children.length,
+ itemBuilder: (BuildContext context, int index) => children[index],
+ ),
),
Breakpoints.mediumAndUp: SlotLayout.from(
key: const Key('body1'),
builder: (_) =>
GridView.count(crossAxisCount: 2, children: children),
- ),
+ )
},
),
// BottomNavigation is only active in small views defined as under 600 dp
// width.
bottomNavigation: SlotLayout(
- config: <Breakpoint, SlotLayoutConfig?>{
+ config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.small: SlotLayout.from(
key: const Key('bn'),
inAnimation: AdaptiveScaffold.bottomToTop,
- builder: (_) => AdaptiveScaffold.toBottomNavigationBar(
- destinations: destinations),
- ),
+ outAnimation: AdaptiveScaffold.topToBottom,
+ builder: (_) => BottomNavigationBarTheme(
+ data: const BottomNavigationBarThemeData(
+ selectedItemColor: Colors.black),
+ child: AdaptiveScaffold.standardBottomNavigationBar(
+ destinations: destinations)),
+ )
},
),
);
+ // #enddocregion
}
}
diff --git a/packages/adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart b/packages/adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart
index c23af35..b7f53b4 100644
--- a/packages/adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart
+++ b/packages/adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart
@@ -14,9 +14,7 @@
@override
Widget build(BuildContext context) {
- return const MaterialApp(
- home: MyHomePage(),
- );
+ return const MaterialApp(home: MyHomePage());
}
}
@@ -26,47 +24,50 @@
/// Creates a const [MyHomePage].
const MyHomePage({Key? key}) : super(key: key);
+ // #docregion Example
@override
Widget build(BuildContext context) {
// Define the children to display within the body at different breakpoints.
final List<Widget> children = <Widget>[
for (int i = 0; i < 10; i++)
Padding(
- padding: const EdgeInsets.all(8.0),
- child: Container(
- color: const Color.fromARGB(255, 255, 201, 197),
- height: 400,
- ),
- )
+ padding: const EdgeInsets.all(8.0),
+ child: Container(
+ color: const Color.fromARGB(255, 255, 201, 197), height: 400))
];
return BottomNavigationBarTheme(
- data: const BottomNavigationBarThemeData(
- unselectedItemColor: Colors.black, selectedItemColor: Colors.black),
- child: AdaptiveScaffold(
- // An option to override the default breakpoints used for small, medium,
- // and large.
- smallBreakpoint: const WidthPlatformBreakpoint(end: 700),
- mediumBreakpoint: const WidthPlatformBreakpoint(begin: 700, end: 1000),
- largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000),
- useDrawer: false,
- destinations: const <NavigationDestination>[
- NavigationDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
- NavigationDestination(icon: Icon(Icons.article), label: 'Articles'),
- NavigationDestination(icon: Icon(Icons.chat), label: 'Chat'),
- NavigationDestination(icon: Icon(Icons.video_call), label: 'Video'),
- ],
- body: (_) => GridView.count(crossAxisCount: 2, children: children),
- smallBody: (_) => ListView.builder(
- itemCount: 10, itemBuilder: (_, int idx) => children[idx]),
- // Define a default secondaryBody.
- secondaryBody: (_) =>
- Container(color: const Color.fromARGB(255, 234, 158, 192)),
- // Override the default secondaryBody during the smallBreakpoint to be
- // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
- // overriden.
- smallSecondaryBody: AdaptiveScaffold.emptyBuilder,
- ),
- );
+ data: const BottomNavigationBarThemeData(
+ unselectedItemColor: Colors.black,
+ selectedItemColor: Colors.black,
+ backgroundColor: Colors.white),
+ child: AdaptiveScaffold(
+ // An option to override the default breakpoints used for small, medium,
+ // and large.
+ smallBreakpoint: const WidthPlatformBreakpoint(end: 700),
+ mediumBreakpoint:
+ const WidthPlatformBreakpoint(begin: 700, end: 1000),
+ largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000),
+ useDrawer: false,
+ destinations: const <NavigationDestination>[
+ NavigationDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
+ NavigationDestination(
+ icon: Icon(Icons.article), label: 'Articles'),
+ NavigationDestination(icon: Icon(Icons.chat), label: 'Chat'),
+ NavigationDestination(
+ icon: Icon(Icons.video_call), label: 'Video')
+ ],
+ body: (_) => GridView.count(crossAxisCount: 2, children: children),
+ smallBody: (_) => ListView.builder(
+ itemCount: children.length,
+ itemBuilder: (_, int idx) => children[idx]),
+ // Define a default secondaryBody.
+ secondaryBody: (_) =>
+ Container(color: const Color.fromARGB(255, 234, 158, 192)),
+ // Override the default secondaryBody during the smallBreakpoint to be
+ // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
+ // overriden.
+ smallSecondaryBody: AdaptiveScaffold.emptyBuilder));
}
+ // #enddocregion
}
diff --git a/packages/adaptive_scaffold/example/lib/main.dart b/packages/adaptive_scaffold/example/lib/main.dart
index 31bc684..f68b668 100644
--- a/packages/adaptive_scaffold/example/lib/main.dart
+++ b/packages/adaptive_scaffold/example/lib/main.dart
@@ -115,6 +115,67 @@
@override
Widget build(BuildContext context) {
const Color iconColor = Color.fromARGB(255, 29, 25, 43);
+ final Widget trailingNavRail = Column(
+ children: <Widget>[
+ const Divider(color: Colors.white, thickness: 1.5),
+ const SizedBox(height: 10),
+ Row(children: <Widget>[
+ const SizedBox(width: 22),
+ Text('Folders',
+ style: TextStyle(fontSize: 13, color: Colors.grey[700]))
+ ]),
+ const SizedBox(height: 22),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Text('Freelance', overflow: TextOverflow.ellipsis),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Text('Mortage', overflow: TextOverflow.ellipsis),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Flexible(
+ child: Text('Taxes', overflow: TextOverflow.ellipsis))
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: <Widget>[
+ const SizedBox(width: 16),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.folder_copy_outlined),
+ iconSize: 21),
+ const SizedBox(width: 21),
+ const Flexible(
+ child: Text('Receipts', overflow: TextOverflow.ellipsis))
+ ],
+ ),
+ ],
+ );
// These are the destinations used within the AdaptiveScaffold navigation
// builders.
@@ -129,7 +190,7 @@
icon: Icon(Icons.chat_bubble_outline, color: iconColor)),
NavigationDestination(
label: 'Video',
- icon: Icon(Icons.video_call_outlined, color: iconColor)),
+ icon: Icon(Icons.video_call_outlined, color: iconColor))
];
// Updating the listener value.
@@ -150,13 +211,11 @@
// Every SlotLayoutConfig takes a key and a builder. The builder
// is to save memory that would be spent on initialization.
key: const Key('primaryNavigation'),
- builder: (_) => SizedBox(
- width: 72,
- height: MediaQuery.of(context).size.height,
- // Usually it would be easier to use a builder from
- // AdaptiveScaffold for these types of navigations but this
- // navigation has custom staggered item animations.
- child: NavigationRail(
+ builder: (_) {
+ return AdaptiveScaffold.standardNavigationRail(
+ // Usually it would be easier to use a builder from
+ // AdaptiveScaffold for these types of navigations but this
+ // navigation has custom staggered item animations.
onDestinationSelected: (int index) {
setState(() {
_navigationIndex = index;
@@ -164,9 +223,7 @@
},
selectedIndex: _navigationIndex,
leading: ScaleTransition(
- scale: _controller1,
- child: const _ComposeIcon(),
- ),
+ scale: _controller1, child: const _MediumComposeIcon()),
backgroundColor: const Color.fromARGB(0, 255, 255, 255),
labelType: NavigationRailLabelType.none,
destinations: <NavigationRailDestination>[
@@ -195,23 +252,26 @@
label: 'Video',
)
],
- ),
- ),
+ );
+ },
),
Breakpoints.large: SlotLayout.from(
key: const Key('primaryNavigation1'),
// The AdaptiveScaffold builder here greatly simplifies
// navigational elements.
- builder: (_) => AdaptiveScaffold.toNavigationRail(
- leading: const _ComposeButton(),
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ leading: const _LargeComposeIcon(),
onDestinationSelected: (int index) {
setState(() {
_navigationIndex = index;
});
},
selectedIndex: _navigationIndex,
+ trailing: trailingNavRail,
extended: true,
- destinations: destinations,
+ destinations: destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList(),
),
),
},
@@ -227,10 +287,9 @@
? Padding(
padding: const EdgeInsets.fromLTRB(0, 32, 0, 0),
child: _ItemList(
- selected: selected,
- items: _all_Items,
- selectCard: selectCard,
- ),
+ selected: selected,
+ items: allItems,
+ selectCard: selectCard),
)
: const _ExamplePage(),
),
@@ -244,9 +303,10 @@
// disappearing as it is animating out.
outAnimation: AdaptiveScaffold.stayOnScreen,
key: const Key('sBody'),
- builder: (_) =>
- _DetailTile(item: _all_Items[selected ?? 0]),
- ),
+ builder: (_) => SafeArea(
+ child: _DetailTile(item: allItems[selected ?? 0]),
+ ),
+ )
},
)
: null,
@@ -259,13 +319,11 @@
outAnimation: AdaptiveScaffold.topToBottom,
builder: (_) => BottomNavigationBarTheme(
data: const BottomNavigationBarThemeData(
- unselectedItemColor: Colors.black,
- selectedItemColor: Colors.black,
- ),
- child: AdaptiveScaffold.toBottomNavigationBar(
+ selectedItemColor: Colors.black),
+ child: AdaptiveScaffold.standardBottomNavigationBar(
destinations: destinations),
),
- ),
+ )
},
),
),
@@ -279,20 +337,22 @@
required String label,
}) {
return NavigationRailDestination(
- icon: SlideTransition(
- position: Tween<Offset>(
- begin: Offset(begin, 0),
- end: Offset.zero,
- ).animate(
- CurvedAnimation(parent: controller, curve: Curves.easeInOutCubic),
- ),
- child: Icon(icon)),
- label: Text(label));
+ icon: SlideTransition(
+ position: Tween<Offset>(
+ begin: Offset(begin, 0),
+ end: Offset.zero,
+ ).animate(
+ CurvedAnimation(parent: controller, curve: Curves.easeInOutCubic),
+ ),
+ child: Icon(icon),
+ ),
+ label: Text(label),
+ );
}
}
-class _ComposeIcon extends StatelessWidget {
- const _ComposeIcon({
+class _SmallComposeIcon extends StatelessWidget {
+ const _SmallComposeIcon({
Key? key,
}) : super(key: key);
@@ -318,44 +378,75 @@
}
}
-class _ComposeButton extends StatelessWidget {
- const _ComposeButton({
+class _MediumComposeIcon extends StatelessWidget {
+ const _MediumComposeIcon({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(children: <Widget>[
+ Container(
+ padding: const EdgeInsets.fromLTRB(0, 10, 0, 18),
+ child: const Icon(Icons.menu),
+ ),
+ const _SmallComposeIcon(),
+ ]);
+ }
+}
+
+class _LargeComposeIcon extends StatelessWidget {
+ const _LargeComposeIcon({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
- padding: const EdgeInsets.fromLTRB(8.0, 0, 0, 0),
- child: Container(
- alignment: Alignment.centerLeft,
- decoration: BoxDecoration(
- color: const Color.fromARGB(255, 255, 216, 228),
- borderRadius: const BorderRadius.all(Radius.circular(15)),
- boxShadow: Breakpoints.mediumAndUp.isActive(context)
- ? null
- : <BoxShadow>[
- BoxShadow(
- color: Colors.grey.withOpacity(0.5),
- spreadRadius: 1,
- blurRadius: 2,
- offset: const Offset(0, 2),
- ),
- ],
- ),
- width: 200,
- height: 50,
- child: Padding(
- padding: const EdgeInsets.fromLTRB(16.0, 0, 0, 0),
+ padding: const EdgeInsets.fromLTRB(8.0, 5, 0, 12),
+ child: Column(children: <Widget>[
+ Container(
+ padding: const EdgeInsets.fromLTRB(6, 0, 0, 0),
child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const <Widget>[
- Icon(Icons.edit_outlined),
- SizedBox(width: 20),
- Center(child: Text('Compose')),
+ Text('REPLY',
+ style: TextStyle(color: Colors.deepPurple, fontSize: 15)),
+ Icon(Icons.menu_open, size: 22)
],
),
),
- ),
+ const SizedBox(height: 10),
+ Container(
+ alignment: Alignment.centerLeft,
+ decoration: BoxDecoration(
+ color: const Color.fromARGB(255, 255, 225, 231),
+ borderRadius: const BorderRadius.all(Radius.circular(15)),
+ boxShadow: Breakpoints.mediumAndUp.isActive(context)
+ ? null
+ : <BoxShadow>[
+ BoxShadow(
+ color: Colors.grey.withOpacity(0.5),
+ spreadRadius: 1,
+ blurRadius: 2,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ width: 200,
+ height: 50,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(16.0, 0, 0, 0),
+ child: Row(
+ children: const <Widget>[
+ Icon(Icons.edit_outlined),
+ SizedBox(width: 20),
+ Center(child: Text('Compose')),
+ ],
+ ),
+ ),
+ )
+ ]),
);
}
}
@@ -379,7 +470,7 @@
backgroundColor: const Color.fromARGB(0, 0, 0, 0),
floatingActionButton: Breakpoints.mediumAndUp.isActive(context)
? null
- : const _ComposeIcon(),
+ : const _SmallComposeIcon(),
body: Column(
children: <Widget>[
Padding(
@@ -393,7 +484,13 @@
suffixIcon: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: CircleAvatar(
- backgroundImage: NetworkImage(_all_Items[0].image),
+ radius: 18,
+ child: Image.asset(
+ 'images/plum.png',
+ width: 100,
+ height: 100,
+ fit: BoxFit.cover,
+ ),
),
),
border: OutlineInputBorder(
@@ -412,10 +509,11 @@
Expanded(
child: ListView.builder(
itemCount: items.length,
- itemBuilder: (_, int index) => _ItemListTile(
- selected: selected,
+ itemBuilder: (BuildContext context, int index) => _ItemListTile(
item: items[index],
+ email: items[index].emails![0],
selectCard: selectCard,
+ selected: selected,
),
),
),
@@ -429,11 +527,13 @@
const _ItemListTile({
Key? key,
required this.item,
+ required this.email,
required this.selectCard,
required this.selected,
}) : super(key: key);
final _Item item;
+ final _Email email;
final int? selected;
final Function selectCard;
@@ -445,24 +545,23 @@
// than large screens.
// Small screens open a modal with the detail view while large screens
// simply show the details on the secondaryBody.
- selectCard(_all_Items.indexOf(item));
+ selectCard(allItems.indexOf(item));
if (!Breakpoints.mediumAndUp.isActive(context)) {
- Navigator.of(context).pushNamed(
- _ExtractRouteArguments.routeName,
- arguments: _ScreenArguments(item: item, selectCard: selectCard),
- );
+ Navigator.of(context).pushNamed(_ExtractRouteArguments.routeName,
+ arguments: _ScreenArguments(item: item, selectCard: selectCard));
} else {
- selectCard(_all_Items.indexOf(item));
+ selectCard(allItems.indexOf(item));
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
- color: selected == _all_Items.indexOf(item)
- ? const Color.fromARGB(255, 234, 222, 255)
- : const Color.fromARGB(255, 243, 237, 247),
- borderRadius: const BorderRadius.all(Radius.circular(10))),
+ color: selected == allItems.indexOf(item)
+ ? const Color.fromARGB(255, 234, 222, 255)
+ : const Color.fromARGB(255, 243, 237, 247),
+ borderRadius: const BorderRadius.all(Radius.circular(10)),
+ ),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@@ -471,16 +570,22 @@
Row(
children: <Widget>[
CircleAvatar(
- backgroundImage: NetworkImage(item.image),
+ radius: 18,
+ child: Image.asset(
+ email.image,
+ width: 100,
+ height: 100,
+ fit: BoxFit.cover,
+ ),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
- Text(item.name,
+ Text(email.sender,
style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 3),
- Text('${item.time} ago',
+ Text('${email.time} ago',
style: Theme.of(context).textTheme.bodySmall),
],
),
@@ -488,18 +593,26 @@
Container(
padding: const EdgeInsets.all(8.0),
decoration: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.all(Radius.circular(50))),
- child: const Icon(Icons.star_outline),
+ color: Colors.white,
+ borderRadius: BorderRadius.all(Radius.circular(50)),
+ ),
+ child: Icon(Icons.star_outline, color: Colors.grey[500]),
),
],
),
- const SizedBox(height: 10),
+ const SizedBox(height: 13),
Text(item.title,
style: Theme.of(context).textTheme.titleMedium),
- const SizedBox(height: 4),
- Text(item.body.replaceRange(80, item.body.length, '...'),
+ const SizedBox(height: 9),
+ Text(email.body.replaceRange(116, email.body.length, '...'),
style: Theme.of(context).textTheme.bodyLarge),
+ const SizedBox(height: 9),
+ SizedBox(
+ width: MediaQuery.of(context).size.width,
+ child: (email.bodyImage != '')
+ ? Image.asset(email.bodyImage)
+ : Container(),
+ ),
],
),
),
@@ -521,25 +634,220 @@
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
- height: 300,
+ height: MediaQuery.of(context).size.height,
child: Container(
decoration: const BoxDecoration(
- color: Color.fromARGB(255, 255, 251, 254),
+ color: Color.fromARGB(255, 245, 241, 248),
borderRadius: BorderRadius.all(Radius.circular(10))),
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: <Widget>[
- Text(item.title, style: Theme.of(context).textTheme.titleLarge),
- Text('3 Messages',
- style: Theme.of(context).textTheme.labelSmall),
- const SizedBox(
- height: 20,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
+ child: Column(
+ children: <Widget>[
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: <Widget>[
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.fromLTRB(5, 0, 5, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Text(item.title,
+ style: Theme.of(context)
+ .textTheme
+ .titleLarge),
+ const SizedBox(height: 7),
+ Text('${item.emails!.length} Messages',
+ style: Theme.of(context)
+ .textTheme
+ .labelSmall)
+ ],
+ ),
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.fromLTRB(5, 0, 5, 0),
+ child: Row(
+ children: <Widget>[
+ Container(
+ padding: const EdgeInsets.all(8.0),
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.all(
+ Radius.circular(15)),
+ ),
+ child: Icon(Icons.restore_from_trash,
+ color: Colors.grey[600]),
+ ),
+ const SizedBox(width: 15),
+ Container(
+ padding: const EdgeInsets.all(8.0),
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.all(
+ Radius.circular(15))),
+ child: Icon(Icons.more_vert,
+ color: Colors.grey[600]),
+ )
+ ],
+ )),
+ ]),
+ const SizedBox(height: 20),
+ ],
+ )),
+ Expanded(
+ child: ListView.builder(
+ itemCount: item.emails!.length,
+ itemBuilder: (BuildContext context, int index) {
+ final _Email thisEmail = item.emails![index];
+ return _EmailTile(
+ sender: thisEmail.sender,
+ time: thisEmail.time,
+ senderIcon: thisEmail.image,
+ recepients: thisEmail.recepients,
+ body: thisEmail.body,
+ bodyImage: thisEmail.bodyImage);
+ },
),
- Text(item.body, style: Theme.of(context).textTheme.bodyLarge),
- ],
- ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _EmailTile extends StatelessWidget {
+ const _EmailTile({
+ required this.sender,
+ required this.time,
+ required this.senderIcon,
+ required this.recepients,
+ required this.body,
+ required this.bodyImage,
+ Key? key,
+ }) : super(key: key);
+
+ final String sender;
+ final String time;
+ final String senderIcon;
+ final String recepients;
+ final String body;
+ final String bodyImage;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(0, 4, 0, 4),
+ child: Container(
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.all(Radius.circular(10))),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Row(
+ children: <Widget>[
+ CircleAvatar(
+ radius: 18,
+ child: Image.asset(
+ senderIcon,
+ width: 100,
+ height: 100,
+ fit: BoxFit.cover,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: <Widget>[
+ Text(sender,
+ style:
+ TextStyle(color: Colors.grey[850], fontSize: 13)),
+ const SizedBox(height: 3),
+ Text('$time ago',
+ style: Theme.of(context).textTheme.bodySmall),
+ ],
+ ),
+ const Spacer(),
+ Container(
+ padding: const EdgeInsets.all(8.0),
+ decoration: const BoxDecoration(
+ color: Color.fromARGB(255, 245, 241, 248),
+ borderRadius: BorderRadius.all(Radius.circular(50))),
+ child: Icon(Icons.star_outline, color: Colors.grey[500]),
+ ),
+ ],
+ ),
+ if (recepients != '')
+ Column(children: <Widget>[
+ const SizedBox(height: 15),
+ Text('To $recepients',
+ style: TextStyle(color: Colors.grey[500], fontSize: 12)),
+ ])
+ else
+ Container(),
+ const SizedBox(height: 15),
+ Text(body,
+ style: TextStyle(
+ color: Colors.grey[700], height: 1.35, fontSize: 14.5)),
+ const SizedBox(height: 9),
+ SizedBox(
+ width: MediaQuery.of(context).size.width,
+ child:
+ (bodyImage != '') ? Image.asset(bodyImage) : Container()),
+ const SizedBox(height: 10),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: <Widget>[
+ SizedBox(
+ width: 126,
+ child: OutlinedButton(
+ onPressed: () {},
+ style: ButtonStyle(
+ shape: MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(30.0)),
+ ),
+ backgroundColor: MaterialStateProperty.all<Color>(
+ const Color.fromARGB(255, 245, 241, 248)),
+ side: MaterialStateProperty.all(const BorderSide(
+ width: 0.0, color: Colors.transparent)),
+ ),
+ child: Text('Reply',
+ style:
+ TextStyle(color: Colors.grey[700], fontSize: 12)),
+ ),
+ ),
+ SizedBox(
+ width: 126,
+ child: OutlinedButton(
+ onPressed: () {},
+ style: ButtonStyle(
+ shape: MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(30.0)),
+ ),
+ backgroundColor: MaterialStateProperty.all<Color>(
+ const Color.fromARGB(255, 245, 241, 248)),
+ side: MaterialStateProperty.all(const BorderSide(
+ width: 0.0, color: Colors.transparent)),
+ ),
+ child: Text(
+ 'Reply all',
+ style: TextStyle(color: Colors.grey[700], fontSize: 12),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
),
),
),
@@ -598,7 +906,7 @@
child: const Icon(Icons.arrow_back),
),
),
- _DetailTile(item: item),
+ Expanded(child: _DetailTile(item: item)),
],
),
);
@@ -616,100 +924,91 @@
class _Item {
const _Item({
- required this.name,
- required this.time,
required this.title,
- required this.body,
- required this.image,
+ required this.emails,
});
- final String name;
- final String time;
final String title;
- final String body;
- final String image;
+ final List<_Email>? emails;
}
-const List<_Item> _all_Items = <_Item>[
+class _Email {
+ const _Email({
+ required this.sender,
+ required this.recepients,
+ required this.image,
+ required this.time,
+ required this.body,
+ required this.bodyImage,
+ });
+
+ final String sender;
+ final String recepients;
+ final String image;
+ final String time;
+ final String body;
+ final String bodyImage;
+}
+
+/// List of items, each representing a thread of emails which will populate
+/// the different layouts.
+const List<_Item> allItems = <_Item>[
_Item(
- name: 'So Duri',
- time: '20 min',
title: 'Dinner Club',
- body:
- "I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTJ8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=400&q=60',
+ emails: <_Email>[
+ _Email(
+ sender: 'So Duri',
+ recepients: 'me, Ziad and Lily',
+ image: 'images/strawberry.png',
+ time: '20 min',
+ body:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec gravida tellus, vel scelerisque nisi. Mauris egestas, augue nec dictum tempus, diam sapien luctus odio, a posuere sem neque at nulla. Vivamus pulvinar nisi et dapibus dapibus. Donec euismod pellentesque ultrices. Vivamus quis condimentum metus, in venenatis lorem. Proin suscipit tincidunt eleifend. Praesent a nisi ac ipsum sodales gravida.',
+ bodyImage: '',
+ ),
+ _Email(
+ sender: 'Me',
+ recepients: 'me, Ziad, and Lily',
+ image: 'images/plum.png',
+ time: '4 min',
+ body:
+ 'Donec non mollis nulla, in varius mi. Ut id lorem eget felis lobortis tincidunt. Curabitur facilisis ex vitae tristique efficitur. Aenean eget augue finibus, tempor eros vitae, tempor neque. In sed pellentesque elit. Donec lacus lacus, malesuada in tincidunt sit amet, condimentum vel enim. Cras dapibus erat quis nisl hendrerit, vel pretium turpis condimentum. ',
+ bodyImage: ''),
+ _Email(
+ sender: 'Ziad Aouad',
+ recepients: 'me, Ziad and Lily',
+ image: 'images/mushroom.png',
+ time: '2 min',
+ body:
+ 'Duis sit amet nibh a diam placerat aliquam nec ac mi. Aenean hendrerit efficitur tellus, non pharetra eros posuere sit amet. Maecenas interdum lacinia eleifend. Nam efficitur tellus et dolor vestibulum, non dictum quam iaculis. Aenean id nulla ut erat placerat feugiat. Mauris in quam metus. Aliquam erat volutpat.',
+ bodyImage: ''),
+ ],
),
_Item(
- name: 'Lily Mac',
- time: '2 hours',
- title: 'This food show is made for you',
- body:
- "3I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1603415526960-f7e0328c63b1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZmlsZSUyMHBpY3R1cmV8ZW58MHx8MHx8&auto=format&fit=crop&w=400&q=60',
+ title: '7 Best Yoga Poses',
+ emails: <_Email>[
+ _Email(
+ sender: 'Elaine Howley',
+ time: '2 hours',
+ body:
+ 'Curabitur tincidunt purus at vulputate mattis. Nam lectus urna, varius eget quam in, ultricies ultrices libero. Curabitur rutrum ultricies varius. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec vulputate auctor est, non semper velit eleifend sit amet.',
+ image: 'images/potato.png',
+ bodyImage: 'images/avocado.png',
+ recepients: '',
+ ),
+ ],
),
_Item(
- name: 'Lani Mansell',
- time: '10 min',
- title: 'Dinner Club 4',
- body:
- "4I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1629467057571-42d22d8f0cbd?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTN8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=400&q=60',
- ),
- _Item(
- name: 'Caitlyn Mars',
- time: '10 min',
- title: 'This food ',
- body:
- "1I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1619895862022-09114b41f16f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Nnx8cHJvZmlsZSUyMHBpY3R1cmV8ZW58MHx8MHx8&auto=format&fit=crop&w=400&q=60',
- ),
- _Item(
- name: 'Robin Goff',
- time: '10 min',
- title: 'Dinner Club 5',
- body:
- "5I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTh8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=400&q=60',
- ),
- _Item(
- name: 'Klara Blan',
- time: '10 min',
- title: 'Dinner Club 6',
- body:
- "6I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTh8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=400&q=60',
- ),
- _Item(
- name: 'Bianka Bass',
- time: '10 min',
- title: 'Dinner Club 7',
- body:
- "7I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTh8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=400&q=60',
- ),
- _Item(
- name: 'Beau Kline',
- time: '10 min',
- title: 'Dinner Club 8',
- body:
- "8I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTh8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=400&q=60',
- ),
- _Item(
- name: 'Fran Martin',
- time: '10 min',
- title: 'Dinner Club 9',
- body:
- "9I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intruiged by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)",
- image:
- 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTh8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=400&q=60',
+ title: 'A Programming Language',
+ emails: <_Email>[
+ _Email(
+ sender: 'Laney Mansell',
+ time: '10 min',
+ body:
+ 'Cras egestas ultricies elit, vitae interdum lorem aliquam et. Donec quis arcu a quam tempor rutrum vitae in lectus. Nullam elit nunc, lacinia sed luctus non, mollis id nulla. Morbi luctus turpis sapien, id molestie ante maximus vel. Vivamus sagittis consequat nisl nec placerat.',
+ image: 'images/habanero.png',
+ bodyImage: '',
+ recepients: '',
+ ),
+ ],
),
];
diff --git a/packages/adaptive_scaffold/example/pubspec.yaml b/packages/adaptive_scaffold/example/pubspec.yaml
index 5643cb0..a43777a 100644
--- a/packages/adaptive_scaffold/example/pubspec.yaml
+++ b/packages/adaptive_scaffold/example/pubspec.yaml
@@ -1,8 +1,6 @@
name: adaptive_scaffold_example
description: Multiple examples of the usage of the AdaptiveScaffold widget and its lower level widgets.
-
-publish_to: none
-
+publish_to: 'none'
version: 0.0.1
environment:
@@ -14,11 +12,20 @@
path: ../../adaptive_scaffold
flutter:
sdk: flutter
+ flutter_lints: ^2.0.0
dev_dependencies:
+ build_runner: ^2.1.10
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
-
\ No newline at end of file
+
+ assets:
+ - images/potato.png
+ - images/habanero.png
+ - images/strawberry.png
+ - images/plum.png
+ - images/mushroom.png
+ - images/avocado.png
\ No newline at end of file
diff --git a/packages/adaptive_scaffold/example/test/adaptive_layout_demo_test.dart b/packages/adaptive_scaffold/example/test/adaptive_layout_demo_test.dart
index 33fd406..90e96d7 100644
--- a/packages/adaptive_scaffold/example/test/adaptive_layout_demo_test.dart
+++ b/packages/adaptive_scaffold/example/test/adaptive_layout_demo_test.dart
@@ -2,11 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:adaptive_scaffold/adaptive_scaffold.dart';
import 'package:adaptive_scaffold_example/adaptive_layout_demo.dart' as example;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
+ final Finder body = find.byKey(const Key('body'));
+ final Finder bn = find.byKey(const Key('bn'));
+
Future<void> updateScreen(double width, WidgetTester tester) async {
await tester.binding.setSurfaceSize(Size(width, 800));
await tester.pumpWidget(
@@ -23,7 +27,7 @@
(WidgetTester tester) async {
await updateScreen(300, tester);
expect(find.byKey(const Key('body')), findsOneWidget);
- expect(find.byKey(const Key('pn')), findsNothing);
+ expect(find.byKey(const Key('pnav')), findsNothing);
expect(find.byKey(const Key('bn')), findsOneWidget);
expect(find.byKey(const Key('body1')), findsNothing);
expect(find.byKey(const Key('pn1')), findsNothing);
@@ -32,11 +36,63 @@
expect(find.byKey(const Key('body')), findsNothing);
expect(find.byKey(const Key('bn')), findsNothing);
expect(find.byKey(const Key('body1')), findsOneWidget);
- expect(find.byKey(const Key('pn')), findsOneWidget);
+ expect(find.byKey(const Key('pnav1')), findsOneWidget);
expect(find.byKey(const Key('pn1')), findsNothing);
+ });
- await updateScreen(900, tester);
- expect(find.byKey(const Key('pn')), findsNothing);
- expect(find.byKey(const Key('pn1')), findsOneWidget);
+ testWidgets(
+ 'adaptive layout bottom navigation displays with correct properties',
+ (WidgetTester tester) async {
+ await updateScreen(400, tester);
+ final BuildContext context = tester.element(find.byType(MaterialApp));
+
+ // Bottom Navigation Bar
+ final Finder findKey = find.byKey(const Key('bn'));
+ final SlotLayoutConfig slotLayoutConfig =
+ tester.firstWidget<SlotLayoutConfig>(findKey);
+ final WidgetBuilder? widgetBuilder = slotLayoutConfig.builder;
+ final Widget Function(BuildContext) widgetFunction =
+ (widgetBuilder ?? () => Container()) as Widget Function(BuildContext);
+ final BottomNavigationBarThemeData bottomNavigationBarThemeData =
+ (widgetFunction(context) as BottomNavigationBarTheme).data;
+
+ expect(bottomNavigationBarThemeData.selectedItemColor, Colors.black);
+ });
+
+ testWidgets(
+ 'adaptive layout navigation rail displays with correct properties',
+ (WidgetTester tester) async {
+ await updateScreen(620, tester);
+ final BuildContext context = tester.element(find.byType(AdaptiveLayout));
+
+ final Finder findKey = find.byKey(const Key('pnav1'));
+ final SlotLayoutConfig slotLayoutConfig =
+ tester.firstWidget<SlotLayoutConfig>(findKey);
+ final WidgetBuilder? widgetBuilder = slotLayoutConfig.builder;
+ final Widget Function(BuildContext) widgetFunction =
+ (widgetBuilder ?? () => Container()) as Widget Function(BuildContext);
+ final SizedBox sizedBox =
+ (((widgetFunction(context) as Builder).builder(context) as Padding)
+ .child ??
+ () => const SizedBox()) as SizedBox;
+ expect(sizedBox.width, 72);
+ });
+
+ testWidgets('adaptive layout displays children in correct places',
+ (WidgetTester tester) async {
+ await updateScreen(400, tester);
+ expect(tester.getBottomLeft(bn), const Offset(0, 800));
+ expect(tester.getBottomRight(bn), const Offset(400, 800));
+ expect(tester.getTopRight(body), const Offset(400, 0));
+ expect(tester.getTopLeft(body), const Offset(0, 0));
+ });
+
+ testWidgets('adaptive layout does not animate when animations off',
+ (WidgetTester tester) async {
+ final Finder b = find.byKey(const Key('body1'));
+ await updateScreen(690, tester);
+
+ expect(tester.getTopLeft(b), const Offset(88, 0));
+ expect(tester.getBottomRight(b), const Offset(690, 800));
});
}
diff --git a/packages/adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart b/packages/adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart
index 5f3011d..0a0f47d 100644
--- a/packages/adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart
+++ b/packages/adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart
@@ -24,30 +24,78 @@
child: const example.MyHomePage()),
),
);
- await tester.pumpAndSettle();
}
testWidgets('dislays correct item of config based on screen width',
(WidgetTester tester) async {
await updateScreen(300, tester);
+ await tester.pumpAndSettle();
expect(smallBody, findsOneWidget);
expect(bnav, findsOneWidget);
+ expect(tester.getTopLeft(smallBody), Offset.zero);
+ expect(tester.getTopLeft(bnav), const Offset(0, 744));
expect(body, findsNothing);
expect(largeBody, findsNothing);
expect(pnav, findsNothing);
expect(pnav1, findsNothing);
await updateScreen(800, tester);
+ await tester.pumpAndSettle();
expect(body, findsOneWidget);
+ expect(tester.getTopLeft(body), const Offset(88, 0));
expect(body, findsOneWidget);
expect(bnav, findsNothing);
expect(largeBody, findsNothing);
expect(pnav, findsOneWidget);
+ expect(tester.getTopLeft(pnav), Offset.zero);
+ expect(tester.getBottomRight(pnav), const Offset(88, 800));
expect(pnav1, findsNothing);
await updateScreen(1100, tester);
+ await tester.pumpAndSettle();
expect(body, findsOneWidget);
expect(pnav, findsNothing);
expect(pnav1, findsOneWidget);
+ expect(tester.getTopLeft(pnav1), Offset.zero);
+ expect(tester.getBottomRight(pnav1), const Offset(208, 800));
+ });
+
+ testWidgets('adaptive scaffold animations work correctly',
+ (WidgetTester tester) async {
+ final Finder b = find.byKey(const Key('body'));
+ final Finder sBody = find.byKey(const Key('sBody'));
+
+ await updateScreen(400, tester);
+ await updateScreen(800, tester);
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 200));
+
+ expect(tester.getTopLeft(b), const Offset(17.6, 0));
+ expect(tester.getBottomRight(b),
+ offsetMoreOrLessEquals(const Offset(778.2, 755.2), epsilon: 1.0));
+ expect(tester.getTopLeft(sBody),
+ offsetMoreOrLessEquals(const Offset(778.2, 0), epsilon: 1.0));
+ expect(tester.getBottomRight(sBody),
+ offsetMoreOrLessEquals(const Offset(1178.2, 755.2), epsilon: 1.0));
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 600));
+
+ expect(tester.getTopLeft(b), const Offset(70.4, 0));
+ expect(tester.getBottomRight(b),
+ offsetMoreOrLessEquals(const Offset(416.0, 788.8), epsilon: 1.0));
+ expect(tester.getTopLeft(sBody),
+ offsetMoreOrLessEquals(const Offset(416, 0), epsilon: 1.0));
+ expect(tester.getBottomRight(sBody),
+ offsetMoreOrLessEquals(const Offset(816, 788.8), epsilon: 1.0));
+
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 200));
+
+ expect(tester.getTopLeft(b), const Offset(88, 0));
+ expect(tester.getBottomRight(b), const Offset(400, 800));
+ expect(tester.getTopLeft(sBody), const Offset(400, 0));
+ expect(tester.getBottomRight(sBody), const Offset(800, 800));
});
}
diff --git a/packages/adaptive_scaffold/lib/src/adaptive_layout.dart b/packages/adaptive_scaffold/lib/src/adaptive_layout.dart
index ff4a513..073f95c 100644
--- a/packages/adaptive_scaffold/lib/src/adaptive_layout.dart
+++ b/packages/adaptive_scaffold/lib/src/adaptive_layout.dart
@@ -124,7 +124,7 @@
///
/// If the content is a flexibly sized Widget like [Container], wrap the content
/// in a [SizedBox] or limit its size (width and height) by another method. See
- /// the builder in [AdaptiveScaffold.toNavigationRail] for an example.
+ /// the builder in [AdaptiveScaffold.standardNavigationRail] for an example.
final SlotLayout? primaryNavigation;
/// The slot placed on the end side of the app window.
@@ -134,21 +134,21 @@
///
/// If the content is a flexibly sized Widget like [Container], wrap the content
/// in a [SizedBox] or limit its size (width and height) by another method. See
- /// the builder in [AdaptiveScaffold.toNavigationRail] for an example.
+ /// the builder in [AdaptiveScaffold.standardNavigationRail] for an example.
final SlotLayout? secondaryNavigation;
/// The slot placed on the top part of the app window.
///
/// If the content is a flexibly sized Widget like [Container], wrap the content
/// in a [SizedBox] or limit its size (width and height) by another method. See
- /// the builder in [AdaptiveScaffold.toNavigationRail] for an example.
+ /// the builder in [AdaptiveScaffold.standardNavigationRail] for an example.
final SlotLayout? topNavigation;
/// The slot placed on the bottom part of the app window.
///
/// If the content is a flexibly sized Widget like [Container], wrap the content
/// in a [SizedBox] or limit its size (width and height) by another method. See
- /// the builder in [AdaptiveScaffold.toNavigationRail] for an example.
+ /// the builder in [AdaptiveScaffold.standardNavigationRail] for an example.
final SlotLayout? bottomNavigation;
/// The slot that fills the rest of the space in the center.
diff --git a/packages/adaptive_scaffold/lib/src/adaptive_scaffold.dart b/packages/adaptive_scaffold/lib/src/adaptive_scaffold.dart
index e1f3fd8..a2f4172 100644
--- a/packages/adaptive_scaffold/lib/src/adaptive_scaffold.dart
+++ b/packages/adaptive_scaffold/lib/src/adaptive_scaffold.dart
@@ -3,12 +3,31 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
+import 'package:masonry_grid/masonry_grid.dart';
+
import 'adaptive_layout.dart';
import 'breakpoints.dart';
import 'slot_layout.dart';
-/// Implements the basic visual layout structure for [Material Design 3](https://m3.material.io/foundations/adaptive-design/overview) that
-/// adapts to a variety of screens.
+/// Gutter value between different parts of the body slot depending on
+/// material 3 design spec.
+const double kMaterialGutterValue = 8;
+
+/// Margin value of the compact breakpoint layout according to the material
+/// design 3 spec.
+const double kMaterialCompactMinMargin = 8;
+
+/// Margin value of the medium breakpoint layout according to the material
+/// design 3 spec.
+const double kMaterialMediumMinMargin = 12;
+
+//// Margin value of the expanded breakpoint layout according to the material
+/// design 3 spec.
+const double kMaterialExpandedMinMargin = 32;
+
+/// Implements the basic visual layout structure for
+/// [Material Design 3](https://m3.material.io/foundations/adaptive-design/overview)
+/// that adapts to a variety of screens.
///
/// 
///
@@ -62,6 +81,9 @@
super.key,
required this.destinations,
this.selectedIndex = 0,
+ this.leadingUnextendedNavRail,
+ this.leadingExtendedNavRail,
+ this.trailingNavRail,
this.smallBody,
this.body,
this.largeBody,
@@ -91,6 +113,18 @@
/// The index to be used by the [NavigationRail].
final int selectedIndex;
+ /// Option to display a leading widget at the top of the navigation rail
+ /// at the middle breakpoint.
+ final Widget? leadingUnextendedNavRail;
+
+ /// Option to display a leading widget at the top of the navigation rail
+ /// at the largest breakpoint.
+ final Widget? leadingExtendedNavRail;
+
+ /// Option to display a trailing widget below the destinations of the
+ /// navigation rail at the largest breakpoint.
+ final Widget? trailingNavRail;
+
/// Widget to be displayed in the body slot at the smallest breakpoint.
///
/// If nothing is entered for this property, then the default [body] is
@@ -199,54 +233,146 @@
/// Callback function for when the index of a [NavigationRail] changes.
static WidgetBuilder emptyBuilder = (_) => const SizedBox();
- /// Public helper method to be used for creating a [NavigationRail] from a
- /// list of [NavigationDestination]s. Takes in a [selectedIndex] property for
- /// the current selected item in the [NavigationRail] and [extended] for
- /// whether the [NavigationRail] is extended or not.
- static Builder toNavigationRail({
- required List<NavigationDestination> destinations,
+ /// Public helper method to be used for creating a [NavigationRailDestination] from
+ /// a [NavigationDestination].
+ static NavigationRailDestination toRailDestination(
+ NavigationDestination destination) {
+ return NavigationRailDestination(
+ label: Text(destination.label),
+ icon: destination.icon,
+ );
+ }
+
+ /// Creates a Material 3 Design Spec abiding [NavigationRail] from a
+ /// list of [NavigationDestination]s.
+ ///
+ /// Takes in a [selectedIndex] property for the current selected item in
+ /// the [NavigationRail] and [extended] for whether the [NavigationRail]
+ /// is extended or not.
+ static Builder standardNavigationRail({
+ required List<NavigationRailDestination> destinations,
double width = 72,
int selectedIndex = 0,
bool extended = false,
Color backgroundColor = Colors.transparent,
+ EdgeInsetsGeometry padding = const EdgeInsets.all(8.0),
Widget? leading,
Widget? trailing,
Function(int)? onDestinationSelected,
+ IconThemeData selectedIconTheme = const IconThemeData(color: Colors.black),
+ IconThemeData unselectedIconTheme =
+ const IconThemeData(color: Colors.black),
+ TextStyle selectedLabelTextStyle = const TextStyle(color: Colors.black),
NavigationRailLabelType labelType = NavigationRailLabelType.none,
}) {
if (extended && width == 72) {
width = 192;
}
- return Builder(
- builder: (BuildContext context) {
- return SizedBox(
+ return Builder(builder: (BuildContext context) {
+ return Padding(
+ padding: padding,
+ child: SizedBox(
width: width,
height: MediaQuery.of(context).size.height,
- child: NavigationRail(
- onDestinationSelected: onDestinationSelected,
- labelType: labelType,
- leading: leading,
- trailing: trailing,
- backgroundColor: backgroundColor,
- extended: extended,
- selectedIndex: selectedIndex,
- destinations: destinations
- .map((NavigationDestination e) => _toRailDestination(e))
- .toList()),
- );
- },
- );
+ child: LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ return SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(minHeight: constraints.maxHeight),
+ child: IntrinsicHeight(
+ child: NavigationRail(
+ labelType: labelType,
+ leading: leading,
+ trailing: trailing,
+ onDestinationSelected: onDestinationSelected,
+ backgroundColor: backgroundColor,
+ extended: extended,
+ selectedIndex: selectedIndex,
+ selectedIconTheme: selectedIconTheme,
+ unselectedIconTheme: unselectedIconTheme,
+ selectedLabelTextStyle: selectedLabelTextStyle,
+ destinations: destinations,
+ ),
+ ),
+ ));
+ },
+ ),
+ ),
+ );
+ });
}
/// Public helper method to be used for creating a [BottomNavigationBar] from
/// a list of [NavigationDestination]s.
- static BottomNavigationBar toBottomNavigationBar(
- {required List<NavigationDestination> destinations}) {
- return BottomNavigationBar(
- items: destinations
- .map((NavigationDestination e) => _toBottomNavItem(e))
- .toList(),
- );
+ static Builder standardBottomNavigationBar(
+ {required List<NavigationDestination> destinations,
+ int currentIndex = 0,
+ double iconSize = 24}) {
+ return Builder(
+ builder: (_) => BottomNavigationBar(
+ currentIndex: currentIndex,
+ iconSize: iconSize,
+ items: destinations
+ .map((NavigationDestination e) => _toBottomNavItem(e))
+ .toList()));
+ }
+
+ /// Public helper method to be used for creating a [MasonryGrid] following m3
+ /// specs from a list of [Widget]s
+ static Builder toMaterialGrid({
+ List<Widget> thisWidgets = const <Widget>[],
+ List<Breakpoint> breakpoints = const <Breakpoint>[
+ Breakpoints.small,
+ Breakpoints.medium,
+ Breakpoints.large,
+ ],
+ double margin = 8,
+ int itemColumns = 1,
+ required BuildContext context,
+ }) {
+ return Builder(builder: (BuildContext context) {
+ Breakpoint? currentBreakpoint;
+ for (final Breakpoint breakpoint in breakpoints) {
+ if (breakpoint.isActive(context)) {
+ currentBreakpoint = breakpoint;
+ }
+ }
+ double? thisMargin = margin;
+
+ if (currentBreakpoint == Breakpoints.small) {
+ if (thisMargin < kMaterialCompactMinMargin) {
+ thisMargin = kMaterialCompactMinMargin;
+ }
+ } else if (currentBreakpoint == Breakpoints.medium) {
+ if (thisMargin < kMaterialMediumMinMargin) {
+ thisMargin = kMaterialMediumMinMargin;
+ }
+ } else if (currentBreakpoint == Breakpoints.large) {
+ if (thisMargin < kMaterialExpandedMinMargin) {
+ thisMargin = kMaterialExpandedMinMargin;
+ }
+ }
+ return CustomScrollView(
+ primary: false,
+ controller: ScrollController(),
+ shrinkWrap: true,
+ physics: const AlwaysScrollableScrollPhysics(),
+ scrollDirection: Axis.vertical,
+ slivers: <Widget>[
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: EdgeInsets.all(thisMargin),
+ child: MasonryGrid(
+ column: itemColumns,
+ crossAxisSpacing: kMaterialGutterValue,
+ mainAxisSpacing: kMaterialGutterValue,
+ children: thisWidgets,
+ ),
+ ),
+ ),
+ ],
+ );
+ });
}
/// Animation from bottom offscreen up onto the screen.
@@ -347,8 +473,9 @@
child: NavigationRail(
extended: true,
selectedIndex: widget.selectedIndex,
- destinations:
- widget.destinations.map(_toRailDestination).toList(),
+ destinations: widget.destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList(),
onDestinationSelected: widget.onSelectedIndexChange,
),
)
@@ -357,32 +484,36 @@
bodyOrientation: widget.bodyOrientation,
bodyRatio: widget.bodyRatio,
internalAnimations: widget.internalAnimations,
- primaryNavigation:
- widget.destinations != null && widget.selectedIndex != null
- ? SlotLayout(
- config: <Breakpoint, SlotLayoutConfig>{
- widget.mediumBreakpoint: SlotLayout.from(
- key: const Key('primaryNavigation'),
- builder: (_) => AdaptiveScaffold.toNavigationRail(
- width: widget.navigationRailWidth,
- selectedIndex: widget.selectedIndex,
- destinations: widget.destinations,
- onDestinationSelected: widget.onSelectedIndexChange,
- ),
- ),
- widget.largeBreakpoint: SlotLayout.from(
- key: const Key('primaryNavigation1'),
- builder: (_) => AdaptiveScaffold.toNavigationRail(
- width: widget.extendedNavigationRailWidth,
- extended: true,
- selectedIndex: widget.selectedIndex,
- destinations: widget.destinations,
- onDestinationSelected: widget.onSelectedIndexChange,
- ),
- ),
- },
- )
- : null,
+ primaryNavigation: widget.destinations != null &&
+ widget.selectedIndex != null
+ ? SlotLayout(
+ config: <Breakpoint, SlotLayoutConfig>{
+ widget.mediumBreakpoint: SlotLayout.from(
+ key: const Key('primaryNavigation'),
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ width: widget.navigationRailWidth,
+ selectedIndex: widget.selectedIndex,
+ destinations: widget.destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList(),
+ onDestinationSelected: widget.onSelectedIndexChange,
+ ),
+ ),
+ widget.largeBreakpoint: SlotLayout.from(
+ key: const Key('primaryNavigation1'),
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ width: widget.extendedNavigationRailWidth,
+ extended: true,
+ selectedIndex: widget.selectedIndex,
+ destinations: widget.destinations
+ .map((_) => AdaptiveScaffold.toRailDestination(_))
+ .toList(),
+ onDestinationSelected: widget.onSelectedIndexChange,
+ ),
+ ),
+ },
+ )
+ : null,
bottomNavigation: widget.destinations != null &&
(!widget.drawerBreakpoint.isActive(context) ||
!widget.useDrawer)
@@ -390,8 +521,9 @@
config: <Breakpoint, SlotLayoutConfig>{
widget.smallBreakpoint: SlotLayout.from(
key: const Key('bottomNavigation'),
- builder: (_) => AdaptiveScaffold.toBottomNavigationBar(
- destinations: widget.destinations),
+ builder: (_) =>
+ AdaptiveScaffold.standardBottomNavigationBar(
+ destinations: widget.destinations),
),
},
)
@@ -478,14 +610,6 @@
}
}
-NavigationRailDestination _toRailDestination(
- NavigationDestination destination) {
- return NavigationRailDestination(
- label: Text(destination.label),
- icon: destination.icon,
- );
-}
-
BottomNavigationBarItem _toBottomNavItem(NavigationDestination destination) {
return BottomNavigationBarItem(
label: destination.label,
diff --git a/packages/adaptive_scaffold/pubspec.yaml b/packages/adaptive_scaffold/pubspec.yaml
index a0a1b0a..87c1952 100644
--- a/packages/adaptive_scaffold/pubspec.yaml
+++ b/packages/adaptive_scaffold/pubspec.yaml
@@ -1,7 +1,7 @@
name: adaptive_scaffold
description: Widgets to easily build adaptive layouts, including navigation elements.
-version: 0.0.1+1
-issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3A%22p%3A+adaptive_scaffold%22
+version: 0.0.2
+issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+adaptive_scaffold%22
repository: https://github.com/flutter/packages/tree/main/packages/adaptive_scaffold
# Temporarily unpublished while in process of releasing full package in multiple stages.
publish_to: none
@@ -13,9 +13,9 @@
dependencies:
flutter:
sdk: flutter
+ masonry_grid: ^1.0.0
dev_dependencies:
- flutter_lints: ^2.0.0
flutter_test:
sdk: flutter
-
\ No newline at end of file
+
diff --git a/packages/adaptive_scaffold/test/adaptive_scaffold_test.dart b/packages/adaptive_scaffold/test/adaptive_scaffold_test.dart
index 97d42c6..3171c0f 100644
--- a/packages/adaptive_scaffold/test/adaptive_scaffold_test.dart
+++ b/packages/adaptive_scaffold/test/adaptive_scaffold_test.dart
@@ -104,10 +104,10 @@
expect(bnav, findsNothing);
expect(pnav, findsOneWidget);
- expect(tester.getTopLeft(body), const Offset(72, 0));
+ expect(tester.getTopLeft(body), const Offset(88, 0));
expect(tester.getTopLeft(sBody), const Offset(450, 0));
expect(tester.getTopLeft(pnav), Offset.zero);
- expect(tester.getBottomRight(pnav), const Offset(72, 800));
+ expect(tester.getBottomRight(pnav), const Offset(88, 800));
await tester.pumpWidget(await scaffold(width: 1100, tester: tester));
await tester.pumpAndSettle();
@@ -119,10 +119,10 @@
expect(pnav, findsNothing);
expect(pnav1, findsOneWidget);
- expect(tester.getTopLeft(largeBody), const Offset(192, 0));
+ expect(tester.getTopLeft(largeBody), const Offset(208, 0));
expect(tester.getTopLeft(largeSBody), const Offset(550, 0));
expect(tester.getTopLeft(pnav1), Offset.zero);
- expect(tester.getBottomRight(pnav1), const Offset(192, 800));
+ expect(tester.getBottomRight(pnav1), const Offset(208, 800));
});
testWidgets('adaptive scaffold animations work correctly',
(WidgetTester tester) async {
@@ -135,7 +135,7 @@
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
- expect(tester.getTopLeft(b), const Offset(14.4, 0));
+ expect(tester.getTopLeft(b), const Offset(17.6, 0));
expect(tester.getBottomRight(b),
offsetMoreOrLessEquals(const Offset(778.2, 755.2), epsilon: 1.0));
expect(tester.getTopLeft(sBody),
@@ -146,7 +146,7 @@
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
- expect(tester.getTopLeft(b), const Offset(57.6, 0));
+ expect(tester.getTopLeft(b), const Offset(70.4, 0));
expect(tester.getBottomRight(b),
offsetMoreOrLessEquals(const Offset(416.0, 788.8), epsilon: 1.0));
expect(tester.getTopLeft(sBody),
@@ -157,7 +157,7 @@
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
- expect(tester.getTopLeft(b), const Offset(72, 0));
+ expect(tester.getTopLeft(b), const Offset(88.0, 0));
expect(tester.getBottomRight(b), const Offset(400, 800));
expect(tester.getTopLeft(sBody), const Offset(400, 0));
expect(tester.getBottomRight(sBody), const Offset(800, 800));
@@ -175,7 +175,7 @@
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
- expect(tester.getTopLeft(b), const Offset(72, 0));
+ expect(tester.getTopLeft(b), const Offset(88.0, 0));
expect(tester.getBottomRight(b), const Offset(400, 800));
expect(tester.getTopLeft(sBody), const Offset(400, 0));
expect(tester.getBottomRight(sBody), const Offset(800, 800));