fix: resolve route planner keyboard overflow
This commit is contained in:
parent
ae13dd81e1
commit
c380f10f21
|
|
@ -573,6 +573,33 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
|||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isNarrow = constraints.maxWidth < 360;
|
||||
final isCompactHeight = constraints.maxHeight < 500;
|
||||
final isVeryCompactHeight = constraints.maxHeight < 360;
|
||||
|
||||
// Dynamically compute map preview height based on available vertical space:
|
||||
// Generous space (>= 620dp): 220dp
|
||||
// Moderate space (500–619dp): 160dp
|
||||
// Constrained space (380–499dp, e.g. IME keyboard open): 110dp
|
||||
// Severely constrained space (280–379dp, e.g. landscape): 70dp
|
||||
// Extreme constraint (< 280dp): 0dp (collapses so inputs stay visible and editable)
|
||||
final double mapHeight;
|
||||
final bool showDisclaimer;
|
||||
if (constraints.maxHeight >= 620) {
|
||||
mapHeight = 220.0;
|
||||
showDisclaimer = true;
|
||||
} else if (constraints.maxHeight >= 500) {
|
||||
mapHeight = 160.0;
|
||||
showDisclaimer = true;
|
||||
} else if (constraints.maxHeight >= 380) {
|
||||
mapHeight = 110.0;
|
||||
showDisclaimer = false;
|
||||
} else if (constraints.maxHeight >= 280) {
|
||||
mapHeight = 70.0;
|
||||
showDisclaimer = false;
|
||||
} else {
|
||||
mapHeight = 0.0;
|
||||
showDisclaimer = false;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
|
@ -616,104 +643,112 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
|||
),
|
||||
),
|
||||
|
||||
// Map Preview Section (220dp high)
|
||||
SizedBox(
|
||||
height: 220,
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
children: [
|
||||
UnifiedMapView(
|
||||
center: mapCenter,
|
||||
zoom: 12.0,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
),
|
||||
// Distance & Stops HUD overlay
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF13151A).withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF2A2E3D)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.stops.length >= 2
|
||||
? 'Straight-line: ${state.formattedDistanceMiles} (${state.formattedDistanceKm})'
|
||||
: 'Add 2+ stops for distance',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF00E5FF),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
// Map Preview Section
|
||||
if (mapHeight > 0)
|
||||
SizedBox(
|
||||
height: mapHeight,
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
children: [
|
||||
UnifiedMapView(
|
||||
center: mapCenter,
|
||||
zoom: 12.0,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
),
|
||||
// Distance & Stops HUD overlay
|
||||
Positioned(
|
||||
top: isCompactHeight ? 4 : 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: isCompactHeight ? 4 : 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
const Color(0xFF13151A).withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF2A2E3D)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.stops.length >= 2
|
||||
? 'Straight-line: ${state.formattedDistanceMiles} (${state.formattedDistanceKm})'
|
||||
: 'Add 2+ stops for distance',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF00E5FF),
|
||||
fontSize: isCompactHeight ? 11 : 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isAtLimit
|
||||
? const Color(
|
||||
0xFFFFB300,
|
||||
).withValues(alpha: 0.2)
|
||||
: const Color(
|
||||
0xFF00E5FF,
|
||||
).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'${state.stops.length}/$maxAllowed STOPS',
|
||||
style: TextStyle(
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isAtLimit
|
||||
? const Color(0xFFFFB300)
|
||||
: const Color(0xFF00E5FF),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w900,
|
||||
? const Color(
|
||||
0xFFFFB300,
|
||||
).withValues(alpha: 0.2)
|
||||
: const Color(
|
||||
0xFF00E5FF,
|
||||
).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'${state.stops.length}/$maxAllowed STOPS',
|
||||
style: TextStyle(
|
||||
color: isAtLimit
|
||||
? const Color(0xFFFFB300)
|
||||
: const Color(0xFF00E5FF),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Straight-line disclaimer note
|
||||
if (showDisclaimer)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
left: 8,
|
||||
right: 8,
|
||||
child: Text(
|
||||
'Straight-line preview. Navigation handoff launches in Google Maps / Waze.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.65),
|
||||
fontSize: 9,
|
||||
shadows: const [
|
||||
Shadow(color: Colors.black, blurRadius: 4),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Straight-line disclaimer note
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
left: 8,
|
||||
right: 8,
|
||||
child: Text(
|
||||
'Straight-line preview. Navigation handoff launches in Google Maps / Waze.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.65),
|
||||
fontSize: 9,
|
||||
shadows: const [
|
||||
Shadow(color: Colors.black, blurRadius: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Route Info Inputs (collapsible / compact)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
isCompactHeight ? 4 : 12,
|
||||
16,
|
||||
isCompactHeight ? 2 : 4,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
|
|
@ -723,14 +758,16 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
|||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Route Title (e.g. Skyline Ridge Loop) *',
|
||||
hintStyle: TextStyle(
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.white30,
|
||||
fontSize: 14,
|
||||
),
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: isCompactHeight ? 4 : 8,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
|
|
@ -745,7 +782,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
|||
onSelected: (v) => setState(() => _visibility = v),
|
||||
color: const Color(0xFF1B1E26),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: isCompactHeight ? 2 : 4,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -795,9 +834,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
|||
|
||||
// Stop List Controls (Add Stop / Add GPS / Reverse)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
vertical: isCompactHeight ? 4 : 8,
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
|
|
@ -884,31 +923,39 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
|||
Expanded(
|
||||
child: state.stops.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.alt_route,
|
||||
size: 48,
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isVeryCompactHeight) ...[
|
||||
Icon(
|
||||
Icons.alt_route,
|
||||
size: isCompactHeight ? 32 : 48,
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
),
|
||||
SizedBox(height: isCompactHeight ? 6 : 12),
|
||||
],
|
||||
const Text(
|
||||
'No stops added yet.',
|
||||
style: TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Tap "Add Stop" or "Current GPS" to begin planning.',
|
||||
style: TextStyle(
|
||||
color: Colors.white30,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'No stops added yet.',
|
||||
style: TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'Tap "Add Stop" or "Current GPS" to begin planning.',
|
||||
style: TextStyle(
|
||||
color: Colors.white30,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ReorderableListView.builder(
|
||||
|
|
|
|||
|
|
@ -110,4 +110,119 @@ void main() {
|
|||
// No RenderFlex overflow
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('RoutePlannerScreen handles title editing under Pixel 8a IME keyboard inset without overflow', (tester) async {
|
||||
// Pixel 8a viewport with keyboard open (height reduced from 857 to 440)
|
||||
await tester.binding.setSurfaceSize(const Size(385, 440));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
currentUserProvider.overrideWith((ref) => const AuthUser(
|
||||
uid: 'alice',
|
||||
isAnonymous: false,
|
||||
)),
|
||||
isProSubscriberProvider.overrideWith((ref) => false),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: RoutePlannerScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find Route Title TextField
|
||||
final titleField = find.byType(TextField).first;
|
||||
expect(titleField, findsOneWidget);
|
||||
|
||||
// Enter title text while constrained
|
||||
await tester.enterText(titleField, 'Marionette Route Validation');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Marionette Route Validation'), findsOneWidget);
|
||||
|
||||
// Verify SAVE button in AppBar and stop controls remain visible and reachable
|
||||
expect(find.text('SAVE'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('route_planner_add_stop_button')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('route_planner_current_gps_button')), findsOneWidget);
|
||||
expect(find.text('No stops added yet.'), findsOneWidget);
|
||||
|
||||
// No RenderFlex overflow
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('RoutePlannerScreen handles constrained 360x360 and 320x320 dimensions without overflow', (tester) async {
|
||||
// Severely constrained 360x360 viewport
|
||||
await tester.binding.setSurfaceSize(const Size(360, 360));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
currentUserProvider.overrideWith((ref) => const AuthUser(
|
||||
uid: 'alice',
|
||||
isAnonymous: false,
|
||||
)),
|
||||
isProSubscriberProvider.overrideWith((ref) => false),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: RoutePlannerScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleField = find.byType(TextField).first;
|
||||
await tester.enterText(titleField, 'Constrained 360');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Constrained 360'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Extremely constrained 320x320 viewport
|
||||
await tester.binding.setSurfaceSize(const Size(320, 320));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(titleField, 'Constrained 320');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Constrained 320'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('RoutePlannerScreen landscape layout (800x360) renders without RenderFlex overflow', (tester) async {
|
||||
// Landscape orientation on wide screen
|
||||
await tester.binding.setSurfaceSize(const Size(800, 360));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
currentUserProvider.overrideWith((ref) => const AuthUser(
|
||||
uid: 'alice',
|
||||
isAnonymous: false,
|
||||
)),
|
||||
isProSubscriberProvider.overrideWith((ref) => false),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: RoutePlannerScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify title field and controls render properly
|
||||
final titleField = find.byType(TextField).first;
|
||||
await tester.enterText(titleField, 'Skyline Ridge Landscape');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Skyline Ridge Landscape'), findsOneWidget);
|
||||
expect(find.text('SAVE'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('route_planner_add_stop_button')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('route_planner_current_gps_button')), findsOneWidget);
|
||||
|
||||
// No RenderFlex overflow
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue