feat: enhance UI, branding assets, onboarding flow, and screen layouts
This commit is contained in:
@@ -43,6 +43,9 @@ class KrowNavBar extends StatelessWidget {
|
||||
/// of pixels apart.
|
||||
static const _topPadding = 12.0;
|
||||
|
||||
static const _labelSize = 11.0;
|
||||
static const _dotSlot = 6.0;
|
||||
|
||||
/// Lets tests find the travelling indicator unambiguously.
|
||||
static const indicatorKey = ValueKey('krow-nav-indicator');
|
||||
|
||||
@@ -55,74 +58,75 @@ class KrowNavBar extends StatelessWidget {
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: SizedBox(
|
||||
height: 76,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final slot = constraints.maxWidth / items.length;
|
||||
// No fixed height: the Stack below takes the height of the item row,
|
||||
// and the pill is positioned within it. A hard number here has to be
|
||||
// retuned every time the label grows — a bigger text scale, a longer
|
||||
// word, a different font — and overflows the moment it is not.
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final slot = constraints.maxWidth / items.length;
|
||||
|
||||
// The selected index animated as a double, so the pill's
|
||||
// position and every icon's colour come from one value.
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(end: currentIndex.toDouble()),
|
||||
duration: const Duration(milliseconds: 360),
|
||||
curve: Curves.easeOutQuint,
|
||||
builder: (context, position, _) {
|
||||
final travel = (position - position.roundToDouble()).abs();
|
||||
final stretch = 20 * (travel * 2);
|
||||
// The selected index animated as a double, so the pill's
|
||||
// position and every icon's colour come from one value.
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(end: currentIndex.toDouble()),
|
||||
duration: const Duration(milliseconds: 360),
|
||||
curve: Curves.easeOutQuint,
|
||||
builder: (context, position, _) {
|
||||
final travel = (position - position.roundToDouble()).abs();
|
||||
final stretch = 20 * (travel * 2);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left:
|
||||
position * slot +
|
||||
(slot - _pillWidth) / 2 -
|
||||
stretch / 2,
|
||||
top: _topPadding,
|
||||
width: _pillWidth + stretch,
|
||||
height: _pillHeight,
|
||||
child: DecoratedBox(
|
||||
key: indicatorKey,
|
||||
decoration: BoxDecoration(
|
||||
color: KrowPalette.blue,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: KrowPalette.blue.withValues(alpha: 0.28),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left:
|
||||
position * slot +
|
||||
(slot - _pillWidth) / 2 -
|
||||
stretch / 2,
|
||||
top: _topPadding,
|
||||
width: _pillWidth + stretch,
|
||||
height: _pillHeight,
|
||||
child: DecoratedBox(
|
||||
key: indicatorKey,
|
||||
decoration: BoxDecoration(
|
||||
color: KrowPalette.blue,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: KrowPalette.blue.withValues(alpha: 0.28),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
for (var i = 0; i < items.length; i++)
|
||||
Expanded(
|
||||
child: _NavItem(
|
||||
item: items[i],
|
||||
// 1 when the pill is centred on this tab.
|
||||
proximity: (1 - (position - i).abs()).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
),
|
||||
onTap: () {
|
||||
if (i != currentIndex) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
onSelect(i);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
for (var i = 0; i < items.length; i++)
|
||||
Expanded(
|
||||
child: _NavItem(
|
||||
item: items[i],
|
||||
// 1 when the pill is centred on this tab.
|
||||
proximity: (1 - (position - i).abs()).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
),
|
||||
onTap: () {
|
||||
if (i != currentIndex) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
onSelect(i);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -171,6 +175,7 @@ class _NavItem extends StatelessWidget {
|
||||
// Started from the top rather than centred, so the icon sits exactly
|
||||
// where the pill is drawn.
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: KrowNavBar._topPadding),
|
||||
SizedBox(
|
||||
@@ -181,7 +186,7 @@ class _NavItem extends StatelessWidget {
|
||||
// The dot's slot is always reserved, so tabs with and without one
|
||||
// keep their labels on the same line.
|
||||
SizedBox(
|
||||
height: 6,
|
||||
height: KrowNavBar._dotSlot,
|
||||
child: Center(
|
||||
child: AnimatedScale(
|
||||
scale: item.showDot ? 1 : 0,
|
||||
@@ -202,11 +207,12 @@ class _NavItem extends StatelessWidget {
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: KrowNavBar._labelSize,
|
||||
fontWeight: proximity > 0.5 ? FontWeight.w600 : FontWeight.w500,
|
||||
color: labelColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user