diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f9aeed08..c69ac9d6 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -7,7 +7,7 @@ on:
workflow_call:
env:
- FLUTTER_VERSION: "3.19.3"
+ FLUTTER_VERSION: "3.24.4"
jobs:
tests:
@@ -33,7 +33,7 @@ jobs:
# Checkout and get packages.
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Get packages
run: flutter pub get
@@ -42,9 +42,11 @@ jobs:
- name: Run Unit Tests
run: flutter test --coverage
- - name: Upload Coverage to Codecov
+ - name: Upload coverage to Codecov
if: ${{ runner.os == 'Linux' }}
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v4
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
format:
name: Format
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7d76e268..09e59b7d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,8 +1,7 @@
{
- "dart.lineLength": 80,
- "[dart]": {
- "editor.rulers": [
- 80
- ],
- }
+ "dart.lineLength": 80,
+ "[dart]": {
+ "editor.rulers": [80]
+ },
+ "cmake.ignoreCMakeListsMissing": true
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ff00f8f..d3a65539 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,39 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
+# ChangelogAll notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+### Added
+- Hide the NavBar on scroll: Set `hideOnScrollVelocity` to x (x != 0) to make the NavBar disappear after x pixels have been scrolled (and reappear respectively)
+- `SelectedTabPressConfig`, which is responsible for any configuration when the selected tab is pressed again.
+ - `SelectedTabPressConfig.popAction` allows to specify how many screens of the current tab should be popped if the tab is pressed again
+ - `SelectedTabPressConfig.scrollToTop` enables automatically scrolling the tab content to top if the current tab is pressed again. This requires a ScrollController on each `PersistentTabConfig.scrollController` for each tab this should be activated for.
+ - `SelectedTabPressConfig.onPressed` is a callback that gets executed whenever the current tab is pressed again. I also provides an argument whether there are any pages pushed to the tab.
+- Navigator function that pop all screens of the current tab: `popAllScreensOfCurrentTab`
+- Animated Icons: Have nice animations of the navigation bar icons simply by using the provided `AnimatedIconWrapper` (see README for more)
+- PersistentTabController now gives you access to the previousIndex. Before the first tab switch, this will be null though
+
+### Breaking Changes
+- Use `NavBarOverlap.none()` as the default for `navBarOverlap`
+- Removed ItemConfig.opacity. Use the opacity of NavBarDecoration.color instead
+- Removed ItemConfig.filter. Use NavBarDecoration.filter instead
+- Removed default value of NavBarDecoration.filter to allow omitting the filter
+- Replaced popAllScreensOnTapAnyTabs with keepNavigatorHistory, which has an inverted meaning. To migrate, invert the boolean value for that parameter if you use it.
+- Combined `popAllScreensOnTapOfSelectedTab` and `popActionScreens` into the `SelectedTabPressConfig.popAction`.
+ - Set `popAction` to `PopActionType.all` to pop all screens of the current tab if it is pressed again
+ - Set `popAction` to `PopActionType.single` to pop a single screen of the current tab if it is pressed again
+ - Set `popAction` to `PopActionType.none` to pop no screen of the current tab if it is pressed again
+- Replaced `onSelectedTabPressWhenNoScreensPushed` with `SelectedTabPressConfig.onPressed`. You need to check the passed argument whether there are any pages pushed to that tab.
+- Removed `navBarHeight` parameter. Use the `height` parameter of each style instead if needed.
+
+### Fixed
+- Adjusting the number of tabs at runtime threw an error
+- The state of each tab was not disposed if stateManagement was true and gestures were enabled
+
+### Removed
+- Removed `selectedTabContext`. Use the list of your tabs instead to get the current tab context like so: `tabs[controller.index].navigatorConfig.navigatorKey.currentContext`
+- Removed `PersistentTabController.onIndexChanged`. Use `PersistentTabController.listen` instead.
+
## [5.3.1] - 2024-10-03
### Fixed
- Improve documentation on the historyLength (https://github.com/jb3rndt/PersistentBottomNavBarV2/pull/138)
@@ -574,6 +604,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Includes platform specific behavior as an option (specify it in the two navigator functions).
- Based on flutter's Cupertino(iOS) bottom navigation bar.
+[Unreleased]: https://github.com/jb3rndt/PersistentBottomNavBarV2/compare/5.3.1...HEAD
[5.3.1]: https://github.com/jb3rndt/PersistentBottomNavBarV2/compare/5.3.0...5.3.1
[5.3.0]: https://github.com/jb3rndt/PersistentBottomNavBarV2/compare/5.2.3...5.3.0
[5.2.3]: https://github.com/jb3rndt/PersistentBottomNavBarV2/compare/5.2.2...5.2.3
diff --git a/README.md b/README.md
index 81b104ba..e70261ab 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,21 @@ A highly customizable bottom navigation bar for Flutter. It is shipped with 17 p
NOTE: This package is a continuation of [persistent_bottom_nav_bar](https://pub.dev/packages/persistent_bottom_nav_bar).
-> [!IMPORTANT]
+## 🎉 Version 6.0.0 Released! 🎉
+
+I'm excited to announce the release of **Version 6.0.0** of the Persistent Bottom Navigation Bar! This version comes packed with new features, improvements, and bug fixes to enhance your experience. 🚀
+
+### New Features
+
+- **Scroll to Top**: Added the ability to scroll to the top of the current tab with a double tap on the tab icon.
+- **Animated Icons**: Introduced support for animated icons in the navigation bar.
+- **Change Number of Tabs at Runtime**: Properly support dynamically hiding and showing tabs at runtime.
+
+### Breaking Changes
+
+See the [Changelog](https://github.com/jb3rndt/PersistentBottomNavBarV2/blob/master/CHANGELOG.md) for breaking changes
+
+> [!NOTE]
> If you are migrating from Version 4.x.x to Version 5 read this [MIGRATION GUIDE](https://github.com/jb3rndt/PersistentBottomNavBarV2/blob/master/MigrationGuide.md).
@@ -21,6 +35,9 @@ NOTE: This package is a continuation of [persistent_bottom_nav_bar](https://pub.
Table of Contents
+- [🎉 Version 6.0.0 Released! 🎉](#-version-600-released-)
+ - [New Features](#new-features)
+ - [Breaking Changes](#breaking-changes)
- [Styles](#styles)
- [Features](#features)
- [Getting Started](#getting-started)
@@ -28,8 +45,11 @@ NOTE: This package is a continuation of [persistent_bottom_nav_bar](https://pub.
- [2. Import the package](#2-import-the-package)
- [3. Use the `PersistentTabView`](#3-use-the-persistenttabview)
- [Styling](#styling)
+ - [Using Different Styles](#using-different-styles)
+ - [Customizing with NavBarDecoration and ItemAnimation](#customizing-with-navbardecoration-and-itemanimation)
- [Using a custom Navigation Bar](#using-a-custom-navigation-bar)
-- [Controlling the Navigation Bar programmatically](#controlling-the-navigation-bar-programmatically)
+- [Switching Tabs programmatically](#switching-tabs-programmatically)
+- [AnimatedIcons](#animatedicons)
- [Custom transition animation when switching pages](#custom-transition-animation-when-switching-pages)
- [Navigation](#navigation)
- [Router API](#router-api)
@@ -69,18 +89,26 @@ Note: These do not include all style variations
- New pages can be pushed with or without showing the navigation bar.
- 17 prebuilt navigation bar styles ready to use.
-- Each style is fully customizable ([see below](#styling))
+- Each style is fully customizable ([see styling section](#styling))
- Supports custom navigation bars
-- Persistent Tabs -> Navigation Stack is not discarded when switching to another tab
-- Supports transparency and blur effects
+- Navigation Stack is not discarded when switching to another tab (can be turned off)
+- Option to hide the navigation bar once the user scrolls down
+- Supports animated icons
- Handles hardware/software Android back button.
- Supports [go_router](https://pub.dev/packages/go_router) to make use of flutters Router API
+- Supports transparency and blur effects
## Getting Started
### 1. Install the package
-Follow the [install instructions](https://pub.dev/packages/persistent_bottom_nav_bar_v2/install).
+Run
+
+```bash
+dart pub add persistent_bottom_nav_bar_v2
+```
+
+or look at the [install instructions](https://pub.dev/packages/persistent_bottom_nav_bar_v2/install).
### 2. Import the package
@@ -90,7 +118,7 @@ import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
### 3. Use the `PersistentTabView`
-The `PersistentTabView` is your top level container that will hold both your navigation bar and all the pages (just like a `Scaffold`). Thats why it is not recommended, to wrap the `PersistentTabView` inside a `Scaffold.body`, because it does all of that for you. So just create the config for each tab and insert the `PersistentTabView` like this and you are good to go:
+The `PersistentTabView` is your top level container that will hold both your navigation bar and all the pages (just like a `Scaffold`). Thats why it is not recommended, to wrap the `PersistentTabView` inside a `Scaffold.body` because it does all of that for you. So just create the config for each tab and insert the `PersistentTabView` like this and you are good to go:
```dart
import 'package:flutter/material.dart';
@@ -138,15 +166,59 @@ class PersistenBottomNavBarDemo extends StatelessWidget {
## Styling
-You can customize the Navigation Bar with all the parameters, each style allows. Every style allows you to pass an instance of `NavBarDecoration`. This inherits from `BoxDecoration` and thus offers everything the `BoxDecoration` is capable of. As an example, you could set a different border radius by passing `BorderRadius.circular(8)` to the `NavBarDecoration.border`. Styles that include animations also allow you to adjust the timings and interpolation curves of the animation.
+You can customize the Navigation Bar with all the parameters each style allows. Every style allows you to pass an instance of `NavBarDecoration`. This inherits from `BoxDecoration` and thus offers everything the `BoxDecoration` is capable of. Styles that include animations also allow you to adjust the timings and interpolation curves of the animation.
+
+### Using Different Styles
+
+To use different styles, you can replace the `Style1BottomNavBar` widget with any other predefined style or your custom widget (see [the list of available styles](#styles)). Here is an example of how to use a different style:
+
+```dart
+PersistentTabView(
+ tabs: ...,
+ navBarBuilder: (navBarConfig) => Style2BottomNavBar(
+ navBarConfig: navBarConfig,
+ ),
+),
+```
+
+### Customizing with NavBarDecoration and ItemAnimation
+
+You can customize the appearance of the navigation bar using `NavBarDecoration`. Here is an example:
+
+```dart
+PersistentTabView(
+ tabs: ...,
+ navBarBuilder: (navBarConfig) => Style2BottomNavBar(
+ navBarConfig: navBarConfig,
+ navBarDecoration: NavBarDecoration(
+ color: Colors.blue,
+ borderRadius: BorderRadius.circular(8),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black26,
+ blurRadius: 10,
+ ),
+ ],
+ ),
+ itemAnimationProperties: ItemAnimation(
+ duration: const Duration(milliseconds: 400),
+ curve: Curves.easeInOut,
+ )
+ ),
+),
+```
+
+In this example, the navigation bar will have a blue background, rounded corners, and a shadow effect. You can customize other properties like padding, gradient, and more as needed. Additionally, the navigation bars item animation is adjusted to take 400ms and use an easeInOut curve.
## Using a custom Navigation Bar
-You can replace the `Style1BottomNavBar` widget with your own custom widget. As you can see, the `navBarBuilder` gives you a `navBarConfig`, which should be everything you need to build your custom navigation bar. Here is an example of a custom navigation bar widget:
+You can replace the `StyleXBottomNavBar` widget with your own custom widget. The `navBarBuilder` gives you a `navBarConfig` which contains core functionality to run the `PersistentTabView`. One example is the `onItemSelected` callback which needs to be called in your custom widget when an item is selected to trigger crucial behavior in the `PersistentTabView`. Here is an example of a custom navigation bar widget:
```dart
class CustomNavBar extends StatelessWidget {
final NavBarConfig navBarConfig;
+
+ // You are free to omit this, but in combination with `DecoratedNavBar` it might make your start easier
final NavBarDecoration navBarDecoration;
const CustomNavBar({
@@ -196,7 +268,7 @@ class CustomNavBar extends StatelessWidget {
Widget build(BuildContext context) {
return DecoratedNavBar(
decoration: navBarDecoration,
- height: navBarConfig.navBarHeight,
+ height: kBottomNavigationBarHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
@@ -231,7 +303,7 @@ The most important thing is that you call the `navBarConfig.onItemSelected` func
You dont need to use either the `DecoratedNavBar` widget, nor the `NavBarDecoration`, it is just a helper for you. You can do whatever you want in that custom navigation bar widget, as long as you remember to invoke the `onItemSelected` callback.
-## Controlling the Navigation Bar programmatically
+## Switching Tabs programmatically
Internally, the `PersistentTabView` uses a `PersistentTabController`. So you can pass a controller to the `PersistentTabView` to use it later for changing the tab programmatically:
@@ -245,9 +317,32 @@ PersistentTabView(
_controller.jumpToTab(2);
-// Navigate to the previously selected Table
+// Navigate to the previously selected Tab
_controller.jumpToPreviousTab();
+```
+
+## AnimatedIcons
+Flutter provides [AnimatedIcons](https://api.flutter.dev/flutter/material/AnimatedIcon-class.html) that have two representations and can smoothly animate between them. You can simply use the provided `AnimatedIconWrapper` and just replace your previous icon with an animated one. The `PersistentTabView` will handle all the animations for you automatically (i.e. the icon will animate whenever you enter or leave the respective tab).
+
+Here is an example:
+
+```dart
+PersistentTabView(
+ tabs: [
+ PersistentTabConfig(
+ screen: const MainScreen(),
+ item: ItemConfig(
+ icon: AnimatedIconWrapper(icon: AnimatedIcons.home), // <- this also allows you to change animation timings. The animation itself will be triggered automatically
+ title: "Home",
+ ),
+ ),
+ ...
+ ],
+ navBarBuilder: (navBarConfig) => Style1BottomNavBar(
+ navBarConfig: navBarConfig,
+ ),
+)
```
## Custom transition animation when switching pages
@@ -270,15 +365,29 @@ This is what the default animation builder looks like:
## Navigation
-Each of your Tabs will get its own Navigator, so they dont interfere with eachother. That means there will now be a difference between calling `Navigator.of(context).push()` (which will push a new screen inside the current tab) and `Navigator.of(context, rootNavigator: true).push()` (which will push a new screen above the whole `PersistentTabView`, ultimately hiding your navigation bar).
+Each of your Tabs will get its own Navigator, so they dont interfere with each other. That means there will now be a difference between calling `Navigator.of(context).push()` (which will push a new screen inside the current tab) and `Navigator.of(context, rootNavigator: true).push()` (which will push a new screen above the whole `PersistentTabView`, ultimately hiding your navigation bar).
-The package includes the following utility functions for expressive navigation.
+The package includes the following utility functions for navigation.
```dart
pushScreen(
context,
screen: MainScreen(),
- withNavBar: true/false,
+ withNavBar: true, // or false
+);
+```
+
+```dart
+pushScreenWithNavBar(
+ context,
+ screen: MainScreen(),
+);
+```
+
+```dart
+pushScreenWithoutNavBar(
+ context,
+ screen: MainScreen(),
);
```
@@ -296,6 +405,10 @@ pushWithoutNavBar(
);
```
+```dart
+popAllScreensOfCurrentTab();
+```
+
By default, each of the tabs navigators will inherit all the settings of the root navigator. So every configuration you do to the named routes (etc.) of the root navigator, will work just the same in each tab. If you want specific settings for each navigator (like additional routes, `NavigatorObservers` etc.), you can do so by passing a `NavigatorConfig` to the respective `PersistentTabConfig`.
The `PersistentTabView` has the ability to remember the navigation stack for each tab, so when you switch back to it you will see the exact same content when you left. This behavior can be toggled with the `PersistentTabView.stateManagement` parameter.
@@ -342,7 +455,7 @@ StatefulShellRoute.indexedStack(
StatefulShellBranch(
routes: [
GoRoute(
- path: "home",
+ path: "/home",
builder: (context, state) => const MainScreen(
useRouter: true,
),
@@ -362,7 +475,7 @@ StatefulShellRoute.indexedStack(
StatefulShellBranch(
routes: [
GoRoute(
- path: "messages",
+ path: "/messages",
builder: (context, state) => const MainScreen(
useRouter: true,
),
@@ -374,7 +487,7 @@ StatefulShellRoute.indexedStack(
StatefulShellBranch(
routes: [
GoRoute(
- path: "settings",
+ path: "/settings",
builder: (context, state) => const MainScreen(
useRouter: true,
),
@@ -389,7 +502,7 @@ StatefulShellRoute.indexedStack(
- Try the [interactive example project](https://github.com/jb3rndt/PersistentBottomNavBarV2/tree/master/example) in the official git repo to get a better feeling for the package
-- Pop to any screen in the navigation graph for a given tab:
+- Pop to any screen in the navigation stack for a given tab:
```dart
Navigator.of(context).popUntil((route) {
@@ -397,50 +510,28 @@ StatefulShellRoute.indexedStack(
});
```
-- Pop back to first screen in the navigation graph for a given tab:
-
- ```dart
- Navigator.of(context).popUntil(ModalRoute.withName("/"));
- ```
-
- In order for this to work, you will need your `PersistentNavBarItem` to be named '/' like:
+- Pop back to first screen in the navigation stack for a given tab:
```dart
- PersistentBottomNavBarItem(
- title: ("Home"),
- routeAndNavigatorSettings:
- RouteAndNavigatorSettings(initialRoute: '/')),
- ```
-
- Or instead of using a named Route you can also do this:
-
- ```dart
- Navigator.of(context).pushAndRemoveUntil(
- CupertinoPageRoute(
- builder: (BuildContext context) {
- return FirstScreen();
- },
- ),
- (_) => false,
- );
+ popAllScreensOfCurrentTab();
```
- To push bottom sheet on top of the Navigation Bar, use showModalBottomScreen and set it's property `useRootNavigator` to true. See example project for an illustration.
- If you need e.g. notification counters on the icons in the navBar, you can use the [badges package](https://pub.dev/packages/badges) like so: (see [Issue 11](https://github.com/jb3rndt/PersistentBottomNavBarV2/issues/11))
- ```dart
- PersistentTabConfig(
- screen: ...,
- item: ItemConfig(
- icon: Badge(
- animationType: BadgeAnimationType.scale,
- badgeContent: UnreadIndicator(),
- child: const Icon(
- Icons.chat_rounded,
- ),
- ),
- title: "Chat",
- ),
- ),
- ```
+ ```dart
+ PersistentTabConfig(
+ screen: ...,
+ item: ItemConfig(
+ icon: Badge(
+ animationType: BadgeAnimationType.scale,
+ badgeContent: UnreadIndicator(),
+ child: const Icon(
+ Icons.chat_rounded,
+ ),
+ ),
+ title: "Chat",
+ ),
+ ),
+ ```
diff --git a/example/lib/interactive_example.dart b/example/lib/interactive_example.dart
index 88a956aa..a263f8c4 100644
--- a/example/lib/interactive_example.dart
+++ b/example/lib/interactive_example.dart
@@ -103,24 +103,8 @@ class _InteractiveExampleState extends State {
handleAndroidBackButtonPress: settings.handleAndroidBackButtonPress,
resizeToAvoidBottomInset: settings.resizeToAvoidBottomInset,
stateManagement: settings.stateManagement,
- onWillPop: (context) async {
- await showDialog(
- context: context,
- builder: (context) => Dialog(
- child: Center(
- child: ElevatedButton(
- child: const Text("Close"),
- onPressed: () {
- Navigator.pop(context);
- },
- ),
- ),
- ),
- );
- return false;
- },
hideNavigationBar: settings.hideNavBar,
- popAllScreensOnTapOfSelectedTab:
- settings.popAllScreensOnTapOfSelectedTab,
+ hideOnScrollVelocity: settings.hideOnScrollVelocity,
+ selectedTabPressConfig: settings.selectedTabPressConfig,
);
}
diff --git a/example/lib/screens.dart b/example/lib/screens.dart
index 8c26a16e..63919965 100644
--- a/example/lib/screens.dart
+++ b/example/lib/screens.dart
@@ -112,6 +112,20 @@ class MainScreen extends StatelessWidget {
child: const Text("Push Dynamic/Modal Screen"),
),
),
+ const SizedBox(height: 16),
+ ...List.generate(
+ 20,
+ (i) => Container(
+ padding: const EdgeInsets.all(16),
+ margin: const EdgeInsets.only(top: 8),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.black),
+ borderRadius: BorderRadius.circular(8),
+ color: Colors.white,
+ ),
+ child: Text("Item $i"),
+ ),
+ ),
],
),
);
diff --git a/example/lib/settings.dart b/example/lib/settings.dart
index 3549fa9e..7be1ca75 100644
--- a/example/lib/settings.dart
+++ b/example/lib/settings.dart
@@ -15,7 +15,10 @@ class Settings {
bool stateManagement = true;
bool handleAndroidBackButtonPress = true;
bool popAllScreensOnTapOfSelectedTab = true;
+ int hideOnScrollVelocity = 200;
bool avoidBottomPadding = true;
+ SelectedTabPressConfig selectedTabPressConfig =
+ const SelectedTabPressConfig();
Color navBarColor = Colors.white;
NavBarBuilder get navBarBuilder => navBarStyles[navBarStyle]!;
String navBarStyle = "Style 1";
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 36e1ae69..65f00b66 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -232,10 +232,10 @@ packages:
dependency: transitive
description:
name: vm_service
- sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
+ sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
- version: "14.2.4"
+ version: "14.2.5"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.19.0"
diff --git a/lib/components/animated_icon_wrapper.dart b/lib/components/animated_icon_wrapper.dart
new file mode 100644
index 00000000..6e7daef9
--- /dev/null
+++ b/lib/components/animated_icon_wrapper.dart
@@ -0,0 +1,66 @@
+import "package:flutter/material.dart";
+
+class AnimatedIconWrapper extends StatefulWidget {
+ AnimatedIconWrapper({
+ required this.icon,
+ Key? key,
+ this.duration = const Duration(milliseconds: 400),
+ this.curve = Curves.linear,
+ this.color,
+ this.size,
+ this.semanticLabel,
+ this.textDirection,
+ }) : super(key: key ?? GlobalKey());
+
+ final Duration duration;
+ final Curve curve;
+
+ final AnimatedIconData icon;
+ final Color? color;
+ final double? size;
+ final String? semanticLabel;
+ final TextDirection? textDirection;
+
+ @override
+ State createState() => AnimatedIconWrapperState();
+}
+
+class AnimatedIconWrapperState extends State
+ with SingleTickerProviderStateMixin {
+ late AnimationController _controller;
+ late Animation _animation;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = AnimationController(vsync: this, duration: widget.duration);
+ _animation = Tween(begin: 0.toDouble(), end: 1.toDouble())
+ .animate(CurvedAnimation(parent: _controller, curve: widget.curve));
+ }
+
+ void forward() {
+ _controller.forward();
+ }
+
+ void reverse() {
+ _controller.reverse();
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ AnimationController get controller => _controller;
+
+ @override
+ Widget build(BuildContext context) => AnimatedIcon(
+ icon: widget.icon,
+ color: widget.color,
+ size: widget.size,
+ semanticLabel: widget.semanticLabel,
+ textDirection: widget.textDirection,
+ progress: _animation,
+ );
+}
diff --git a/lib/components/decorated_navbar.dart b/lib/components/decorated_navbar.dart
index e85f528e..b0f4e0f9 100644
--- a/lib/components/decorated_navbar.dart
+++ b/lib/components/decorated_navbar.dart
@@ -1,29 +1,25 @@
part of "../persistent_bottom_nav_bar_v2.dart";
class DecoratedNavBar extends StatelessWidget {
- DecoratedNavBar({
+ const DecoratedNavBar({
required this.child,
super.key,
this.decoration = const NavBarDecoration(),
- ImageFilter? filter,
- this.opacity = 1,
- this.height = kBottomNavigationBarHeight,
- }) : filter = filter ?? ImageFilter.blur(sigmaX: 3, sigmaY: 3);
+ this.height,
+ });
final NavBarDecoration decoration;
- final ImageFilter filter;
final Widget child;
- final double opacity;
- final double height;
+ final double? height;
@override
Widget build(BuildContext context) => Stack(
children: [
- if (opacity < 1)
+ if ((decoration.color?.opacity ?? 1) < 1 && decoration.filter != null)
Positioned.fill(
child: ClipRect(
child: BackdropFilter(
- filter: decoration.filter ?? filter,
+ filter: decoration.filter!,
child: Container(
color: Colors.transparent,
),
@@ -31,18 +27,15 @@ class DecoratedNavBar extends StatelessWidget {
),
),
DecoratedBox(
- decoration: decoration.copyWith(
- color: opacity != 1
- ? decoration.color?.withOpacity(opacity)
- : decoration.color,
- ),
+ decoration: decoration,
child: SafeArea(
top: false,
right: false,
left: false,
child: Container(
padding: decoration.padding,
- height: height - decoration.borderHeight(),
+ height:
+ height != null ? height! - decoration.borderHeight() : null,
child: child,
),
),
diff --git a/lib/components/persistent_tab_view.dart b/lib/components/persistent_tab_view.dart
index 589c7d50..f93f5730 100644
--- a/lib/components/persistent_tab_view.dart
+++ b/lib/components/persistent_tab_view.dart
@@ -15,26 +15,20 @@ class PersistentTabView extends StatefulWidget {
required this.navBarBuilder,
super.key,
this.controller,
- this.navBarHeight = kBottomNavigationBarHeight,
- this.navBarOverlap = const NavBarOverlap.full(),
+ this.navBarOverlap = const NavBarOverlap.none(),
this.margin = EdgeInsets.zero,
this.backgroundColor = Colors.white,
this.onTabChanged,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.resizeToAvoidBottomInset = true,
- this.selectedTabContext,
- this.popAllScreensOnTapOfSelectedTab = true,
- this.popAllScreensOnTapAnyTabs = false,
- this.popActionScreens = PopActionScreensType.all,
+ this.keepNavigatorHistory = true,
+ this.selectedTabPressConfig = const SelectedTabPressConfig(),
this.avoidBottomPadding = true,
- @Deprecated(
- "Wrap [PersistentTabView] with [PopScope] instead. Look here for migration: https://docs.flutter.dev/release/breaking-changes/android-predictive-back",
- )
- this.onWillPop,
this.stateManagement = true,
this.handleAndroidBackButtonPress = true,
this.hideNavigationBar = false,
+ this.hideOnScrollVelocity = 0,
this.screenTransitionAnimation = const ScreenTransitionAnimation(),
this.drawer,
this.drawerEdgeDragWidth,
@@ -47,24 +41,18 @@ class PersistentTabView extends StatefulWidget {
required this.navBarBuilder,
required StatefulNavigationShell this.navigationShell,
super.key,
- this.navBarHeight = kBottomNavigationBarHeight,
- this.navBarOverlap = const NavBarOverlap.full(),
+ this.navBarOverlap = const NavBarOverlap.none(),
this.margin = EdgeInsets.zero,
this.backgroundColor = Colors.white,
this.onTabChanged,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.resizeToAvoidBottomInset = true,
- this.selectedTabContext,
- this.popAllScreensOnTapOfSelectedTab = true,
- this.popAllScreensOnTapAnyTabs = false,
- this.popActionScreens = PopActionScreensType.all,
+ this.keepNavigatorHistory = true,
+ this.selectedTabPressConfig = const SelectedTabPressConfig(),
this.avoidBottomPadding = true,
- @Deprecated(
- "Wrap [PersistentTabView] with [PopScope] instead. Look here for migration: https://docs.flutter.dev/release/breaking-changes/android-predictive-back",
- )
- this.onWillPop,
this.stateManagement = true,
+ this.hideOnScrollVelocity = 0,
this.handleAndroidBackButtonPress = true,
this.hideNavigationBar = false,
this.drawer,
@@ -103,13 +91,20 @@ class PersistentTabView extends StatefulWidget {
/// Defaults to [FloatingActionButtonLocation.endFloat].
final FloatingActionButtonLocation? floatingActionButtonLocation;
- /// Specifies the navBarHeight
+ /// Works similar to [Scaffold.extendBody].
///
- /// Defaults to `kBottomNavigationBarHeight` which is `56.0`.
- final double navBarHeight;
-
- /// Specifies how much the navBar should float above
- /// the tab content. Defaults to [NavBarOverlap.full].
+ /// If set to [NavBarOverlap.full], the tabs will extend to the bottom of
+ /// the screen, so the [bottomNavigationBar] will overlap the tab content.
+ ///
+ /// If set to [NavBarOverlap.none], the tabs will only extend to the top of
+ /// the [bottomNavigationBar], so it will not overlap the tab content.
+ ///
+ /// This is useful when the [bottomNavigationBar] has a non-rectangular shape,
+ /// like rounded corners or [CircularNotchedRectangle]. In this case
+ /// specifying `NavBarOverlap.full` ensures that the tab content will be
+ /// visible through the exposed spaces.
+ ///
+ /// Defaults to [NavBarOverlap.none].
final NavBarOverlap navBarOverlap;
/// The margin around the navigation bar.
@@ -120,7 +115,9 @@ class PersistentTabView extends StatefulWidget {
/// Widget or choose one of the predefined Navigation Bars.
final Widget Function(NavBarConfig) navBarBuilder;
- /// If `true`, the navBar will be positioned so the content does not overlap with the bottom padding caused by system elements. If ``false``, the navBar will be positioned at the bottom of the screen. Defaults to `true`.
+ /// If `true`, the navBar will be positioned so the content does not overlap
+ /// with the bottom padding caused by system elements. If `false`, the navBar
+ /// will be positioned at the bottom of the screen. Defaults to `true`.
final bool avoidBottomPadding;
/// Handles android back button actions. Defaults to `true`.
@@ -130,37 +127,34 @@ class PersistentTabView extends StatefulWidget {
/// 2. If there are no screens pushed on the selected tab, it will go to the previous tab or exit the app, depending on what you set for [PersistentTabController.historyLength].
final bool handleAndroidBackButtonPress;
- /// If an already selected tab is pressed/tapped again, all the screens pushed
- /// on that particular tab will pop until the first screen in the stack.
- /// Defaults to `true`.
- final bool popAllScreensOnTapOfSelectedTab;
-
- /// All the screens pushed on that particular tab will pop until the first
- /// screen in the stack, whether the tab is already selected or not.
- /// Defaults to `false`.
- final bool popAllScreensOnTapAnyTabs;
-
- /// If set all pop until to first screen else set once pop once
- final PopActionScreensType? popActionScreens;
+ /// This defines the behavior when the selected tab is pressed again.
+ /// Possible configs are:
+ /// 1. `onPressed` - A callback that gets called when the selected tab is pressed again.
+ /// 2. `popAction` - Defines how many screens should be popped of the navigator of the selected tab, when the selected tab is pressed again.
+ /// 3. `scrollToTop` - If set to `true`, the selected tab will scroll to the top when the selected tab is pressed again. (Requires a [ScrollController] to be set in the [PersistentTabConfig] of the tab this should apply to.)
+ final SelectedTabPressConfig selectedTabPressConfig;
final bool resizeToAvoidBottomInset;
- /// Preserves the state of each tab's screen. `true` by default.
+ /// Preserves the state of each tab's screen, including pushed screens inside that tab. `true` by default.
+ /// If you only want to preserve the state of each tab but not the screens pushed inside that tab, set `keepNavigatorHistory` to `false`.
final bool stateManagement;
- /// If you want to perform a custom action on Android when exiting the app,
- /// you can write your logic here. Returns context of the selected screen.
- @Deprecated(
- "Wrap [PersistentTabView] with [PopScope] instead. Look here for migration: https://docs.flutter.dev/release/breaking-changes/android-predictive-back",
- )
- final Future Function(BuildContext)? onWillPop;
-
- /// Returns the context of the selected tab.
- final Function(BuildContext)? selectedTabContext;
+ /// If set to `false`, the history of each tab's navigator will be cleared when switching tabs. Defaults to `true`.
+ ///
+ /// NOTE: This will only have an effect if `stateManagement` is set to `true`.
+ final bool keepNavigatorHistory;
/// Screen transition animation properties when switching tabs.
final ScreenTransitionAnimation screenTransitionAnimation;
+ /// Use this to hide the navigation bar when the user scrolls down and show
+ /// it when the user scrolls up. This feature will be enabled if you provide
+ /// a value greater than 0. Defaults to `0`. Recommended value is `200`. This
+ /// means that the user has to scroll 200 pixels in the opposite direction to
+ /// hide/show the navigation bar.
+ final int hideOnScrollVelocity;
+
/// Hides the navigation bar with a transition animation. Defaults to `false`.
final bool hideNavigationBar;
@@ -216,19 +210,12 @@ class PersistentTabView extends StatefulWidget {
}
class _PersistentTabViewState extends State {
- late List _contextList;
late PersistentTabController _controller;
- bool _sendScreenContext = false;
late final List> _tabKeys = List.generate(
widget.tabs.length,
(index) => GlobalKey(),
);
- late bool canPop =
- widget.handleAndroidBackButtonPress && widget.onWillPop == null;
- late final _navigatorKeys = widget.tabs
- .map((config) => config.navigatorConfig.navigatorKey)
- .fillNullsWith((index) => GlobalKey())
- .toList();
+ late bool canPop = widget.handleAndroidBackButtonPress;
@override
void initState() {
@@ -238,26 +225,28 @@ class _PersistentTabViewState extends State {
PersistentTabController(
initialIndex: widget.navigationShell?.currentIndex ?? 0,
);
- _controller.onIndexChanged = widget.onTabChanged;
-
- _contextList = List.filled(widget.tabs.length, null);
_controller.addListener(() {
- if (widget.selectedTabContext != null) {
- _sendScreenContext = true;
- }
if (mounted) {
setState(() {
canPop = calcCanPop();
});
}
+ widget.onTabChanged?.call(_controller.index);
+
+ tryGetAnimatedIconWrapperState(_controller.index)?.forward();
+ if (_controller.previousIndex != null) {
+ tryGetAnimatedIconWrapperState(_controller.previousIndex!)?.reverse();
+ }
});
- if (widget.selectedTabContext != null) {
- _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
- widget.selectedTabContext!(_contextList[_controller.index]!);
- });
- }
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ tryGetAnimatedIconWrapperState(_controller.index)
+ ?.controller
+ .animateTo(1, duration: Duration.zero);
+ }
+ });
}
@override
@@ -280,26 +269,18 @@ class _PersistentTabViewState extends State {
Widget _buildScreen(int index) => CustomTabView(
key: _tabKeys[index],
- navigatorConfig: widget.tabs[index].navigatorConfig
- .copyWith(navigatorKey: _navigatorKeys[index]),
- home: (screenContext) {
- _contextList[index] = screenContext;
- if (_sendScreenContext && index == _controller.index) {
- _sendScreenContext = false;
- widget.selectedTabContext!(_contextList[_controller.index]!);
- }
- return widget.tabs[index].screen;
- },
+ navigatorConfig: widget.tabs[index].navigatorConfig,
+ home: (screenContext) => widget.tabs[index].screen,
);
Widget navigationBarWidget() => PersistentTabViewScaffold(
controller: _controller,
hideNavigationBar: widget.hideNavigationBar,
+ hideOnScrollVelocity: widget.hideOnScrollVelocity,
tabCount: widget.tabs.length,
stateManagement: widget.stateManagement,
backgroundColor: widget.backgroundColor,
navBarOverlap: widget.navBarOverlap,
- opacities: widget.tabs.map((e) => e.item.opacity).toList(),
screenTransitionAnimation: widget.screenTransitionAnimation,
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
avoidBottomPadding: widget.avoidBottomPadding,
@@ -309,62 +290,28 @@ class _PersistentTabViewState extends State {
drawer: widget.drawer,
drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
gestureNavigationEnabled: widget.gestureNavigationEnabled,
+ tabBuilder: (context, index) => _buildScreen(index),
+ animatedTabBuilder: widget.animatedTabBuilder,
+ navigationShell: widget.navigationShell,
tabBar: widget.navBarBuilder(
NavBarConfig(
selectedIndex: _controller.index,
items: widget.tabs.map((e) => e.item).toList(),
- navBarHeight: widget.navBarHeight,
- onItemSelected: (index) {
- if (widget.tabs[index].onPressed != null) {
- widget.tabs[index].onPressed!(context);
- } else {
- if (widget.navigationShell != null) {
- widget.navigationShell!.goBranch(
- index,
- initialLocation: widget.popAllScreensOnTapOfSelectedTab &&
- index == widget.navigationShell!.currentIndex,
- );
- } else {
- final oldIndex = _controller.index;
- _controller.jumpToTab(index);
- if ((widget.popAllScreensOnTapOfSelectedTab &&
- oldIndex == index) ||
- widget.popAllScreensOnTapAnyTabs) {
- popAllScreens();
- }
- }
- }
- },
+ onItemSelected: onItemSelected,
),
),
- tabBuilder: (context, index) => _buildScreen(index),
- animatedTabBuilder: widget.animatedTabBuilder,
- navigationShell: widget.navigationShell,
);
@override
Widget build(BuildContext context) {
- if (_contextList.length != widget.tabs.length) {
- _contextList = List.filled(widget.tabs.length, null);
- }
- if ((widget.handleAndroidBackButtonPress || widget.onWillPop != null) &&
- widget.navigationShell == null) {
+ if (widget.navigationShell == null && widget.handleAndroidBackButtonPress) {
return PopScope(
canPop: canPop,
- onPopInvoked: (didPop) async {
+ onPopInvokedWithResult: (didPop, result) {
if (didPop) {
return;
}
- final navigator = Navigator.of(context);
- final shouldPop = await _canPopTabView();
- // This is only used when onWillPop is provided
- if (shouldPop) {
- if (navigator.canPop()) {
- navigator.pop();
- } else {
- await SystemNavigator.pop();
- }
- }
+ _handlePop();
},
child: NotificationListener(
onNotification: (notification) {
@@ -385,50 +332,102 @@ class _PersistentTabViewState extends State {
}
}
- Future _canPopTabView() async {
- if (!widget.handleAndroidBackButtonPress && widget.onWillPop != null) {
- return widget.onWillPop!(_contextList[_controller.index]!);
- } else {
- final navigator = _navigatorKeys[_controller.index].currentState!;
- if (_controller.historyIsEmpty() && !navigator.canPop()) {
- if (widget.handleAndroidBackButtonPress && widget.onWillPop != null) {
- return widget.onWillPop!(_contextList[_controller.index]!);
+ void onItemSelected(index) {
+ if (widget.tabs[index].onPressed != null) {
+ widget.tabs[index].onPressed!.call(context);
+ return;
+ }
+
+ final oldIndex = _controller.index;
+
+ if (widget.navigationShell != null) {
+ final isSameTab = index == widget.navigationShell!.currentIndex;
+ if (isSameTab) {
+ widget.selectedTabPressConfig.onPressed?.call(false);
+ if (widget.selectedTabPressConfig.scrollToTop) {
+ tryScrollToTop(index);
}
- // CanPop should be true in this case, so we dont return true because the pop already happened
- return false;
+ }
+
+ widget.navigationShell!.goBranch(
+ index,
+ initialLocation:
+ widget.selectedTabPressConfig.popAction == PopActionType.all &&
+ isSameTab,
+ );
+ return;
+ }
+
+ _controller.jumpToTab(index);
+ if (!widget.keepNavigatorHistory) {
+ popScreensAccodingToAction(PopActionType.all);
+ }
+ if (oldIndex == index) {
+ final canPopScreens = _currentNavigatorState()?.canPop() ?? false;
+ widget.selectedTabPressConfig.onPressed?.call(canPopScreens);
+ if (canPopScreens) {
+ popScreensAccodingToAction(
+ widget.selectedTabPressConfig.popAction,
+ );
} else {
- if (navigator.canPop()) {
- navigator.pop();
- } else {
- _controller.jumpToPreviousTab();
+ if (widget.selectedTabPressConfig.scrollToTop) {
+ tryScrollToTop(index);
}
- return false;
}
}
}
- void popAllScreens() {
- final navigator = _navigatorKeys[_controller.index].currentState;
+ void tryScrollToTop(int index) {
+ if (widget.tabs[index].scrollController != null) {
+ widget.tabs[index].scrollController!.animateTo(
+ 0,
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ );
+ }
+ }
+
+ void _handlePop() {
+ final navigator = _currentNavigatorState()!;
+ if (navigator.canPop()) {
+ navigator.pop();
+ } else if (!_controller.historyIsEmpty()) {
+ _controller.jumpToPreviousTab();
+ }
+ }
+
+ NavigatorState? _currentNavigatorState() =>
+ widget.tabs[_controller.index].navigatorConfig.navigatorKey.currentState;
+
+ void popScreensAccodingToAction(PopActionType action) {
+ final navigator = _currentNavigatorState();
if (navigator != null) {
- if (!navigator.canPop()) {
- widget.tabs[_controller.index].onSelectedTabPressWhenNoScreensPushed
- ?.call();
- } else {
- if (widget.popActionScreens == PopActionScreensType.once) {
- navigator.maybePop(context);
- } else {
+ switch (action) {
+ case PopActionType.single:
+ navigator.maybePop();
+ break;
+ case PopActionType.all:
navigator.popUntil((route) => route.isFirst);
- }
+ break;
+ case PopActionType.none:
+ break;
}
}
}
bool calcCanPop({bool? subtreeCantHandlePop}) =>
widget.handleAndroidBackButtonPress &&
- widget.onWillPop == null &&
_controller.historyIsEmpty() &&
- _navigatorKeys[_controller.index].currentState !=
+ _currentNavigatorState() !=
null && // Required if historyLength == 0 because historyIsEmpty() is already true when switching to uninitialized tabs instead of only when going back.
- (subtreeCantHandlePop ??
- !(_navigatorKeys[_controller.index].currentState?.canPop() ?? false));
+ (subtreeCantHandlePop ?? !(_currentNavigatorState()?.canPop() ?? false));
+
+ AnimatedIconWrapperState? tryGetAnimatedIconWrapperState(int index) {
+ if (widget.tabs[index].item.icon is AnimatedIconWrapper) {
+ final key = widget.tabs[index].item.icon.key!
+ as GlobalKey;
+ return key.currentState;
+ }
+ return null;
+ }
}
diff --git a/lib/components/persistent_tab_view_scaffold.dart b/lib/components/persistent_tab_view_scaffold.dart
index 54482502..c9b5db74 100644
--- a/lib/components/persistent_tab_view_scaffold.dart
+++ b/lib/components/persistent_tab_view_scaffold.dart
@@ -7,7 +7,6 @@ class PersistentTabViewScaffold extends StatefulWidget {
required this.controller,
required this.tabCount,
super.key,
- this.opacities = const [],
this.backgroundColor,
this.avoidBottomPadding = true,
this.margin = EdgeInsets.zero,
@@ -16,6 +15,7 @@ class PersistentTabViewScaffold extends StatefulWidget {
this.gestureNavigationEnabled = false,
this.screenTransitionAnimation = const ScreenTransitionAnimation(),
this.hideNavigationBar = false,
+ this.hideOnScrollVelocity = 0,
this.navBarOverlap = const NavBarOverlap.full(),
this.floatingActionButton,
this.floatingActionButtonLocation,
@@ -45,10 +45,10 @@ class PersistentTabViewScaffold extends StatefulWidget {
final EdgeInsets margin;
- final List opacities;
-
final bool hideNavigationBar;
+ final int hideOnScrollVelocity;
+
final bool stateManagement;
final bool gestureNavigationEnabled;
@@ -123,12 +123,7 @@ class _PersistentTabViewScaffoldState extends State
Widget buildTab(BuildContext context, int index) {
double overlap = 0;
- final bool isNotOpaque = index > widget.opacities.length
- ? false
- : widget.opacities[index] != 1.0;
- if ((isNotOpaque && widget.navBarOverlap.fullOverlapWhenNotOpaque) ||
- !_navBarFullyShown ||
- widget.margin.bottom != 0) {
+ if (!_navBarFullyShown || widget.margin.bottom != 0) {
overlap = double.infinity;
} else {
overlap = widget.navBarOverlap.overlap;
@@ -144,66 +139,83 @@ class _PersistentTabViewScaffoldState extends State
}
@override
- Widget build(BuildContext context) => Scaffold(
- key: widget.controller.scaffoldKey,
- resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
- backgroundColor: widget.backgroundColor,
- extendBody: widget.navBarOverlap.overlap != 0 || !_navBarFullyShown,
- floatingActionButton: widget.floatingActionButton,
- floatingActionButtonLocation: widget.floatingActionButtonLocation,
- drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
- drawer: widget.drawer,
- body: Builder(
- builder: (bodyContext) => MediaQuery(
- data: MediaQuery.of(bodyContext).copyWith(
- padding:
- _navBarFullyShown ? null : MediaQuery.of(context).padding,
- viewPadding: !_navBarFullyShown
- ? MediaQuery.of(context).viewPadding
- : widget.navBarOverlap.overlap != 0
- ? MediaQuery.of(bodyContext).padding
- : null,
+ Widget build(BuildContext context) => HideOnScroll(
+ hideOnScrollVelocity: widget.hideOnScrollVelocity,
+ onScroll: (offsetPercentage) {
+ final currentValue = _hideNavBarAnimationController.value;
+ if (currentValue < 1 && offsetPercentage > 0) {
+ // Hide NavBar
+ _hideNavBarAnimationController.value =
+ min(currentValue + offsetPercentage, 1);
+ } else if (currentValue > 0 && offsetPercentage < 0) {
+ // Reveal NavBar
+ _hideNavBarAnimationController.value =
+ max(currentValue + offsetPercentage, 0);
+ }
+ },
+ child: Scaffold(
+ key: widget.controller.scaffoldKey,
+ resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
+ backgroundColor: widget.backgroundColor,
+ extendBody: widget.navBarOverlap.overlap != 0 || !_navBarFullyShown,
+ floatingActionButton: widget.floatingActionButton,
+ floatingActionButtonLocation: widget.floatingActionButtonLocation,
+ drawerEdgeDragWidth: widget.drawerEdgeDragWidth,
+ drawer: widget.drawer,
+ body: Builder(
+ builder: (bodyContext) => MediaQuery(
+ data: MediaQuery.of(bodyContext).copyWith(
+ padding:
+ _navBarFullyShown ? null : MediaQuery.of(context).padding,
+ viewPadding: !_navBarFullyShown
+ ? MediaQuery.of(context).viewPadding
+ : widget.navBarOverlap.overlap != 0
+ ? MediaQuery.of(bodyContext).padding
+ : null,
+ viewInsets: EdgeInsets.zero,
+ ),
+ child: widget.navigationShell ??
+ (widget.gestureNavigationEnabled
+ ? _SwipableTabSwitchingView(
+ currentTabIndex: widget.controller.index,
+ tabCount: widget.tabCount,
+ controller: widget.controller,
+ tabBuilder: buildTab,
+ stateManagement: widget.stateManagement,
+ screenTransitionAnimation:
+ widget.screenTransitionAnimation,
+ )
+ : _TabSwitchingView(
+ currentTabIndex: widget.controller.index,
+ tabCount: widget.tabCount,
+ tabBuilder: buildTab,
+ stateManagement: widget.stateManagement,
+ screenTransitionAnimation:
+ widget.screenTransitionAnimation,
+ animatedTabBuilder: widget.animatedTabBuilder,
+ )),
),
- child: widget.navigationShell ??
- (widget.gestureNavigationEnabled
- ? _SwipableTabSwitchingView(
- currentTabIndex: widget.controller.index,
- tabCount: widget.tabCount,
- controller: widget.controller,
- tabBuilder: buildTab,
- stateManagement: widget.stateManagement,
- screenTransitionAnimation:
- widget.screenTransitionAnimation,
- )
- : _TabSwitchingView(
- currentTabIndex: widget.controller.index,
- tabCount: widget.tabCount,
- tabBuilder: buildTab,
- stateManagement: widget.stateManagement,
- screenTransitionAnimation:
- widget.screenTransitionAnimation,
- animatedTabBuilder: widget.animatedTabBuilder,
- )),
),
- ),
- bottomNavigationBar: NCSizeTransition(
- sizeFactor: _animation,
- child: Padding(
- padding: widget.margin,
- child: MediaQuery(
- data: MediaQuery.of(context).copyWith(
- padding: !widget.avoidBottomPadding
- // safespace should be ignored, so the bottom inset is removed before it could be applied by any safearea child (e.g. in DecoratedNavBar).
- ? EdgeInsets.zero
- // The padding might have been consumed by the keyboard, so it is maintained here. Using maintainBottomViewPadding would require that in the DecoratedNavBar as well, but only if the bottom padding should not be avoided. So it is easier to just maintain the padding here.
- : MediaQuery.of(context).viewPadding,
- ),
- child: SafeArea(
- top: false,
- right: false,
- left: false,
- bottom: widget.avoidBottomPadding && widget.margin.bottom != 0,
- child: widget.tabBar,
+ bottomNavigationBar: NCSizeTransition(
+ sizeFactor: _animation,
+ child: Padding(
+ padding: widget.margin,
+ child: MediaQuery(
+ data: MediaQuery.of(context).copyWith(
+ padding: !widget.avoidBottomPadding
+ // safespace should be ignored, so the bottom inset is removed before it could be applied by any safearea child (e.g. in DecoratedNavBar).
+ ? EdgeInsets.zero
+ // The padding might have been consumed by the keyboard, so it is maintained here. Using maintainBottomViewPadding would require that in the DecoratedNavBar as well, but only if the bottom padding should not be avoided. So it is easier to just maintain the padding here.
+ : MediaQuery.of(context).viewPadding,
+ ),
+ child: SafeArea(
+ top: false,
+ right: false,
+ left: false,
+ bottom:
+ widget.avoidBottomPadding && widget.margin.bottom != 0,
+ child: widget.tabBar,
+ ),
),
),
),
@@ -228,6 +240,42 @@ class PersistentTab extends StatelessWidget {
);
}
+class HideOnScroll extends StatefulWidget {
+ const HideOnScroll({
+ required this.onScroll,
+ required this.child,
+ super.key,
+ this.hideOnScrollVelocity = 0,
+ });
+
+ final int hideOnScrollVelocity;
+ final Widget child;
+ final Function(double) onScroll;
+
+ @override
+ State createState() => _HideOnScrollState();
+}
+
+class _HideOnScrollState extends State {
+ double scrollOffset = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ if (widget.hideOnScrollVelocity == 0) {
+ return widget.child;
+ }
+ return NotificationListener(
+ onNotification: (scrollInfo) {
+ final diff = scrollInfo.metrics.pixels - scrollOffset;
+ scrollOffset = scrollInfo.metrics.pixels;
+ widget.onScroll(diff / widget.hideOnScrollVelocity);
+ return true;
+ },
+ child: widget.child,
+ );
+ }
+}
+
class _SwipableTabSwitchingView extends StatefulWidget {
const _SwipableTabSwitchingView({
required this.currentTabIndex,
@@ -267,15 +315,11 @@ class _SwipableTabSwitchingViewState extends State<_SwipableTabSwitchingView> {
if (widget.currentTabIndex != oldWidget.currentTabIndex &&
!pageUpdateCausedBySwipe) {
isSwiping = false;
- if (widget.screenTransitionAnimation.duration == Duration.zero) {
- _pageController.jumpToPage(widget.currentTabIndex);
- } else {
- _pageController.animateToPage(
- widget.currentTabIndex,
- duration: widget.screenTransitionAnimation.duration,
- curve: widget.screenTransitionAnimation.curve,
- );
- }
+ _pageController.animateToPage(
+ widget.currentTabIndex,
+ duration: widget.screenTransitionAnimation.duration,
+ curve: widget.screenTransitionAnimation.curve,
+ );
}
pageUpdateCausedBySwipe = false;
}
@@ -302,6 +346,7 @@ class _SwipableTabSwitchingViewState extends State<_SwipableTabSwitchingView> {
children: List.generate(
widget.tabCount,
(index) => FocusScope(
+ key: widget.stateManagement ? null : UniqueKey(),
node: FocusScopeNode(),
child: widget.stateManagement
? KeepAlivePage(
@@ -378,10 +423,8 @@ class _TabSwitchingView extends StatefulWidget {
class _TabSwitchingViewState extends State<_TabSwitchingView>
with TickerProviderStateMixin {
- late final List shouldBuildTab =
- List.filled(widget.tabCount, false);
- final List tabFocusNodes = [];
- final List discardedNodes = [];
+ late final List tabMetaData =
+ List.generate(widget.tabCount, (index) => TabMetaData());
late AnimationController _animationController;
late Animation _animation;
late int _currentTabIndex = widget.currentTabIndex;
@@ -393,10 +436,10 @@ class _TabSwitchingViewState extends State<_TabSwitchingView>
@override
void initState() {
super.initState();
- _initAnimationControllers();
+ _initAnimationController();
}
- void _initAnimationControllers() {
+ void _initAnimationController() {
_animationController = AnimationController(
vsync: this,
duration: widget.screenTransitionAnimation.duration,
@@ -407,6 +450,8 @@ class _TabSwitchingViewState extends State<_TabSwitchingView>
setState(() {
key = UniqueKey();
});
+ } else {
+ setState(() {});
}
}
});
@@ -420,23 +465,8 @@ class _TabSwitchingViewState extends State<_TabSwitchingView>
}
void _focusActiveTab() {
- if (tabFocusNodes.length != widget.tabCount) {
- if (tabFocusNodes.length > widget.tabCount) {
- discardedNodes.addAll(tabFocusNodes.sublist(widget.tabCount));
- tabFocusNodes.removeRange(widget.tabCount, tabFocusNodes.length);
- } else {
- tabFocusNodes.addAll(
- List.generate(
- widget.tabCount - tabFocusNodes.length,
- (index) => FocusScopeNode(
- debugLabel:
- "$CupertinoTabScaffold Tab ${index + tabFocusNodes.length}",
- ),
- ),
- );
- }
- }
- FocusScope.of(context).setFirstFocus(tabFocusNodes[_currentTabIndex]);
+ FocusScope.of(context)
+ .setFirstFocus(tabMetaData[_currentTabIndex].focusNode);
}
void _startAnimation() {
@@ -483,17 +513,17 @@ class _TabSwitchingViewState extends State<_TabSwitchingView>
(!_animation.isCompleted &&
index == _previousTabIndex &&
_showAnimation);
- shouldBuildTab[index] =
- active || (shouldBuildTab[index] && widget.stateManagement);
+ tabMetaData[index].shouldBuild = active ||
+ (tabMetaData[index].shouldBuild && widget.stateManagement);
return Offstage(
offstage: !active,
child: TickerMode(
enabled: active,
child: FocusScope(
- node: tabFocusNodes[index],
+ node: tabMetaData[index].focusNode,
child: Builder(
- builder: (context) => shouldBuildTab[index]
+ builder: (context) => tabMetaData[index].shouldBuild
? (_showAnimation
? AnimatedBuilder(
animation: _animation,
@@ -534,34 +564,37 @@ class _TabSwitchingViewState extends State<_TabSwitchingView>
@override
void didUpdateWidget(_TabSwitchingView oldWidget) {
super.didUpdateWidget(oldWidget);
- final int lengthDiff = widget.tabCount - shouldBuildTab.length;
+ final int lengthDiff = widget.tabCount - oldWidget.tabCount;
if (lengthDiff != 0 ||
oldWidget.screenTransitionAnimation !=
widget.screenTransitionAnimation) {
- _initAnimationControllers();
+ _animationController.dispose();
+ _initAnimationController();
}
if (lengthDiff > 0) {
- shouldBuildTab.addAll(List.filled(lengthDiff, false));
+ tabMetaData.addAll(
+ List.generate(lengthDiff, (index) => TabMetaData()),
+ );
} else if (lengthDiff < 0) {
- shouldBuildTab.removeRange(widget.tabCount, shouldBuildTab.length);
+ tabMetaData.skip(widget.tabCount).forEach((tabMetaData) {
+ tabMetaData.focusNode.dispose();
+ });
+ tabMetaData.removeRange(widget.tabCount, tabMetaData.length);
}
if (widget.currentTabIndex != oldWidget.currentTabIndex) {
_currentTabIndex = widget.currentTabIndex;
_previousTabIndex = oldWidget.currentTabIndex;
- _focusActiveTab();
if (_showAnimation) {
_startAnimation();
}
}
+ _focusActiveTab();
}
@override
void dispose() {
- for (FocusScopeNode focusScopeNode in tabFocusNodes) {
- focusScopeNode.dispose();
- }
- for (FocusScopeNode focusScopeNode in discardedNodes) {
- focusScopeNode.dispose();
+ for (final tabMetaData in tabMetaData) {
+ tabMetaData.focusNode.dispose();
}
_animationController.dispose();
super.dispose();
@@ -594,3 +627,12 @@ class NCSizeTransition extends AnimatedWidget {
child: child,
);
}
+
+class TabMetaData {
+ TabMetaData()
+ : shouldBuild = false,
+ focusNode = FocusScopeNode();
+
+ bool shouldBuild;
+ FocusScopeNode focusNode;
+}
diff --git a/lib/models/configs.dart b/lib/models/configs.dart
index bdc60f01..5a517093 100644
--- a/lib/models/configs.dart
+++ b/lib/models/configs.dart
@@ -12,8 +12,6 @@ class ItemConfig {
this.inactiveForegroundColor = CupertinoColors.systemGrey,
Color? activeColorSecondary,
this.inactiveBackgroundColor = Colors.transparent,
- this.opacity = 1.0,
- this.filter,
this.textStyle = const TextStyle(
color: CupertinoColors.systemGrey,
fontWeight: FontWeight.w400,
@@ -22,11 +20,7 @@ class ItemConfig {
this.iconSize = 26.0,
}) : inactiveIcon = inactiveIcon ?? icon,
activeBackgroundColor =
- activeColorSecondary ?? activeForegroundColor.withOpacity(0.2),
- assert(
- opacity >= 0 && opacity <= 1.0,
- "Opacity must be between 0 and 1.0",
- );
+ activeColorSecondary ?? activeForegroundColor.withOpacity(0.2);
/// Icon for the bar item.
final Widget icon;
@@ -49,18 +43,6 @@ class ItemConfig {
/// Color for the item background if unselected (not used in every prebuilt style). Defaults to `Colors.transparent`
final Color inactiveBackgroundColor;
- /// Enables and controls the transparency effect of the entire NavBar when this tab is selected.
- ///
- /// `Warning: Screen will cover the entire extent of the display`
- @Deprecated("Use the opacity of NavBarDecoration.color instead")
- final double opacity;
-
- /// Filter used when `opacity < 1.0`. Can be used to create 'frosted glass' effect.
- ///
- /// By default -> `ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0)`.
- @Deprecated("Use NavBarDecoration.filter instead")
- final ImageFilter? filter;
-
/// `TextStyle` of the title's text. Defaults to `TextStyle(color: CupertinoColors.systemGrey, fontWeight: FontWeight.w400, fontSize: 12.0)`
final TextStyle textStyle;
@@ -76,16 +58,18 @@ class PersistentTabConfig {
PersistentTabConfig({
required this.screen,
required this.item,
- this.navigatorConfig = const NavigatorConfig(),
- this.onSelectedTabPressWhenNoScreensPushed,
- }) : onPressed = null;
+ NavigatorConfig? navigatorConfig,
+ this.scrollController,
+ }) : navigatorConfig = navigatorConfig ?? NavigatorConfig(),
+ onPressed = null;
PersistentTabConfig.noScreen({
required this.item,
required void Function(BuildContext) this.onPressed,
- this.navigatorConfig = const NavigatorConfig(),
- this.onSelectedTabPressWhenNoScreensPushed,
- }) : screen = Container();
+ NavigatorConfig? navigatorConfig,
+ this.scrollController,
+ }) : navigatorConfig = navigatorConfig ?? NavigatorConfig(),
+ screen = Container();
final Widget screen;
@@ -93,21 +77,43 @@ class PersistentTabConfig {
final NavigatorConfig navigatorConfig;
- /// If you want custom behavior on a press of a NavBar item like display a modal screen, you can declare your logic here.
+ /// Use this if you want an item in your navigation bar to perform an action instead of showing a tab. Example: display a modal screen.
///
/// NOTE: This will override the default tab switiching behavior for this particular item.
final void Function(BuildContext)? onPressed;
- /// Use it when you want to run some code when user presses the NavBar when on the initial screen of that respective tab. The inspiration was taken from the native iOS navigation bar behavior where when performing similar operation, you taken to the top of the list.
+ /// This is required for each tab that should scroll to the top when the tab is pressed again. Pass the scroll controller of the content of the tab.
+ final ScrollController? scrollController;
+}
+
+class SelectedTabPressConfig {
+ const SelectedTabPressConfig({
+ this.onPressed,
+ this.popAction = PopActionType.all,
+ this.scrollToTop = true,
+ });
+
+ /// Use this callback to get notified when the selected tab is pressed again. The provided parameter indicates whether there were any screens pushed on the navigator of that tab **before** the user pressed the selected tab.
///
- /// NOTE: This feature is experimental at the moment and might not work as intended for some.
- final Function()? onSelectedTabPressWhenNoScreensPushed;
+ // ignore: avoid_positional_boolean_parameters
+ final void Function(bool)? onPressed;
+
+ /// Defines how many screens should be popped of the navigator of the selected tab, when the selected tab is pressed again.
+ /// If set to `PopActionType.all`, all screens will be popped on a single press.
+ /// If set to `PopActionType.single`, only one screen will be popped on each press.
+ /// If set to `PopActionType.none`, no screens will be popped on a press.
+ /// Defaults to `PopActionScreensType.all`.
+ final PopActionType popAction;
+
+ /// Defines whether the content of the selected tab should be scrolled to the top when the selected tab is pressed again. This requires the content to be a scrollable widget and the scroll controller has to be passed to `PersistentTabConfig.scrollController` of the tab this should apply to.
+ /// Defaults to `true`.
+ final bool scrollToTop;
}
class PersistentRouterTabConfig extends PersistentTabConfig {
PersistentRouterTabConfig({
required super.item,
- super.onSelectedTabPressWhenNoScreensPushed,
+ super.scrollController,
}) : super(screen: Container());
}
@@ -119,39 +125,35 @@ class NavBarConfig {
required this.selectedIndex,
required this.items,
required this.onItemSelected,
- this.navBarHeight = kBottomNavigationBarHeight,
});
final int selectedIndex;
final List items;
final void Function(int) onItemSelected;
- final double navBarHeight;
ItemConfig get selectedItem => items[selectedIndex];
NavBarConfig copyWith({
int? selectedIndex,
List? items,
- bool Function(int)? onItemSelected,
- double? navBarHeight,
+ void Function(int)? onItemSelected,
}) =>
NavBarConfig(
selectedIndex: selectedIndex ?? this.selectedIndex,
items: items ?? this.items,
onItemSelected: onItemSelected ?? this.onItemSelected,
- navBarHeight: navBarHeight ?? this.navBarHeight,
);
}
class NavigatorConfig {
- const NavigatorConfig({
+ NavigatorConfig({
this.defaultTitle,
this.routes = const {},
this.onGenerateRoute,
this.onUnknownRoute,
this.initialRoute,
this.navigatorObservers = const [],
- this.navigatorKey,
- });
+ GlobalKey? navigatorKey,
+ }) : navigatorKey = navigatorKey ?? GlobalKey();
final String? defaultTitle;
@@ -165,7 +167,7 @@ class NavigatorConfig {
final List navigatorObservers;
- final GlobalKey? navigatorKey;
+ final GlobalKey navigatorKey;
NavigatorConfig copyWith({
String? defaultTitle,
diff --git a/lib/models/navbar_decoration.dart b/lib/models/navbar_decoration.dart
index 9ce02ff8..914c21a8 100644
--- a/lib/models/navbar_decoration.dart
+++ b/lib/models/navbar_decoration.dart
@@ -1,6 +1,6 @@
part of "../persistent_bottom_nav_bar_v2.dart";
-enum PopActionScreensType { once, all }
+enum PopActionType { single, all, none }
/// Decoration configuration for the persistent navigation bar.
/// It is suggested to be used in conjunction with [DecoratedNavBar]
@@ -8,7 +8,7 @@ enum PopActionScreensType { once, all }
/// used styling properties.
class NavBarDecoration extends BoxDecoration {
const NavBarDecoration({
- Color super.color = Colors.white,
+ super.color = Colors.white,
super.image,
super.border,
super.borderRadius,
@@ -20,7 +20,7 @@ class NavBarDecoration extends BoxDecoration {
this.filter,
});
- /// `padding` for the persistent navigation bar content.
+ /// `padding` for the NavigationBar content.
@override
final EdgeInsets padding;
diff --git a/lib/models/navbar_overlap.dart b/lib/models/navbar_overlap.dart
index 096faf33..548ab1b2 100644
--- a/lib/models/navbar_overlap.dart
+++ b/lib/models/navbar_overlap.dart
@@ -1,20 +1,13 @@
part of "../persistent_bottom_nav_bar_v2.dart";
class NavBarOverlap {
- const NavBarOverlap.full()
- : overlap = double
- .infinity, // This is the placeholder so [PersistentTabScaffold] uses the navBarHeight instead
- fullOverlapWhenNotOpaque = true;
+ const NavBarOverlap.full() : overlap = double.infinity;
- const NavBarOverlap.none({
- this.fullOverlapWhenNotOpaque = true,
- }) : overlap = 0.0;
+ const NavBarOverlap.none() : overlap = 0.0;
const NavBarOverlap.custom({
this.overlap = 0.0,
- this.fullOverlapWhenNotOpaque = true,
});
final double overlap;
- final bool fullOverlapWhenNotOpaque;
}
diff --git a/lib/models/page_route_transitions.dart b/lib/models/page_route_transitions.dart
index a5ad3c0a..531b9b39 100644
--- a/lib/models/page_route_transitions.dart
+++ b/lib/models/page_route_transitions.dart
@@ -12,229 +12,152 @@ enum PageTransitionAnimation {
slideUp
}
-Widget _slideRightRoute(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
-) =>
- SlideTransition(
- position: Tween(begin: const Offset(-1, 0), end: Offset.zero)
- .animate(animation),
- child: child,
- );
-
-Widget _slideUp(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
-) =>
- SlideTransition(
- position: Tween(begin: const Offset(0, 1), end: Offset.zero)
- .animate(animation),
- child: child,
- );
-
-Widget _scaleRoute(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
-) =>
- ScaleTransition(
- scale: Tween(begin: 0, end: 1).animate(
- CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn),
- ),
- child: child,
- );
-
-Widget _rotationRoute(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
-) =>
- RotationTransition(
- turns: Tween(begin: 0, end: 1)
- .animate(CurvedAnimation(parent: animation, curve: Curves.linear)),
- child: child,
- );
+class SlideUpPageRoute extends PageRouteBuilder {
+ SlideUpPageRoute({required Widget page, RouteSettings? routeSettings})
+ : super(
+ settings: routeSettings,
+ pageBuilder: (context, animation, _) => page,
+ transitionsBuilder: (context, animation, _, child) => SlideTransition(
+ position: Tween(begin: const Offset(0, 1), end: Offset.zero)
+ .animate(animation),
+ child: child,
+ ),
+ );
+}
-Widget _sizeRoute(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
-) =>
- Align(
- child: SizeTransition(sizeFactor: animation, child: child),
- );
+class SlideRightPageRoute extends PageRouteBuilder {
+ SlideRightPageRoute({required Widget page, RouteSettings? routeSettings})
+ : super(
+ settings: routeSettings,
+ pageBuilder: (context, animation, _) => page,
+ transitionsBuilder: (context, animation, _, child) => SlideTransition(
+ position:
+ Tween(begin: const Offset(-1, 0), end: Offset.zero)
+ .animate(animation),
+ child: child,
+ ),
+ );
+}
-Widget _fadeRoute(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
-) =>
- FadeTransition(opacity: animation, child: child);
+class ScalePageRoute extends PageRouteBuilder {
+ ScalePageRoute({required Widget page, RouteSettings? routeSettings})
+ : super(
+ settings: routeSettings,
+ pageBuilder: (context, animation, _) => page,
+ transitionsBuilder: (context, animation, _, child) => ScaleTransition(
+ scale: Tween(begin: 0, end: 1).animate(
+ CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn),
+ ),
+ child: child,
+ ),
+ );
+}
-Widget _scaleRotate(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
-) =>
- ScaleTransition(
- scale: Tween(begin: 0, end: 1).animate(
- CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn),
- ),
- child: RotationTransition(
- turns: Tween(begin: 0, end: 1).animate(
- CurvedAnimation(parent: animation, curve: Curves.linear),
- ),
- child: child,
- ),
- );
+class RotationPageRoute extends PageRouteBuilder {
+ RotationPageRoute({required Widget page, RouteSettings? routeSettings})
+ : super(
+ settings: routeSettings,
+ pageBuilder: (context, animation, _) => page,
+ transitionsBuilder: (context, animation, _, child) =>
+ RotationTransition(
+ turns: Tween(begin: 0, end: 1).animate(
+ CurvedAnimation(parent: animation, curve: Curves.linear),
+ ),
+ child: child,
+ ),
+ );
+}
-class _AnimatedPageRoute extends PageRouteBuilder {
- _AnimatedPageRoute({
- this.exitPage,
- this.enterPage,
- this.transitionAnimation,
- this.routeSettings,
- }) : super(
+class SizePageRoute extends PageRouteBuilder {
+ SizePageRoute({required Widget page, RouteSettings? routeSettings})
+ : super(
settings: routeSettings,
- pageBuilder: (
- context,
- animation,
- secondaryAnimation,
- ) =>
- enterPage!,
- transitionsBuilder: (
- context,
- animation,
- secondaryAnimation,
- child,
- ) =>
- Stack(
- children: [
- _getAnimation(
- context,
- animation,
- secondaryAnimation,
- exitPage,
- transitionAnimation!,
- ),
- _getAnimation(
- context,
- animation,
- secondaryAnimation,
- enterPage,
- transitionAnimation,
- ),
- ],
+ pageBuilder: (context, animation, _) => page,
+ transitionsBuilder: (context, animation, _, child) => Align(
+ child: SizeTransition(sizeFactor: animation, child: child),
),
);
+}
- final Widget? enterPage;
- final Widget? exitPage;
- final PageTransitionAnimation? transitionAnimation;
- final RouteSettings? routeSettings;
+class FadePageRoute extends PageRouteBuilder {
+ FadePageRoute({required Widget page, RouteSettings? routeSettings})
+ : super(
+ settings: routeSettings,
+ pageBuilder: (context, animation, _) => page,
+ transitionsBuilder: (context, animation, _, child) =>
+ FadeTransition(opacity: animation, child: child),
+ );
}
-class _SinglePageRoute extends PageRouteBuilder {
- _SinglePageRoute({this.page, this.transitionAnimation, this.routeSettings})
+class ScaleRotatePageRoute extends PageRouteBuilder {
+ ScaleRotatePageRoute({required Widget page, RouteSettings? routeSettings})
: super(
settings: routeSettings,
- pageBuilder: (
- context,
- animation,
- secondaryAnimation,
- ) =>
- page!,
- transitionsBuilder: (
- context,
- animation,
- secondaryAnimation,
- child,
- ) =>
- _getAnimation(
- context,
- animation,
- secondaryAnimation,
- child,
- transitionAnimation!,
+ pageBuilder: (context, animation, _) => page,
+ transitionsBuilder: (context, animation, _, child) => ScaleTransition(
+ scale: Tween(begin: 0, end: 1).animate(
+ CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn),
+ ),
+ child: RotationTransition(
+ turns: Tween(begin: 0, end: 1).animate(
+ CurvedAnimation(parent: animation, curve: Curves.linear),
+ ),
+ child: child,
+ ),
),
);
-
- final Widget? page;
- final PageTransitionAnimation? transitionAnimation;
- final RouteSettings? routeSettings;
}
-Widget _getAnimation(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget? child,
- PageTransitionAnimation transitionAnimation,
-) {
+Route getPageRoute(
+ PageTransitionAnimation transitionAnimation, {
+ required Widget screen,
+ RouteSettings? settings,
+}) {
switch (transitionAnimation) {
case PageTransitionAnimation.cupertino:
+ return CupertinoPageRoute(
+ settings: settings,
+ builder: (context) => screen,
+ );
case PageTransitionAnimation.platform:
- break;
+ return MaterialPageRoute(
+ settings: settings,
+ builder: (context) => screen,
+ );
case PageTransitionAnimation.slideRight:
- return _slideRightRoute(context, animation, secondaryAnimation, child);
+ return SlideRightPageRoute(
+ routeSettings: settings,
+ page: screen,
+ );
case PageTransitionAnimation.scale:
- return _scaleRoute(context, animation, secondaryAnimation, child);
+ return ScalePageRoute(
+ routeSettings: settings,
+ page: screen,
+ );
case PageTransitionAnimation.rotate:
- return _rotationRoute(context, animation, secondaryAnimation, child);
+ return RotationPageRoute(
+ routeSettings: settings,
+ page: screen,
+ );
case PageTransitionAnimation.sizeUp:
- return _sizeRoute(context, animation, secondaryAnimation, child);
+ return SizePageRoute(
+ routeSettings: settings,
+ page: screen,
+ );
case PageTransitionAnimation.fade:
- return _fadeRoute(context, animation, secondaryAnimation, child);
+ return FadePageRoute(
+ routeSettings: settings,
+ page: screen,
+ );
case PageTransitionAnimation.scaleRotate:
- return _scaleRotate(context, animation, secondaryAnimation, child);
+ return ScaleRotatePageRoute(
+ routeSettings: settings,
+ page: screen,
+ );
case PageTransitionAnimation.slideUp:
- return _slideUp(context, animation, secondaryAnimation, child);
- }
- return Container();
-}
-
-dynamic getPageRoute(
- PageTransitionAnimation transitionAnimation, {
- RouteSettings? settings,
- Widget? enterPage,
- Widget? exitPage,
-}) {
- switch (transitionAnimation) {
- case PageTransitionAnimation.cupertino:
- return settings == null
- ? CupertinoPageRoute(builder: (context) => enterPage!)
- : CupertinoPageRoute(
- settings: settings,
- builder: (context) => enterPage!,
- );
- case PageTransitionAnimation.platform:
- return settings == null
- ? MaterialPageRoute(builder: (context) => enterPage!)
- : MaterialPageRoute(
- settings: settings,
- builder: (context) => enterPage!,
- );
- default:
- return exitPage == null
- ? _SinglePageRoute(
- page: enterPage,
- transitionAnimation: transitionAnimation,
- routeSettings: settings,
- )
- : _AnimatedPageRoute(
- enterPage: enterPage,
- exitPage: exitPage,
- transitionAnimation: transitionAnimation,
- routeSettings: settings,
- );
+ return SlideUpPageRoute(
+ routeSettings: settings,
+ page: screen,
+ );
}
}
diff --git a/lib/models/persistent_tab_controller.dart b/lib/models/persistent_tab_controller.dart
index ea97b452..ebfcad8e 100644
--- a/lib/models/persistent_tab_controller.dart
+++ b/lib/models/persistent_tab_controller.dart
@@ -32,9 +32,10 @@ class PersistentTabController extends ChangeNotifier {
final bool clearHistoryOnInitialIndex;
int get index => _index;
int _index;
+ int? _previousIndex;
+ int? get previousIndex => _previousIndex;
GlobalKey scaffoldKey = GlobalKey();
final List _tabHistory = [];
- ValueChanged? onIndexChanged;
void _updateIndex(int value, [bool isUndo = false]) {
assert(value >= 0, "Nav Bar item index cannot be less than 0");
@@ -62,8 +63,8 @@ class PersistentTabController extends ChangeNotifier {
}
}
}
+ _previousIndex = _index;
_index = value;
- onIndexChanged?.call(value);
notifyListeners();
}
diff --git a/lib/persistent_bottom_nav_bar_v2.dart b/lib/persistent_bottom_nav_bar_v2.dart
index 7c6e7eb3..ef2fe511 100644
--- a/lib/persistent_bottom_nav_bar_v2.dart
+++ b/lib/persistent_bottom_nav_bar_v2.dart
@@ -1,15 +1,13 @@
// Original Author: Bilal Shahid (bilalscheema@gmail.com)
// Version 2 maintained by: Jannis Berndt (berndtjannis@gmail.com)
-library persistent_bottom_nav_bar_v2;
-
import "dart:math";
import "dart:ui";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
-import "package:flutter/services.dart";
import "package:go_router/go_router.dart";
+import "package:persistent_bottom_nav_bar_v2/components/animated_icon_wrapper.dart";
part "components/custom_tab_view.dart";
part "components/decorated_navbar.dart";
diff --git a/lib/styles/neumorphic_bottom_navbar.dart b/lib/styles/neumorphic_bottom_navbar.dart
index 3077db9a..2510bf1f 100644
--- a/lib/styles/neumorphic_bottom_navbar.dart
+++ b/lib/styles/neumorphic_bottom_navbar.dart
@@ -6,11 +6,13 @@ class NeumorphicBottomNavBar extends StatelessWidget {
super.key,
this.navBarDecoration = const NavBarDecoration(),
this.neumorphicProperties = const NeumorphicProperties(),
+ this.height = kBottomNavigationBarHeight,
});
final NavBarConfig navBarConfig;
final NeumorphicProperties neumorphicProperties;
final NavBarDecoration navBarDecoration;
+ final double height;
Widget _getNavItem(
ItemConfig item,
@@ -48,12 +50,9 @@ class NeumorphicBottomNavBar extends StatelessWidget {
ItemConfig item,
bool isSelected,
) =>
- item.opacity == 1.0
+ neumorphicProperties.decoration?.color?.opacity == 1.0
? NeumorphicContainer(
- decoration: neumorphicProperties.decoration?.copyWith(
- color: neumorphicProperties.decoration?.color ??
- navBarDecoration.color,
- ),
+ decoration: neumorphicProperties.decoration,
bevel: neumorphicProperties.bevel,
curveType: isSelected
? CurveType.emboss
@@ -65,12 +64,7 @@ class NeumorphicBottomNavBar extends StatelessWidget {
: Container(
decoration: BoxDecoration(
borderRadius: neumorphicProperties.decoration?.borderRadius,
- color: getBackgroundColor(
- context,
- navBarConfig.items,
- navBarDecoration.color,
- navBarConfig.selectedIndex,
- ),
+ color: neumorphicProperties.decoration?.color,
),
padding: const EdgeInsets.all(6),
margin: const EdgeInsets.symmetric(horizontal: 4),
@@ -80,9 +74,7 @@ class NeumorphicBottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) => DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_10_bottom_navbar.dart b/lib/styles/style_10_bottom_navbar.dart
index 26449741..692bd335 100644
--- a/lib/styles/style_10_bottom_navbar.dart
+++ b/lib/styles/style_10_bottom_navbar.dart
@@ -5,11 +5,13 @@ class Style10BottomNavBar extends StatefulWidget {
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -41,7 +43,7 @@ class _Style10BottomNavBarState extends State
);
_animationList.add(
Tween(
- begin: Offset(0, widget.navBarConfig.navBarHeight),
+ begin: Offset(0, widget.height),
end: Offset.zero,
)
.chain(CurveTween(curve: widget.itemAnimationProperties.curve))
@@ -107,11 +109,7 @@ class _Style10BottomNavBarState extends State
}
return DecoratedNavBar(
decoration: widget.navBarDecoration,
- filter:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].filter,
- opacity:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].opacity,
- height: widget.navBarConfig.navBarHeight,
+ height: widget.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widget.navBarConfig.items.map((item) {
diff --git a/lib/styles/style_11_bottom_navbar.dart b/lib/styles/style_11_bottom_navbar.dart
index 6340b2be..dc24bb9b 100644
--- a/lib/styles/style_11_bottom_navbar.dart
+++ b/lib/styles/style_11_bottom_navbar.dart
@@ -5,11 +5,13 @@ class Style11BottomNavBar extends StatefulWidget {
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -40,7 +42,7 @@ class _Style11BottomNavBarState extends State
);
_animationList.add(
Tween(
- begin: Offset(0, widget.navBarConfig.navBarHeight / 1.5),
+ begin: Offset(0, widget.height / 1.5),
end: Offset.zero,
)
.chain(CurveTween(curve: widget.itemAnimationProperties.curve))
@@ -110,11 +112,7 @@ class _Style11BottomNavBarState extends State
}
return DecoratedNavBar(
decoration: widget.navBarDecoration,
- filter:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].filter,
- opacity:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].opacity,
- height: widget.navBarConfig.navBarHeight,
+ height: widget.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widget.navBarConfig.items.map((item) {
diff --git a/lib/styles/style_12_bottom_navbar.dart b/lib/styles/style_12_bottom_navbar.dart
index 96e5d63e..3a42577b 100644
--- a/lib/styles/style_12_bottom_navbar.dart
+++ b/lib/styles/style_12_bottom_navbar.dart
@@ -5,11 +5,13 @@ class Style12BottomNavBar extends StatefulWidget {
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -41,7 +43,7 @@ class _Style12BottomNavBarState extends State
);
_animationList.add(
Tween(
- begin: Offset(0, widget.navBarConfig.navBarHeight / 1.5),
+ begin: Offset(0, widget.height / 1.5),
end: Offset.zero,
)
.chain(CurveTween(curve: widget.itemAnimationProperties.curve))
@@ -125,11 +127,7 @@ class _Style12BottomNavBarState extends State
}
return DecoratedNavBar(
decoration: widget.navBarDecoration,
- filter:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].filter,
- opacity:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].opacity,
- height: widget.navBarConfig.navBarHeight,
+ height: widget.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widget.navBarConfig.items.map((item) {
diff --git a/lib/styles/style_13_bottom_navbar.dart b/lib/styles/style_13_bottom_navbar.dart
index 691024e9..a8be176e 100644
--- a/lib/styles/style_13_bottom_navbar.dart
+++ b/lib/styles/style_13_bottom_navbar.dart
@@ -4,6 +4,7 @@ class Style13BottomNavBar extends StatelessWidget {
Style13BottomNavBar({
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
+ this.height = kBottomNavigationBarHeight,
super.key,
}) : assert(
navBarConfig.items.length.isOdd,
@@ -12,6 +13,7 @@ class Style13BottomNavBar extends StatelessWidget {
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
Widget _buildItem(BuildContext context, ItemConfig item, bool isSelected) =>
Column(
@@ -48,7 +50,7 @@ class Style13BottomNavBar extends StatelessWidget {
children: [
Container(
width: 150,
- height: navBarConfig.navBarHeight,
+ height: height,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: item.activeForegroundColor,
@@ -96,9 +98,7 @@ class Style13BottomNavBar extends StatelessWidget {
const SizedBox(height: 23),
DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_14_bottom_navbar.dart b/lib/styles/style_14_bottom_navbar.dart
index c2612b71..153e2b0c 100644
--- a/lib/styles/style_14_bottom_navbar.dart
+++ b/lib/styles/style_14_bottom_navbar.dart
@@ -4,6 +4,7 @@ class Style14BottomNavBar extends StatelessWidget {
Style14BottomNavBar({
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
+ this.height = kBottomNavigationBarHeight,
super.key,
}) : assert(
navBarConfig.items.length.isOdd,
@@ -12,6 +13,7 @@ class Style14BottomNavBar extends StatelessWidget {
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
Widget _buildItem(ItemConfig item, bool isSelected) => Column(
mainAxisSize: MainAxisSize.min,
@@ -52,8 +54,8 @@ class Style14BottomNavBar extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
- width: navBarConfig.navBarHeight,
- height: navBarConfig.navBarHeight,
+ width: height,
+ height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: item.activeForegroundColor,
@@ -101,9 +103,7 @@ class Style14BottomNavBar extends StatelessWidget {
const SizedBox(height: 23),
DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_15_bottom_navbar.dart b/lib/styles/style_15_bottom_navbar.dart
index 9d5828bf..4a27bf4d 100644
--- a/lib/styles/style_15_bottom_navbar.dart
+++ b/lib/styles/style_15_bottom_navbar.dart
@@ -70,9 +70,6 @@ class Style15BottomNavBar extends StatelessWidget {
final midIndex = (navBarConfig.items.length / 2).floor();
return DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_16_bottom_navbar.dart b/lib/styles/style_16_bottom_navbar.dart
index 19456784..2f77de99 100644
--- a/lib/styles/style_16_bottom_navbar.dart
+++ b/lib/styles/style_16_bottom_navbar.dart
@@ -4,6 +4,7 @@ class Style16BottomNavBar extends StatelessWidget {
Style16BottomNavBar({
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
+ this.height = kBottomNavigationBarHeight,
super.key,
}) : assert(
navBarConfig.items.length.isOdd,
@@ -12,6 +13,7 @@ class Style16BottomNavBar extends StatelessWidget {
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
Widget _buildItem(ItemConfig item, bool isSelected) => Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -76,9 +78,7 @@ class Style16BottomNavBar extends StatelessWidget {
final midIndex = (navBarConfig.items.length / 2).floor();
return DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_1_bottom_navbar.dart b/lib/styles/style_1_bottom_navbar.dart
index 7550f238..9b05e4e8 100644
--- a/lib/styles/style_1_bottom_navbar.dart
+++ b/lib/styles/style_1_bottom_navbar.dart
@@ -5,10 +5,12 @@ class Style1BottomNavBar extends StatelessWidget {
required this.navBarConfig,
super.key,
this.navBarDecoration = const NavBarDecoration(),
+ this.height = kBottomNavigationBarHeight,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
Widget _buildItem(ItemConfig item, bool isSelected) => Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -47,9 +49,7 @@ class Style1BottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) => DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_2_bottom_navbar.dart b/lib/styles/style_2_bottom_navbar.dart
index f75f1229..45e3f927 100644
--- a/lib/styles/style_2_bottom_navbar.dart
+++ b/lib/styles/style_2_bottom_navbar.dart
@@ -6,12 +6,14 @@ class Style2BottomNavBar extends StatelessWidget {
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
this.itemPadding = const EdgeInsets.all(5),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
final EdgeInsets itemPadding;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -62,9 +64,7 @@ class Style2BottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) => DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_3_bottom_navbar.dart b/lib/styles/style_3_bottom_navbar.dart
index 71299c52..bbd1dce8 100644
--- a/lib/styles/style_3_bottom_navbar.dart
+++ b/lib/styles/style_3_bottom_navbar.dart
@@ -4,11 +4,13 @@ class Style3BottomNavBar extends StatelessWidget {
const Style3BottomNavBar({
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
Widget _buildItem(ItemConfig item, bool isSelected) => Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -41,9 +43,7 @@ class Style3BottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) => DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_4_bottom_navbar.dart b/lib/styles/style_4_bottom_navbar.dart
index 4784a943..8b19d4be 100644
--- a/lib/styles/style_4_bottom_navbar.dart
+++ b/lib/styles/style_4_bottom_navbar.dart
@@ -5,11 +5,13 @@ class Style4BottomNavBar extends StatelessWidget {
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -49,9 +51,7 @@ class Style4BottomNavBar extends StatelessWidget {
navBarConfig.items.length;
return DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Column(
children: [
Row(
diff --git a/lib/styles/style_5_bottom_navbar.dart b/lib/styles/style_5_bottom_navbar.dart
index 40d58730..05d3279c 100644
--- a/lib/styles/style_5_bottom_navbar.dart
+++ b/lib/styles/style_5_bottom_navbar.dart
@@ -4,11 +4,13 @@ class Style5BottomNavBar extends StatelessWidget {
const Style5BottomNavBar({
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
Widget _buildItem(ItemConfig item, bool isSelected) => Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -39,9 +41,7 @@ class Style5BottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) => DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_6_bottom_navbar.dart b/lib/styles/style_6_bottom_navbar.dart
index 619f2d83..f2a0c16f 100644
--- a/lib/styles/style_6_bottom_navbar.dart
+++ b/lib/styles/style_6_bottom_navbar.dart
@@ -5,11 +5,13 @@ class Style6BottomNavBar extends StatefulWidget {
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -103,11 +105,7 @@ class _Style6BottomNavBarState extends State
}
return DecoratedNavBar(
decoration: widget.navBarDecoration,
- filter:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].filter,
- opacity:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].opacity,
- height: widget.navBarConfig.navBarHeight,
+ height: widget.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widget.navBarConfig.items.map((item) {
diff --git a/lib/styles/style_7_bottom_navbar.dart b/lib/styles/style_7_bottom_navbar.dart
index 5fb98147..cc63c7bf 100644
--- a/lib/styles/style_7_bottom_navbar.dart
+++ b/lib/styles/style_7_bottom_navbar.dart
@@ -5,11 +5,13 @@ class Style7BottomNavBar extends StatefulWidget {
required this.navBarConfig,
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -102,11 +104,7 @@ class _Style7BottomNavBarState extends State
}
return DecoratedNavBar(
decoration: widget.navBarDecoration,
- filter:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].filter,
- opacity:
- widget.navBarConfig.items[widget.navBarConfig.selectedIndex].opacity,
- height: widget.navBarConfig.navBarHeight,
+ height: widget.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widget.navBarConfig.items.map((item) {
diff --git a/lib/styles/style_8_bottom_navbar.dart b/lib/styles/style_8_bottom_navbar.dart
index 3a80e713..af7b0316 100644
--- a/lib/styles/style_8_bottom_navbar.dart
+++ b/lib/styles/style_8_bottom_navbar.dart
@@ -6,12 +6,14 @@ class Style8BottomNavBar extends StatelessWidget {
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
this.itemPadding = const EdgeInsets.all(5),
+ this.height = kBottomNavigationBarHeight,
super.key,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
final EdgeInsets itemPadding;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -62,9 +64,7 @@ class Style8BottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) => DecoratedNavBar(
decoration: navBarDecoration,
- filter: navBarConfig.selectedItem.filter,
- opacity: navBarConfig.selectedItem.opacity,
- height: navBarConfig.navBarHeight,
+ height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: navBarConfig.items.map((item) {
diff --git a/lib/styles/style_9_bottom_navbar.dart b/lib/styles/style_9_bottom_navbar.dart
index 7bfc0190..dc1c21e9 100644
--- a/lib/styles/style_9_bottom_navbar.dart
+++ b/lib/styles/style_9_bottom_navbar.dart
@@ -3,13 +3,15 @@ part of "../persistent_bottom_nav_bar_v2.dart";
class Style9BottomNavBar extends StatefulWidget {
const Style9BottomNavBar({
required this.navBarConfig,
+ super.key,
this.navBarDecoration = const NavBarDecoration(),
this.itemAnimationProperties = const ItemAnimation(),
- super.key,
+ this.height = kBottomNavigationBarHeight,
});
final NavBarConfig navBarConfig;
final NavBarDecoration navBarDecoration;
+ final double height;
/// This controls the animation properties of the items of the NavBar.
final ItemAnimation itemAnimationProperties;
@@ -40,7 +42,7 @@ class _Style9BottomNavBarState extends State
);
_animationList.add(
Tween(
- begin: Offset(0, widget.navBarConfig.navBarHeight),
+ begin: Offset(0, widget.height),
end: Offset.zero,
)
.chain(CurveTween(curve: widget.itemAnimationProperties.curve))
@@ -109,9 +111,7 @@ class _Style9BottomNavBarState extends State
}
return DecoratedNavBar(
decoration: widget.navBarDecoration,
- filter: widget.navBarConfig.items[_selectedIndex].filter,
- opacity: widget.navBarConfig.items[_selectedIndex].opacity,
- height: widget.navBarConfig.navBarHeight,
+ height: widget.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widget.navBarConfig.items.map((item) {
diff --git a/lib/utils/functions_utils.dart b/lib/utils/functions_utils.dart
index ecd76481..fbf4eda3 100644
--- a/lib/utils/functions_utils.dart
+++ b/lib/utils/functions_utils.dart
@@ -1,44 +1,5 @@
part of "../persistent_bottom_nav_bar_v2.dart";
-bool isColorOpaque(BuildContext context, Color? color) {
- final Color backgroundColor =
- color ?? CupertinoTheme.of(context).barBackgroundColor;
- return CupertinoDynamicColor.resolve(backgroundColor, context).alpha == 0xFF;
-}
-
-bool opaque(List items, int? selectedIndex) {
- for (int i = 0; i < items.length; ++i) {
- if (items[i].opacity < 1.0 && i == selectedIndex) {
- return false;
- }
- }
- return true;
-}
-
-double getTranslucencyAmount(List items, int? selectedIndex) {
- for (int i = 0; i < items.length; ++i) {
- if (items[i].opacity < 1.0 && i == selectedIndex) {
- return items[i].opacity;
- }
- }
- return 1;
-}
-
-Color getBackgroundColor(
- BuildContext context,
- List? items,
- Color? color,
- int? selectedIndex,
-) {
- if (color == null) {
- return Colors.white;
- } else if (!opaque(items!, selectedIndex) && isColorOpaque(context, color)) {
- return color.withOpacity(getTranslucencyAmount(items, selectedIndex));
- } else {
- return color;
- }
-}
-
/// Needed to make this package backwards compatible with versions of flutter
/// before 3.0.0 while working for flutter 3.0.0 and above without triggering
/// any warnings.
@@ -49,17 +10,3 @@ Color getBackgroundColor(
/// We use this so that APIs that have become non-nullable can still be used
/// with `!` and `?` to support older versions of the API as well.
T? _ambiguate(T? value) => value;
-
-extension IterableSeparatorExtension on Iterable {
- Iterable fillNullsWith(T Function(int) generator) sync* {
- var index = 0;
- for (var element in this) {
- if (element == null) {
- yield generator(index);
- } else {
- yield element;
- }
- index++;
- }
- }
-}
diff --git a/lib/utils/navigator_functions.dart b/lib/utils/navigator_functions.dart
index ad486129..03a38f39 100644
--- a/lib/utils/navigator_functions.dart
+++ b/lib/utils/navigator_functions.dart
@@ -7,16 +7,14 @@ Future pushScreen(
bool withNavBar = false,
PageTransitionAnimation pageTransitionAnimation =
PageTransitionAnimation.platform,
- PageRoute? customPageRoute,
RouteSettings? settings,
}) =>
Navigator.of(context, rootNavigator: !withNavBar).push(
- customPageRoute as Route? ??
- getPageRoute(
- pageTransitionAnimation,
- enterPage: screen,
- settings: settings,
- ),
+ getPageRoute(
+ pageTransitionAnimation,
+ screen: screen,
+ settings: settings,
+ ) as Route,
);
@optionalTypeArgs
@@ -50,3 +48,7 @@ Future pushReplacementWithoutNavBar(
TO? result,
}) =>
Navigator.of(context, rootNavigator: true).pushReplacement(route);
+
+void popAllScreensOfCurrentTab(BuildContext context) {
+ Navigator.popUntil(context, (route) => route.isFirst);
+}
diff --git a/pubspec.lock b/pubspec.lock
index a17b0703..485fb8af 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -217,10 +217,10 @@ packages:
dependency: transitive
description:
name: vm_service
- sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
+ sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
- version: "14.2.4"
+ version: "14.2.5"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.19.0"
diff --git a/test/models_test.dart b/test/models_test.dart
new file mode 100644
index 00000000..e02008de
--- /dev/null
+++ b/test/models_test.dart
@@ -0,0 +1,158 @@
+// ignore_for_file: avoid_redundant_argument_values
+
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart";
+
+void main() {
+ group("ScreenTransitionAnimation", () {
+ test("== and hashCode", () {
+ const animation = ScreenTransitionAnimation();
+ expect(animation, equals(animation));
+ expect(animation, equals(const ScreenTransitionAnimation()));
+ expect(
+ animation.hashCode,
+ equals(const ScreenTransitionAnimation().hashCode),
+ );
+ });
+ });
+
+ group("NavBarConfig", () {
+ test("copyWith", () {
+ bool didRun = false;
+
+ final config = NavBarConfig(
+ selectedIndex: 0,
+ items: List.empty(),
+ onItemSelected: (index) {},
+ );
+ final newConfig = config.copyWith(
+ selectedIndex: 1,
+ items: [ItemConfig(icon: const Icon(Icons.home), title: "Home")],
+ onItemSelected: (index) {
+ didRun = true;
+ },
+ );
+ expect(newConfig.selectedIndex, equals(1));
+ expect(newConfig.items.first.title, equals("Home"));
+ newConfig.onItemSelected(0);
+ expect(didRun, isTrue);
+ });
+
+ test("copyWith without new values", () {
+ bool didRun = false;
+
+ final config = NavBarConfig(
+ selectedIndex: 0,
+ items: List.empty(),
+ onItemSelected: (index) {
+ didRun = true;
+ },
+ );
+ final newConfig = config.copyWith();
+ expect(newConfig.selectedIndex, equals(0));
+ expect(newConfig.items, isEmpty);
+ newConfig.onItemSelected(0);
+ expect(didRun, isTrue);
+ });
+ });
+
+ group("NavigatorConfig", () {
+ test("copyWith", () {
+ final config = NavigatorConfig(
+ defaultTitle: "Default Title",
+ routes: {"route": (context) => const SizedBox()},
+ onGenerateRoute: (settings) => MaterialPageRoute(
+ builder: (context) => const SizedBox(),
+ ),
+ onUnknownRoute: (settings) => MaterialPageRoute(
+ builder: (context) => const SizedBox(),
+ ),
+ initialRoute: "/",
+ navigatorObservers: [NavigatorObserver()],
+ navigatorKey: GlobalKey(),
+ );
+ final newConfig = config.copyWith(
+ defaultTitle: "New Title",
+ routes: {"newRoute": (context) => const SizedBox()},
+ onGenerateRoute: (settings) => MaterialPageRoute(
+ builder: (context) => const SizedBox(),
+ ),
+ onUnknownRoute: (settings) => MaterialPageRoute(
+ builder: (context) => const SizedBox(),
+ ),
+ initialRoute: "/new",
+ navigatorObservers: [NavigatorObserver()],
+ navigatorKey: GlobalKey(),
+ );
+ expect(newConfig.defaultTitle, equals("New Title"));
+ expect(newConfig.routes.keys.first, equals("newRoute"));
+ expect(newConfig.initialRoute, equals("/new"));
+ });
+
+ test("copyWith without new values", () {
+ final config = NavigatorConfig(
+ defaultTitle: "Default Title",
+ routes: {"route": (context) => const SizedBox()},
+ onGenerateRoute: (settings) => MaterialPageRoute(
+ builder: (context) => const SizedBox(),
+ ),
+ onUnknownRoute: (settings) => MaterialPageRoute(
+ builder: (context) => const SizedBox(),
+ ),
+ initialRoute: "/",
+ navigatorObservers: [NavigatorObserver()],
+ navigatorKey: GlobalKey(),
+ );
+ final newConfig = config.copyWith();
+ expect(newConfig.defaultTitle, equals("Default Title"));
+ expect(newConfig.routes.keys.first, equals("route"));
+ expect(newConfig.initialRoute, equals("/"));
+ });
+ });
+
+ group("NavBarDecoration", () {
+ test("exposedHeight is calculated correctly", () {
+ final decoration = NavBarDecoration(
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(width: 2),
+ );
+ expect(decoration.exposedHeight(), equals(14));
+ });
+ });
+
+ group("NeumorphicDecoration", () {
+ test("copyWith", () {
+ final decoration = NeumorphicDecoration(
+ color: Colors.red,
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(width: 2),
+ shape: BoxShape.rectangle,
+ );
+ final newDecoration = decoration.copyWith(
+ color: Colors.blue,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(width: 4),
+ shape: BoxShape.circle,
+ );
+ expect(newDecoration.color, equals(Colors.blue));
+ expect(newDecoration.borderRadius, equals(BorderRadius.circular(20)));
+ expect(newDecoration.border, equals(Border.all(width: 4)));
+ expect(newDecoration.shape, equals(BoxShape.circle));
+ });
+
+ test("copyWith without new values", () {
+ final decoration = NeumorphicDecoration(
+ color: Colors.red,
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(width: 2),
+ shape: BoxShape.rectangle,
+ );
+ final newDecoration = decoration.copyWith();
+ expect(newDecoration.color, equals(Colors.red));
+ expect(newDecoration.borderRadius, equals(BorderRadius.circular(10)));
+ expect(newDecoration.border, equals(Border.all(width: 2)));
+ expect(newDecoration.shape, equals(BoxShape.rectangle));
+ });
+ });
+}
diff --git a/test/nav_bar_styles_test.dart b/test/nav_bar_styles_test.dart
index 2e2164ff..614b74de 100644
--- a/test/nav_bar_styles_test.dart
+++ b/test/nav_bar_styles_test.dart
@@ -2,42 +2,47 @@ import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart";
+import "persistent_tab_view_test.dart";
+
typedef StyleBuilder = Widget Function(NavBarConfig navBarConfig);
-PersistentTabConfig tabConfig(int id) => PersistentTabConfig(
- screen: Text("Screen$id"),
- item: ItemConfig(title: "Item$id", icon: const Icon(Icons.add)),
- );
+Future testStyle(WidgetTester tester, StyleBuilder builder) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(3, (id) => tabConfig(id, defaultScreen(id))),
+ navBarBuilder: (navBarConfig) => builder(navBarConfig),
+ ),
+ ),
+ );
-void main() {
- Widget wrapTabView(WidgetBuilder builder) => MaterialApp(
- home: Builder(
- builder: (context) => builder(context),
- ),
- );
+ await tester.pumpAndSettle();
- Future testStyle(WidgetTester tester, StyleBuilder builder) async {
- await tester.pumpWidget(
- wrapTabView(
- (context) => PersistentTabView(
- tabs: [1, 2, 3].map(tabConfig).toList(),
- navBarBuilder: (navBarConfig) => builder(navBarConfig),
- ),
- ),
- );
- await tester.pumpAndSettle();
+ expect(
+ find.byType(DecoratedNavBar).hitTestable(at: Alignment.centerLeft),
+ findsOneWidget,
+ );
- expect(
- find.byType(DecoratedNavBar).hitTestable(at: Alignment.centerLeft),
- findsOneWidget,
- );
- }
+ await tapItem(tester, 1);
+}
+void main() {
testWidgets("builds every style", (tester) async {
await testStyle(
tester,
(config) => NeumorphicBottomNavBar(navBarConfig: config),
);
+ await testStyle(
+ tester,
+ (config) => NeumorphicBottomNavBar(
+ navBarConfig: config,
+ height: 80,
+ neumorphicProperties: const NeumorphicProperties(
+ decoration: NeumorphicDecoration(color: Colors.red),
+ showSubtitleText: true,
+ ),
+ ),
+ );
await testStyle(
tester,
(config) => Style1BottomNavBar(navBarConfig: config),
diff --git a/test/navigator_functions_test.dart b/test/navigator_functions_test.dart
index cd1d7215..1eeb59e3 100644
--- a/test/navigator_functions_test.dart
+++ b/test/navigator_functions_test.dart
@@ -2,53 +2,28 @@ import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart";
-PersistentTabConfig tabConfig(int id, Widget screen) => PersistentTabConfig(
- screen: screen,
- item: ItemConfig(title: "Item$id", icon: const Icon(Icons.add)),
- );
-
-Widget defaultScreen(int id) => Text("Screen$id");
-
-Widget screenWithButton(int id, void Function(BuildContext) onTap) => Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- defaultScreen(id),
- Builder(
- builder: (context) => ElevatedButton(
- onPressed: () => onTap(context),
- child: const Text("SubPage"),
- ),
- ),
- ],
- );
+import "persistent_tab_view_test.dart";
void main() {
- Widget wrapTabView(WidgetBuilder builder) => MaterialApp(
- home: Builder(
- builder: (context) => builder(context),
- ),
- );
-
group("pushScreen", () {
testWidgets("pushes with navBar", (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map(
- (id) => tabConfig(
- id,
- screenWithButton(
- id,
- (context) => pushScreen(
- context,
- screen: defaultScreen(id * 10 + (id % 10)),
- withNavBar: true,
- ),
- ),
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushScreen(
+ context,
+ screen: defaultScreen(id, level: 1),
+ withNavBar: true,
),
- )
- .toList(),
+ ),
+ ),
+ ),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
),
),
@@ -66,21 +41,19 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map(
- (id) => tabConfig(
- id,
- screenWithButton(
- id,
- (context) => pushScreen(
- context,
- screen: defaultScreen(id * 10 + (id % 10)),
- withNavBar: false,
- ),
- ),
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushScreen(
+ context,
+ screen: defaultScreen(id, level: 1),
),
- )
- .toList(),
+ ),
+ ),
+ ),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
),
),
@@ -99,23 +72,153 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map(
- (id) => tabConfig(
- id,
- screenWithButton(
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushWithNavBar(
+ context,
+ MaterialPageRoute(
+ builder: (context) => defaultScreen(id, level: 1),
+ ),
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+ });
+
+ testWidgets("pushWithoutNavBar pushes without navBar", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushWithoutNavBar(
+ context,
+ MaterialPageRoute(
+ builder: (context) => defaultScreen(id, level: 1),
+ ),
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsNothing);
+ });
+
+ testWidgets("pushScreenWithNavBar pushes with navBar", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushScreenWithNavBar(
+ context,
+ defaultScreen(id, level: 1),
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+ });
+
+ testWidgets("pushScreenWithoutNavBar pushes without navBar", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushScreenWithoutNavBar(
+ context,
+ defaultScreen(id, level: 1),
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsNothing);
+ });
+
+ testWidgets("pushReplacementWithNavBar pushes replacement with navBar",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushScreenWithNavBar(
+ context,
+ defaultScreen(
id,
- (context) => pushWithNavBar(
+ level: 1,
+ onTap: (context) => pushReplacementWithNavBar(
context,
MaterialPageRoute(
- builder: (context) =>
- defaultScreen(id * 10 + (id % 10)),
+ builder: (context) => defaultScreen(id, level: 2),
),
),
),
),
- )
- .toList(),
+ ),
+ ),
+ ),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
),
),
@@ -123,33 +226,48 @@ void main() {
expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+ expectTabAndLevel(tab: 0, level: 0);
+
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ expectTabAndLevel(tab: 0, level: 2);
expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+
+ await tapAndroidBackButton(tester);
+ expectTabAndLevel(tab: 0, level: 0);
});
- testWidgets("pushWithoutNavBar pushes without navBar", (tester) async {
+ testWidgets("pushReplacementWithoutNavBar pushes replacement without navBar",
+ (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map(
- (id) => tabConfig(
- id,
- screenWithButton(
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushScreenWithoutNavBar(
+ context,
+ defaultScreen(
id,
- (context) => pushWithoutNavBar(
+ level: 1,
+ onTap: (context) => pushReplacementWithoutNavBar(
context,
MaterialPageRoute(
- builder: (context) =>
- defaultScreen(id * 10 + (id % 10)),
+ builder: (context) => defaultScreen(id, level: 2),
),
),
),
),
- )
- .toList(),
+ ),
+ ),
+ ),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
),
),
@@ -157,9 +275,63 @@ void main() {
expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+ expectTabAndLevel(tab: 0, level: 0);
+
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
+ await tester.tap(find.byType(ElevatedButton));
+ await tester.pumpAndSettle();
+
+ expectTabAndLevel(tab: 0, level: 2);
expect(find.byType(DecoratedNavBar).hitTestable(), findsNothing);
+
+ await tapAndroidBackButton(tester);
+ expectTabAndLevel(tab: 0, level: 0);
+ });
+
+ testWidgets("popAllScreensOfCurrentTab pops all screens of current tab",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ defaultScreen(
+ id,
+ onTap: (context) => pushScreenWithNavBar(
+ context,
+ defaultScreen(
+ id,
+ level: 1,
+ onTap: (context) => pushScreenWithNavBar(
+ context,
+ defaultScreen(
+ id,
+ level: 2,
+ onTap: popAllScreensOfCurrentTab,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
+
+ await tapElevatedButton(tester);
+ await tapElevatedButton(tester);
+
+ expectTabAndLevel(tab: 0, level: 2);
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 0);
});
}
diff --git a/test/page_route_transitions_test.dart b/test/page_route_transitions_test.dart
new file mode 100644
index 00000000..51f1a59c
--- /dev/null
+++ b/test/page_route_transitions_test.dart
@@ -0,0 +1,242 @@
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart";
+
+Widget mainScreen(void Function() onPressed) => MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: ElevatedButton(
+ onPressed: onPressed,
+ child: const Text("Push Route"),
+ ),
+ ),
+ ),
+ );
+
+void main() {
+ group("PageTransitionAnimation", () {
+ testWidgets("material", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.platform,
+ screen: const Column(
+ children: [
+ Text("Material"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pumpAndSettle();
+
+ expect(find.text("Material"), findsOneWidget);
+ });
+
+ testWidgets("cupertino", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.cupertino,
+ screen: const Column(
+ children: [
+ Text("Cupertino"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pumpAndSettle();
+
+ expect(find.text("Cupertino"), findsOneWidget);
+ });
+
+ testWidgets("slideUp", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.slideUp,
+ screen: const Column(
+ children: [
+ Text("SlideUp"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pump(Durations.short1);
+ await tester.pump(Durations.short1);
+ expect(tester.getCenter(find.byType(Column)).dy, greaterThan(400));
+
+ await tester.pumpAndSettle();
+
+ expect(find.text("SlideUp"), findsOneWidget);
+ });
+
+ testWidgets("slideRight", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.slideRight,
+ screen: const Column(
+ children: [
+ Text("SlideRight"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pump(Durations.short1);
+ await tester.pump(Durations.short1);
+ expect(tester.getCenter(find.byType(Column)).dx, lessThan(0));
+
+ await tester.pumpAndSettle();
+
+ expect(find.text("SlideRight"), findsOneWidget);
+ });
+
+ testWidgets("scale", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.scale,
+ screen: const Column(
+ children: [
+ Text("Scale"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pump(Durations.short1);
+ await tester.pump(Durations.short1);
+
+ expect(tester.getRect(find.byType(Column)).width, lessThan(600));
+ expect(tester.getRect(find.byType(Column)).height, lessThan(800));
+
+ await tester.pumpAndSettle();
+
+ expect(find.text("Scale"), findsOneWidget);
+ });
+
+ testWidgets("rotate", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.rotate,
+ screen: const Column(
+ children: [
+ Text("Rotate"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pumpAndSettle();
+
+ expect(find.text("Rotate"), findsOneWidget);
+ });
+
+ testWidgets("sizeUp", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.sizeUp,
+ screen: const Column(
+ children: [
+ Text("SizeUp"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pump(Durations.short1);
+ await tester.pump(Durations.short1);
+
+ expect(tester.getSize(find.byType(Column)).width, lessThan(600));
+ expect(tester.getSize(find.byType(Column)).height, lessThan(800));
+
+ await tester.pumpAndSettle();
+
+ expect(find.text("SizeUp"), findsOneWidget);
+ });
+
+ testWidgets("fade", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.fade,
+ screen: const Column(
+ children: [
+ Text("Fade"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pumpAndSettle();
+
+ expect(find.text("Fade"), findsOneWidget);
+ });
+
+ testWidgets("scaleRotate", (tester) async {
+ await tester.pumpWidget(
+ mainScreen(() {
+ Navigator.of(tester.element(find.text("Push Route"))).push(
+ getPageRoute(
+ PageTransitionAnimation.scaleRotate,
+ screen: const Column(
+ children: [
+ Text("ScaleRotate"),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+
+ await tester.tap(find.text("Push Route"));
+ await tester.pump(Durations.short1);
+ await tester.pump(Durations.short1);
+
+ expect(tester.getRect(find.byType(Column)).width, lessThan(600));
+ expect(tester.getRect(find.byType(Column)).height, lessThan(800));
+
+ await tester.pumpAndSettle();
+
+ expect(find.text("ScaleRotate"), findsOneWidget);
+ });
+ });
+}
diff --git a/test/persistent_tab_view_test.dart b/test/persistent_tab_view_test.dart
index 2c01c07d..b416ae3f 100644
--- a/test/persistent_tab_view_test.dart
+++ b/test/persistent_tab_view_test.dart
@@ -1,32 +1,95 @@
+// ignore_for_file: avoid_redundant_argument_values
+
+import "dart:ui";
+
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
+import "package:go_router/go_router.dart";
+import "package:persistent_bottom_nav_bar_v2/components/animated_icon_wrapper.dart";
import "package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart";
-PersistentTabConfig tabConfig(int id, Widget screen) => PersistentTabConfig(
+PersistentTabConfig tabConfig(
+ int id,
+ Widget screen, [
+ ScrollController? scrollController,
+]) =>
+ PersistentTabConfig(
screen: screen,
- item: ItemConfig(title: "Item$id", icon: const Icon(Icons.add)),
+ scrollController: scrollController,
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
);
-Widget defaultScreen(int id) => Text("Screen$id");
-
-Widget screenWithSubPages(int id) => id > 99
- ? defaultScreen(id)
- : Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- defaultScreen(id),
- Builder(
- builder: (context) => ElevatedButton(
- onPressed: () => Navigator.of(context).push(
+Widget defaultScreen(
+ int tab, {
+ int level = 0,
+ void Function(BuildContext)? onTap,
+}) =>
+ Column(
+ children: [
+ Text("Tab $tab"),
+ Text("Level $level"),
+ Builder(
+ builder: (context) => ElevatedButton(
+ onPressed: () {
+ if (onTap != null) {
+ onTap(context);
+ return;
+ }
+ Navigator.of(context).push(
MaterialPageRoute(
- builder: (context) => screenWithSubPages(id * 10 + (id % 10)),
+ builder: (context) => defaultScreen(tab, level: level + 1),
),
- ),
- child: const Text("Push SubPage"),
- ),
+ );
+ },
+ child: const Text("Push Screen"),
),
- ],
- );
+ ),
+ ],
+ );
+
+Widget scrollableScreen(
+ int tab, {
+ int level = 0,
+ ScrollController? controller,
+}) =>
+ ListView(
+ controller: controller,
+ children: [
+ Text("Tab $tab"),
+ Text("Level $level"),
+ ...List.generate(
+ 40,
+ (id) => Container(
+ padding: const EdgeInsets.all(16),
+ child: Text("Item $id"),
+ ),
+ ),
+ ],
+ );
+
+List tabs([int count = 3]) => List.generate(
+ count,
+ (id) => tabConfig(id, defaultScreen(id)),
+ );
+
+List routerTabs([
+ int count = 3,
+ List? scrollControllers,
+]) =>
+ List.generate(
+ count,
+ (id) => PersistentRouterTabConfig(
+ scrollController:
+ scrollControllers != null ? scrollControllers[id] : null,
+ item: ItemConfig(
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ title: "Item$id",
+ ),
+ ),
+ );
Future tapAndroidBackButton(WidgetTester tester) async {
final dynamic widgetsAppState = tester.state(find.byType(WidgetsApp));
@@ -41,20 +104,36 @@ Future tapElevatedButton(WidgetTester tester) async {
}
Future tapItem(WidgetTester tester, int id) async {
- await tester.tap(find.text("Item$id"));
+ await tester.tap(find.byKey(Key("Item$id")));
await tester.pumpAndSettle();
}
-void expectScreen(int id, {int screenCount = 3}) {
- find.byType(Text).hitTestable().evaluate().forEach((element) {
- final Text text = element.widget as Text;
- if (text.data?.startsWith("Screen") ?? false) {
- expect(
- text.data,
- equals("Screen$id"),
- );
- }
- });
+Future scroll(
+ WidgetTester tester,
+ Offset start,
+ Offset moveBy,
+) async {
+ final gesture = await tester.startGesture(start);
+
+ await gesture.moveBy(moveBy);
+ await tester.pumpAndSettle();
+
+ await gesture.removePointer();
+ await gesture.cancel();
+}
+
+void expectTabAndLevel({required int tab, required int level}) {
+ expect(find.text("Tab $tab"), findsOneWidget);
+ expect(find.text("Level $level"), findsOneWidget);
+}
+
+void expectNotTabAndLevel({required int tab, required int level}) {
+ expect(find.text("Tab $tab"), findsNothing);
+ expect(find.text("Level $level"), findsNothing);
+}
+
+void expectMainScreen() {
+ expect(find.text("Main Screen"), findsOneWidget);
}
Widget wrapTabView(WidgetBuilder builder) => MaterialApp(
@@ -70,19 +149,45 @@ Widget wrapTabViewWithMainScreen(WidgetBuilder builder) => wrapTabView(
builder: (context) => builder(context),
),
),
- child: const Text("Screen0"),
+ child: const Text("Main Screen"),
),
);
+GoRouter wrapWithGoRouter(
+ Widget Function(BuildContext, GoRouterState, StatefulNavigationShell)?
+ builder, {
+ List? scrollControllers,
+}) =>
+ GoRouter(
+ initialLocation: "/tab-0",
+ routes: [
+ StatefulShellRoute.indexedStack(
+ builder: builder,
+ branches: List.generate(
+ 3,
+ (id) => StatefulShellBranch(
+ initialLocation: "/tab-$id",
+ routes: [
+ GoRoute(
+ path: "/tab-$id",
+ builder: (context, state) => scrollControllers != null
+ ? scrollableScreen(id, controller: scrollControllers[id])
+ : defaultScreen(id),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+
void main() {
group("PersistentTabView", () {
testWidgets("builds a DecoratedNavBar", (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
),
),
@@ -95,30 +200,60 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
),
),
);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
await tapItem(tester, 2);
- expectScreen(2);
- await tapItem(tester, 3);
- expectScreen(3);
+ expectTabAndLevel(tab: 2, level: 0);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
+ });
+
+ testWidgets("runs onPressed instead of switching the tab", (tester) async {
+ int count = 0;
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: [
+ PersistentTabConfig(
+ screen: defaultScreen(0),
+ item: ItemConfig(
+ title: "Item0",
+ icon: const Icon(key: Key("Item0"), Icons.add),
+ ),
+ ),
+ PersistentTabConfig.noScreen(
+ onPressed: (context) {
+ count++;
+ },
+ item: ItemConfig(
+ title: "Item1",
+ icon: const Icon(key: Key("Item1"), Icons.add),
+ ),
+ ),
+ ],
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
await tapItem(tester, 1);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
+ expect(count, 1);
});
testWidgets("hides the navbar when hideNavBar is true", (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
hideNavigationBar: true,
),
@@ -128,17 +263,59 @@ void main() {
expect(find.byType(DecoratedNavBar).hitTestable(), findsNothing);
});
- testWidgets("sizes the navbar according to navBarHeight", (tester) async {
- const double height = 42;
+ testWidgets("changes hideNavBar status at runtime", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ hideNavigationBar: false,
+ ),
+ ),
+ );
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ hideNavigationBar: true,
+ ),
+ ),
+ );
+
+ await tester.pumpAndSettle();
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsNothing);
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- navBarHeight: height,
+ hideNavigationBar: false,
+ ),
+ ),
+ );
+
+ await tester.pumpAndSettle();
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+ });
+
+ testWidgets("sizes the navbar according to the height", (tester) async {
+ const double height = 42;
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(
+ navBarConfig: config,
+ height: height,
+ ),
),
),
);
@@ -156,9 +333,7 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
margin: margin,
),
@@ -220,9 +395,7 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(
navBarConfig: config,
navBarDecoration:
@@ -245,48 +418,69 @@ void main() {
);
});
+ testWidgets("navbar applies filter if color is (partially) transparent",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(
+ navBarConfig: config,
+ navBarDecoration: NavBarDecoration(
+ color: const Color.fromARGB(45, 255, 193, 7),
+ filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(BackdropFilter), findsOneWidget);
+ });
+
testWidgets("executes onItemSelected when tapping items", (tester) async {
int count = 0;
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
onTabChanged: (index) => count++,
),
),
);
- await tapItem(tester, 2);
+ await tapItem(tester, 1);
expect(count, 1);
- await tapItem(tester, 3);
+ await tapItem(tester, 2);
expect(count, 2);
});
- testWidgets("executes onWillPop when exiting", (tester) async {
- int count = 0;
+ testWidgets(
+ "executes onPopInvokedWithResult of enclosing PopScope when exiting",
+ (tester) async {
+ bool? popResult;
await tester.pumpWidget(
wrapTabView(
- (context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
- navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- onWillPop: (context) async {
- count++;
- return true;
+ (context) => PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (didPop, result) {
+ popResult = didPop;
},
+ child: PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
),
),
);
await tapAndroidBackButton(tester);
- expect(count, 1);
+ expect(popResult, false);
});
group("should handle Android back button press and thus", () {
@@ -294,31 +488,25 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
),
);
- expectScreen(1);
- await tapItem(tester, 2);
- expectScreen(2);
-
+ expectTabAndLevel(tab: 0, level: 0);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
await tapAndroidBackButton(tester);
-
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
});
testWidgets("pops one screen on back button press", (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -326,13 +514,10 @@ void main() {
);
await tapElevatedButton(tester);
- expect(find.text("Screen1"), findsNothing);
- expect(find.text("Screen11"), findsOneWidget);
+ expectTabAndLevel(tab: 0, level: 1);
await tapAndroidBackButton(tester);
-
- expect(find.text("Screen1"), findsOneWidget);
- expect(find.text("Screen11"), findsNothing);
+ expectTabAndLevel(tab: 0, level: 0);
});
testWidgets("pops main screen when historyLength is 0", (tester) async {
@@ -340,9 +525,7 @@ void main() {
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller: PersistentTabController(historyLength: 0),
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -350,13 +533,13 @@ void main() {
);
await tapElevatedButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
- await tapItem(tester, 2);
- expectScreen(2);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(0);
+ expectMainScreen();
});
testWidgets("pops main screen when historyLength is 1", (tester) async {
@@ -364,9 +547,7 @@ void main() {
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller: PersistentTabController(historyLength: 1),
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -374,16 +555,16 @@ void main() {
);
await tapElevatedButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
- await tapItem(tester, 2);
- expectScreen(2);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(0);
+ expectMainScreen();
});
testWidgets(
@@ -393,9 +574,7 @@ void main() {
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller: PersistentTabController(historyLength: 1),
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -403,16 +582,16 @@ void main() {
);
await tapElevatedButton(tester);
- expectScreen(1);
-
- await tapItem(tester, 2);
- expectScreen(2);
+ expectTabAndLevel(tab: 0, level: 0);
await tapItem(tester, 1);
- expectScreen(1);
+ expectTabAndLevel(tab: 1, level: 0);
+
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(0);
+ expectMainScreen();
});
testWidgets(
@@ -422,9 +601,7 @@ void main() {
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller: PersistentTabController(historyLength: 1),
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -432,22 +609,22 @@ void main() {
);
await tapElevatedButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
await tapElevatedButton(tester);
- expectScreen(11);
+ expectTabAndLevel(tab: 0, level: 1);
- await tapItem(tester, 2);
- expectScreen(2);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(11);
+ expectTabAndLevel(tab: 0, level: 1);
await tapAndroidBackButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(0);
+ expectMainScreen();
});
testWidgets("pops main screen when historyLength is 2", (tester) async {
@@ -455,9 +632,7 @@ void main() {
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller: PersistentTabController(historyLength: 2),
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -465,22 +640,22 @@ void main() {
);
await tapElevatedButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
- await tapItem(tester, 2);
- expectScreen(2);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
- await tapItem(tester, 3);
- expectScreen(3);
+ await tapItem(tester, 2);
+ expectTabAndLevel(tab: 2, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(2);
+ expectTabAndLevel(tab: 1, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(0);
+ expectMainScreen();
});
testWidgets(
@@ -493,9 +668,7 @@ void main() {
historyLength: 2,
clearHistoryOnInitialIndex: true,
),
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -503,28 +676,77 @@ void main() {
);
await tapElevatedButton(tester);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 0);
- await tapItem(tester, 2);
- expectScreen(2);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
+
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
+
+ await tapAndroidBackButton(tester);
+ expectMainScreen();
+ });
+
+ testWidgets(
+ "pops main screen when historyLength is 2 and clears repeating tabs in history",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabViewWithMainScreen(
+ (context) => PersistentTabView(
+ controller: PersistentTabController(historyLength: 2),
+ tabs: tabs(),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 0);
+
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
+
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
await tapItem(tester, 1);
- expectScreen(1);
+ expectTabAndLevel(tab: 1, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(0);
+ await tapAndroidBackButton(tester);
+ expectMainScreen();
});
});
+ testWidgets("controller can open drawer", (tester) async {
+ final controller = PersistentTabController();
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ controller: controller,
+ tabs: tabs(),
+ drawer: const Drawer(child: Text("Drawer")),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ controller.openDrawer();
+ await tester.pumpAndSettle();
+
+ expect(find.text("Drawer"), findsOneWidget);
+ });
+
group("should not handle Android back button press and thus", () {
testWidgets("does not switch the tab on back button press",
(tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
handleAndroidBackButtonPress: false,
@@ -532,22 +754,20 @@ void main() {
),
);
- expectScreen(1);
- await tapItem(tester, 2);
- expectScreen(2);
+ expectTabAndLevel(tab: 0, level: 0);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
await tapAndroidBackButton(tester);
- expectScreen(2);
+ expectTabAndLevel(tab: 1, level: 0);
});
testWidgets("pops no screen on back button press", (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
handleAndroidBackButtonPress: false,
@@ -556,62 +776,186 @@ void main() {
);
await tapElevatedButton(tester);
- expectScreen(11);
+ expectTabAndLevel(tab: 0, level: 1);
await tapAndroidBackButton(tester);
- expectScreen(11);
+ expectTabAndLevel(tab: 0, level: 1);
});
});
- testWidgets("navBarPadding adds padding inside navBar", (tester) async {
- await tester.pumpWidget(
- wrapTabView(
- (context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
- navBarBuilder: (config) => Style1BottomNavBar(
- navBarConfig: config,
- navBarDecoration:
- const NavBarDecoration(padding: EdgeInsets.zero),
+ group("handles altering tabs at runtime when", () {
+ testWidgets("removing tabs", (tester) async {
+ final List localTabs = tabs();
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: localTabs,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
),
),
- ),
- );
- final double originalIconSize =
- tester.getSize(find.byType(Icon).first).height;
+ );
- await tester.pumpWidget(
- wrapTabView(
- (context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
- navBarBuilder: (config) => Style1BottomNavBar(
- navBarConfig: config,
- navBarDecoration:
- const NavBarDecoration(padding: EdgeInsets.all(4)),
+ expectTabAndLevel(tab: 0, level: 0);
+ expect(find.byType(Icon), findsNWidgets(3));
+
+ localTabs.removeAt(0);
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: localTabs,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
),
),
- ),
- );
- expect(
- tester.getSize(find.byType(Icon).first).height,
- equals(originalIconSize - 4 * 2),
- );
- });
+ );
+ await tester.pump();
- testWidgets("navBarPadding does not make navbar bigger", (tester) async {
- await tester.pumpWidget(
- wrapTabView(
- (context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
- navBarBuilder: (config) => Style1BottomNavBar(
- navBarConfig: config,
- navBarDecoration:
+ expectTabAndLevel(tab: 1, level: 0);
+ expect(find.byType(Icon), findsNWidgets(2));
+ });
+
+ testWidgets("removing and re-adding tabs", (tester) async {
+ final List localTabs = tabs();
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: localTabs,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
+ expect(find.byType(Icon), findsNWidgets(3));
+
+ localTabs.removeAt(0);
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: localTabs,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expectTabAndLevel(tab: 1, level: 0);
+ expect(find.byType(Icon), findsNWidgets(2));
+
+ localTabs.insert(0, tabConfig(0, defaultScreen(0)));
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: localTabs,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expectTabAndLevel(tab: 0, level: 0);
+ expect(find.byType(Icon), findsNWidgets(3));
+ });
+ });
+
+ testWidgets(
+ "hides nav bar after the specified amount of pixels have been scrolled",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(3, (id) => tabConfig(id, scrollableScreen(id))),
+ navBarBuilder: (config) => Style1BottomNavBar(
+ navBarConfig: config,
+ ),
+ hideOnScrollVelocity: 200,
+ ),
+ ),
+ );
+
+ final initialHeight =
+ tester.getSize(find.byType(DecoratedNavBar).first).height;
+
+ await scroll(tester, const Offset(0, 200), const Offset(0, -100));
+
+ expect(
+ find.byType(DecoratedNavBar).hitTestable(at: Alignment.topCenter),
+ findsOneWidget,
+ );
+ expect(
+ (tester.getRect(find.byType(DecoratedNavBar).first).bottom - 600)
+ .toStringAsPrecision(2),
+ equals(
+ (Curves.ease.transform(0.5) * initialHeight).toStringAsPrecision(2),
+ ),
+ );
+
+ await scroll(tester, const Offset(0, 200), const Offset(0, -100));
+ expect(
+ tester.getRect(find.byType(DecoratedNavBar).first).bottom - 600,
+ equals(kBottomNavigationBarHeight),
+ );
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsNothing);
+ await scroll(tester, const Offset(0, 200), const Offset(0, 200));
+
+ expect(find.byType(DecoratedNavBar).hitTestable(), findsOneWidget);
+ expect(
+ tester.getSize(find.byType(DecoratedNavBar).first).height,
+ equals(initialHeight),
+ );
+ });
+
+ testWidgets("navBarPadding adds padding inside navBar", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(
+ navBarConfig: config,
+ navBarDecoration:
+ const NavBarDecoration(padding: EdgeInsets.zero),
+ ),
+ ),
+ ),
+ );
+ final double originalIconSize =
+ tester.getSize(find.byType(Icon).first).height;
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(
+ navBarConfig: config,
+ navBarDecoration:
+ const NavBarDecoration(padding: EdgeInsets.all(4)),
+ ),
+ ),
+ ),
+ );
+ expect(
+ tester.getSize(find.byType(Icon).first).height,
+ equals(originalIconSize - 4 * 2),
+ );
+ });
+
+ testWidgets("navBarPadding does not make navbar bigger", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(
+ navBarConfig: config,
+ navBarDecoration:
const NavBarDecoration(padding: EdgeInsets.all(4)),
),
),
@@ -636,9 +980,7 @@ void main() {
),
child: Builder(
builder: (context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
@@ -661,9 +1003,7 @@ void main() {
),
child: Builder(
builder: (context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
resizeToAvoidBottomInset: false,
@@ -675,7 +1015,7 @@ void main() {
expect(
tester.getSize(find.byType(CustomTabView).first).height,
- equals(600),
+ equals(600 - kBottomNavigationBarHeight),
);
});
@@ -684,10 +1024,9 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ navBarOverlap: const NavBarOverlap.full(),
),
),
);
@@ -700,11 +1039,8 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- navBarOverlap: const NavBarOverlap.none(),
),
),
);
@@ -717,9 +1053,7 @@ void main() {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
navBarOverlap: const NavBarOverlap.custom(overlap: 30),
),
@@ -733,229 +1067,1120 @@ void main() {
});
testWidgets(
- "returns current screen context through selectedTabScreenContext",
+ "doesnt pop any screen when tapping same tab when `selectedTabPressConfig.popAction == PopActionType.none`",
(tester) async {
- BuildContext? screenContext;
-
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- selectedTabContext: (context) => screenContext = context,
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ popAction: PopActionType.none,
+ ),
),
),
);
- expect(
- screenContext?.findAncestorWidgetOfExactType()?.offstage,
- isFalse,
- );
- final BuildContext? oldContext = screenContext;
- await tapItem(tester, 2);
- expect(screenContext, isNot(equals(oldContext)));
- expect(
- screenContext?.findAncestorWidgetOfExactType()?.offstage,
- isFalse,
- );
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 1);
});
testWidgets(
- "doesnt pop all screens when tapping same tab when `popAllScreensOnTapOfSelectedTab: false`",
+ "pops all screens when tapping same tab when `selectedTabPressConfig.popAction == PopActionType.all`",
(tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- popAllScreensOnTapOfSelectedTab: false,
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ popAction: PopActionType.all,
+ ),
),
),
);
await tapElevatedButton(tester);
- expectScreen(11);
- await tapItem(tester, 1);
- expectScreen(11);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 2);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
});
- testWidgets("pops all screens when tapping same tab", (tester) async {
+ testWidgets(
+ "pops a single screen when tapping same tab when `selectedTabPressConfig.popAction == PopActionType.single`",
+ (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ popAction: PopActionType.single,
+ ),
),
),
);
await tapElevatedButton(tester);
- expectScreen(11);
+ expectTabAndLevel(tab: 0, level: 1);
await tapElevatedButton(tester);
- expectScreen(111);
- await tapItem(tester, 1);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 2);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 1);
});
testWidgets(
- "doesnt pop all screens when tapping any tab when `popAllScreensOnTapAnyTabs: false`",
+ "runs callback when selected tab is tapped again and there are no screens pushed",
(tester) async {
+ bool callbackGotExecuted = false;
+ bool areScreensPushed = false;
+
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ selectedTabPressConfig: SelectedTabPressConfig(
+ onPressed: (hasPages) {
+ areScreensPushed = hasPages;
+ callbackGotExecuted = true;
+ },
+ ),
),
),
);
- await tapElevatedButton(tester);
- expectScreen(11);
- await tapItem(tester, 2);
- await tapItem(tester, 1);
- expectScreen(11);
+ expectTabAndLevel(tab: 0, level: 0);
+ await tapItem(tester, 0);
+ expect(areScreensPushed, equals(false));
+ expect(callbackGotExecuted, equals(true));
});
testWidgets(
- "pops all screens when tapping any tab when `popAllScreensOnTapAnyTabs: true`",
+ "runs callback when selected tab is tapped again and there are screens pushed",
(tester) async {
+ bool callbackGotExecuted = false;
+ bool areScreensPushed = false;
+
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- popAllScreensOnTapAnyTabs: true,
+ selectedTabPressConfig: SelectedTabPressConfig(
+ onPressed: (hasPages) {
+ areScreensPushed = hasPages;
+ callbackGotExecuted = true;
+ },
+ ),
),
),
);
await tapElevatedButton(tester);
- expectScreen(11);
- await tapItem(tester, 2);
- await tapItem(tester, 1);
- expectScreen(1);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 0);
+ expect(areScreensPushed, equals(true));
+ expect(callbackGotExecuted, equals(true));
});
- testWidgets("persists screens while switching if stateManagement turned on",
+ testWidgets(
+ "scrolls the tab content to top when the selected tab is tapped again and it is enabled",
(tester) async {
+ final controllers = [
+ ScrollController(),
+ ScrollController(),
+ ScrollController(),
+ ];
+
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ scrollableScreen(
+ id,
+ controller: controllers[id],
+ ),
+ controllers[id],
+ ),
+ ),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ scrollToTop: true,
+ ),
),
),
);
- await tapElevatedButton(tester);
- expectScreen(11);
- await tapItem(tester, 2);
- expectScreen(2);
- await tapItem(tester, 1);
- expectScreen(11);
+ await scroll(tester, const Offset(0, 200), const Offset(0, -400));
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+
+ await tapItem(tester, 0);
+
+ expectTabAndLevel(tab: 0, level: 0);
+ expect(controllers[0].offset, equals(0));
});
- testWidgets("trashes screens while switching if stateManagement turned off",
+ testWidgets(
+ "does not scroll the tab content to top when the selected tab is tapped again when it is disabled",
(tester) async {
+ final controllers = [
+ ScrollController(),
+ ScrollController(),
+ ScrollController(),
+ ];
+
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ scrollableScreen(
+ id,
+ controller: controllers[id],
+ ),
+ controllers[id],
+ ),
+ ),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- stateManagement: false,
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ scrollToTop: false,
+ ),
),
),
);
- await tapElevatedButton(tester);
- expectScreen(11);
- await tapItem(tester, 2);
- expectScreen(2);
- await tapItem(tester, 1);
- expectScreen(1);
+ await scroll(tester, const Offset(0, 200), const Offset(0, -400));
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+
+ await tapItem(tester, 0);
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+ expect(controllers[0].offset, isNot(0));
});
- testWidgets("shows FloatingActionButton if specified", (tester) async {
+ testWidgets(
+ "scrollToTop is enabled but switching to a new tab does not scroll immediately",
+ (tester) async {
+ final controllers = [
+ ScrollController(),
+ ScrollController(),
+ ScrollController(),
+ ];
+
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, screenWithSubPages(id)))
- .toList(),
+ tabs: List.generate(
+ 3,
+ (id) => tabConfig(
+ id,
+ scrollableScreen(
+ id,
+ controller: controllers[id],
+ ),
+ controllers[id],
+ ),
+ ),
navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
- floatingActionButton: FloatingActionButton(
- onPressed: () {},
- child: const Icon(Icons.add),
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ scrollToTop: true,
),
),
),
);
- expect(find.byType(FloatingActionButton).hitTestable(), findsOneWidget);
+ await scroll(tester, const Offset(0, 200), const Offset(0, -400));
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+
+ await tapItem(tester, 1);
+ await tapItem(tester, 0);
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+ expect(controllers[0].offset, isNot(0));
});
testWidgets(
- "Style 13 and Style 14 center button are tappable above the navBar",
+ "does keep navigator history when `keepNaviagotHistory == true`",
(tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
- navBarBuilder: (config) =>
- Style13BottomNavBar(navBarConfig: config),
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ keepNavigatorHistory: true,
),
),
);
- Offset topCenter = tester.getRect(find.byType(DecoratedNavBar)).topCenter;
- await tester.tapAt(topCenter.translate(0, -10));
- await tester.pumpAndSettle();
- expectScreen(2);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 1);
+ });
+ testWidgets(
+ "doesnt keep any navigator history when `keepNaviagotHistory == false`",
+ (tester) async {
await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
- navBarBuilder: (config) =>
- Style14BottomNavBar(navBarConfig: config),
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ keepNavigatorHistory: false,
),
),
);
- topCenter = tester.getRect(find.byType(DecoratedNavBar)).topCenter;
- await tester.tapAt(topCenter.translate(0, -10));
- await tester.pumpAndSettle();
- expectScreen(2);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
});
- });
- group("Regression", () {
- testWidgets("#31 one navbar border side does not throw error",
- (widgetTester) async {
- await widgetTester.pumpWidget(
+ testWidgets("persists screens while switching if stateManagement turned on",
+ (tester) async {
+ await tester.pumpWidget(
wrapTabView(
(context) => PersistentTabView(
- tabs: [1, 2, 3]
- .map((id) => tabConfig(id, defaultScreen(id)))
- .toList(),
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 1);
+ });
+
+ testWidgets("trashes screens while switching if stateManagement turned off",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ stateManagement: false,
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
+ });
+
+ testWidgets(
+ "changes animation direction when switching tabs if TextDirection is RTL",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => Directionality(
+ textDirection: TextDirection.rtl,
+ child: PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
+ await tester.tap(find.text("Item1"));
+ await tester.pump(Durations.short1);
+ await tester.pump(Durations.short1);
+ // 400 is the horizontal center of the screen, so tab 0 should leave towards the right and tab 1 should enter from the left
+ expect(tester.getCenter(find.text("Tab 0")).dx, greaterThan(400));
+ expect(tester.getCenter(find.text("Tab 1")).dx, lessThan(400));
+ await tester.pumpAndSettle();
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets("custom animated builder", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ animatedTabBuilder:
+ (context, index, animation, oldTabIndex, newTabIndex, child) {
+ final double yOffset = newTabIndex > index
+ ? -animation
+ : (newTabIndex < index
+ ? animation
+ : (index < oldTabIndex ? animation - 1 : 1 - animation));
+ return FractionalTranslation(
+ translation: Offset(yOffset, 0),
+ child: child,
+ );
+ },
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
+ await tester.tap(find.text("Item1"));
+ await tester.pump(Durations.short1);
+ await tester.pump(Durations.short1);
+ // 400 is the horizontal center of the screen, so tab 0 should leave towards the left and tab 1 should enter from the right
+ expect(tester.getCenter(find.text("Tab 0")).dx, lessThan(400));
+ expect(tester.getCenter(find.text("Tab 1")).dx, greaterThan(400));
+ await tester.pumpAndSettle();
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets("no animation when ScreenTransitionAnimation.none()",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ screenTransitionAnimation: const ScreenTransitionAnimation.none(),
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
+ await tester.tap(find.text("Item1"));
+ await tester.pump();
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets("shows FloatingActionButton if specified", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ floatingActionButton: FloatingActionButton(
+ onPressed: () {},
+ child: const Icon(Icons.add),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(FloatingActionButton).hitTestable(), findsOneWidget);
+ });
+
+ testWidgets("changes screens on gestures", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ gestureNavigationEnabled: true,
+ ),
+ ),
+ );
+
+ await tester.fling(
+ find.byType(PersistentTabView),
+ const Offset(-200, 0),
+ 800,
+ );
+ await tester.pumpAndSettle();
+
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets("changes screens programmatically when gestures are enabled",
+ (tester) async {
+ final controller = PersistentTabController();
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ controller: controller,
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ gestureNavigationEnabled: true,
+ ),
+ ),
+ );
+
+ controller.jumpToTab(1);
+ await tester.pumpAndSettle();
+
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets(
+ "trashes screens while switching if stateManagement turned off with gestures enabled",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ stateManagement: false,
+ gestureNavigationEnabled: true,
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 0);
+ });
+
+ testWidgets(
+ "persists screens while switching if stateManagement turned on with gestures enabled",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ stateManagement: true,
+ gestureNavigationEnabled: true,
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 1);
+ });
+
+ testWidgets(
+ "Style 13 and Style 14 center button are tappable above the navBar",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) =>
+ Style13BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ Offset topCenter = tester.getRect(find.byType(DecoratedNavBar)).topCenter;
+ await tester.tapAt(topCenter.translate(0, -10));
+ await tester.pumpAndSettle();
+ expectTabAndLevel(tab: 1, level: 0);
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
+ navBarBuilder: (config) =>
+ Style14BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ topCenter = tester.getRect(find.byType(DecoratedNavBar)).topCenter;
+ await tester.tapAt(topCenter.translate(0, -10));
+ await tester.pumpAndSettle();
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets("automatically animates animated icons", (tester) async {
+ final keys = [
+ GlobalKey(),
+ GlobalKey(),
+ GlobalKey(),
+ ];
+
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(id),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: AnimatedIconWrapper(
+ icon: AnimatedIcons.add_event,
+ key: keys[id],
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(keys[0].currentState!.controller.value, equals(1));
+ expect(keys[1].currentState!.controller.value, equals(0));
+ await tester.tap(find.text("Item1"));
+ await tester.pumpAndSettle();
+
+ expect(keys[0].currentState!.controller.value, equals(0));
+ expect(keys[1].currentState!.controller.value, equals(1));
+ });
+
+ group("uses navigator config", () {
+ testWidgets("to populate routes of each tab navigator", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/details");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ navigatorConfig: NavigatorConfig(
+ routes: {
+ "/details": (context) => defaultScreen(id, level: 1),
+ },
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 1, level: 1);
+ await tapItem(tester, 2);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 2, level: 1);
+ });
+
+ testWidgets("to run onGenerateRoute of each tab navigator",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/details");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ navigatorConfig: NavigatorConfig(
+ onGenerateRoute: (settings) => MaterialPageRoute(
+ builder: (context) => defaultScreen(id, level: 1),
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 1, level: 1);
+ await tapItem(tester, 2);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 2, level: 1);
+ });
+
+ testWidgets("to run onUnknownRoute of each tab navigator",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/unknown-route");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ navigatorConfig: NavigatorConfig(
+ onUnknownRoute: (settings) => MaterialPageRoute(
+ builder: (context) => defaultScreen(id, level: -1),
+ ),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: -1);
+ await tapItem(tester, 1);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 1, level: -1);
+ await tapItem(tester, 2);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 2, level: -1);
+ });
+
+ testWidgets("to report missing onUnknownRoute as error", (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/unknown-route");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ final exception = tester.takeException();
+ expect(exception, isFlutterError);
+ });
+
+ testWidgets("to report onUnknownRoute returning null as error",
+ (tester) async {
+ await tester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/unknown-route");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ navigatorConfig: NavigatorConfig(
+ onUnknownRoute: (settings) => null,
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ final exception = tester.takeException();
+ expect(exception, isFlutterError);
+ });
+ });
+
+ group("uses root navigator", () {
+ testWidgets("to populate routes of each tab navigator", (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ routes: {
+ "/details": (context) => defaultScreen(0, level: 1),
+ },
+ home: PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/details");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 2);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ });
+
+ testWidgets("to run onGenerateRoute of each tab navigator",
+ (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ onGenerateRoute: (settings) => MaterialPageRoute(
+ builder: (context) => defaultScreen(0, level: 1),
+ ),
+ home: PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/details");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 1);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 2);
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ });
+
+ testWidgets("to report missing onUnknownRoute as error", (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/unknown-route");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ final exception = tester.takeException();
+ expect(exception, isFlutterError);
+ });
+
+ testWidgets("to report onUnknownRoute returning null as error",
+ (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ onGenerateRoute: (settings) => null,
+ home: PersistentTabView(
+ tabs: List.generate(
+ 3,
+ (id) => PersistentTabConfig(
+ screen: defaultScreen(
+ id,
+ onTap: (context) {
+ Navigator.of(context).pushNamed("/unknown-route");
+ },
+ ),
+ item: ItemConfig(
+ title: "Item$id",
+ icon: Icon(key: Key("Item$id"), Icons.add),
+ ),
+ ),
+ ),
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ final exception = tester.takeException();
+ expect(exception, isFlutterError);
+ });
+ });
+ });
+
+ group("PersistentTabView.router", () {
+ testWidgets("can switch tabs", (tester) async {
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routerConfig: wrapWithGoRouter(
+ (context, state, shell) => PersistentTabView.router(
+ tabs: routerTabs(),
+ navigationShell: shell,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
+ await tapItem(tester, 1);
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets("switches tabs when triggered by go_router", (tester) async {
+ final router = wrapWithGoRouter(
+ (context, state, shell) => PersistentTabView.router(
+ tabs: routerTabs(),
+ navigationShell: shell,
+ navBarBuilder: (config) => Style1BottomNavBar(navBarConfig: config),
+ ),
+ );
+
+ await tester.pumpWidget(MaterialApp.router(routerConfig: router));
+
+ expectTabAndLevel(tab: 0, level: 0);
+ router.go("/tab-1");
+ await tester.pumpAndSettle();
+ expectTabAndLevel(tab: 1, level: 0);
+ });
+
+ testWidgets(
+ "doesnt pop any screen when tapping same tab when `selectedTabPressConfig.popAction == PopActionType.none`",
+ (tester) async {
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routerConfig: wrapWithGoRouter(
+ (context, state, shell) => PersistentTabView.router(
+ tabs: routerTabs(),
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ popAction: PopActionType.none,
+ ),
+ navigationShell: shell,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ ),
+ );
+
+ await tapElevatedButton(tester);
+ expectTabAndLevel(tab: 0, level: 1);
+ await tapItem(tester, 0);
+ expectTabAndLevel(tab: 0, level: 1);
+ });
+
+ testWidgets(
+ "runs callback when selected tab is tapped again and there are no screens pushed",
+ (tester) async {
+ bool callbackGotExecuted = false;
+
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routerConfig: wrapWithGoRouter(
+ (context, state, shell) => PersistentTabView.router(
+ tabs: routerTabs(),
+ selectedTabPressConfig: SelectedTabPressConfig(
+ onPressed: (hasPages) {
+ callbackGotExecuted = true;
+ },
+ ),
+ navigationShell: shell,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ ),
+ ),
+ );
+
+ expectTabAndLevel(tab: 0, level: 0);
+ await tapItem(tester, 0);
+ expect(callbackGotExecuted, equals(true));
+ });
+
+ testWidgets(
+ "scrolls the tab content to top when the selected tab is tapped again and it is enabled",
+ (tester) async {
+ final controllers = [
+ ScrollController(),
+ ScrollController(),
+ ScrollController(),
+ ];
+
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routerConfig: wrapWithGoRouter(
+ (context, state, shell) => PersistentTabView.router(
+ tabs: routerTabs(3, controllers),
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ scrollToTop: true,
+ ),
+ navigationShell: shell,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ scrollControllers: controllers,
+ ),
+ ),
+ );
+
+ await scroll(tester, const Offset(0, 200), const Offset(0, -400));
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+
+ await tapItem(tester, 0);
+
+ expectTabAndLevel(tab: 0, level: 0);
+ expect(controllers[0].offset, equals(0));
+ });
+
+ testWidgets(
+ "does not scroll the tab content to top when the selected tab is tapped again when it is disabled",
+ (tester) async {
+ final controllers = [
+ ScrollController(),
+ ScrollController(),
+ ScrollController(),
+ ];
+
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routerConfig: wrapWithGoRouter(
+ (context, state, shell) => PersistentTabView.router(
+ tabs: routerTabs(3, controllers),
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ scrollToTop: false,
+ ),
+ navigationShell: shell,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ scrollControllers: controllers,
+ ),
+ ),
+ );
+
+ await scroll(tester, const Offset(0, 200), const Offset(0, -400));
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+
+ await tapItem(tester, 0);
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+ expect(controllers[0].offset, isNot(0));
+ });
+
+ testWidgets(
+ "scrollToTop is enabled but switching to a new tab does not scroll immediately",
+ (tester) async {
+ final controllers = [
+ ScrollController(),
+ ScrollController(),
+ ScrollController(),
+ ];
+
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routerConfig: wrapWithGoRouter(
+ (context, state, shell) => PersistentTabView.router(
+ tabs: routerTabs(3, controllers),
+ selectedTabPressConfig: const SelectedTabPressConfig(
+ scrollToTop: true,
+ ),
+ navigationShell: shell,
+ navBarBuilder: (config) =>
+ Style1BottomNavBar(navBarConfig: config),
+ ),
+ scrollControllers: controllers,
+ ),
+ ),
+ );
+
+ await scroll(tester, const Offset(0, 200), const Offset(0, -400));
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+
+ await tapItem(tester, 1);
+ await tapItem(tester, 0);
+
+ expectNotTabAndLevel(tab: 0, level: 0);
+ expect(controllers[0].offset, isNot(0));
+ });
+ });
+
+ group("Regression", () {
+ testWidgets("#31 one navbar border side does not throw error",
+ (widgetTester) async {
+ await widgetTester.pumpWidget(
+ wrapTabView(
+ (context) => PersistentTabView(
+ tabs: tabs(),
navBarBuilder: (config) => Style1BottomNavBar(
navBarConfig: config,
navBarDecoration: const NavBarDecoration(