Commit 8d41fa78 authored by gspencergoog's avatar gspencergoog Committed by GitHub

Adding proper accommodation for textScaleFactor in bottom navigation bar. (#12421)

This updates the bottom navigation bar to be able to handle more general widgets in the place of the label in the bottom navigation bar, so that Text with a textScaleFactor larger than 1.0 will behave nicely in a bottom navigation bar.

It also means that other widgets given instead of a Text widget for the label will work more predictably.

I also vastly simplified the layout logic, eliminating many computations that were not needed, and refactored the build function to use a separate private navigation tile widget.

Also, the color splash animations were coming from the wrong location (they were coming from far to the right of the touched widget), so that works as specified now.
parent 0a85db29
...@@ -18,6 +18,10 @@ import 'typography.dart'; ...@@ -18,6 +18,10 @@ import 'typography.dart';
const double _kActiveMaxWidth = 168.0; const double _kActiveMaxWidth = 168.0;
const double _kInactiveMaxWidth = 96.0; const double _kInactiveMaxWidth = 96.0;
const double _kActiveFontSize = 14.0;
const double _kInactiveFontSize = 12.0;
const double _kTopMargin = 6.0;
const double _kBottomMargin = 8.0;
/// Defines the layout and behavior of a [BottomNavigationBar]. /// Defines the layout and behavior of a [BottomNavigationBar].
/// ///
...@@ -27,11 +31,13 @@ const double _kInactiveMaxWidth = 96.0; ...@@ -27,11 +31,13 @@ const double _kInactiveMaxWidth = 96.0;
/// * [BottomNavigationBarItem] /// * [BottomNavigationBarItem]
/// * <https://material.google.com/components/bottom-navigation.html#bottom-navigation-specs> /// * <https://material.google.com/components/bottom-navigation.html#bottom-navigation-specs>
enum BottomNavigationBarType { enum BottomNavigationBarType {
/// The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width. /// The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width, always
/// display their text labels, and do not shift when tapped.
fixed, fixed,
/// The location and size of the [BottomNavigationBar] [BottomNavigationBarItem]s /// The location and size of the [BottomNavigationBar] [BottomNavigationBarItem]s
/// animate larger when they are tapped. /// animate and labels fade in when they are tapped. Only the selected item
/// displays its text label.
shifting, shifting,
} }
...@@ -39,12 +45,12 @@ enum BottomNavigationBarType { ...@@ -39,12 +45,12 @@ enum BottomNavigationBarType {
/// small number of views. /// small number of views.
/// ///
/// The bottom navigation bar consists of multiple items in the form of /// The bottom navigation bar consists of multiple items in the form of
/// labels, icons, or both, laid out on top of a piece of material. It provides /// text labels, icons, or both, laid out on top of a piece of material. It
/// quick navigation between the top-level views of an app. For larger screens, /// provides quick navigation between the top-level views of an app. For larger
/// side navigation may be a better fit. /// screens, side navigation may be a better fit.
/// ///
/// A bottom navigation bar is usually used in conjunction with [Scaffold] where /// A bottom navigation bar is usually used in conjunction with a [Scaffold],
/// it is provided as the [Scaffold.bottomNavigationBar] argument. /// where it is provided as the [Scaffold.bottomNavigationBar] argument.
/// ///
/// See also: /// See also:
/// ///
...@@ -55,26 +61,29 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -55,26 +61,29 @@ class BottomNavigationBar extends StatefulWidget {
/// Creates a bottom navigation bar, typically used in a [Scaffold] where it /// Creates a bottom navigation bar, typically used in a [Scaffold] where it
/// is provided as the [Scaffold.bottomNavigationBar] argument. /// is provided as the [Scaffold.bottomNavigationBar] argument.
/// ///
/// The arguments [items] and [type] should not be null. /// The argument [items] should not be null.
/// ///
/// The number of items passed should be equal or greater than 2. /// The number of items passed should be equal to, or greater than, two. If
/// three or fewer items are passed, then the default [type] (if [type] is
/// null or not given) will be [BottomNavigationBarType.fixed], and if more
/// than three items are passed, will be [BottomNavigationBarType.shifting].
/// ///
/// Passing a null [fixedColor] will cause a fallback to the theme's primary /// Passing a null [fixedColor] will cause a fallback to the theme's primary
/// color. /// color. The [fixedColor] field will be ignored if the [BottomNavigationBar.type] is
/// not [BottomNavigationBarType.fixed].
BottomNavigationBar({ BottomNavigationBar({
Key key, Key key,
@required this.items, @required this.items,
this.onTap, this.onTap,
this.currentIndex: 0, this.currentIndex: 0,
this.type: BottomNavigationBarType.fixed, BottomNavigationBarType type,
this.fixedColor, this.fixedColor,
this.iconSize: 24.0, this.iconSize: 24.0,
}) : assert(items != null), }) : assert(items != null),
assert(items.length >= 2), assert(items.length >= 2),
assert(0 <= currentIndex && currentIndex < items.length), assert(0 <= currentIndex && currentIndex < items.length),
assert(type != null),
assert(type == BottomNavigationBarType.fixed || fixedColor == null),
assert(iconSize != null), assert(iconSize != null),
type = type ?? (items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting),
super(key: key); super(key: key);
/// The interactive items laid out within the bottom navigation bar. /// The interactive items laid out within the bottom navigation bar.
...@@ -91,34 +100,191 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -91,34 +100,191 @@ class BottomNavigationBar extends StatefulWidget {
final int currentIndex; final int currentIndex;
/// Defines the layout and behavior of a [BottomNavigationBar]. /// Defines the layout and behavior of a [BottomNavigationBar].
///
/// See documentation for [BottomNavigationBarType] for information on the meaning
/// of different types.
final BottomNavigationBarType type; final BottomNavigationBarType type;
/// The color of the selected item when bottom navigation bar is /// The color of the selected item when bottom navigation bar is
/// [BottomNavigationBarType.fixed]. /// [BottomNavigationBarType.fixed].
///
/// If [fixedColor] is null, it will use the theme's primary color. The [fixedColor]
/// field will be ignored if the [type] is not [BottomNavigationBarType.fixed].
final Color fixedColor; final Color fixedColor;
/// The size of all of the [BottomNavigationBarItem] icons. /// The size of all of the [BottomNavigationBarItem] icons.
/// ///
/// This value is used to to configure the [IconTheme] for the navigation /// See [BottomNavigationBarItem.icon] for more information.
/// bar. When a [BottomNavigationBarItem.icon] widget is not an [Icon] the widget
/// should configure itself to match the icon theme's size and color.
final double iconSize; final double iconSize;
@override @override
_BottomNavigationBarState createState() => new _BottomNavigationBarState(); _BottomNavigationBarState createState() => new _BottomNavigationBarState();
} }
// This represents a single tile in the bottom navigation bar. It is intended
// to go into a flex container.
class _BottomNavigationTile extends StatelessWidget {
_BottomNavigationTile(
this.type,
this.item,
this.animation,
this.iconSize, {
this.onTap,
this.colorTween,
this.flex
}
);
final BottomNavigationBarType type;
final BottomNavigationBarItem item;
final Animation<double> animation;
final double iconSize;
final VoidCallback onTap;
final ColorTween colorTween;
final double flex;
Widget _buildIcon() {
double tweenStart;
Color iconColor;
switch (type) {
case BottomNavigationBarType.fixed:
tweenStart = 8.0;
iconColor = colorTween.evaluate(animation);
break;
case BottomNavigationBarType.shifting:
tweenStart = 16.0;
iconColor = Colors.white;
break;
}
return new Align(
alignment: Alignment.topCenter,
heightFactor: 1.0,
child: new Container(
margin: new EdgeInsets.only(
top: new Tween<double>(
begin: tweenStart,
end: _kTopMargin,
).evaluate(animation),
),
child: new IconTheme(
data: new IconThemeData(
color: iconColor,
size: iconSize,
),
child: item.icon,
),
),
);
}
Widget _buildFixedLabel() {
return new Align(
alignment: Alignment.bottomCenter,
heightFactor: 1.0,
child: new Container(
margin: const EdgeInsets.only(bottom: _kBottomMargin),
child: DefaultTextStyle.merge(
style: new TextStyle(
fontSize: _kActiveFontSize,
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: new Transform(
transform: new Matrix4.diagonal3(
new Vector3.all(
new Tween<double>(
begin: _kInactiveFontSize / _kActiveFontSize,
end: 1.0,
).evaluate(animation),
),
),
alignment: Alignment.bottomCenter,
child: item.title,
),
),
),
);
}
Widget _buildShiftingLabel() {
return new Align(
alignment: Alignment.bottomCenter,
heightFactor: 1.0,
child: new Container(
margin: new EdgeInsets.only(
bottom: new Tween<double>(
// In the spec, they just remove the label for inactive items and
// specify a 16dp bottom margin. We don't want to actually remove
// the label because we want to fade it in and out, so this modifies
// the bottom margin to take that into account.
begin: 2.0,
end: _kBottomMargin,
).evaluate(animation),
),
child: new FadeTransition(
opacity: animation,
child: DefaultTextStyle.merge(
style: const TextStyle(
fontSize: _kActiveFontSize,
color: Colors.white,
),
child: item.title,
),
),
),
);
}
@override
Widget build(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.
int size;
Widget label;
switch (type) {
case BottomNavigationBarType.fixed:
size = 1;
label = _buildFixedLabel();
break;
case BottomNavigationBarType.shifting:
size = (flex * 1000.0).round();
label = _buildShiftingLabel();
break;
}
return new Expanded(
flex: size,
child: new InkResponse(
onTap: onTap,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildIcon(),
label,
],
),
),
);
}
}
class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerProviderStateMixin { class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerProviderStateMixin {
List<AnimationController> _controllers; List<AnimationController> _controllers;
List<CurvedAnimation> _animations; List<CurvedAnimation> _animations;
double _weight;
// A queue of color splashes currently being animated.
final Queue<_Circle> _circles = new Queue<_Circle>(); final Queue<_Circle> _circles = new Queue<_Circle>();
Color _backgroundColor; // Last growing circle's color.
static final Tween<double> _flexTween = new Tween<double>( // Last splash circle's color, and the final color of the control after
begin: 1.0, // animation is complete.
end: 1.5 Color _backgroundColor;
);
static final Tween<double> _flexTween = new Tween<double>(begin: 1.0, end: 1.5);
@override @override
void initState() { void initState() {
...@@ -140,15 +306,6 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -140,15 +306,6 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
_backgroundColor = widget.items[widget.currentIndex].backgroundColor; _backgroundColor = widget.items[widget.currentIndex].backgroundColor;
} }
@override
void dispose() {
for (AnimationController controller in _controllers)
controller.dispose();
for (_Circle circle in _circles)
circle.dispose();
super.dispose();
}
void _rebuild() { void _rebuild() {
setState(() { setState(() {
// Rebuilding when any of the controllers tick, i.e. when the items are // Rebuilding when any of the controllers tick, i.e. when the items are
...@@ -156,289 +313,168 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -156,289 +313,168 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
}); });
} }
double get _maxWidth { @override
assert(widget.type != null); void dispose() {
switch (widget.type) { for (AnimationController controller in _controllers)
case BottomNavigationBarType.fixed: controller.dispose();
return widget.items.length * _kActiveMaxWidth; for (_Circle circle in _circles)
case BottomNavigationBarType.shifting: circle.dispose();
return _kActiveMaxWidth + (widget.items.length - 1) * _kInactiveMaxWidth; super.dispose();
}
return null;
}
bool _isAnimating(Animation<double> animation) {
return animation.status == AnimationStatus.forward ||
animation.status == AnimationStatus.reverse;
}
// Because of non-linear nature of the animations, the animations that are
// currently animating might not add up to the flex weight we are expecting.
// (1.5 + N - 1, since the max flex that the animating ones can have is 1.5)
// This causes instability in the animation when multiple items are tapped.
// To solves this, we always store a weight that normalizes animating
// animations such that their resulting flex values will add up to the desired
// value.
void _computeWeight() {
final Iterable<Animation<double>> animating = _animations.where(_isAnimating);
if (animating.isNotEmpty) {
final double sum = animating.fold(0.0, (double sum, Animation<double> animation) {
return sum + _flexTween.evaluate(animation);
});
_weight = (animating.length + 0.5) / sum;
} else {
_weight = 1.0;
}
}
double _flex(Animation<double> animation) {
if (_isAnimating(animation)) {
assert(_weight != null);
return _flexTween.evaluate(animation) * _weight;
} else {
return _flexTween.evaluate(animation);
}
}
double _xOffset(int index) {
double weightSum(Iterable<Animation<double>> animations) {
// We're adding flex values instead of animation values to have correct ratios.
return animations.map(_flex).fold(0.0, (double sum, double value) => sum + value);
}
final double allWeights = weightSum(_animations);
// This weight corresponds to the left edge of the indexed item.
final double leftWeights = weightSum(_animations.sublist(0, index));
// Add half of its flex value in order to get the center.
return (leftWeights + _flex(_animations[index]) / 2.0) / allWeights;
} }
Alignment _circleOffset(int index) { double _evaluateFlex(Animation<double> animation) => _flexTween.evaluate(animation);
final double iconSize = widget.iconSize;
final Tween<double> yOffsetTween = new Tween<double>(
begin: (18.0 + iconSize / 2.0) / kBottomNavigationBarHeight, // 18dp + icon center
end: (6.0 + iconSize / 2.0) / kBottomNavigationBarHeight // 6dp + icon center
);
return new Alignment(
_xOffset(index),
yOffsetTween.evaluate(_animations[index])
);
}
void _pushCircle(int index) { void _pushCircle(int index) {
if (widget.items[index].backgroundColor != null) if (widget.items[index].backgroundColor != null) {
_circles.add( _circles.add(
new _Circle( new _Circle(
state: this, state: this,
index: index, index: index,
color: widget.items[index].backgroundColor, color: widget.items[index].backgroundColor,
vsync: this, vsync: this,
)..controller.addStatusListener((AnimationStatus status) { )..controller.addStatusListener(
if (status == AnimationStatus.completed) { (AnimationStatus status) {
switch (status) {
case AnimationStatus.completed:
setState(() { setState(() {
final _Circle circle = _circles.removeFirst(); final _Circle circle = _circles.removeFirst();
_backgroundColor = circle.color; _backgroundColor = circle.color;
circle.dispose(); circle.dispose();
}); });
break;
case AnimationStatus.dismissed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
} }
}) },
),
); );
} }
}
@override @override
void didUpdateWidget(BottomNavigationBar oldWidget) { void didUpdateWidget(BottomNavigationBar oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.currentIndex != oldWidget.currentIndex) { if (widget.currentIndex != oldWidget.currentIndex) {
if (widget.type == BottomNavigationBarType.shifting) switch (widget.type) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
_pushCircle(widget.currentIndex); _pushCircle(widget.currentIndex);
break;
}
_controllers[oldWidget.currentIndex].reverse(); _controllers[oldWidget.currentIndex].reverse();
_controllers[widget.currentIndex].forward(); _controllers[widget.currentIndex].forward();
} }
} }
@override List<Widget> _createTiles() {
Widget build(BuildContext context) { final List<Widget> children = <Widget>[];
Widget bottomNavigation;
switch (widget.type) { switch (widget.type) {
case BottomNavigationBarType.fixed: case BottomNavigationBarType.fixed:
final List<Widget> children = <Widget>[];
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme; final TextTheme textTheme = themeData.textTheme;
Color themeColor;
switch (themeData.brightness) {
case Brightness.light:
themeColor = themeData.primaryColor;
break;
case Brightness.dark:
themeColor = themeData.accentColor;
break;
}
final ColorTween colorTween = new ColorTween( final ColorTween colorTween = new ColorTween(
begin: textTheme.caption.color, begin: textTheme.caption.color,
end: widget.fixedColor ?? ( end: widget.fixedColor ?? themeColor,
themeData.brightness == Brightness.light ?
themeData.primaryColor : themeData.accentColor
)
); );
for (int i = 0; i < widget.items.length; i += 1) { for (int i = 0; i < widget.items.length; i += 1) {
children.add( children.add(
new Expanded( new _BottomNavigationTile(
child: new InkResponse( widget.type,
widget.items[i],
_animations[i],
widget.iconSize,
onTap: () { onTap: () {
if (widget.onTap != null) if (widget.onTap != null)
widget.onTap(i); widget.onTap(i);
}, },
child: new Stack( colorTween: colorTween),
alignment: Alignment.center,
children: <Widget>[
new Align(
alignment: Alignment.topCenter,
child: new Container(
margin: new EdgeInsets.only(
top: new Tween<double>(
begin: 8.0,
end: 6.0,
).evaluate(_animations[i]),
),
child: new IconTheme(
data: new IconThemeData(
color: colorTween.evaluate(_animations[i]),
size: widget.iconSize,
),
child: widget.items[i].icon,
),
),
),
new Align(
alignment: Alignment.bottomCenter,
child: new Container(
margin: const EdgeInsets.only(bottom: 10.0),
child: DefaultTextStyle.merge(
style: new TextStyle(
fontSize: 14.0,
color: colorTween.evaluate(_animations[i]),
),
child: new Transform(
transform: new Matrix4.diagonal3(new Vector3.all(
new Tween<double>(
begin: 0.85,
end: 1.0,
).evaluate(_animations[i]),
)),
alignment: Alignment.bottomCenter,
child: widget.items[i].title,
),
),
),
),
],
),
),
),
); );
} }
bottomNavigation = new SizedBox(
width: _maxWidth,
child: new Row(children: children),
);
break; break;
case BottomNavigationBarType.shifting: case BottomNavigationBarType.shifting:
final List<Widget> children = <Widget>[];
_computeWeight();
for (int i = 0; i < widget.items.length; i += 1) { for (int i = 0; i < widget.items.length; i += 1) {
children.add( children.add(
new Expanded( new _BottomNavigationTile(
// Since Flexible only supports integers, we're using large widget.type,
// numbers in order to simulate floating point flex values. widget.items[i],
flex: (_flex(_animations[i]) * 1000.0).round(), _animations[i],
child: new InkResponse( widget.iconSize,
onTap: () { onTap: () {
if (widget.onTap != null) if (widget.onTap != null)
widget.onTap(i); widget.onTap(i);
}, },
child: new Stack( flex: _evaluateFlex(_animations[i])),
alignment: Alignment.center,
children: <Widget>[
new Align(
alignment: Alignment.topCenter,
child: new Container(
margin: new EdgeInsets.only(
top: new Tween<double>(
begin: 18.0,
end: 6.0,
).evaluate(_animations[i]),
),
child: new IconTheme(
data: new IconThemeData(
color: Colors.white,
size: widget.iconSize,
),
child: widget.items[i].icon,
),
),
),
new Align(
alignment: Alignment.bottomCenter,
child: new Container(
margin: const EdgeInsets.only(bottom: 10.0),
child: new FadeTransition(
opacity: _animations[i],
child: DefaultTextStyle.merge(
style: const TextStyle(
fontSize: 14.0,
color: Colors.white
),
child: widget.items[i].title
),
),
),
),
],
),
),
),
); );
} }
bottomNavigation = new SizedBox( break;
width: _maxWidth, }
return children;
}
Widget _createContainer(List<Widget> tiles) {
return DefaultTextStyle.merge(
overflow: TextOverflow.ellipsis,
child: new Row( child: new Row(
children: children mainAxisAlignment: MainAxisAlignment.spaceBetween,
) children: tiles,
),
); );
break;
} }
@override
Widget build(BuildContext context) {
Color backgroundColor;
switch (widget.type) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
backgroundColor = _backgroundColor;
break;
}
return new Stack( return new Stack(
children: <Widget>[ children: <Widget>[
new Positioned.fill( new Positioned.fill(
child: new Material( // Casts shadow. child: new Material( // Casts shadow.
elevation: 8.0, elevation: 8.0,
color: widget.type == BottomNavigationBarType.shifting ? _backgroundColor : null color: backgroundColor,
)
), ),
new SizedBox( ),
height: kBottomNavigationBarHeight, new ConstrainedBox(
child: new Center( constraints: new BoxConstraints(minHeight: kBottomNavigationBarHeight),
child: new Stack( child: new Stack(
children: <Widget>[ children: <Widget>[
new Positioned.fill( new Positioned.fill(
child: new CustomPaint( child: new CustomPaint(
painter: new _RadialPainter( painter: new _RadialPainter(
circles: _circles.toList(), circles: _circles.toList(),
bottomNavMaxWidth: _maxWidth,
), ),
), ),
), ),
new Material( // Splashes. new Material( // Splashes.
type: MaterialType.transparency, type: MaterialType.transparency,
child: new Center( child: _createContainer(_createTiles()),
child: bottomNavigation
),
), ),
], ],
), ),
), ),
),
], ],
); );
} }
} }
// Describes an animating color splash circle.
class _Circle { class _Circle {
_Circle({ _Circle({
@required this.state, @required this.state,
...@@ -465,8 +501,19 @@ class _Circle { ...@@ -465,8 +501,19 @@ class _Circle {
AnimationController controller; AnimationController controller;
CurvedAnimation animation; CurvedAnimation animation;
Alignment get offset { double get horizontalOffset {
return state._circleOffset(index); double weightSum(Iterable<Animation<double>> animations) {
// We're adding flex values instead of animation values to produce correct
// ratios.
return animations.map(state._evaluateFlex).fold(0.0, (double sum, double value) => sum + value);
}
final double allWeights = weightSum(state._animations);
// These weights sum to the left edge of the indexed item.
final double leftWeights = weightSum(state._animations.sublist(0, index));
// Add half of its flex value in order to get to the center.
return (leftWeights + state._evaluateFlex(state._animations[index]) / 2.0) / allWeights;
} }
void dispose() { void dispose() {
...@@ -474,62 +521,49 @@ class _Circle { ...@@ -474,62 +521,49 @@ class _Circle {
} }
} }
// Paints the animating color splash circles.
class _RadialPainter extends CustomPainter { class _RadialPainter extends CustomPainter {
_RadialPainter({ _RadialPainter({
this.circles, this.circles,
this.bottomNavMaxWidth,
}); });
final List<_Circle> circles; final List<_Circle> circles;
final double bottomNavMaxWidth;
// Computes the maximum radius attainable such that at least one of the // Computes the maximum radius attainable such that at least one of the
// bounding rectangle's corners touches the egde of the circle. Drawing a // bounding rectangle's corners touches the edge of the circle. Drawing a
// circle beyond this radius is futile since there is no perceivable // circle larger than this radius is not needed, since there is no perceivable
// difference within the cropped rectangle. // difference within the cropped rectangle.
double _maxRadius(Alignment alignment, Size size) { static double _maxRadius(Offset center, Size size) {
final double dx = alignment.x; final double maxX = math.max(center.dx, size.width - center.dx);
final double dy = alignment.y; final double maxY = math.max(center.dy, size.height - center.dy);
final double halfWidth = size.width / 2.0; return math.sqrt(maxX * maxX + maxY * maxY);
final double halfHeight = size.height / 2.0;
final double x = halfWidth + dx.abs() * halfWidth;
final double y = halfHeight + dy.abs() * halfHeight;
return math.sqrt(x * x + y * y);
} }
@override @override
bool shouldRepaint(_RadialPainter oldPainter) { bool shouldRepaint(_RadialPainter oldPainter) {
if (bottomNavMaxWidth != oldPainter.bottomNavMaxWidth)
return true;
if (circles == oldPainter.circles) if (circles == oldPainter.circles)
return false; return false;
if (circles.length != oldPainter.circles.length) if (circles.length != oldPainter.circles.length)
return true; return true;
for (int i = 0; i < circles.length; i += 1) for (int i = 0; i < circles.length; i += 1)
if (circles[i] != oldPainter.circles[i]) if (circles[i] != oldPainter.circles[i])
return true; return true;
return false; return false;
} }
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
for (_Circle circle in circles) { for (_Circle circle in circles) {
final Tween<double> radiusTween = new Tween<double>(
begin: 0.0,
end: _maxRadius(circle.offset, size),
);
final Paint paint = new Paint()..color = circle.color; final Paint paint = new Paint()..color = circle.color;
final Rect rect = new Rect.fromLTWH(0.0, 0.0, size.width, size.height); final Rect rect = new Rect.fromLTWH(0.0, 0.0, size.width, size.height);
canvas.clipRect(rect); canvas.clipRect(rect);
final double navWidth = math.min(bottomNavMaxWidth, size.width);
final double halfNavWidth = navWidth / 2.0;
final double halfHeight = size.height / 2.0;
final Offset center = new Offset( final Offset center = new Offset(
(size.width - navWidth) / 2.0 + halfNavWidth + circle.offset.x * halfNavWidth, circle.horizontalOffset * size.width,
halfHeight + circle.offset.y * halfHeight, size.height / 2.0,
);
final Tween<double> radiusTween = new Tween<double>(
begin: 0.0,
end: _maxRadius(center, size),
); );
canvas.drawCircle( canvas.drawCircle(
center, center,
......
...@@ -8,7 +8,7 @@ import 'package:flutter/painting.dart'; ...@@ -8,7 +8,7 @@ import 'package:flutter/painting.dart';
const double kToolbarHeight = 56.0; const double kToolbarHeight = 56.0;
/// The height of the bottom navigation bar. /// The height of the bottom navigation bar.
const double kBottomNavigationBarHeight = 60.0; const double kBottomNavigationBarHeight = 56.0;
/// The height of a tab bar containing text. /// The height of a tab bar containing text.
const double kTextTabBarHeight = 48.0; const double kTextTabBarHeight = 48.0;
......
...@@ -1201,12 +1201,12 @@ class Align extends SingleChildRenderObjectWidget { ...@@ -1201,12 +1201,12 @@ class Align extends SingleChildRenderObjectWidget {
/// with the center of the parent. /// with the center of the parent.
final AlignmentGeometry alignment; final AlignmentGeometry alignment;
/// If non-null, sets its width to the child's width multipled by this factor. /// If non-null, sets its width to the child's width multiplied by this factor.
/// ///
/// Can be both greater and less than 1.0 but must be positive. /// Can be both greater and less than 1.0 but must be positive.
final double widthFactor; final double widthFactor;
/// If non-null, sets its height to the child's height multipled by this factor. /// If non-null, sets its height to the child's height multiplied by this factor.
/// ///
/// Can be both greater and less than 1.0 but must be positive. /// Can be both greater and less than 1.0 but must be positive.
final double heightFactor; final double heightFactor;
...@@ -3408,7 +3408,7 @@ class Flexible extends ParentDataWidget<Flex> { ...@@ -3408,7 +3408,7 @@ class Flexible extends ParentDataWidget<Flex> {
/// Using an [Expanded] widget makes a child of a [Row], [Column], or [Flex] /// Using an [Expanded] widget makes a child of a [Row], [Column], or [Flex]
/// expand to fill the available space in the main axis (e.g., horizontally for /// expand to fill the available space in the main axis (e.g., horizontally for
/// a [Row] or vertically for a [Column]). If multiple children are expanded, /// a [Row] or vertically for a [Column]). If multiple children are expanded,
/// the available space is divided amoung them according to the [flex] factor. /// the available space is divided among them according to the [flex] factor.
/// ///
/// An [Expanded] widget must be a descendant of a [Row], [Column], or [Flex], /// An [Expanded] widget must be a descendant of a [Row], [Column], or [Flex],
/// and the path from the [Expanded] widget to its enclosing [Row], [Column], or /// and the path from the [Expanded] widget to its enclosing [Row], [Column], or
......
...@@ -11,7 +11,7 @@ import 'framework.dart'; ...@@ -11,7 +11,7 @@ import 'framework.dart';
/// An interactive button within either material's [BottomNavigationBar] /// An interactive button within either material's [BottomNavigationBar]
/// or the iOS themed [CupertinoTabBar] with an icon and title. /// or the iOS themed [CupertinoTabBar] with an icon and title.
/// ///
/// This calss is rarely used in isolation. Commonly embedded in one of the /// This class is rarely used in isolation. Commonly embedded in one of the
/// bottom navigation widgets above. /// bottom navigation widgets above.
/// ///
/// See also: /// See also:
......
...@@ -57,7 +57,7 @@ void main() { ...@@ -57,7 +57,7 @@ void main() {
); );
final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar));
expect(box.size.height, 60.0); expect(box.size.height, kBottomNavigationBarHeight);
expect(find.text('AC'), findsOneWidget); expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget); expect(find.text('Alarm'), findsOneWidget);
}); });
...@@ -85,8 +85,8 @@ void main() { ...@@ -85,8 +85,8 @@ void main() {
Iterable<RenderBox> actions = tester.renderObjectList(find.byType(InkResponse)); Iterable<RenderBox> actions = tester.renderObjectList(find.byType(InkResponse));
expect(actions.length, 2); expect(actions.length, 2);
expect(actions.elementAt(0).size.width, 158.4); expect(actions.elementAt(0).size.width, 480.0);
expect(actions.elementAt(1).size.width, 105.6); expect(actions.elementAt(1).size.width, 320.0);
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
...@@ -113,8 +113,8 @@ void main() { ...@@ -113,8 +113,8 @@ void main() {
actions = tester.renderObjectList(find.byType(InkResponse)); actions = tester.renderObjectList(find.byType(InkResponse));
expect(actions.length, 2); expect(actions.length, 2);
expect(actions.elementAt(0).size.width, 105.6); expect(actions.elementAt(0).size.width, 320.0);
expect(actions.elementAt(1).size.width, 158.4); expect(actions.elementAt(1).size.width, 480.0);
}); });
testWidgets('BottomNavigationBar multiple taps test', (WidgetTester tester) async { testWidgets('BottomNavigationBar multiple taps test', (WidgetTester tester) async {
...@@ -288,4 +288,108 @@ void main() { ...@@ -288,4 +288,108 @@ void main() {
}); });
testWidgets('BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
title: const Text('A'),
icon: const Icon(Icons.ac_unit),
),
const BottomNavigationBarItem(
title: const Text('B'),
icon: const Icon(Icons.battery_alert),
),
],
),
),
),
);
final RenderBox defaultBox = tester.renderObject(find.byType(BottomNavigationBar));
expect(defaultBox.size.height, equals(kBottomNavigationBarHeight));
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.shifting,
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
title: const Text('A'),
icon: const Icon(Icons.ac_unit),
),
const BottomNavigationBarItem(
title: const Text('B'),
icon: const Icon(Icons.battery_alert),
),
],
),
),
),
);
final RenderBox shiftingBox = tester.renderObject(find.byType(BottomNavigationBar));
expect(shiftingBox.size.height, equals(kBottomNavigationBarHeight));
await tester.pumpWidget(
new MaterialApp(
home: new MediaQuery(
data: const MediaQueryData(textScaleFactor: 2.0),
child: new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
title: const Text('A'),
icon: const Icon(Icons.ac_unit),
),
const BottomNavigationBarItem(
title: const Text('B'),
icon: const Icon(Icons.battery_alert),
),
],
),
),
),
),
);
final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar));
expect(box.size.height, equals(68.0));
});
testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async {
final Text longTextA = new Text(''.padLeft(100, 'A'));
final Text longTextB = new Text(''.padLeft(100, 'B'));
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
title: longTextA,
icon: const Icon(Icons.ac_unit),
),
new BottomNavigationBarItem(
title: longTextB,
icon: const Icon(Icons.battery_alert),
),
],
),
),
),
);
final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar));
expect(box.size.height, equals(kBottomNavigationBarHeight));
final RenderBox itemBoxA = tester.renderObject(find.text(longTextA.data));
expect(itemBoxA.size, equals(const Size(400.0, 14.0)));
final RenderBox itemBoxB = tester.renderObject(find.text(longTextB.data));
expect(itemBoxB.size, equals(const Size(400.0, 14.0)));
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment