Skip to content

Commit 76e1d77

Browse files
committed
Improve suggestions design, fix bugs
Design changes: - App bar: - Adjust floating app bar down by 8dp - App bar now overlayed in Stack, as opposed to sliver app bar - Home page: - Change list overflow refresh to refresh button on suggestions card - Tracked buses: - Stop tracking all button on bus card, with undo snackbar - Nearby stops: - Nearby stops card now expandable even if location not loaded Bug fixes: - Pressing routes then back causes main home page to be grey - Change live notification priority to High - StreamBuilder widgets not showing data initially (Stream did not broadcast anything when first listened to) - Pinning a stop doesn't cause it to show immediately. Other changes: - Bus timing row listens to change in follow status and updates bell icon accordingly - Don't setState SearchPage to set MapVisible false when the search bar is pressed, if map is already hidden
1 parent 25d1ecc commit 76e1d77

File tree

7 files changed

+303
-149
lines changed

7 files changed

+303
-149
lines changed

lib/routes/home_page.dart

Lines changed: 188 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import '../routes/settings_page.dart';
1212
import '../utils/bus.dart';
1313
import '../utils/bus_api.dart';
1414
import '../utils/bus_stop.dart';
15-
import '../utils/bus_utils.dart';
1615
import '../utils/database_utils.dart';
1716
import '../utils/location_utils.dart';
1817
import '../utils/reorder_status_notification.dart';
@@ -40,6 +39,7 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
4039
Widget _busStopOverviewList;
4140
int _bottomNavIndex;
4241
Map<String, dynamic> _nearestBusStops;
42+
List<Bus> _followedBuses;
4343
ScrollController _scrollController;
4444
bool canScroll;
4545
AnimationController _fabScaleAnimationController;
@@ -188,96 +188,146 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
188188
}
189189

190190
Widget _buildBody() {
191-
return RefreshIndicator(
192-
onRefresh: refreshLocation,
193-
child: Stack(
194-
children: <Widget>[
195-
CustomScrollView(
196-
controller: _scrollController,
197-
scrollDirection: Axis.vertical,
198-
physics: canScroll ? const AlwaysScrollableScrollPhysics() : const NeverScrollableScrollPhysics(),
199-
slivers: <Widget>[
200-
SliverToBoxAdapter(
201-
child: Container(
202-
alignment: Alignment.topCenter,
203-
height: 64.0 + MediaQuery.of(context).padding.top,
204-
),
205-
),
206-
SliverToBoxAdapter(
207-
child: HomePageContentSwitcher(
208-
scrollController: _scrollController,
209-
child: _buildContent(),
210-
),
191+
return Stack(
192+
children: <Widget>[
193+
CustomScrollView(
194+
controller: _scrollController,
195+
scrollDirection: Axis.vertical,
196+
physics: canScroll ? const AlwaysScrollableScrollPhysics() : const NeverScrollableScrollPhysics(),
197+
slivers: <Widget>[
198+
SliverToBoxAdapter(
199+
child: Container(
200+
alignment: Alignment.topCenter,
201+
height: 64.0 + MediaQuery.of(context).padding.top,
211202
),
212-
],
213-
),
214-
// Hide the overscroll contents from the status bar
215-
Container(
216-
height: kToolbarHeight / 2 + MediaQuery.of(context).padding.top,
217-
color: Theme.of(context).scaffoldBackgroundColor,
218-
),
219-
Positioned(
220-
top: 0,
221-
left: 0,
222-
right: 0,
223-
child: AppBar(
224-
brightness: Theme.of(context).brightness,
225-
backgroundColor: Colors.transparent,
226-
leading: null,
227-
automaticallyImplyLeading: false,
228-
titleSpacing: 8.0,
229-
elevation: 0.0,
230-
title: Container(
231-
child: _buildSearchField(),
203+
),
204+
SliverToBoxAdapter(
205+
child: HomePageContentSwitcher(
206+
scrollController: _scrollController,
207+
child: _buildContent(),
232208
),
233209
),
210+
],
211+
),
212+
// Hide the overscroll contents from the status bar
213+
Container(
214+
height: kToolbarHeight / 2 + MediaQuery.of(context).padding.top,
215+
color: Theme.of(context).scaffoldBackgroundColor,
216+
),
217+
Positioned(
218+
top: 8,
219+
left: 0,
220+
right: 0,
221+
child: AppBar(
222+
brightness: Theme.of(context).brightness,
223+
backgroundColor: Colors.transparent,
224+
leading: null,
225+
automaticallyImplyLeading: false,
226+
titleSpacing: 8.0,
227+
elevation: 0.0,
228+
title: Container(
229+
child: _buildSearchField(),
230+
),
234231
),
235-
],
236-
),
232+
),
233+
],
237234
);
238235
}
239236

240237
Widget _buildTrackedBuses() {
241-
return FutureBuilder<List<Bus>>(
242-
future: getFollowedBuses(),
243-
builder: (BuildContext context, AsyncSnapshot<List<Bus>> snapshot) {
244-
return Card(
245-
margin: const EdgeInsets.all(8.0),
246-
child: Padding(
247-
padding: const EdgeInsets.all(16.0),
248-
child: Column(
249-
crossAxisAlignment: CrossAxisAlignment.start,
250-
children: <Widget>[
251-
Text('Tracked buses', style: Theme.of(context).textTheme.headline4),
252-
ListView.builder(
253-
shrinkWrap: true,
254-
physics: const NeverScrollableScrollPhysics(),
255-
itemBuilder: (BuildContext context, int position) {
256-
final Bus bus = snapshot.data[position];
257-
258-
return ListTile(
259-
leading: Text(
260-
bus.busService.number.padAsServiceNumber(),
261-
style: Theme.of(context).textTheme.headline6.copyWith(fontFamily: 'B612 Mono')
262-
),
263-
title: FutureBuilder<DateTime>(
264-
future: BusAPI().getArrivalTime(bus.busStop, bus.busService.number),
265-
builder: (BuildContext context, AsyncSnapshot<DateTime> snapshot) {
266-
return Text(snapshot.hasData ? '${snapshot.data.getMinutesFromNow()} min' : '',
267-
style: Theme.of(context).textTheme.subtitle1,
238+
return AnimatedSize(
239+
alignment: Alignment.topCenter,
240+
vsync: this,
241+
duration: const Duration(milliseconds: 400),
242+
curve: Curves.easeInOutCubic,
243+
child: StreamBuilder<List<Bus>>(
244+
initialData: _followedBuses,
245+
stream: followedBusesStream(),
246+
builder: (BuildContext context, AsyncSnapshot<List<Bus>> snapshot) {
247+
final bool isLoaded = snapshot.hasData && snapshot.data.isNotEmpty;
248+
if (isLoaded)
249+
_followedBuses = snapshot.data;
250+
251+
return AnimatedOpacity(
252+
opacity: isLoaded ? 1 : 0,
253+
duration: isLoaded ? const Duration(milliseconds: 650) : Duration.zero,
254+
curve: const Interval(0.66, 1),
255+
child: isLoaded ? Card(
256+
margin: const EdgeInsets.all(8.0),
257+
child: Padding(
258+
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
259+
child: Column(
260+
crossAxisAlignment: CrossAxisAlignment.start,
261+
children: <Widget>[
262+
Padding(
263+
padding: const EdgeInsets.symmetric(horizontal: 16.0),
264+
child: Text('Tracked buses', style: Theme.of(context).textTheme.headline4),
265+
),
266+
AnimatedSize(
267+
alignment: Alignment.topCenter,
268+
vsync: this,
269+
duration: const Duration(milliseconds: 400),
270+
curve: Curves.easeInOutCubic,
271+
child: ListView.builder(
272+
shrinkWrap: true,
273+
physics: const NeverScrollableScrollPhysics(),
274+
itemBuilder: (BuildContext context, int position) {
275+
final Bus bus = snapshot.data[position];
276+
277+
return ListTile(
278+
onTap: () {
279+
showBusDetailSheet(bus.busStop, UserRoute.home);
280+
},
281+
title: FutureBuilder<DateTime>(
282+
future: BusAPI().getArrivalTime(bus.busStop, bus.busService.number),
283+
builder: (BuildContext context, AsyncSnapshot<DateTime> snapshot) {
284+
return Text(snapshot.hasData ? '${bus.busService.number} - ${snapshot.data.getMinutesFromNow()} min' : '',
285+
style: Theme.of(context).textTheme.headline6,
286+
);
287+
},
288+
),
289+
subtitle: Text(bus.busStop.displayName),
268290
);
269291
},
292+
itemCount: snapshot.data.length,
270293
),
271-
subtitle: Text(bus.busStop.displayName),
272-
);
273-
},
274-
itemCount: snapshot?.data?.length ?? 0,
294+
),
295+
Row(
296+
children: <Widget>[
297+
FlatButton.icon(
298+
icon: const Icon(Icons.notifications_off),
299+
label: const Text(
300+
'STOP TRACKING ALL BUSES',
301+
style: TextStyle(fontWeight: FontWeight.bold),
302+
),
303+
textColor: Theme.of(context).accentColor,
304+
onPressed: () async {
305+
final List<Map<String, dynamic>> trackedBuses = await unfollowAllBuses();
306+
Scaffold.of(context).showSnackBar(SnackBar(
307+
content: const Text('Stopped tracking all buses'),
308+
action: SnackBarAction(
309+
label: 'Undo',
310+
onPressed: () async {
311+
for (Map<String, dynamic> trackedBus in trackedBuses) {
312+
await followBus(stop: trackedBus['stop'], bus: trackedBus['bus'], arrivalTime: trackedBus['arrivalTime']);
313+
}
314+
315+
// Update the bus stop detail sheet to reflect change in bus stop follow status
316+
widget.bottomSheetKey.currentState.setState(() {});
317+
},
318+
),
319+
));
320+
},
321+
),
322+
],
323+
),
324+
],
275325
),
276-
],
277-
),
278-
),
279-
);
280-
},
326+
),
327+
) : Container(),
328+
);
329+
},
330+
),
281331
);
282332
}
283333

@@ -286,12 +336,27 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
286336
future: _getNearestBusStops(),
287337
initialData: _nearestBusStops,
288338
builder: (BuildContext context, AsyncSnapshot<Map<String, dynamic>> snapshot) {
289-
if (snapshot.hasData)
339+
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done)
290340
_nearestBusStops = snapshot.data;
291341
else if (snapshot.connectionState == ConnectionState.done || !LocationUtils.isLocationAllowed()) {
292342
return Container();
293343
}
294-
final bool isLoaded = snapshot.hasData && snapshot.data['busStops'].length == 5;
344+
final bool isLoaded = _nearestBusStops != null && _nearestBusStops['busStops'].length == 5;
345+
346+
final Widget refreshButton = Row(
347+
children: <Widget>[
348+
FlatButton.icon(
349+
icon: const Icon(Icons.refresh),
350+
label: const Text(
351+
'REFRESH LOCATION',
352+
style: TextStyle(fontWeight: FontWeight.bold),
353+
),
354+
textColor: Theme.of(context).accentColor,
355+
onPressed: refreshLocation,
356+
),
357+
],
358+
);
359+
295360
return Card(
296361
elevation: 0.0,
297362
shape: RoundedRectangleBorder(
@@ -304,36 +369,42 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
304369
margin: const EdgeInsets.all(8.0),
305370
child: Padding(
306371
padding: const EdgeInsets.only(bottom: 8.0),
307-
child: ExpandablePanel(
308-
tapHeaderToExpand: isLoaded,
309-
hasIcon: isLoaded,
310-
headerAlignment: ExpandablePanelHeaderAlignment.center,
311-
header: Container(
312-
alignment: Alignment.centerLeft,
313-
padding: const EdgeInsets.all(16.0),
314-
child: Text('Nearby stops', style: Theme.of(context).textTheme.headline4),
315-
),
316-
collapsed: Column(
317-
crossAxisAlignment: CrossAxisAlignment.stretch,
318-
children: <Widget>[
319-
if (isLoaded)
320-
_buildSuggestionItem(snapshot.data['busStops'][0], snapshot.data['distances'][0]),
321-
if (!isLoaded)
322-
_buildSuggestionItem(null, null),
323-
],
324-
),
325-
expanded: isLoaded ? ListView.separated(
326-
physics: const NeverScrollableScrollPhysics(),
327-
scrollDirection: Axis.vertical,
328-
shrinkWrap: true,
329-
itemCount: 5,
330-
separatorBuilder: (BuildContext context, int position) => const Divider(),
331-
itemBuilder: (BuildContext context, int position) {
332-
final BusStop busStop = snapshot.data['busStops'][position ];
333-
final double distanceInMeters = snapshot.data['distances'][position];
334-
return _buildSuggestionItem(busStop, distanceInMeters);
335-
},
336-
) : Container(),
372+
child: Column(
373+
children: [
374+
ExpandablePanel(
375+
tapHeaderToExpand: true,
376+
hasIcon: true,
377+
headerAlignment: ExpandablePanelHeaderAlignment.center,
378+
header: Container(
379+
alignment: Alignment.centerLeft,
380+
padding: const EdgeInsets.all(16.0),
381+
child: Text('Nearby stops', style: Theme.of(context).textTheme.headline4),
382+
),
383+
collapsed: Column(
384+
mainAxisAlignment: MainAxisAlignment.start,
385+
crossAxisAlignment: CrossAxisAlignment.stretch,
386+
children: <Widget>[
387+
if (isLoaded)
388+
_buildSuggestionItem(_nearestBusStops['busStops'][0], _nearestBusStops['distances'][0]),
389+
if (!isLoaded)
390+
_buildSuggestionItem(null, null),
391+
],
392+
),
393+
expanded: ListView.separated(
394+
physics: const NeverScrollableScrollPhysics(),
395+
scrollDirection: Axis.vertical,
396+
shrinkWrap: true,
397+
itemCount: 5,
398+
separatorBuilder: (BuildContext context, int position) => const Divider(),
399+
itemBuilder: (BuildContext context, int position) {
400+
final BusStop busStop = isLoaded ? _nearestBusStops['busStops'][position] : null;
401+
final double distanceInMeters = isLoaded ? _nearestBusStops['distances'][position] : null;
402+
return _buildSuggestionItem(busStop, distanceInMeters);
403+
},
404+
),
405+
),
406+
refreshButton,
407+
],
337408
),
338409
),
339410
);
@@ -449,6 +520,12 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
449520
}
450521

451522
Future<void> refreshLocation() async {
523+
setState((){
524+
_nearestBusStops = null;
525+
});
526+
}
527+
528+
Future<void> refresh() async {
452529
setState(() {});
453530
}
454531

lib/routes/search_page.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ class _SearchPageState extends BottomSheetPageState<SearchPage> {
235235

236236
Widget _buildSearchCard() {
237237
final TextField searchField = TextField(
238-
autofocus: false,
238+
autofocus: !_isMapVisible,
239239
controller: _textController,
240240
onChanged: (String newText) {
241241
setState(() {
@@ -245,9 +245,10 @@ class _SearchPageState extends BottomSheetPageState<SearchPage> {
245245
},
246246
onTap: () {
247247
hideBusDetailSheet();
248-
setState(() {
249-
_isMapVisible = false;
250-
});
248+
if (_isMapVisible)
249+
setState(() {
250+
_isMapVisible = false;
251+
});
251252
},
252253
decoration: InputDecoration(
253254
contentPadding: const EdgeInsets.only(left: 16.0, top: 16.0, right: 16.0, bottom: 16.0),
@@ -382,7 +383,7 @@ class _SearchPageState extends BottomSheetPageState<SearchPage> {
382383
],
383384
),
384385
Positioned(
385-
top: 0,
386+
top: 8,
386387
left: 0,
387388
right: 0,
388389
child: AppBar(

0 commit comments

Comments
 (0)