mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
add web_dashboard API (#333)
* add web_dashboard sample * add docs * address code review comments * restructure web_dashboard * set up provider and mock service * add copyright headers, use relative imports * add API class, add tests * fmt * rename services -> API, remove data library * use new API in app * add stream to items api * convert from StreamBuilder to StreamProvider * add subscription to Entry API * rename API classes * address comments * Update README.md * update README * remove routing_demo
This commit is contained in:
9
experimental/web_dashboard/lib/main.dart
Normal file
9
experimental/web_dashboard/lib/main.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'src/app.dart';
|
||||
|
||||
void main() => runApp(DashboardApp());
|
||||
45
experimental/web_dashboard/lib/src/api/api.dart
Normal file
45
experimental/web_dashboard/lib/src/api/api.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// Manipulates app data,
|
||||
abstract class DashboardApi {
|
||||
ItemApi get items;
|
||||
EntryApi get entries;
|
||||
}
|
||||
|
||||
/// Manipulates [Item] data.
|
||||
abstract class ItemApi {
|
||||
Future<Item> delete(String id);
|
||||
Future<Item> get(String id);
|
||||
Future<Item> insert(Item item);
|
||||
Future<List<Item>> list();
|
||||
Future<Item> update(Item item, String id);
|
||||
Stream<List<Item>> allItemsStream();
|
||||
}
|
||||
|
||||
/// Something being tracked.
|
||||
class Item {
|
||||
final String name;
|
||||
String id;
|
||||
|
||||
Item(this.name);
|
||||
}
|
||||
|
||||
/// Manipulates [Entry] data.
|
||||
abstract class EntryApi {
|
||||
Future<Entry> delete(String itemId, String id);
|
||||
Future<Entry> insert(String itemId, Entry entry);
|
||||
Future<List<Entry>> list(String itemId);
|
||||
Future<Entry> update(String itemId, String id, Entry entry);
|
||||
Stream<List<Entry>> allEntriesStream(String itemId);
|
||||
}
|
||||
|
||||
/// A number tracked at a point in time.
|
||||
class Entry {
|
||||
final int value;
|
||||
final DateTime time;
|
||||
String id;
|
||||
|
||||
Entry(this.value, this.time);
|
||||
}
|
||||
3
experimental/web_dashboard/lib/src/api/firebase.dart
Normal file
3
experimental/web_dashboard/lib/src/api/firebase.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
106
experimental/web_dashboard/lib/src/api/mock.dart
Normal file
106
experimental/web_dashboard/lib/src/api/mock.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:uuid/uuid.dart' as uuid;
|
||||
|
||||
import 'api.dart';
|
||||
|
||||
class MockDashboardApi implements DashboardApi {
|
||||
@override
|
||||
final EntryApi entries = MockEntryApi();
|
||||
|
||||
@override
|
||||
final ItemApi items = MockItemApi();
|
||||
}
|
||||
|
||||
class MockItemApi implements ItemApi {
|
||||
Map<String, Item> _storage = {};
|
||||
StreamController<List<Item>> _streamController =
|
||||
StreamController<List<Item>>.broadcast();
|
||||
|
||||
@override
|
||||
Future<Item> delete(String id) async {
|
||||
_emit();
|
||||
return _storage.remove(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Item> get(String id) async {
|
||||
return _storage[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Item> insert(Item item) async {
|
||||
var id = uuid.Uuid().v4();
|
||||
var newItem = Item(item.name)..id = id;
|
||||
_storage[id] = newItem;
|
||||
_emit();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Item>> list() async {
|
||||
return _storage.values.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Item> update(Item item, String id) async {
|
||||
_storage[id] = item;
|
||||
return item..id = id;
|
||||
}
|
||||
|
||||
Stream<List<Item>> allItemsStream() {
|
||||
return _streamController.stream;
|
||||
}
|
||||
|
||||
void _emit() {
|
||||
_streamController.add(_storage.values.toList());
|
||||
}
|
||||
}
|
||||
|
||||
class MockEntryApi implements EntryApi {
|
||||
Map<String, Entry> _storage = {};
|
||||
StreamController<List<Entry>> _streamController =
|
||||
StreamController<List<Entry>>.broadcast();
|
||||
|
||||
@override
|
||||
Future<Entry> delete(String itemId, String id) async {
|
||||
_emit();
|
||||
return _storage.remove('$itemId-$id');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Entry> insert(String itemId, Entry entry) async {
|
||||
var id = uuid.Uuid().v4();
|
||||
var newEntry = Entry(entry.value, entry.time)..id = id;
|
||||
_storage['$itemId-$id'] = newEntry;
|
||||
_emit();
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Entry>> list(String itemId) async {
|
||||
return _storage.keys
|
||||
.where((k) => k.startsWith(itemId))
|
||||
.map((k) => _storage[k])
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Entry> update(String itemId, String id, Entry entry) async {
|
||||
_storage['$itemId-$id'] = entry;
|
||||
return entry..id = id;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<Entry>> allEntriesStream(String itemId) {
|
||||
return _streamController.stream;
|
||||
}
|
||||
|
||||
void _emit() {
|
||||
_streamController.add(_storage.values.toList());
|
||||
}
|
||||
}
|
||||
25
experimental/web_dashboard/lib/src/app.dart
Normal file
25
experimental/web_dashboard/lib/src/app.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'pages/home.dart';
|
||||
import 'api/api.dart';
|
||||
import 'api/mock.dart';
|
||||
|
||||
/// An app that shows a responsive dashboard.
|
||||
class DashboardApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider<DashboardApi>(create: (_) => MockDashboardApi()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: HomePage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
experimental/web_dashboard/lib/src/pages/home.dart
Normal file
49
experimental/web_dashboard/lib/src/pages/home.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../api/api.dart';
|
||||
import 'item_details.dart';
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
var api = Provider.of<DashboardApi>(context);
|
||||
|
||||
return Scaffold(
|
||||
body: StreamProvider<List<Item>>(
|
||||
initialData: [],
|
||||
create: (context) => api.items.allItemsStream(),
|
||||
child: Consumer<List<Item>>(
|
||||
builder: (context, items, child) {
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, idx) {
|
||||
return ListTile(
|
||||
title: Text(items[idx].name),
|
||||
onTap: () {
|
||||
_showDetails(items[idx], context);
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: Icon(Icons.add),
|
||||
onPressed: () {
|
||||
api.items.insert(Item('Coffees Drank'));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDetails(Item item, BuildContext context) {
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
|
||||
return ItemDetailsPage(item);
|
||||
}));
|
||||
}
|
||||
}
|
||||
22
experimental/web_dashboard/lib/src/pages/item_details.dart
Normal file
22
experimental/web_dashboard/lib/src/pages/item_details.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:web_dashboard/src/api/api.dart';
|
||||
|
||||
class ItemDetailsPage extends StatelessWidget {
|
||||
final Item item;
|
||||
|
||||
ItemDetailsPage(this.item);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Text('${item.name}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
154
experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart
vendored
Normal file
154
experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'navigation_rail.dart';
|
||||
|
||||
bool _isLargeScreen(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width > 960.0;
|
||||
}
|
||||
|
||||
bool _isMediumScreen(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width > 640.0;
|
||||
}
|
||||
|
||||
/// See bottomNavigationBarItem or NavigationRailDestination
|
||||
class AdaptiveScaffoldDestination {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
|
||||
const AdaptiveScaffoldDestination({
|
||||
@required this.title,
|
||||
@required this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
/// A widget that adapts to the current display size, displaying a [Drawer],
|
||||
/// [NavigationRail], or [BottomNavigationBar]. Navigation destinations are
|
||||
/// defined in the [destinations] parameter.
|
||||
class AdaptiveScaffold extends StatefulWidget {
|
||||
final Widget title;
|
||||
final Widget body;
|
||||
final int currentIndex;
|
||||
final List<AdaptiveScaffoldDestination> destinations;
|
||||
final ValueChanged<int> onNavigationIndexChange;
|
||||
final FloatingActionButton floatingActionButton;
|
||||
|
||||
AdaptiveScaffold({
|
||||
this.title,
|
||||
this.body,
|
||||
@required this.currentIndex,
|
||||
@required this.destinations,
|
||||
this.onNavigationIndexChange,
|
||||
this.floatingActionButton,
|
||||
});
|
||||
|
||||
@override
|
||||
_AdaptiveScaffoldState createState() => _AdaptiveScaffoldState();
|
||||
}
|
||||
|
||||
class _AdaptiveScaffoldState extends State<AdaptiveScaffold> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Show a Drawer
|
||||
if (_isLargeScreen(context)) {
|
||||
return Row(
|
||||
children: [
|
||||
Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
DrawerHeader(
|
||||
child: Center(
|
||||
child: widget.title,
|
||||
),
|
||||
),
|
||||
for (var d in widget.destinations)
|
||||
ListTile(
|
||||
leading: Icon(d.icon),
|
||||
title: Text(d.title),
|
||||
selected:
|
||||
widget.destinations.indexOf(d) == widget.currentIndex,
|
||||
onTap: () => _destinationTapped(d),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: widget.body,
|
||||
floatingActionButton: widget.floatingActionButton,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Show a navigation rail
|
||||
if (_isMediumScreen(context)) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.title,
|
||||
),
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
leading: widget.floatingActionButton,
|
||||
destinations: [
|
||||
...widget.destinations.map(
|
||||
(d) => NavigationRailDestination(
|
||||
icon: Icon(d.icon),
|
||||
title: Text(d.title),
|
||||
),
|
||||
),
|
||||
],
|
||||
currentIndex: widget.currentIndex,
|
||||
onDestinationSelected: widget.onNavigationIndexChange,
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
Expanded(
|
||||
child: widget.body,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Show a bottom app bar
|
||||
return Scaffold(
|
||||
body: widget.body,
|
||||
appBar: AppBar(title: widget.title),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: [
|
||||
...widget.destinations.map(
|
||||
(d) => BottomNavigationBarItem(
|
||||
icon: Icon(d.icon),
|
||||
title: Text(d.title),
|
||||
),
|
||||
),
|
||||
],
|
||||
currentIndex: widget.currentIndex,
|
||||
onTap: widget.onNavigationIndexChange,
|
||||
),
|
||||
floatingActionButton: widget.floatingActionButton,
|
||||
);
|
||||
}
|
||||
|
||||
void _destinationTapped(AdaptiveScaffoldDestination destination) {
|
||||
var idx = widget.destinations.indexOf(destination);
|
||||
if (idx != widget.currentIndex) {
|
||||
widget.onNavigationIndexChange(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
402
experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail.dart
vendored
Normal file
402
experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail.dart
vendored
Normal file
@@ -0,0 +1,402 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// Original pull request: https://github.com/flutter/flutter/pull/49574
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Defines the behavior of the labels of a [NavigationRail].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRail]
|
||||
enum NavigationRailLabelType {
|
||||
/// Only the icons of a navigation rail item are shown.
|
||||
none,
|
||||
|
||||
/// Only the selected navigation rail item will show its label.
|
||||
///
|
||||
/// The label will animate in and out as new items are selected.
|
||||
selected,
|
||||
|
||||
/// All navigation rail items will show their label.
|
||||
all,
|
||||
}
|
||||
|
||||
/// Defines the alignment for the group of [NavigationRailDestination]s within
|
||||
/// a [NavigationRail].
|
||||
///
|
||||
/// Navigation rail destinations can be aligned as a group to the [top],
|
||||
/// [bottom], or [center] of a layout.
|
||||
enum NavigationRailGroupAlignment {
|
||||
/// Place the [NavigationRailDestination]s at the top of the rail.
|
||||
top,
|
||||
|
||||
/// Place the [NavigationRailDestination]s in the center of the rail.
|
||||
center,
|
||||
|
||||
/// Place the [NavigationRailDestination]s at the bottom of the rail.
|
||||
bottom,
|
||||
}
|
||||
|
||||
/// A description for an interactive button within a [NavigationRail].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRail]
|
||||
class NavigationRailDestination {
|
||||
/// Creates an destination that is used with [NavigationRail.destinations].
|
||||
///
|
||||
/// [icon] should not be null and [title] should not be null when this
|
||||
/// destination is used in the [NavigationRail].
|
||||
const NavigationRailDestination({
|
||||
@required this.icon,
|
||||
Widget activeIcon,
|
||||
this.title,
|
||||
}) : activeIcon = activeIcon ?? icon,
|
||||
assert(icon != null);
|
||||
|
||||
/// The icon of the destination.
|
||||
///
|
||||
/// Typically the icon is an [Icon] or an [ImageIcon] widget. If another type
|
||||
/// of widget is provided then it should configure itself to match the current
|
||||
/// [IconTheme] size and color.
|
||||
///
|
||||
/// If [activeIcon] is provided, this will only be displayed when the
|
||||
/// destination is not selected.
|
||||
///
|
||||
/// To make the [NavigationRail] more accessible, consider choosing an
|
||||
/// icon with a stroked and filled version, such as [Icons.cloud] and
|
||||
/// [Icons.cloud_queue]. [icon] should be set to the stroked version and
|
||||
/// [activeIcon] to the filled version.
|
||||
final Widget icon;
|
||||
|
||||
/// An alternative icon displayed when this destination is selected.
|
||||
///
|
||||
/// If this icon is not provided, the [NavigationRail] will display [icon] in
|
||||
/// either state.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRailDestination.icon], for a description of how to pair
|
||||
/// icons.
|
||||
final Widget activeIcon;
|
||||
|
||||
/// The title of the item. If the title is not provided only the icon will be
|
||||
/// shown when not used in a [NavigationRail].
|
||||
final Widget title;
|
||||
}
|
||||
|
||||
/// TODO
|
||||
class NavigationRail extends StatefulWidget {
|
||||
/// TODO
|
||||
NavigationRail({
|
||||
this.leading,
|
||||
this.destinations,
|
||||
this.currentIndex,
|
||||
this.onDestinationSelected,
|
||||
this.groupAlignment = NavigationRailGroupAlignment.top,
|
||||
this.labelType = NavigationRailLabelType.none,
|
||||
this.labelTextStyle,
|
||||
this.selectedLabelTextStyle,
|
||||
this.iconTheme,
|
||||
this.selectedIconTheme,
|
||||
});
|
||||
|
||||
/// The leading widget in the rail that is placed above the items.
|
||||
///
|
||||
/// This is commonly a [FloatingActionButton], but may also be a non-button,
|
||||
/// such as a logo.
|
||||
final Widget leading;
|
||||
|
||||
/// Defines the appearance of the button items that are arrayed within the
|
||||
/// navigation rail.
|
||||
final List<NavigationRailDestination> destinations;
|
||||
|
||||
/// The index into [destinations] for the current active [NavigationRailDestination].
|
||||
final int currentIndex;
|
||||
|
||||
/// Called when one of the [destinations] is selected.
|
||||
///
|
||||
/// The stateful widget that creates the navigation rail needs to keep
|
||||
/// track of the index of the selected [NavigationRailDestination] and call
|
||||
/// `setState` to rebuild the navigation rail with the new [currentIndex].
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
/// The alignment for the [NavigationRailDestination]s as they are positioned
|
||||
/// within the [NavigationRail].
|
||||
///
|
||||
/// Navigation rail destinations can be aligned as a group to the [top],
|
||||
/// [bottom], or [center] of a layout.
|
||||
final NavigationRailGroupAlignment groupAlignment;
|
||||
|
||||
/// Defines the layout and behavior of the labels in the [NavigationRail].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRailLabelType] for information on the meaning of different
|
||||
/// types.
|
||||
final NavigationRailLabelType labelType;
|
||||
|
||||
/// The [TextStyle] of the [NavigationRailDestination] labels.
|
||||
///
|
||||
/// This is the default [TextStyle] for all labels. When the
|
||||
/// [NavigationRailDestination] is selected, the [selectedLabelTextStyle] will be
|
||||
/// used instead.
|
||||
final TextStyle labelTextStyle;
|
||||
|
||||
/// The [TextStyle] of the [NavigationRailDestination] labels when they are
|
||||
/// selected.
|
||||
///
|
||||
/// This field overrides the [labelTextStyle] for selected items.
|
||||
///
|
||||
/// When the [NavigationRailDestination] is not selected, [labelTextStyle] will be
|
||||
/// used.
|
||||
final TextStyle selectedLabelTextStyle;
|
||||
|
||||
/// The default size, opacity, and color of the icon in the
|
||||
/// [NavigationRailDestination].
|
||||
///
|
||||
/// If this field is not provided, or provided with any null properties, then
|
||||
///a copy of the [IconThemeData.fallback] with a custom [NavigationRail]
|
||||
/// specific color will be used.
|
||||
final IconTheme iconTheme;
|
||||
|
||||
/// The size, opacity, and color of the icon in the selected
|
||||
/// [NavigationRailDestination].
|
||||
///
|
||||
/// This field overrides the [iconTheme] for selected items.
|
||||
///
|
||||
/// When the [NavigationRailDestination] is not selected, [iconTheme] will be
|
||||
/// used.
|
||||
final IconTheme selectedIconTheme;
|
||||
|
||||
@override
|
||||
_NavigationRailState createState() => _NavigationRailState();
|
||||
}
|
||||
|
||||
class _NavigationRailState extends State<NavigationRail>
|
||||
with TickerProviderStateMixin {
|
||||
List<AnimationController> _controllers = <AnimationController>[];
|
||||
List<Animation<double>> _animations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initControllers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposeControllers();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(NavigationRail oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// No animated segue if the length of the items list changes.
|
||||
if (widget.destinations.length != oldWidget.destinations.length) {
|
||||
_resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.currentIndex != oldWidget.currentIndex) {
|
||||
_controllers[oldWidget.currentIndex].reverse();
|
||||
_controllers[widget.currentIndex].forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget leading = widget.leading;
|
||||
return DefaultTextStyle(
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
child: Container(
|
||||
width: _railWidth,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_verticalSpacing,
|
||||
if (leading != null) ...<Widget>[
|
||||
SizedBox(
|
||||
height: _railItemHeight,
|
||||
width: _railItemWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: leading,
|
||||
),
|
||||
),
|
||||
_verticalSpacing,
|
||||
],
|
||||
for (int i = 0; i < widget.destinations.length; i++)
|
||||
_RailItem(
|
||||
animation: _animations[i],
|
||||
labelKind: widget.labelType,
|
||||
selected: widget.currentIndex == i,
|
||||
icon: widget.currentIndex == i
|
||||
? widget.destinations[i].activeIcon
|
||||
: widget.destinations[i].icon,
|
||||
title: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
color: widget.currentIndex == i
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.64)),
|
||||
child: widget.destinations[i].title,
|
||||
),
|
||||
onTap: () {
|
||||
widget.onDestinationSelected(i);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _disposeControllers() {
|
||||
for (final AnimationController controller in _controllers)
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
void _initControllers() {
|
||||
_controllers = List<AnimationController>.generate(
|
||||
widget.destinations.length, (int index) {
|
||||
return AnimationController(
|
||||
duration: kThemeAnimationDuration,
|
||||
vsync: this,
|
||||
)..addListener(_rebuild);
|
||||
});
|
||||
_animations = _controllers
|
||||
.map((AnimationController controller) => controller.view)
|
||||
.toList();
|
||||
_controllers[widget.currentIndex].value = 1.0;
|
||||
}
|
||||
|
||||
void _resetState() {
|
||||
_disposeControllers();
|
||||
_initControllers();
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
setState(() {
|
||||
// Rebuilding when any of the controllers tick, i.e. when the items are
|
||||
// animated.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _RailItem extends StatelessWidget {
|
||||
_RailItem({
|
||||
this.animation,
|
||||
this.labelKind,
|
||||
this.selected,
|
||||
this.icon,
|
||||
this.title,
|
||||
this.onTap,
|
||||
}) : assert(labelKind != null),
|
||||
_positionAnimation = CurvedAnimation(
|
||||
parent: ReverseAnimation(animation),
|
||||
curve: Curves.easeInOut,
|
||||
reverseCurve: Curves.easeInOut.flipped,
|
||||
);
|
||||
|
||||
final Animation<double> _positionAnimation;
|
||||
|
||||
final Animation<double> animation;
|
||||
final NavigationRailLabelType labelKind;
|
||||
final bool selected;
|
||||
final Widget icon;
|
||||
final Widget title;
|
||||
final VoidCallback onTap;
|
||||
|
||||
double _fadeInValue() {
|
||||
if (animation.value < 0.25) {
|
||||
return 0;
|
||||
} else if (animation.value < 0.75) {
|
||||
return (animation.value - 0.25) * 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
double _fadeOutValue() {
|
||||
if (animation.value > 0.75) {
|
||||
return (animation.value - 0.75) * 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content;
|
||||
switch (labelKind) {
|
||||
case NavigationRailLabelType.none:
|
||||
content = SizedBox(width: _railItemWidth, child: icon);
|
||||
break;
|
||||
case NavigationRailLabelType.selected:
|
||||
content = SizedBox(
|
||||
width: 72,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(height: _positionAnimation.value * 18),
|
||||
icon,
|
||||
Opacity(
|
||||
alwaysIncludeSemantics: true,
|
||||
opacity: selected ? _fadeInValue() : _fadeOutValue(),
|
||||
child: title,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
break;
|
||||
case NavigationRailLabelType.all:
|
||||
content = SizedBox(
|
||||
width: 72,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
icon,
|
||||
title,
|
||||
],
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
final ColorScheme colors = Theme.of(context).colorScheme;
|
||||
return IconTheme(
|
||||
data: IconThemeData(
|
||||
color: selected ? colors.primary : colors.onSurface.withOpacity(0.64),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 72,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
clipBehavior: Clip.none,
|
||||
child: InkResponse(
|
||||
onTap: onTap,
|
||||
onHover: (_) {},
|
||||
splashColor:
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.12),
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.04),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const double _railWidth = 72;
|
||||
const double _railItemWidth = _railWidth;
|
||||
const double _railItemHeight = _railItemWidth;
|
||||
const double _spacing = 8;
|
||||
const Widget _verticalSpacing = SizedBox(height: _spacing);
|
||||
Reference in New Issue
Block a user