Skip to content

Instantly share code, notes, and snippets.

@mondoktamas
Created April 22, 2022 07:09
Show Gist options
  • Save mondoktamas/ae8c3971eab6fcf41b8128c8a55b2c00 to your computer and use it in GitHub Desktop.
Save mondoktamas/ae8c3971eab6fcf41b8128c8a55b2c00 to your computer and use it in GitHub Desktop.
import 'dart:collection' show Queue;
import 'dart:math' as math;
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart' hide BottomNavigationBar;
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sas/application/presentation/resources/redesign/colors.dart';
import 'package:sas/application/presentation/resources/redesign/theme.dart';
import 'package:sas/application/presentation/widgets/redesign/avatar/simple_circle_avatar.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
//This is the second version of CustomBottomNavigationBar. I suggest to use this widget in the app
const double _navBarHeight = 84;
class CustomBottomNavBarItem extends Equatable {
const CustomBottomNavBarItem.icon(
final String this.icon, {
this.label = '',
this.iconSize = 32,
this.tooltip = '',
}) : image = null;
const CustomBottomNavBarItem.image(
final String this.image, {
this.label = '',
this.iconSize = 20,
this.tooltip = '',
}) : icon = null;
final String? icon;
final String? image;
final String label;
final double iconSize;
final String tooltip;
@override
List<Object?> get props => [icon, image, label, iconSize, tooltip];
}
class CustomBottomNavigationBar extends StatefulWidget {
const CustomBottomNavigationBar({
required this.onTap,
required this.tabs,
this.backgroundColor,
final Key? key,
}) : super(key: key);
final void Function(int)? onTap;
final List<CustomBottomNavBarItem> tabs;
final Color? backgroundColor;
@override
State<CustomBottomNavigationBar> createState() => _CustomBottomNavigationBarState();
}
class _CustomBottomNavigationBarState extends State<CustomBottomNavigationBar> {
int _selectedTabIndex = 0;
@override
Widget build(final BuildContext context) => _BottomNavigationBar(
backgroundColor: widget.backgroundColor ?? Theme.of(context).bottomNavigationBarTheme.backgroundColor,
elevation: 0,
itemDecoration: (final context, final index, final item) => AnimatedContainer(
height: _navBarHeight,
duration: const Duration(milliseconds: 300),
decoration: index == _selectedTabIndex
? BoxDecoration(
gradient: Theme.of(context).bottomNavigationBarTheme.backgroundGradient(context),
)
: const BoxDecoration(),
child: Theme(
data: Theme.of(context).copyWith(
highlightColor: transparent,
splashColor: transparent,
),
child: item,
),
),
currentIndex: _selectedTabIndex,
onTap: (final int index) {
setState(() => _selectedTabIndex = index);
if (widget.onTap != null) {
widget.onTap!(index);
}
},
items: List.generate(
widget.tabs.length,
(final int index) {
final navBarItem = widget.tabs[index];
if (navBarItem.image != null) {
return BottomNavigationBarItem(
icon: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: SimpleCircleAvatar(
imageUrl: navBarItem.image!,
size: navBarItem.iconSize,
),
),
label: navBarItem.label,
tooltip: navBarItem.tooltip,
);
} else {
return BottomNavigationBarItem(
icon: navBarItem.icon != null
? SvgPicture.asset(
navBarItem.icon!,
color: Theme.of(context).bottomNavigationBarTheme.unselectedItemColor,
)
: const SizedBox.shrink(),
activeIcon: navBarItem.icon != null
? SvgPicture.asset(
navBarItem.icon!,
color: Theme.of(context).bottomNavigationBarTheme.selectedItemColor,
)
: const SizedBox.shrink(),
label: navBarItem.label,
tooltip: navBarItem.tooltip,
);
}
},
),
);
}
typedef BottomNavigationBarItemDecoration = Widget Function(BuildContext context, int index, Widget item);
class _BottomNavigationBar extends StatefulWidget {
_BottomNavigationBar({
final Key? key,
required this.items,
this.itemDecoration,
this.onTap,
this.currentIndex = 0,
this.elevation,
this.type,
final Color? fixedColor,
this.backgroundColor,
this.iconSize = 24.0,
final Color? selectedItemColor,
this.unselectedItemColor,
this.selectedIconTheme,
this.unselectedIconTheme,
this.selectedFontSize = 14.0,
this.unselectedFontSize = 12.0,
this.selectedLabelStyle,
this.unselectedLabelStyle,
this.showSelectedLabels,
this.showUnselectedLabels,
this.mouseCursor,
this.enableFeedback,
this.landscapeLayout,
}) : assert(items.length >= 2),
assert(
items.every((final BottomNavigationBarItem item) => item.label != null),
'Every item must have a non-null label',
),
assert(0 <= currentIndex && currentIndex < items.length),
assert(elevation == null || elevation >= 0.0),
assert(iconSize >= 0.0),
assert(
selectedItemColor == null || fixedColor == null,
'Either selectedItemColor or fixedColor can be specified, but not both',
),
assert(selectedFontSize >= 0.0),
assert(unselectedFontSize >= 0.0),
selectedItemColor = selectedItemColor ?? fixedColor,
super(key: key);
/// Defines the appearance of the button items that are arrayed within the
/// bottom navigation bar.
final List<BottomNavigationBarItem> items;
final BottomNavigationBarItemDecoration? itemDecoration;
/// Called when one of the [items] is tapped.
///
/// The stateful widget that creates the bottom navigation bar needs to keep
/// track of the index of the selected [BottomNavigationBarItem] and call
/// `setState` to rebuild the bottom navigation bar with the new [currentIndex].
final ValueChanged<int>? onTap;
/// The index into [items] for the current active [BottomNavigationBarItem].
final int currentIndex;
/// The z-coordinate of this [BottomNavigationBar].
///
/// If null, defaults to `8.0`.
///
/// {@macro flutter.material.material.elevation}
final double? elevation;
/// Defines the layout and behavior of a [BottomNavigationBar].
///
/// See documentation for [BottomNavigationBarType] for information on the
/// meaning of different types.
final BottomNavigationBarType? type;
/// The value of [selectedItemColor].
///
/// This getter only exists for backwards compatibility, the
/// [selectedItemColor] property is preferred.
Color? get fixedColor => selectedItemColor;
/// The color of the [BottomNavigationBar] itself.
///
/// If [type] is [BottomNavigationBarType.shifting] and the
/// [items] have [BottomNavigationBarItem.backgroundColor] set, the [items]'
/// backgroundColor will splash and overwrite this color.
final Color? backgroundColor;
/// The size of all of the [BottomNavigationBarItem] icons.
///
/// See [BottomNavigationBarItem.icon] for more information.
final double iconSize;
/// The color of the selected [BottomNavigationBarItem.icon] and
/// [BottomNavigationBarItem.label].
///
/// If null then the [ThemeData.primaryColor] is used.
final Color? selectedItemColor;
/// The color of the unselected [BottomNavigationBarItem.icon] and
/// [BottomNavigationBarItem.label]s.
///
/// If null then the [ThemeData.unselectedWidgetColor]'s color is used.
final Color? unselectedItemColor;
/// The size, opacity, and color of the icon in the currently selected
/// [BottomNavigationBarItem.icon].
///
/// If this is not provided, the size will default to [iconSize], the color
/// will default to [selectedItemColor].
///
/// It this field is provided, it must contain non-null [IconThemeData.size]
/// and [IconThemeData.color] properties. Also, if this field is supplied,
/// [unselectedIconTheme] must be provided.
final IconThemeData? selectedIconTheme;
/// The size, opacity, and color of the icon in the currently unselected
/// [BottomNavigationBarItem.icon]s.
///
/// If this is not provided, the size will default to [iconSize], the color
/// will default to [unselectedItemColor].
///
/// It this field is provided, it must contain non-null [IconThemeData.size]
/// and [IconThemeData.color] properties. Also, if this field is supplied,
/// [selectedIconTheme] must be provided.
final IconThemeData? unselectedIconTheme;
/// The [TextStyle] of the [BottomNavigationBarItem] labels when they are
/// selected.
final TextStyle? selectedLabelStyle;
/// The [TextStyle] of the [BottomNavigationBarItem] labels when they are not
/// selected.
final TextStyle? unselectedLabelStyle;
/// The font size of the [BottomNavigationBarItem] labels when they are selected.
///
/// If [TextStyle.fontSize] of [selectedLabelStyle] is non-null, it will be
/// used instead of this.
///
/// Defaults to `14.0`.
final double selectedFontSize;
/// The font size of the [BottomNavigationBarItem] labels when they are not
/// selected.
///
/// If [TextStyle.fontSize] of [unselectedLabelStyle] is non-null, it will be
/// used instead of this.
///
/// Defaults to `12.0`.
final double unselectedFontSize;
/// Whether the labels are shown for the unselected [BottomNavigationBarItem]s.
final bool? showUnselectedLabels;
/// Whether the labels are shown for the selected [BottomNavigationBarItem].
final bool? showSelectedLabels;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// tiles.
///
/// If this property is null, [SystemMouseCursors.click] will be used.
final MouseCursor? mouseCursor;
/// Whether detected gestures should provide acoustic and/or haptic feedback.
///
/// For example, on Android a tap will produce a clicking sound and a
/// long-press will produce a short vibration, when feedback is enabled.
///
/// See also:
///
/// * [Feedback] for providing platform-specific feedback to certain actions.
final bool? enableFeedback;
/// The arrangement of the bar's [items] when the enclosing
/// [MediaQueryData.orientation] is [Orientation.landscape].
///
/// The following alternatives are supported:
///
/// * [BottomNavigationBarLandscapeLayout.spread] - the items are
/// evenly spaced and spread out across the available width. Each
/// item's label and icon are arranged in a column.
/// * [BottomNavigationBarLandscapeLayout.centered] - the items are
/// evenly spaced in a row but only consume as much width as they
/// would in portrait orientation. The row of items is centered within
/// the available width. Each item's label and icon are arranged
/// in a column.
/// * [BottomNavigationBarLandscapeLayout.linear] - the items are
/// evenly spaced and each item's icon and label are lined up in a
/// row instead of a column.
///
/// If this property is null, then the value of the enclosing
/// [BottomNavigationBarThemeData.landscapeLayout is used. If that
/// property is also null, then
/// [BottomNavigationBarLandscapeLayout.spread] is used.
///
/// This property is null by default.
///
/// See also:
///
/// * [ThemeData.bottomNavigationBarTheme] - which can be used to specify
/// bottom navigation bar defaults for an entire application.
/// * [BottomNavigationBarTheme] - which can be used to specify
/// bottom navigation bar defaults for a widget subtree.
/// * [MediaQuery.of] - which can be used to determine the current
/// orientation.
final BottomNavigationBarLandscapeLayout? landscapeLayout;
@override
State<_BottomNavigationBar> createState() => _BottomNavigationBarState();
}
// This represents a single tile in the bottom navigation bar. It is intended
// to go into a flex container.
class _BottomNavigationTile extends StatelessWidget {
const _BottomNavigationTile(
this.type,
this.item,
this.itemIndex,
this.animation,
this.iconSize, {
this.onTap,
this.colorTween,
this.flex,
this.selected = false,
this.itemDecoration,
required this.selectedLabelStyle,
required this.unselectedLabelStyle,
required this.selectedIconTheme,
required this.unselectedIconTheme,
required this.showSelectedLabels,
required this.showUnselectedLabels,
this.indexLabel,
required this.mouseCursor,
required this.enableFeedback,
required this.layout,
});
final BottomNavigationBarType type;
final BottomNavigationBarItem item;
final BottomNavigationBarItemDecoration? itemDecoration;
final int itemIndex;
final Animation<double> animation;
final double iconSize;
final VoidCallback? onTap;
final ColorTween? colorTween;
final double? flex;
final bool selected;
final IconThemeData? selectedIconTheme;
final IconThemeData? unselectedIconTheme;
final TextStyle selectedLabelStyle;
final TextStyle unselectedLabelStyle;
final String? indexLabel;
final bool showSelectedLabels;
final bool showUnselectedLabels;
final MouseCursor mouseCursor;
final bool enableFeedback;
final BottomNavigationBarLandscapeLayout layout;
@override
Widget build(final BuildContext context) {
// In order to use the flex container to grow the tile during animation, we
// need to divide the changes in flex allotment into smaller pieces to
// produce smooth animation. We do this by multiplying the flex value
// (which is an integer) by a large number.
final int size;
final selectedFontSize = selectedLabelStyle.fontSize!;
final selectedIconSize = selectedIconTheme?.size ?? iconSize;
final unselectedIconSize = unselectedIconTheme?.size ?? iconSize;
// The amount that the selected icon is bigger than the unselected icons,
// (or zero if the selected icon is not bigger than the unselected icons).
final double selectedIconDiff = math.max(selectedIconSize - unselectedIconSize, 0);
// The amount that the unselected icons are bigger than the selected icon,
// (or zero if the unselected icons are not any bigger than the selected icon).
final double unselectedIconDiff = math.max(unselectedIconSize - selectedIconSize, 0);
// The effective tool tip message to be shown on the BottomNavigationBarItem.
final effectiveTooltip = item.tooltip == '' ? null : item.tooltip ?? item.label;
// Defines the padding for the animating icons + labels.
//
// The animations go from "Unselected":
// =======
// | <-- Padding equal to the text height + 1/2 selectedIconDiff.
// | ☆
// | text <-- Invisible text + padding equal to 1/2 selectedIconDiff.
// =======
//
// To "Selected":
//
// =======
// | <-- Padding equal to 1/2 text height + 1/2 unselectedIconDiff.
// | ☆
// | text
// | <-- Padding equal to 1/2 text height + 1/2 unselectedIconDiff.
// =======
double bottomPadding;
double topPadding;
if (showSelectedLabels && !showUnselectedLabels) {
bottomPadding = Tween<double>(
begin: selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 - unselectedIconDiff / 2.0,
).evaluate(animation);
topPadding = Tween<double>(
begin: selectedFontSize + selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 - unselectedIconDiff / 2.0,
).evaluate(animation);
} else if (!showSelectedLabels && !showUnselectedLabels) {
bottomPadding = Tween<double>(
begin: selectedIconDiff / 2.0,
end: unselectedIconDiff / 2.0,
).evaluate(animation);
topPadding = Tween<double>(
begin: selectedFontSize + selectedIconDiff / 2.0,
end: selectedFontSize + unselectedIconDiff / 2.0,
).evaluate(animation);
} else {
bottomPadding = Tween<double>(
begin: selectedFontSize / 2.0 + selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 + unselectedIconDiff / 2.0,
).evaluate(animation);
topPadding = Tween<double>(
begin: selectedFontSize / 2.0 + selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 + unselectedIconDiff / 2.0,
).evaluate(animation);
}
switch (type) {
case BottomNavigationBarType.fixed:
size = 1;
break;
case BottomNavigationBarType.shifting:
size = (flex! * 1000.0).round();
break;
}
Widget result = InkResponse(
onTap: onTap,
mouseCursor: mouseCursor,
enableFeedback: enableFeedback,
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: _Tile(
layout: layout,
icon: _TileIcon(
colorTween: colorTween!,
animation: animation,
iconSize: iconSize,
selected: selected,
item: item,
selectedIconTheme: selectedIconTheme,
unselectedIconTheme: unselectedIconTheme,
),
label: _Label(
colorTween: colorTween!,
animation: animation,
item: item,
selectedLabelStyle: selectedLabelStyle,
unselectedLabelStyle: unselectedLabelStyle,
showSelectedLabels: showSelectedLabels,
showUnselectedLabels: showUnselectedLabels,
),
),
),
);
if (effectiveTooltip != null) {
result = Tooltip(
message: effectiveTooltip,
preferBelow: false,
verticalOffset: selectedIconSize + selectedFontSize,
excludeFromSemantics: true,
child: result,
);
}
result = Semantics(
selected: selected,
container: true,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
result,
Semantics(
label: indexLabel,
),
],
),
);
return Expanded(
flex: size,
child: itemDecoration?.call(context, itemIndex, result) ?? result,
);
}
}
// If the orientation is landscape and layout is
// BottomNavigationBarLandscapeLayout.linear then return a
// icon-space-label row, where space is 8 pixels. Otherwise return a
// icon-label column.
class _Tile extends StatelessWidget {
const _Tile({final Key? key, required this.layout, required this.icon, required this.label}) : super(key: key);
final BottomNavigationBarLandscapeLayout layout;
final Widget icon;
final Widget label;
@override
Widget build(final BuildContext context) {
final data = MediaQuery.of(context);
if (data.orientation == Orientation.landscape && layout == BottomNavigationBarLandscapeLayout.linear) {
return Align(
heightFactor: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, const SizedBox(width: 8), label],
),
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, label],
);
}
}
class _TileIcon extends StatelessWidget {
const _TileIcon({
final Key? key,
required this.colorTween,
required this.animation,
required this.iconSize,
required this.selected,
required this.item,
required this.selectedIconTheme,
required this.unselectedIconTheme,
}) : super(key: key);
final ColorTween colorTween;
final Animation<double> animation;
final double iconSize;
final bool selected;
final BottomNavigationBarItem item;
final IconThemeData? selectedIconTheme;
final IconThemeData? unselectedIconTheme;
@override
Widget build(final BuildContext context) {
final iconColor = colorTween.evaluate(animation);
final defaultIconTheme = IconThemeData(
color: iconColor,
size: iconSize,
);
final iconThemeData = IconThemeData.lerp(
defaultIconTheme.merge(unselectedIconTheme),
defaultIconTheme.merge(selectedIconTheme),
animation.value,
);
return Align(
alignment: Alignment.topCenter,
heightFactor: 1.0,
child: IconTheme(
data: iconThemeData,
child: selected ? item.activeIcon : item.icon,
),
);
}
}
class _Label extends StatelessWidget {
const _Label({
final Key? key,
required this.colorTween,
required this.animation,
required this.item,
required this.selectedLabelStyle,
required this.unselectedLabelStyle,
required this.showSelectedLabels,
required this.showUnselectedLabels,
}) : super(key: key);
final ColorTween colorTween;
final Animation<double> animation;
final BottomNavigationBarItem item;
final TextStyle selectedLabelStyle;
final TextStyle unselectedLabelStyle;
final bool showSelectedLabels;
final bool showUnselectedLabels;
@override
Widget build(final BuildContext context) {
final selectedFontSize = selectedLabelStyle.fontSize;
final unselectedFontSize = unselectedLabelStyle.fontSize;
final customStyle = TextStyle.lerp(
unselectedLabelStyle,
selectedLabelStyle,
animation.value,
)!;
var text = DefaultTextStyle.merge(
style: customStyle.copyWith(
fontSize: selectedFontSize,
color: colorTween.evaluate(animation),
),
// The font size should grow here when active, but because of the way
// font rendering works, it doesn't grow smoothly if we just animate
// the font size, so we use a transform instead.
child: Transform(
transform: Matrix4.diagonal3(
Vector3.all(
Tween<double>(
begin: unselectedFontSize! / selectedFontSize!,
end: 1.0,
).evaluate(animation),
),
),
alignment: Alignment.bottomCenter,
child: Text(item.label!),
),
);
if (!showUnselectedLabels && !showSelectedLabels) {
// Never show any labels.
text = Opacity(
alwaysIncludeSemantics: true,
opacity: 0.0,
child: text,
);
} else if (!showUnselectedLabels) {
// Fade selected labels in.
text = FadeTransition(
alwaysIncludeSemantics: true,
opacity: animation,
child: text,
);
} else if (!showSelectedLabels) {
// Fade selected labels out.
text = FadeTransition(
alwaysIncludeSemantics: true,
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
child: text,
);
}
text = Align(
alignment: Alignment.bottomCenter,
heightFactor: 1.0,
child: Container(child: text),
);
if (item.label != null) {
// Do not grow text in bottom navigation bar when we can show a tooltip
// instead.
final mediaQueryData = MediaQuery.of(context);
text = MediaQuery(
data: mediaQueryData.copyWith(
textScaleFactor: math.min(1.0, mediaQueryData.textScaleFactor),
),
child: text,
);
}
return text;
}
}
class _BottomNavigationBarState extends State<_BottomNavigationBar> with TickerProviderStateMixin {
List<AnimationController> _controllers = <AnimationController>[];
late List<CurvedAnimation> _animations;
// A queue of color splashes currently being animated.
final Queue<_Circle> _circles = Queue<_Circle>();
// Last splash circle's color, and the final color of the control after
// animation is complete.
Color? _backgroundColor;
static final Animatable<double> _flexTween = Tween<double>(begin: 1.0, end: 1.5);
void _resetState() {
for (final controller in _controllers) {
controller.dispose();
}
for (final circle in _circles) {
circle.dispose();
}
_circles.clear();
_controllers = List<AnimationController>.generate(
widget.items.length,
(final int index) => AnimationController(
duration: kThemeAnimationDuration,
vsync: this,
)..addListener(_rebuild));
_animations = List<CurvedAnimation>.generate(
widget.items.length,
(final int index) => CurvedAnimation(
parent: _controllers[index],
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.fastOutSlowIn.flipped,
));
_controllers[widget.currentIndex].value = 1.0;
_backgroundColor = widget.items[widget.currentIndex].backgroundColor;
}
// Computes the default value for the [type] parameter.
//
// If type is provided, it is returned. Next, if the bottom navigation bar
// theme provides a type, it is used. Finally, the default behavior will be
// [BottomNavigationBarType.fixed] for 3 or fewer items, and
// [BottomNavigationBarType.shifting] is used for 4+ items.
BottomNavigationBarType get _effectiveType =>
widget.type ??
BottomNavigationBarTheme.of(context).type ??
(widget.items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting);
// Computes the default value for the [showUnselected] parameter.
//
// Unselected labels are shown by default for [BottomNavigationBarType.fixed],
// and hidden by default for [BottomNavigationBarType.shifting].
bool get _defaultShowUnselected {
switch (_effectiveType) {
case BottomNavigationBarType.shifting:
return false;
case BottomNavigationBarType.fixed:
return true;
}
}
@override
void initState() {
super.initState();
_resetState();
}
void _rebuild() {
//ignore:no-empty-block
setState(() {});
}
@override
void dispose() {
for (final controller in _controllers) {
controller.dispose();
}
for (final circle in _circles) {
circle.dispose();
}
super.dispose();
}
double _evaluateFlex(final Animation<double> animation) => _flexTween.evaluate(animation);
void _pushCircle(final int index) {
if (widget.items[index].backgroundColor != null) {
_circles.add(
_Circle(
state: this,
index: index,
color: widget.items[index].backgroundColor!,
vsync: this,
)..controller.addStatusListener(
(final AnimationStatus status) {
switch (status) {
case AnimationStatus.completed:
setState(() {
final circle = _circles.removeFirst();
_backgroundColor = circle.color;
circle.dispose();
});
break;
case AnimationStatus.dismissed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
}
},
),
);
}
}
@override
void didUpdateWidget(final _BottomNavigationBar oldWidget) {
super.didUpdateWidget(oldWidget);
// No animated segue if the length of the items list changes.
if (widget.items.length != oldWidget.items.length) {
_resetState();
return;
}
if (widget.currentIndex != oldWidget.currentIndex) {
switch (_effectiveType) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
_pushCircle(widget.currentIndex);
break;
}
_controllers[oldWidget.currentIndex].reverse();
_controllers[widget.currentIndex].forward();
} else {
if (_backgroundColor != widget.items[widget.currentIndex].backgroundColor) {
_backgroundColor = widget.items[widget.currentIndex].backgroundColor;
}
}
}
// If the given [TextStyle] has a non-null `fontSize`, it should be used.
// Otherwise, the [selectedFontSize] parameter should be used.
static TextStyle _effectiveTextStyle(TextStyle? textStyle, final double fontSize) {
textStyle ??= const TextStyle();
// Prefer the font size on textStyle if present.
return textStyle.fontSize == null ? textStyle.copyWith(fontSize: fontSize) : textStyle;
}
List<Widget> _createTiles(final BottomNavigationBarLandscapeLayout layout) {
final localizations = MaterialLocalizations.of(context);
final themeData = Theme.of(context);
final bottomTheme = BottomNavigationBarTheme.of(context);
final effectiveSelectedLabelStyle = _effectiveTextStyle(
widget.selectedLabelStyle ?? bottomTheme.selectedLabelStyle,
widget.selectedFontSize,
);
final effectiveUnselectedLabelStyle = _effectiveTextStyle(
widget.unselectedLabelStyle ?? bottomTheme.unselectedLabelStyle,
widget.unselectedFontSize,
);
final Color themeColor;
switch (themeData.brightness) {
case Brightness.light:
themeColor = themeData.colorScheme.primary;
break;
case Brightness.dark:
themeColor = themeData.colorScheme.secondary;
break;
}
final ColorTween colorTween;
switch (_effectiveType) {
case BottomNavigationBarType.fixed:
colorTween = ColorTween(
begin: widget.unselectedItemColor ?? bottomTheme.unselectedItemColor ?? themeData.unselectedWidgetColor,
end: widget.selectedItemColor ?? bottomTheme.selectedItemColor ?? widget.fixedColor ?? themeColor,
);
break;
case BottomNavigationBarType.shifting:
colorTween = ColorTween(
begin: widget.unselectedItemColor ?? bottomTheme.unselectedItemColor ?? themeData.colorScheme.surface,
end: widget.selectedItemColor ?? bottomTheme.selectedItemColor ?? themeData.colorScheme.surface,
);
break;
}
final effectiveMouseCursor = widget.mouseCursor ?? SystemMouseCursors.click;
final tiles = <Widget>[];
for (var i = 0; i < widget.items.length; i++) {
tiles.add(_BottomNavigationTile(
_effectiveType,
widget.items[i],
i,
_animations[i],
widget.iconSize,
itemDecoration: widget.itemDecoration,
selectedIconTheme: widget.selectedIconTheme ?? bottomTheme.selectedIconTheme,
unselectedIconTheme: widget.unselectedIconTheme ?? bottomTheme.unselectedIconTheme,
selectedLabelStyle: effectiveSelectedLabelStyle,
unselectedLabelStyle: effectiveUnselectedLabelStyle,
enableFeedback: widget.enableFeedback ?? bottomTheme.enableFeedback ?? true,
onTap: () {
widget.onTap?.call(i);
},
colorTween: colorTween,
flex: _evaluateFlex(_animations[i]),
selected: i == widget.currentIndex,
showSelectedLabels: widget.showSelectedLabels ?? bottomTheme.showSelectedLabels ?? true,
showUnselectedLabels: widget.showUnselectedLabels ?? bottomTheme.showUnselectedLabels ?? _defaultShowUnselected,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
mouseCursor: effectiveMouseCursor,
layout: layout,
));
}
return tiles;
}
@override
Widget build(final BuildContext context) {
assert(debugCheckHasDirectionality(context));
assert(debugCheckHasMaterialLocalizations(context));
assert(debugCheckHasMediaQuery(context));
assert(Overlay.of(context, debugRequiredFor: widget) != null);
final bottomTheme = BottomNavigationBarTheme.of(context);
final layout = widget.landscapeLayout ?? bottomTheme.landscapeLayout ?? BottomNavigationBarLandscapeLayout.spread;
final additionalBottomPadding = MediaQuery.of(context).padding.bottom;
Color? backgroundColor;
switch (_effectiveType) {
case BottomNavigationBarType.fixed:
backgroundColor = widget.backgroundColor ?? bottomTheme.backgroundColor;
break;
case BottomNavigationBarType.shifting:
backgroundColor = _backgroundColor;
break;
}
return Semantics(
explicitChildNodes: true,
child: _Bar(
layout: layout,
elevation: widget.elevation ?? bottomTheme.elevation ?? 8.0,
color: backgroundColor,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding),
child: CustomPaint(
painter: _RadialPainter(
circles: _circles.toList(),
textDirection: Directionality.of(context),
),
child: Material(
// Splashes.
type: MaterialType.transparency,
child: Padding(
padding: EdgeInsets.only(bottom: additionalBottomPadding),
child: MediaQuery.removePadding(
context: context,
removeBottom: true,
child: DefaultTextStyle.merge(
overflow: TextOverflow.ellipsis,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _createTiles(layout),
),
),
),
),
),
),
),
),
);
}
}
// Optionally center a Material child for landscape layouts when layout is
// BottomNavigationBarLandscapeLayout.centered
class _Bar extends StatelessWidget {
const _Bar({
final Key? key,
required this.child,
required this.layout,
required this.elevation,
required this.color,
}) : super(key: key);
final Widget child;
final BottomNavigationBarLandscapeLayout layout;
final double elevation;
final Color? color;
@override
Widget build(final BuildContext context) {
final data = MediaQuery.of(context);
var alignedChild = child;
if (data.orientation == Orientation.landscape && layout == BottomNavigationBarLandscapeLayout.centered) {
alignedChild = Align(
alignment: Alignment.bottomCenter,
heightFactor: 1,
child: SizedBox(
width: data.size.height,
child: child,
),
);
}
return Material(
elevation: elevation,
color: color,
child: alignedChild,
);
}
}
// Describes an animating color splash circle.
class _Circle {
_Circle({
required this.state,
required this.index,
required this.color,
required final TickerProvider vsync,
}) {
controller = AnimationController(
duration: kThemeAnimationDuration,
vsync: vsync,
);
animation = CurvedAnimation(
parent: controller,
curve: Curves.fastOutSlowIn,
);
controller.forward();
}
final _BottomNavigationBarState state;
final int index;
final Color color;
late AnimationController controller;
late CurvedAnimation animation;
double get horizontalLeadingOffset {
double weightSum(final Iterable<Animation<double>> animations) => animations
.map<double>(state._evaluateFlex)
.fold<double>(0.0, (final double sum, final double value) => sum + value);
final allWeights = weightSum(state._animations);
// These weights sum to the start edge of the indexed item.
final leadingWeights = weightSum(state._animations.sublist(0, index));
// Add half of its flex value in order to get to the center.
return (leadingWeights + state._evaluateFlex(state._animations[index]) / 2.0) / allWeights;
}
void dispose() {
controller.dispose();
}
}
// Paints the animating color splash circles.
class _RadialPainter extends CustomPainter {
_RadialPainter({
required this.circles,
required this.textDirection,
});
final List<_Circle> circles;
final TextDirection textDirection;
// Computes the maximum radius attainable such that at least one of the
// bounding rectangle's corners touches the edge of the circle. Drawing a
// circle larger than this radius is not needed, since there is no perceivable
// difference within the cropped rectangle.
static double _maxRadius(final Offset center, final Size size) {
final double maxX = math.max(center.dx, size.width - center.dx);
final double maxY = math.max(center.dy, size.height - center.dy);
return math.sqrt(maxX * maxX + maxY * maxY);
}
@override
bool shouldRepaint(final _RadialPainter oldPainter) {
if (textDirection != oldPainter.textDirection) return true;
if (circles == oldPainter.circles) return false;
if (circles.length != oldPainter.circles.length) return true;
for (var i = 0; i < circles.length; i += 1) {
if (circles[i] != oldPainter.circles[i]) return true;
}
return false;
}
@override
void paint(final Canvas canvas, final Size size) {
for (final circle in circles) {
final paint = Paint()..color = circle.color;
final rect = Rect.fromLTWH(0.0, 0.0, size.width, size.height);
canvas.clipRect(rect);
final double leftFraction;
switch (textDirection) {
case TextDirection.rtl:
leftFraction = 1.0 - circle.horizontalLeadingOffset;
break;
case TextDirection.ltr:
leftFraction = circle.horizontalLeadingOffset;
break;
}
final center = Offset(leftFraction * size.width, size.height / 2.0);
final radiusTween = Tween<double>(
begin: 0.0,
end: _maxRadius(center, size),
);
canvas.drawCircle(
center,
radiusTween.transform(circle.animation.value),
paint,
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment