|
|
|
|
|
|
using Unity.UIWidgets.gestures; |
|
|
|
using Unity.UIWidgets.painting; |
|
|
|
using Unity.UIWidgets.rendering; |
|
|
|
using Unity.UIWidgets.scheduler; |
|
|
|
using Unity.UIWidgets.service; |
|
|
|
using Unity.UIWidgets.ui; |
|
|
|
using Unity.UIWidgets.widgets; |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
public enum BottomNavigationBarType { |
|
|
|
fix, |
|
|
|
shifting |
|
|
|
fix, |
|
|
|
shifting |
|
|
|
BottomNavigationBar( |
|
|
|
Key key = null, |
|
|
|
List<BottomNavigationBarItem> items = null, |
|
|
|
ValueChanged<int> onTap = null, |
|
|
|
int currentIndex = 0, |
|
|
|
BottomNavigationBarType? type = null, |
|
|
|
Color fixedColor = null, |
|
|
|
float iconSize = 24.0f |
|
|
|
) : base(key: key) { |
|
|
|
D.assert(items != null); |
|
|
|
D.assert(items.Count >= 2); |
|
|
|
D.assert( items.All((BottomNavigationBarItem item) => item.title != null) == true, |
|
|
|
"Every item must have a non-null title" |
|
|
|
); |
|
|
|
D.assert(0 <= currentIndex && currentIndex < items.Count); |
|
|
|
this.items = items; |
|
|
|
this.onTap = onTap; |
|
|
|
this.currentIndex = currentIndex; |
|
|
|
this.type = type ?? (items.Count <= 3 ? BottomNavigationBarType.fix : BottomNavigationBarType.shifting); |
|
|
|
this.fixedColor = fixedColor; |
|
|
|
this.iconSize = iconSize; |
|
|
|
} |
|
|
|
public BottomNavigationBar( |
|
|
|
Key key = null, |
|
|
|
List<BottomNavigationBarItem> items = null, |
|
|
|
ValueChanged<int> onTap = null, |
|
|
|
int currentIndex = 0, |
|
|
|
BottomNavigationBarType? type = null, |
|
|
|
Color fixedColor = null, |
|
|
|
float iconSize = 24.0f |
|
|
|
) : base(key: key) { |
|
|
|
D.assert(items != null); |
|
|
|
D.assert(items.Count >= 2); |
|
|
|
D.assert(items.All((BottomNavigationBarItem item) => item.title != null) == true, |
|
|
|
"Every item must have a non-null title" |
|
|
|
); |
|
|
|
D.assert(0 <= currentIndex && currentIndex < items.Count); |
|
|
|
this.items = items; |
|
|
|
this.onTap = onTap; |
|
|
|
this.currentIndex = currentIndex; |
|
|
|
this.type = type ?? (items.Count <= 3 ? BottomNavigationBarType.fix : BottomNavigationBarType.shifting); |
|
|
|
this.fixedColor = fixedColor; |
|
|
|
this.iconSize = iconSize; |
|
|
|
} |
|
|
|
public readonly List<BottomNavigationBarItem> items; |
|
|
|
public readonly List<BottomNavigationBarItem> items; |
|
|
|
public readonly ValueChanged<int> onTap; |
|
|
|
public readonly ValueChanged<int> onTap; |
|
|
|
public readonly int currentIndex; |
|
|
|
public readonly int currentIndex; |
|
|
|
public readonly BottomNavigationBarType? type; |
|
|
|
public readonly BottomNavigationBarType? type; |
|
|
|
public readonly Color fixedColor; |
|
|
|
public readonly Color fixedColor; |
|
|
|
public readonly float iconSize; |
|
|
|
public readonly float iconSize; |
|
|
|
public override State createState() => new _BottomNavigationBarState(); |
|
|
|
public override State createState() { |
|
|
|
return new _BottomNavigationBarState(); |
|
|
|
} |
|
|
|
public _BottomNavigationTile( |
|
|
|
BottomNavigationBarType type, |
|
|
|
BottomNavigationBarItem item, |
|
|
|
Animation<float> animation, |
|
|
|
float? iconSize = null, |
|
|
|
VoidCallback onTap = null, |
|
|
|
ColorTween colorTween = null, |
|
|
|
float? flex = null, |
|
|
|
bool selected = false, |
|
|
|
string indexLabel = null |
|
|
|
) { |
|
|
|
public _BottomNavigationTile( |
|
|
|
BottomNavigationBarType? type, |
|
|
|
BottomNavigationBarItem item, |
|
|
|
Animation<float> animation, |
|
|
|
float? iconSize = null, |
|
|
|
VoidCallback onTap = null, |
|
|
|
ColorTween colorTween = null, |
|
|
|
float? flex = null, |
|
|
|
bool selected = false, |
|
|
|
string indexLabel = null |
|
|
|
) { |
|
|
|
this.type = type; |
|
|
|
this.item = item; |
|
|
|
this.animation = animation; |
|
|
|
this.iconSize = iconSize; |
|
|
|
this.onTap = onTap; |
|
|
|
this.colorTween = colorTween; |
|
|
|
this.flex = flex; |
|
|
|
this.selected = selected; |
|
|
|
this.indexLabel = indexLabel; |
|
|
|
this.type = type; |
|
|
|
this.item = item; |
|
|
|
this.animation = animation; |
|
|
|
this.iconSize = iconSize; |
|
|
|
this.onTap = onTap; |
|
|
|
this.colorTween = colorTween; |
|
|
|
this.flex = flex; |
|
|
|
this.selected = selected; |
|
|
|
this.indexLabel = indexLabel; |
|
|
|
public readonly BottomNavigationBarType type; |
|
|
|
public readonly BottomNavigationBarItem item; |
|
|
|
public readonly Animation<float> animation; |
|
|
|
public readonly float? iconSize; |
|
|
|
public readonly VoidCallback onTap; |
|
|
|
public readonly ColorTween colorTween; |
|
|
|
public readonly float? flex; |
|
|
|
public readonly bool selected; |
|
|
|
public readonly string indexLabel; |
|
|
|
public readonly BottomNavigationBarType? type; |
|
|
|
public readonly BottomNavigationBarItem item; |
|
|
|
public readonly Animation<float> animation; |
|
|
|
public readonly float? iconSize; |
|
|
|
public readonly VoidCallback onTap; |
|
|
|
public readonly ColorTween colorTween; |
|
|
|
public readonly float? flex; |
|
|
|
public readonly bool selected; |
|
|
|
public readonly string indexLabel; |
|
|
|
|
|
|
|
Widget _buildIcon() { |
|
|
|
float tweenStart; |
|
|
|
Color iconColor; |
|
|
|
switch (this.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
tweenStart = 8.0f; |
|
|
|
iconColor = this.colorTween.evaluate(this.animation); |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
tweenStart = 16.0f; |
|
|
|
iconColor = Colors.white; |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new Exception("Unknown BottomNavigationBarType: " + this.type); |
|
|
|
} |
|
|
|
Widget _buildIcon() { |
|
|
|
float tweenStart; |
|
|
|
Color iconColor; |
|
|
|
switch (this.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
tweenStart = 8.0f; |
|
|
|
iconColor = this.colorTween.evaluate(this.animation); |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
tweenStart = 16.0f; |
|
|
|
iconColor = Colors.white; |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new Exception("Unknown BottomNavigationBarType: " + this.type); |
|
|
|
return new Align( |
|
|
|
alignment: Alignment.topCenter, |
|
|
|
heightFactor: 1.0f, |
|
|
|
child: new Container( |
|
|
|
margin: EdgeInsets.only( |
|
|
|
top: new FloatTween( |
|
|
|
begin: tweenStart, |
|
|
|
end: BottomNavigationBarUtils._kTopMargin |
|
|
|
).evaluate(this.animation) |
|
|
|
), |
|
|
|
child: new IconTheme( |
|
|
|
data: new IconThemeData( |
|
|
|
color: iconColor, |
|
|
|
size: this.iconSize |
|
|
|
), |
|
|
|
child: this.selected ? this.item.activeIcon : this.item.icon |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
return new Align( |
|
|
|
alignment: Alignment.topCenter, |
|
|
|
heightFactor: 1.0f, |
|
|
|
child: new Container( |
|
|
|
margin: EdgeInsets.only( |
|
|
|
top: new FloatTween( |
|
|
|
begin: tweenStart, |
|
|
|
end: BottomNavigationBarUtils._kTopMargin |
|
|
|
).evaluate(this.animation) |
|
|
|
), |
|
|
|
child: new IconTheme( |
|
|
|
data: new IconThemeData( |
|
|
|
color: iconColor, |
|
|
|
size: this.iconSize |
|
|
|
), |
|
|
|
child: this.selected ? this.item.activeIcon : this.item.icon |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
Widget _buildFixedLabel() { |
|
|
|
float t = new FloatTween( |
|
|
|
begin: BottomNavigationBarUtils._kInactiveFontSize / BottomNavigationBarUtils._kActiveFontSize, |
|
|
|
end: 1.0f |
|
|
|
).evaluate(this.animation); |
|
|
|
return new Align( |
|
|
|
alignment: Alignment.bottomCenter, |
|
|
|
heightFactor: 1.0f, |
|
|
|
child: new Container( |
|
|
|
margin: EdgeInsets.only(bottom: BottomNavigationBarUtils._kBottomMargin), |
|
|
|
child: DefaultTextStyle.merge( |
|
|
|
style: new TextStyle( |
|
|
|
fontSize: BottomNavigationBarUtils._kActiveFontSize, |
|
|
|
color: this.colorTween.evaluate(this.animation) |
|
|
|
), |
|
|
|
child: new Transform( |
|
|
|
transform: Matrix3.makeAll(t,0,0,0,t,0,0,0,t), |
|
|
|
Widget _buildFixedLabel() { |
|
|
|
float t = new FloatTween( |
|
|
|
begin: BottomNavigationBarUtils._kInactiveFontSize / BottomNavigationBarUtils._kActiveFontSize, |
|
|
|
end: 1.0f |
|
|
|
).evaluate(this.animation); |
|
|
|
return new Align( |
|
|
|
child: this.item.title |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildShiftingLabel() { |
|
|
|
return new Align( |
|
|
|
alignment: Alignment.bottomCenter, |
|
|
|
heightFactor: 1.0f, |
|
|
|
child: new Container( |
|
|
|
margin: EdgeInsets.only( |
|
|
|
bottom: new FloatTween( |
|
|
|
begin: 2.0f, |
|
|
|
end: BottomNavigationBarUtils._kBottomMargin |
|
|
|
).evaluate(this.animation) |
|
|
|
), |
|
|
|
child: new FadeTransition( |
|
|
|
opacity: this.animation, |
|
|
|
child: DefaultTextStyle.merge( |
|
|
|
style: new TextStyle( |
|
|
|
fontSize: BottomNavigationBarUtils._kActiveFontSize, |
|
|
|
color: Colors.white |
|
|
|
), |
|
|
|
child: this.item.title |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
heightFactor: 1.0f, |
|
|
|
child: new Container( |
|
|
|
margin: EdgeInsets.only(bottom: BottomNavigationBarUtils._kBottomMargin), |
|
|
|
child: DefaultTextStyle.merge( |
|
|
|
style: new TextStyle( |
|
|
|
fontSize: BottomNavigationBarUtils._kActiveFontSize, |
|
|
|
color: this.colorTween.evaluate(this.animation) |
|
|
|
), |
|
|
|
child: new Transform( |
|
|
|
transform: Matrix3.makeAll(t, 0, 0, 0, t, 0, 0, 0, t), |
|
|
|
alignment: Alignment.bottomCenter, |
|
|
|
child: this.item.title |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
public override Widget build(BuildContext context) { |
|
|
|
int size; |
|
|
|
Widget label; |
|
|
|
switch (this.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
size = 1; |
|
|
|
label = this._buildFixedLabel(); |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
size = ((this.flex * 1000.0f) ?? 0.0f).round(); |
|
|
|
label = this._buildShiftingLabel(); |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new Exception("Unknown BottomNavigationBarType: " + this.type); |
|
|
|
Widget _buildShiftingLabel() { |
|
|
|
return new Align( |
|
|
|
alignment: Alignment.bottomCenter, |
|
|
|
heightFactor: 1.0f, |
|
|
|
child: new Container( |
|
|
|
margin: EdgeInsets.only( |
|
|
|
bottom: new FloatTween( |
|
|
|
begin: 2.0f, |
|
|
|
end: BottomNavigationBarUtils._kBottomMargin |
|
|
|
).evaluate(this.animation) |
|
|
|
), |
|
|
|
child: new FadeTransition( |
|
|
|
opacity: this.animation, |
|
|
|
child: DefaultTextStyle.merge( |
|
|
|
style: new TextStyle( |
|
|
|
fontSize: BottomNavigationBarUtils._kActiveFontSize, |
|
|
|
color: Colors.white |
|
|
|
), |
|
|
|
child: this.item.title |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
return new Expanded( |
|
|
|
flex: size, |
|
|
|
child: new Stack( |
|
|
|
children: new List<Widget>{ |
|
|
|
new InkResponse( |
|
|
|
onTap: this.onTap == null ? (GestureTapCallback) null : () => {this.onTap();}, |
|
|
|
child: new Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
children: new List<Widget>{this._buildIcon(), label} |
|
|
|
) |
|
|
|
|
|
|
|
public override Widget build(BuildContext context) { |
|
|
|
int size; |
|
|
|
Widget label; |
|
|
|
switch (this.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
size = 1; |
|
|
|
label = this._buildFixedLabel(); |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
size = ((this.flex * 1000.0f) ?? 0.0f).round(); |
|
|
|
label = this._buildShiftingLabel(); |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new Exception("Unknown BottomNavigationBarType: " + this.type); |
|
|
|
} |
|
|
|
|
|
|
|
return new Expanded( |
|
|
|
flex: size, |
|
|
|
child: new Stack( |
|
|
|
children: new List<Widget> { |
|
|
|
new InkResponse( |
|
|
|
onTap: this.onTap == null ? (GestureTapCallback) null : () => { this.onTap(); }, |
|
|
|
child: new Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
children: new List<Widget> {this._buildIcon(), label} |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
List<AnimationController> _controllers = new List<AnimationController>{}; |
|
|
|
List<CurvedAnimation> _animations; |
|
|
|
public List<AnimationController> _controllers = new List<AnimationController> { }; |
|
|
|
public List<CurvedAnimation> _animations; |
|
|
|
|
|
|
|
Queue<_Circle> _circles = new Queue<_Circle>(); |
|
|
|
|
|
|
|
Color _backgroundColor; |
|
|
|
Queue<_Circle> _circles = new Queue<_Circle>(); |
|
|
|
static readonly Animatable<float> _flexTween = new FloatTween(begin: 1.0f, end: 1.5f); |
|
|
|
Color _backgroundColor; |
|
|
|
public _BottomNavigationBarState() { |
|
|
|
} |
|
|
|
static readonly Animatable<float> _flexTween = new FloatTween(begin: 1.0f, end: 1.5f); |
|
|
|
|
|
|
|
public _BottomNavigationBarState() {} |
|
|
|
void _resetState() { |
|
|
|
foreach (AnimationController controller in this._controllers) { |
|
|
|
controller.dispose(); |
|
|
|
} |
|
|
|
void _resetState() { |
|
|
|
foreach (AnimationController controller in this._controllers) |
|
|
|
controller.dispose(); |
|
|
|
foreach (_Circle circle in this._circles) |
|
|
|
circle.dispose(); |
|
|
|
this._circles.Clear(); |
|
|
|
foreach (_Circle circle in this._circles) { |
|
|
|
circle.dispose(); |
|
|
|
} |
|
|
|
this._controllers = List<AnimationController>.generate(this.widget.items.Count, (int index) => { |
|
|
|
AnimationController controller = new AnimationController( |
|
|
|
duration: ThemeUtils.kThemeAnimationDuration, |
|
|
|
vsync: this |
|
|
|
); |
|
|
|
controller.addListener(this._rebuild); |
|
|
|
}); |
|
|
|
this._animations = List<CurvedAnimation>.generate(this.widget.items.Count, (int index) => { |
|
|
|
return new CurvedAnimation( |
|
|
|
parent: this._controllers[index], |
|
|
|
curve: Curves.fastOutSlowIn, |
|
|
|
reverseCurve: Curves.fastOutSlowIn.flipped |
|
|
|
); |
|
|
|
}); |
|
|
|
this._controllers[this.widget.currentIndex].setValue(1.0f); |
|
|
|
this._backgroundColor = this.widget.items[this.widget.currentIndex].backgroundColor; |
|
|
|
} |
|
|
|
this._circles.Clear(); |
|
|
|
public override void initState() { |
|
|
|
base.initState(); |
|
|
|
this._resetState(); |
|
|
|
} |
|
|
|
this._controllers = new List<AnimationController>(capacity: this.widget.items.Count); |
|
|
|
for (int index = 0; index < this.widget.items.Count; index++) { |
|
|
|
AnimationController controller = new AnimationController( |
|
|
|
duration: ThemeUtils.kThemeAnimationDuration, |
|
|
|
vsync: this |
|
|
|
); |
|
|
|
controller.addListener(this._rebuild); |
|
|
|
this._controllers.Add(controller); |
|
|
|
} |
|
|
|
void _rebuild() { |
|
|
|
this.setState(() => { |
|
|
|
}); |
|
|
|
} |
|
|
|
this._animations = new List<CurvedAnimation>(capacity: this.widget.items.Count); |
|
|
|
for (int index = 0; index < this.widget.items.Count; index++) { |
|
|
|
this._animations.Add(new CurvedAnimation( |
|
|
|
parent: this._controllers[index], |
|
|
|
curve: Curves.fastOutSlowIn, |
|
|
|
reverseCurve: Curves.fastOutSlowIn.flipped |
|
|
|
)); |
|
|
|
} |
|
|
|
public override void dispose() { |
|
|
|
foreach (AnimationController controller in this._controllers) |
|
|
|
controller.dispose(); |
|
|
|
foreach (_Circle circle in this._circles) |
|
|
|
circle.dispose(); |
|
|
|
base.dispose(); |
|
|
|
} |
|
|
|
this._controllers[this.widget.currentIndex].setValue(1.0f); |
|
|
|
this._backgroundColor = this.widget.items[this.widget.currentIndex].backgroundColor; |
|
|
|
} |
|
|
|
float _evaluateFlex(Animation<float> animation) => _flexTween.evaluate(animation); |
|
|
|
public override void initState() { |
|
|
|
base.initState(); |
|
|
|
this._resetState(); |
|
|
|
} |
|
|
|
void _pushCircle(int index) { |
|
|
|
if (this.widget.items[index].backgroundColor != null) { |
|
|
|
this._circles.Add( |
|
|
|
new _Circle( |
|
|
|
state: this, |
|
|
|
index: index, |
|
|
|
color: this.widget.items[index].backgroundColor, |
|
|
|
vsync: this |
|
|
|
)..controller.addStatusListener( |
|
|
|
(AnimationStatus status) => { |
|
|
|
switch (status) { |
|
|
|
case AnimationStatus.completed: |
|
|
|
this.setState(() => { |
|
|
|
_Circle circle = this._circles.RemoveFirst(); |
|
|
|
this._backgroundColor = circle.color; |
|
|
|
circle.dispose(); |
|
|
|
}); |
|
|
|
break; |
|
|
|
case AnimationStatus.dismissed: |
|
|
|
case AnimationStatus.forward: |
|
|
|
case AnimationStatus.reverse: |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
); |
|
|
|
void _rebuild() { |
|
|
|
this.setState(() => { }); |
|
|
|
} |
|
|
|
public override void didUpdateWidget(BottomNavigationBar oldWidget) { |
|
|
|
base.didUpdateWidget(oldWidget); |
|
|
|
public override void dispose() { |
|
|
|
foreach (AnimationController controller in this._controllers) { |
|
|
|
controller.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
foreach (_Circle circle in this._circles) { |
|
|
|
circle.dispose(); |
|
|
|
} |
|
|
|
if (this.widget.items.length != oldWidget.items.Count) { |
|
|
|
this._resetState(); |
|
|
|
return; |
|
|
|
base.dispose(); |
|
|
|
if (this.widget.currentIndex != oldWidget.currentIndex) { |
|
|
|
switch (this.widget.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
this._pushCircle(this.widget.currentIndex); |
|
|
|
break; |
|
|
|
} |
|
|
|
public float _evaluateFlex(Animation<float> animation) { |
|
|
|
return _flexTween.evaluate(animation); |
|
|
|
} |
|
|
|
this._controllers[oldWidget.currentIndex].reverse(); |
|
|
|
this._controllers[this.widget.currentIndex].forward(); |
|
|
|
} else { |
|
|
|
if (this._backgroundColor != this.widget.items[this.widget.currentIndex].backgroundColor) this._backgroundColor = this.widget.items[this.widget.currentIndex].backgroundColor; |
|
|
|
void _pushCircle(int index) { |
|
|
|
if (this.widget.items[index].backgroundColor != null) { |
|
|
|
_Circle circle = new _Circle( |
|
|
|
state: this, |
|
|
|
index: index, |
|
|
|
color: this.widget.items[index].backgroundColor, |
|
|
|
vsync: this |
|
|
|
); |
|
|
|
circle.controller.addStatusListener( |
|
|
|
(AnimationStatus status) => { |
|
|
|
switch (status) { |
|
|
|
case AnimationStatus.completed: |
|
|
|
this.setState(() => { |
|
|
|
_Circle cir = this._circles.Dequeue(); |
|
|
|
this._backgroundColor = cir.color; |
|
|
|
cir.dispose(); |
|
|
|
}); |
|
|
|
break; |
|
|
|
case AnimationStatus.dismissed: |
|
|
|
case AnimationStatus.forward: |
|
|
|
case AnimationStatus.reverse: |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
); |
|
|
|
this._circles.Enqueue(circle); |
|
|
|
} |
|
|
|
} |
|
|
|
List<Widget> _createTiles() { |
|
|
|
MaterialLocalizations localizations = MaterialLocalizations.of(this.context); |
|
|
|
D.assert(localizations != null); |
|
|
|
List<Widget> children = new List<Widget> {}; |
|
|
|
switch (this.widget.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
ThemeData themeData = Theme.of(this.context); |
|
|
|
TextTheme textTheme = themeData.textTheme; |
|
|
|
Color themeColor; |
|
|
|
switch (themeData.brightness) { |
|
|
|
case Brightness.light: |
|
|
|
themeColor = themeData.primaryColor; |
|
|
|
break; |
|
|
|
case Brightness.dark: |
|
|
|
themeColor = themeData.accentColor; |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new Exception("Unknown brightness: " + themeData.brightness); |
|
|
|
public override void didUpdateWidget(StatefulWidget _oldWidget) { |
|
|
|
base.didUpdateWidget(_oldWidget); |
|
|
|
BottomNavigationBar oldWidget = _oldWidget as BottomNavigationBar; |
|
|
|
if (this.widget.items.Count != oldWidget.items.Count) { |
|
|
|
this._resetState(); |
|
|
|
return; |
|
|
|
ColorTween colorTween = new ColorTween( |
|
|
|
begin: textTheme.caption.color, |
|
|
|
end: this.widget.fixedColor ?? themeColor |
|
|
|
); |
|
|
|
for (int i = 0; i < this.widget.items.Count; i += 1) { |
|
|
|
children.Add( |
|
|
|
new _BottomNavigationTile(this.widget.type, this.widget.items[i], this._animations[i], this.widget.iconSize, |
|
|
|
onTap: () => { |
|
|
|
if (this.widget.onTap != null) this.widget.onTap(i); |
|
|
|
}, |
|
|
|
colorTween: colorTween, |
|
|
|
selected: i == this.widget.currentIndex, |
|
|
|
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: this.widget.items.Count) |
|
|
|
) |
|
|
|
); |
|
|
|
|
|
|
|
if (this.widget.currentIndex != oldWidget.currentIndex) { |
|
|
|
switch (this.widget.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
this._pushCircle(this.widget.currentIndex); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
this._controllers[oldWidget.currentIndex].reverse(); |
|
|
|
this._controllers[this.widget.currentIndex].forward(); |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
for (int i = 0; i < this.widget.items.Count; i += 1) { |
|
|
|
children.Add( |
|
|
|
new _BottomNavigationTile(this.widget.type, this.widget.items[i], this._animations[i], this.widget.iconSize, |
|
|
|
onTap: () => { |
|
|
|
if (this.widget.onTap != null) this.widget.onTap(i); |
|
|
|
}, |
|
|
|
flex: |
|
|
|
this._evaluateFlex(this._animations[i]), |
|
|
|
selected: i == this.widget.currentIndex, |
|
|
|
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: this.widget.items.length) |
|
|
|
) |
|
|
|
); |
|
|
|
else { |
|
|
|
if (this._backgroundColor != this.widget.items[this.widget.currentIndex].backgroundColor) { |
|
|
|
this._backgroundColor = this.widget.items[this.widget.currentIndex].backgroundColor; |
|
|
|
} |
|
|
|
break; |
|
|
|
return children; |
|
|
|
} |
|
|
|
|
|
|
|
List<Widget> _createTiles() { |
|
|
|
MaterialLocalizations localizations = MaterialLocalizations.of(this.context); |
|
|
|
D.assert(localizations != null); |
|
|
|
List<Widget> children = new List<Widget> { }; |
|
|
|
switch (this.widget.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
ThemeData themeData = Theme.of(this.context); |
|
|
|
TextTheme textTheme = themeData.textTheme; |
|
|
|
Color themeColor; |
|
|
|
switch (themeData.brightness) { |
|
|
|
case Brightness.light: |
|
|
|
themeColor = themeData.primaryColor; |
|
|
|
break; |
|
|
|
case Brightness.dark: |
|
|
|
themeColor = themeData.accentColor; |
|
|
|
break; |
|
|
|
default: |
|
|
|
throw new Exception("Unknown brightness: " + themeData.brightness); |
|
|
|
} |
|
|
|
Widget _createContainer(List<Widget> tiles) { |
|
|
|
return DefaultTextStyle.merge( |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
child: new Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
children: tiles |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
ColorTween colorTween = new ColorTween( |
|
|
|
begin: textTheme.caption.color, |
|
|
|
end: this.widget.fixedColor ?? themeColor |
|
|
|
); |
|
|
|
for (int i = 0; i < this.widget.items.Count; i += 1) { |
|
|
|
int index = i; |
|
|
|
children.Add( |
|
|
|
new _BottomNavigationTile(this.widget.type, this.widget.items[i], this._animations[i], |
|
|
|
this.widget.iconSize, |
|
|
|
onTap: () => { |
|
|
|
if (this.widget.onTap != null) { |
|
|
|
this.widget.onTap(index); |
|
|
|
} |
|
|
|
}, |
|
|
|
colorTween: colorTween, |
|
|
|
selected: i == this.widget.currentIndex, |
|
|
|
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: this.widget.items.Count) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
public override Widget build(BuildContext context) { |
|
|
|
D.assert(debugCheckHasDirectionality(context)); |
|
|
|
D.assert(debugCheckHasMaterialLocalizations(context)); |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
for (int i = 0; i < this.widget.items.Count; i += 1) { |
|
|
|
int index = i; |
|
|
|
children.Add( |
|
|
|
new _BottomNavigationTile(this.widget.type, this.widget.items[i], this._animations[i], |
|
|
|
this.widget.iconSize, |
|
|
|
onTap: () => { |
|
|
|
if (this.widget.onTap != null) { |
|
|
|
this.widget.onTap(index); |
|
|
|
} |
|
|
|
}, |
|
|
|
flex: |
|
|
|
this._evaluateFlex(this._animations[i]), |
|
|
|
selected: i == this.widget.currentIndex, |
|
|
|
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: this.widget.items.Count) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
float additionalBottomPadding = Mathf.Max(MediaQuery.of(context).padding.bottom - BottomNavigationBarUtils._kBottomMargin, 0.0f); |
|
|
|
Color backgroundColor; |
|
|
|
switch (this.widget.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
backgroundColor = this._backgroundColor; |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
return children; |
|
|
|
return new Semantics( |
|
|
|
container: true, |
|
|
|
explicitChildNodes: true, |
|
|
|
child: new Stack( |
|
|
|
children: new List<Widget>[ |
|
|
|
Positioned.fill( |
|
|
|
child: new Material( // Casts shadow.
|
|
|
|
elevation: 8.0f, |
|
|
|
color: backgroundColor |
|
|
|
|
|
|
|
Widget _createContainer(List<Widget> tiles) { |
|
|
|
return DefaultTextStyle.merge( |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
child: new Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
children: tiles |
|
|
|
), |
|
|
|
ConstrainedBox( |
|
|
|
constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding), |
|
|
|
child: new Stack( |
|
|
|
children: new List<Widget>[ |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
public override Widget build(BuildContext context) { |
|
|
|
D.assert(WidgetsD.debugCheckHasDirectionality(context)); |
|
|
|
D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); |
|
|
|
|
|
|
|
float additionalBottomPadding = |
|
|
|
Mathf.Max(MediaQuery.of(context).padding.bottom - BottomNavigationBarUtils._kBottomMargin, 0.0f); |
|
|
|
Color backgroundColor = null; |
|
|
|
switch (this.widget.type) { |
|
|
|
case BottomNavigationBarType.fix: |
|
|
|
break; |
|
|
|
case BottomNavigationBarType.shifting: |
|
|
|
backgroundColor = this._backgroundColor; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
return new Stack( |
|
|
|
children: new List<Widget> { |
|
|
|
child: new CustomPaint( |
|
|
|
painter: _RadialPainter( |
|
|
|
circles: this._circles.ToList(), |
|
|
|
textDirection: Directionality.of(context) |
|
|
|
child: new Material( // Casts shadow.
|
|
|
|
elevation: 8.0f, |
|
|
|
color: backgroundColor |
|
|
|
) |
|
|
|
Material( // Splashes.
|
|
|
|
type: MaterialType.transparency, |
|
|
|
child: new Padding( |
|
|
|
padding: EdgeInsets.only(bottom: additionalBottomPadding), |
|
|
|
child: MediaQuery.removePadding( |
|
|
|
context: context, |
|
|
|
removeBottom: true, |
|
|
|
child: new _createContainer(this._createTiles()) |
|
|
|
new ConstrainedBox( |
|
|
|
constraints: new BoxConstraints( |
|
|
|
minHeight: Constants.kBottomNavigationBarHeight + additionalBottomPadding), |
|
|
|
child: new Stack( |
|
|
|
children: new List<Widget> { |
|
|
|
Positioned.fill( |
|
|
|
child: new CustomPaint( |
|
|
|
painter: new _RadialPainter( |
|
|
|
circles: this._circles.ToList() |
|
|
|
) |
|
|
|
) |
|
|
|
), |
|
|
|
new Material( // Splashes.
|
|
|
|
type: MaterialType.transparency, |
|
|
|
child: new Padding( |
|
|
|
padding: EdgeInsets.only(bottom: additionalBottomPadding), |
|
|
|
child: MediaQuery.removePadding( |
|
|
|
context: context, |
|
|
|
removeBottom: true, |
|
|
|
child: this._createContainer(this._createTiles()) |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
) |
|
|
|
] |
|
|
|
) |
|
|
|
) |
|
|
|
] |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
_Circle({ |
|
|
|
@required this.state, |
|
|
|
@required this.index, |
|
|
|
@required this.color, |
|
|
|
@required TickerProvider vsync |
|
|
|
) : D.assert(this.state != null), |
|
|
|
D.assert(this.index != null), |
|
|
|
D.assert(this.color != null) { |
|
|
|
this.controller = AnimationController( |
|
|
|
duration: kThemeAnimationDuration, |
|
|
|
vsync: vsync |
|
|
|
); |
|
|
|
this.animation = CurvedAnimation( |
|
|
|
parent: this.controller, |
|
|
|
curve: Curves.fastOutSlowIn |
|
|
|
); |
|
|
|
this.controller.forward(); |
|
|
|
} |
|
|
|
public _Circle( |
|
|
|
_BottomNavigationBarState state = null, |
|
|
|
int? index = null, |
|
|
|
Color color = null, |
|
|
|
TickerProvider vsync = null |
|
|
|
) { |
|
|
|
D.assert(state != null); |
|
|
|
D.assert(index != null); |
|
|
|
D.assert(color != null); |
|
|
|
this.state = state; |
|
|
|
this.index = index; |
|
|
|
this.color = color; |
|
|
|
this.controller = new AnimationController( |
|
|
|
duration: ThemeUtils.kThemeAnimationDuration, |
|
|
|
vsync: vsync |
|
|
|
); |
|
|
|
this.animation = new CurvedAnimation( |
|
|
|
parent: this.controller, |
|
|
|
curve: Curves.fastOutSlowIn |
|
|
|
); |
|
|
|
this.controller.forward(); |
|
|
|
} |
|
|
|
public readonly _BottomNavigationBarState state; |
|
|
|
public readonly int index; |
|
|
|
public readonly Color color; |
|
|
|
AnimationController controller; |
|
|
|
CurvedAnimation animation; |
|
|
|
public readonly _BottomNavigationBarState state; |
|
|
|
public readonly int? index; |
|
|
|
public readonly Color color; |
|
|
|
public readonly AnimationController controller; |
|
|
|
public readonly CurvedAnimation animation; |
|
|
|
float get horizontalLeadingOffset { |
|
|
|
float weightSum(Iterable<Animation<float>> animations) { |
|
|
|
return animations.map<float>(this.state._evaluateFlex).fold<float>(0.0f, (float sum, float value) => sum + value); |
|
|
|
} |
|
|
|
public float horizontalLeadingOffset { |
|
|
|
get { |
|
|
|
float weightSum(IEnumerable<Animation<float>> animations) { |
|
|
|
return animations.Select(this.state._evaluateFlex).Sum(); |
|
|
|
} |
|
|
|
float allWeights = weightSum(this.state._animations); |
|
|
|
float leadingWeights = weightSum(this.state._animations.sublist(0, this.index)); |
|
|
|
float allWeights = weightSum(this.state._animations); |
|
|
|
float leadingWeights = weightSum(this.state._animations.GetRange(0, this.index ?? 0)); |
|
|
|
return (leadingWeights + this.state._evaluateFlex(this.state._animations[this.index]) / 2.0f) / allWeights; |
|
|
|
} |
|
|
|
return (leadingWeights + this.state._evaluateFlex(this.state._animations[this.index ?? 0]) / 2.0f) / |
|
|
|
allWeights; |
|
|
|
} |
|
|
|
} |
|
|
|
public void dispose() { |
|
|
|
this.controller.dispose(); |
|
|
|
} |
|
|
|
public void dispose() { |
|
|
|
this.controller.dispose(); |
|
|
|
} |
|
|
|
class _RadialPainter : CustomPainter { |
|
|
|
_RadialPainter({ |
|
|
|
@required this.circles, |
|
|
|
@required this.textDirection |
|
|
|
) : D.assert(this.circles != null), |
|
|
|
D.assert(this.textDirection != null); |
|
|
|
class _RadialPainter : AbstractCustomPainter { |
|
|
|
public _RadialPainter( |
|
|
|
List<_Circle> circles |
|
|
|
) { |
|
|
|
D.assert(circles != null); |
|
|
|
this.circles = circles; |
|
|
|
} |
|
|
|
public readonly List<_Circle> circles; |
|
|
|
public readonly TextDirection textDirection; |
|
|
|
public readonly List<_Circle> circles; |
|
|
|
static float _maxRadius(Offset center, Size size) { |
|
|
|
float maxX = Mathf.Max(center.dx, size.width - center.dx); |
|
|
|
float maxY = Mathf.Max(center.dy, size.height - center.dy); |
|
|
|
return Mathf.Sqrt(maxX * maxX + maxY * maxY); |
|
|
|
} |
|
|
|
static float _maxRadius(Offset center, Size size) { |
|
|
|
float maxX = Mathf.Max(center.dx, size.width - center.dx); |
|
|
|
float maxY = Mathf.Max(center.dy, size.height - center.dy); |
|
|
|
return Mathf.Sqrt(maxX * maxX + maxY * maxY); |
|
|
|
} |
|
|
|
public override bool shouldRepaint(_RadialPainter oldPainter) { |
|
|
|
if (this.textDirection != oldPainter.textDirection) |
|
|
|
return true; |
|
|
|
if (this.circles == oldPainter.circles) |
|
|
|
return false; |
|
|
|
if (this.circles.length != oldPainter.circles.length) |
|
|
|
return true; |
|
|
|
for (int i = 0; i < this.circles.length; i += 1) |
|
|
|
if (this.circles[i] != oldPainter.circles[i]) |
|
|
|
return true; |
|
|
|
return false; |
|
|
|
} |
|
|
|
public override bool shouldRepaint(CustomPainter _oldPainter) { |
|
|
|
_RadialPainter oldPainter = _oldPainter as _RadialPainter; |
|
|
|
if (this.circles == oldPainter.circles) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
public override void paint(Canvas canvas, Size size) { |
|
|
|
for (_Circle circle in |
|
|
|
this.circles) { |
|
|
|
Paint paint = Paint()..color = circle.color; |
|
|
|
Rect rect = Rect.fromLTWH(0.0f, 0.0f, size.width, size.height); |
|
|
|
canvas.clipRect(rect); |
|
|
|
float leftFraction; |
|
|
|
switch (this.textDirection) { |
|
|
|
case TextDirection.rtl: |
|
|
|
leftFraction = 1.0f - circle.horizontalLeadingOffset; |
|
|
|
break; |
|
|
|
case TextDirection.ltr: |
|
|
|
leftFraction = circle.horizontalLeadingOffset; |
|
|
|
break; |
|
|
|
} |
|
|
|
Offset center = Offset(leftFraction * size.width, size.height / 2.0f); |
|
|
|
FloatTween radiusTween = FloatTween( |
|
|
|
begin: 0.0f, |
|
|
|
end: _maxRadius(center, size) |
|
|
|
); |
|
|
|
canvas.drawCircle( |
|
|
|
center, |
|
|
|
radiusTween.transform(circle.animation.value), |
|
|
|
paint |
|
|
|
); |
|
|
|
if (this.circles.Count != oldPainter.circles.Count) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
for (int i = 0; i < this.circles.Count; i += 1) { |
|
|
|
if (this.circles[i] != oldPainter.circles[i]) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
public override void paint(Canvas canvas, Size size) { |
|
|
|
foreach (_Circle circle in this.circles) { |
|
|
|
Paint paint = new Paint(); |
|
|
|
paint.color = circle.color; |
|
|
|
Rect rect = Rect.fromLTWH(0.0f, 0.0f, size.width, size.height); |
|
|
|
canvas.clipRect(rect); |
|
|
|
float leftFraction = circle.horizontalLeadingOffset; |
|
|
|
Offset center = new Offset(leftFraction * size.width, size.height / 2.0f); |
|
|
|
FloatTween radiusTween = new FloatTween( |
|
|
|
begin: 0.0f, |
|
|
|
end: _maxRadius(center, size) |
|
|
|
); |
|
|
|
canvas.drawCircle( |
|
|
|
center, |
|
|
|
radiusTween.evaluate(circle.animation), |
|
|
|
paint |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |