Port widgets that depend on scrolling to fn3
This commit is contained in:
parent
042f49a711
commit
9f176fc86e
403
packages/flutter/lib/src/fn3/date_picker.dart
Normal file
403
packages/flutter/lib/src/fn3/date_picker.dart
Normal file
@ -0,0 +1,403 @@
|
||||
// Copyright 2015 The Chromium Authors. 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:intl/date_symbols.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:sky/material.dart';
|
||||
import 'package:sky/painting.dart';
|
||||
import 'package:sky/services.dart';
|
||||
import 'package:sky/src/fn3/basic.dart';
|
||||
import 'package:sky/src/fn3/framework.dart';
|
||||
import 'package:sky/src/fn3/gesture_detector.dart';
|
||||
import 'package:sky/src/fn3/ink_well.dart';
|
||||
import 'package:sky/src/fn3/scrollable.dart';
|
||||
import 'package:sky/src/fn3/theme.dart';
|
||||
|
||||
typedef void DatePickerValueChanged(DateTime dateTime);
|
||||
|
||||
enum DatePickerMode { day, year }
|
||||
|
||||
typedef void DatePickerModeChanged(DatePickerMode value);
|
||||
|
||||
class DatePicker extends StatefulComponent {
|
||||
DatePicker({
|
||||
this.selectedDate,
|
||||
this.onChanged,
|
||||
this.firstDate,
|
||||
this.lastDate
|
||||
}) {
|
||||
assert(selectedDate != null);
|
||||
assert(firstDate != null);
|
||||
assert(lastDate != null);
|
||||
}
|
||||
|
||||
final DateTime selectedDate;
|
||||
final DatePickerValueChanged onChanged;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
DatePickerState createState() => new DatePickerState(this);
|
||||
}
|
||||
|
||||
class DatePickerState extends ComponentState<DatePicker> {
|
||||
DatePickerState(DatePicker config) : super(config);
|
||||
|
||||
DatePickerMode _mode = DatePickerMode.day;
|
||||
|
||||
void _handleModeChanged(DatePickerMode mode) {
|
||||
userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY);
|
||||
setState(() {
|
||||
_mode = mode;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleYearChanged(DateTime dateTime) {
|
||||
userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY);
|
||||
setState(() {
|
||||
_mode = DatePickerMode.day;
|
||||
});
|
||||
if (config.onChanged != null)
|
||||
config.onChanged(dateTime);
|
||||
}
|
||||
|
||||
void _handleDayChanged(DateTime dateTime) {
|
||||
userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY);
|
||||
if (config.onChanged != null)
|
||||
config.onChanged(dateTime);
|
||||
}
|
||||
|
||||
static const double _calendarHeight = 210.0;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
Widget header = new DatePickerHeader(
|
||||
selectedDate: config.selectedDate,
|
||||
mode: _mode,
|
||||
onModeChanged: _handleModeChanged
|
||||
);
|
||||
Widget picker;
|
||||
switch (_mode) {
|
||||
case DatePickerMode.day:
|
||||
picker = new MonthPicker(
|
||||
selectedDate: config.selectedDate,
|
||||
onChanged: _handleDayChanged,
|
||||
firstDate: config.firstDate,
|
||||
lastDate: config.lastDate,
|
||||
itemExtent: _calendarHeight
|
||||
);
|
||||
break;
|
||||
case DatePickerMode.year:
|
||||
picker = new YearPicker(
|
||||
selectedDate: config.selectedDate,
|
||||
onChanged: _handleYearChanged,
|
||||
firstDate: config.firstDate,
|
||||
lastDate: config.lastDate
|
||||
);
|
||||
break;
|
||||
}
|
||||
return new Column([
|
||||
header,
|
||||
new Container(
|
||||
height: _calendarHeight,
|
||||
child: picker
|
||||
)
|
||||
], alignItems: FlexAlignItems.stretch);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Shows the selected date in large font and toggles between year and day mode
|
||||
class DatePickerHeader extends StatelessComponent {
|
||||
DatePickerHeader({ this.selectedDate, this.mode, this.onModeChanged }) {
|
||||
assert(selectedDate != null);
|
||||
assert(mode != null);
|
||||
}
|
||||
|
||||
DateTime selectedDate;
|
||||
DatePickerMode mode;
|
||||
DatePickerModeChanged onModeChanged;
|
||||
|
||||
void _handleChangeMode(DatePickerMode value) {
|
||||
if (value != mode)
|
||||
onModeChanged(value);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData theme = Theme.of(context);
|
||||
TextTheme headerTheme;
|
||||
Color dayColor;
|
||||
Color yearColor;
|
||||
switch(theme.primaryColorBrightness) {
|
||||
case ThemeBrightness.light:
|
||||
headerTheme = Typography.black;
|
||||
dayColor = mode == DatePickerMode.day ? Colors.black87 : Colors.black54;
|
||||
yearColor = mode == DatePickerMode.year ? Colors.black87 : Colors.black54;
|
||||
break;
|
||||
case ThemeBrightness.dark:
|
||||
headerTheme = Typography.white;
|
||||
dayColor = mode == DatePickerMode.day ? Colors.white87 : Colors.white54;
|
||||
yearColor = mode == DatePickerMode.year ? Colors.white87 : Colors.white54;
|
||||
break;
|
||||
}
|
||||
TextStyle dayStyle = headerTheme.display3.copyWith(color: dayColor, height: 1.0, fontSize: 100.0);
|
||||
TextStyle monthStyle = headerTheme.headline.copyWith(color: dayColor, height: 1.0);
|
||||
TextStyle yearStyle = headerTheme.headline.copyWith(color: yearColor, height: 1.0);
|
||||
|
||||
return new Container(
|
||||
padding: new EdgeDims.all(10.0),
|
||||
decoration: new BoxDecoration(backgroundColor: theme.primaryColor),
|
||||
child: new Column([
|
||||
new GestureDetector(
|
||||
onTap: () => _handleChangeMode(DatePickerMode.day),
|
||||
child: new Text(new DateFormat("MMM").format(selectedDate).toUpperCase(), style: monthStyle)
|
||||
),
|
||||
new GestureDetector(
|
||||
onTap: () => _handleChangeMode(DatePickerMode.day),
|
||||
child: new Text(new DateFormat("d").format(selectedDate), style: dayStyle)
|
||||
),
|
||||
new GestureDetector(
|
||||
onTap: () => _handleChangeMode(DatePickerMode.year),
|
||||
child: new Text(new DateFormat("yyyy").format(selectedDate), style: yearStyle)
|
||||
)
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed height component shows a single month and allows choosing a day
|
||||
class DayPicker extends StatelessComponent {
|
||||
DayPicker({
|
||||
this.selectedDate,
|
||||
this.currentDate,
|
||||
this.onChanged,
|
||||
this.displayedMonth
|
||||
}) {
|
||||
assert(selectedDate != null);
|
||||
assert(currentDate != null);
|
||||
assert(onChanged != null);
|
||||
assert(displayedMonth != null);
|
||||
}
|
||||
|
||||
final DateTime selectedDate;
|
||||
final DateTime currentDate;
|
||||
final DatePickerValueChanged onChanged;
|
||||
final DateTime displayedMonth;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData theme = Theme.of(context);
|
||||
TextStyle headerStyle = theme.text.caption.copyWith(fontWeight: FontWeight.w700);
|
||||
TextStyle monthStyle = headerStyle.copyWith(fontSize: 14.0, height: 24.0 / 14.0);
|
||||
TextStyle dayStyle = headerStyle.copyWith(fontWeight: FontWeight.w500);
|
||||
DateFormat dateFormat = new DateFormat();
|
||||
DateSymbols symbols = dateFormat.dateSymbols;
|
||||
|
||||
List<Text> headers = [];
|
||||
for (String weekDay in symbols.NARROWWEEKDAYS) {
|
||||
headers.add(new Text(weekDay, style: headerStyle));
|
||||
}
|
||||
List<Widget> rows = [
|
||||
new Text(new DateFormat("MMMM y").format(displayedMonth), style: monthStyle),
|
||||
new Flex(
|
||||
headers,
|
||||
justifyContent: FlexJustifyContent.spaceAround
|
||||
)
|
||||
];
|
||||
int year = displayedMonth.year;
|
||||
int month = displayedMonth.month;
|
||||
// Dart's Date time constructor is very forgiving and will understand
|
||||
// month 13 as January of the next year. :)
|
||||
int daysInMonth = new DateTime(year, month + 1).difference(new DateTime(year, month)).inDays;
|
||||
int firstDay = new DateTime(year, month).day;
|
||||
int weeksShown = 6;
|
||||
List<int> days = [
|
||||
DateTime.SUNDAY,
|
||||
DateTime.MONDAY,
|
||||
DateTime.TUESDAY,
|
||||
DateTime.WEDNESDAY,
|
||||
DateTime.THURSDAY,
|
||||
DateTime.FRIDAY,
|
||||
DateTime.SATURDAY
|
||||
];
|
||||
int daySlots = weeksShown * days.length;
|
||||
List<Widget> labels = [];
|
||||
for (int i = 0; i < daySlots; i++) {
|
||||
// This assumes a start day of SUNDAY, but could be changed.
|
||||
int day = i - firstDay + 1;
|
||||
Widget item;
|
||||
if (day < 1 || day > daysInMonth) {
|
||||
item = new Text("");
|
||||
} else {
|
||||
// Put a light circle around the selected day
|
||||
BoxDecoration decoration = null;
|
||||
if (selectedDate.year == year &&
|
||||
selectedDate.month == month &&
|
||||
selectedDate.day == day)
|
||||
decoration = new BoxDecoration(
|
||||
backgroundColor: theme.primarySwatch[100],
|
||||
shape: Shape.circle
|
||||
);
|
||||
|
||||
// Use a different font color for the current day
|
||||
TextStyle itemStyle = dayStyle;
|
||||
if (currentDate.year == year &&
|
||||
currentDate.month == month &&
|
||||
currentDate.day == day)
|
||||
itemStyle = itemStyle.copyWith(color: theme.primaryColor);
|
||||
|
||||
item = new GestureDetector(
|
||||
onTap: () {
|
||||
DateTime result = new DateTime(year, month, day);
|
||||
onChanged(result);
|
||||
},
|
||||
child: new Container(
|
||||
height: 30.0,
|
||||
decoration: decoration,
|
||||
child: new Center(
|
||||
child: new Text(day.toString(), style: itemStyle)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
labels.add(new Flexible(child: item));
|
||||
}
|
||||
for (int w = 0; w < weeksShown; w++) {
|
||||
int startIndex = w * days.length;
|
||||
rows.add(new Row(
|
||||
labels.sublist(startIndex, startIndex + days.length)
|
||||
));
|
||||
}
|
||||
|
||||
return new Column(rows);
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable list of DayPickers to allow choosing a month
|
||||
class MonthPicker extends ScrollableWidgetList {
|
||||
MonthPicker({
|
||||
this.selectedDate,
|
||||
this.onChanged,
|
||||
this.firstDate,
|
||||
this.lastDate,
|
||||
double itemExtent
|
||||
}) : super(itemExtent: itemExtent) {
|
||||
assert(selectedDate != null);
|
||||
assert(onChanged != null);
|
||||
assert(lastDate.isAfter(firstDate));
|
||||
}
|
||||
|
||||
final DateTime selectedDate;
|
||||
final DatePickerValueChanged onChanged;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
MonthPickerState createState() => new MonthPickerState(this);
|
||||
}
|
||||
|
||||
class MonthPickerState extends ScrollableWidgetListState<MonthPicker> {
|
||||
MonthPickerState(MonthPicker config) : super(config) {
|
||||
_updateCurrentDate();
|
||||
}
|
||||
|
||||
DateTime _currentDate;
|
||||
Timer _timer;
|
||||
|
||||
void _updateCurrentDate() {
|
||||
_currentDate = new DateTime.now();
|
||||
DateTime tomorrow = new DateTime(_currentDate.year, _currentDate.month, _currentDate.day + 1);
|
||||
Duration timeUntilTomorrow = tomorrow.difference(_currentDate);
|
||||
timeUntilTomorrow += const Duration(seconds: 1); // so we don't miss it by rounding
|
||||
if (_timer != null)
|
||||
_timer.cancel();
|
||||
_timer = new Timer(timeUntilTomorrow, () {
|
||||
setState(() {
|
||||
_updateCurrentDate();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
int get itemCount => (config.lastDate.year - config.firstDate.year) * 12 + config.lastDate.month - config.firstDate.month + 1;
|
||||
|
||||
List<Widget> buildItems(BuildContext context, int start, int count) {
|
||||
List<Widget> result = new List<Widget>();
|
||||
DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
DateTime displayedMonth = new DateTime(startDate.year + i ~/ 12, startDate.month + i % 12);
|
||||
Widget item = new Container(
|
||||
height: config.itemExtent,
|
||||
key: new ObjectKey(displayedMonth),
|
||||
child: new DayPicker(
|
||||
selectedDate: config.selectedDate,
|
||||
currentDate: _currentDate,
|
||||
onChanged: config.onChanged,
|
||||
displayedMonth: displayedMonth
|
||||
)
|
||||
);
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (_timer != null)
|
||||
_timer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable list of years to allow picking a year
|
||||
class YearPicker extends ScrollableWidgetList {
|
||||
YearPicker({
|
||||
this.selectedDate,
|
||||
this.onChanged,
|
||||
this.firstDate,
|
||||
this.lastDate
|
||||
}) : super(itemExtent: 50.0) {
|
||||
assert(selectedDate != null);
|
||||
assert(onChanged != null);
|
||||
assert(lastDate.isAfter(firstDate));
|
||||
}
|
||||
|
||||
final DateTime selectedDate;
|
||||
final DatePickerValueChanged onChanged;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
|
||||
YearPickerState createState() => new YearPickerState(this);
|
||||
}
|
||||
|
||||
class YearPickerState extends ScrollableWidgetListState<YearPicker> {
|
||||
YearPickerState(YearPicker config) : super(config);
|
||||
|
||||
int get itemCount => config.lastDate.year - config.firstDate.year + 1;
|
||||
|
||||
List<Widget> buildItems(BuildContext context, int start, int count) {
|
||||
TextStyle style = Theme.of(context).text.body1.copyWith(color: Colors.black54);
|
||||
List<Widget> items = new List<Widget>();
|
||||
for(int i = start; i < start + count; i++) {
|
||||
int year = config.firstDate.year + i;
|
||||
String label = year.toString();
|
||||
Widget item = new GestureDetector(
|
||||
key: new Key(label),
|
||||
onTap: () {
|
||||
DateTime result = new DateTime(year, config.selectedDate.month, config.selectedDate.day);
|
||||
config.onChanged(result);
|
||||
},
|
||||
child: new InkWell(
|
||||
child: new Container(
|
||||
height: config.itemExtent,
|
||||
decoration: year == config.selectedDate.year ? new BoxDecoration(
|
||||
backgroundColor: Theme.of(context).primarySwatch[100],
|
||||
shape: Shape.circle
|
||||
) : null,
|
||||
child: new Center(
|
||||
child: new Text(label, style: style)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
169
packages/flutter/lib/src/fn3/dialog.dart
Normal file
169
packages/flutter/lib/src/fn3/dialog.dart
Normal file
@ -0,0 +1,169 @@
|
||||
// Copyright 2015 The Chromium Authors. 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:sky/animation.dart';
|
||||
import 'package:sky/material.dart';
|
||||
import 'package:sky/src/fn3/basic.dart';
|
||||
import 'package:sky/src/fn3/focus.dart';
|
||||
import 'package:sky/src/fn3/framework.dart';
|
||||
import 'package:sky/src/fn3/gesture_detector.dart';
|
||||
import 'package:sky/src/fn3/material.dart';
|
||||
import 'package:sky/src/fn3/navigator.dart';
|
||||
import 'package:sky/src/fn3/scrollable.dart';
|
||||
import 'package:sky/src/fn3/theme.dart';
|
||||
import 'package:sky/src/fn3/transitions.dart';
|
||||
|
||||
typedef Widget DialogBuilder(Navigator navigator);
|
||||
|
||||
/// A material design dialog
|
||||
///
|
||||
/// <https://www.google.com/design/spec/components/dialogs.html>
|
||||
class Dialog extends StatelessComponent {
|
||||
Dialog({
|
||||
Key key,
|
||||
this.title,
|
||||
this.titlePadding,
|
||||
this.content,
|
||||
this.contentPadding,
|
||||
this.actions,
|
||||
this.onDismiss
|
||||
}): super(key: key);
|
||||
|
||||
/// The (optional) title of the dialog is displayed in a large font at the top
|
||||
/// of the dialog.
|
||||
final Widget title;
|
||||
|
||||
// Padding around the title; uses material design default if none is supplied
|
||||
// If there is no title, no padding will be provided
|
||||
final EdgeDims titlePadding;
|
||||
|
||||
/// The (optional) content of the dialog is displayed in the center of the
|
||||
/// dialog in a lighter font.
|
||||
final Widget content;
|
||||
|
||||
// Padding around the content; uses material design default if none is supplied
|
||||
final EdgeDims contentPadding;
|
||||
|
||||
/// The (optional) set of actions that are displayed at the bottom of the
|
||||
/// dialog.
|
||||
final List<Widget> actions;
|
||||
|
||||
/// An (optional) callback that is called when the dialog is dismissed.
|
||||
final Function onDismiss;
|
||||
|
||||
Color _getColor(BuildContext context) {
|
||||
switch (Theme.of(context).brightness) {
|
||||
case ThemeBrightness.light:
|
||||
return Colors.white;
|
||||
case ThemeBrightness.dark:
|
||||
return Colors.grey[800];
|
||||
}
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
List<Widget> dialogBody = new List<Widget>();
|
||||
|
||||
if (title != null) {
|
||||
EdgeDims padding = titlePadding;
|
||||
if (padding == null)
|
||||
padding = new EdgeDims(24.0, 24.0, content == null ? 20.0 : 0.0, 24.0);
|
||||
dialogBody.add(new Padding(
|
||||
padding: padding,
|
||||
child: new DefaultTextStyle(
|
||||
style: Theme.of(context).text.title,
|
||||
child: title
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
EdgeDims padding = contentPadding;
|
||||
if (padding == null)
|
||||
padding = const EdgeDims(20.0, 24.0, 24.0, 24.0);
|
||||
dialogBody.add(new Padding(
|
||||
padding: padding,
|
||||
child: new DefaultTextStyle(
|
||||
style: Theme.of(context).text.subhead,
|
||||
child: content
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
if (actions != null) {
|
||||
dialogBody.add(new Container(
|
||||
child: new Row(actions,
|
||||
justifyContent: FlexJustifyContent.end
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
return new Stack([
|
||||
new GestureDetector(
|
||||
onTap: onDismiss,
|
||||
child: new Container(
|
||||
decoration: const BoxDecoration(
|
||||
backgroundColor: const Color(0x7F000000)
|
||||
)
|
||||
)
|
||||
),
|
||||
new Center(
|
||||
child: new Container(
|
||||
margin: new EdgeDims.symmetric(horizontal: 40.0, vertical: 24.0),
|
||||
child: new ConstrainedBox(
|
||||
constraints: new BoxConstraints(minWidth: 280.0),
|
||||
child: new Material(
|
||||
level: 4,
|
||||
color: _getColor(context),
|
||||
child: new IntrinsicWidth(
|
||||
child: new Block(dialogBody)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const Duration _kTransitionDuration = const Duration(milliseconds: 150);
|
||||
|
||||
class DialogRoute extends RouteBase {
|
||||
DialogRoute({ this.completer, this.builder });
|
||||
|
||||
final Completer completer;
|
||||
final RouteBuilder builder;
|
||||
|
||||
Duration get transitionDuration => _kTransitionDuration;
|
||||
bool get isOpaque => false;
|
||||
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) {
|
||||
return new FadeTransition(
|
||||
performance: performance,
|
||||
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
|
||||
child: builder(navigator, this)
|
||||
);
|
||||
}
|
||||
|
||||
void popState([dynamic result]) {
|
||||
completer.complete(result);
|
||||
}
|
||||
}
|
||||
|
||||
Future showDialog(NavigatorState navigator, DialogBuilder builder) {
|
||||
Completer completer = new Completer();
|
||||
navigator.push(new DialogRoute(
|
||||
completer: completer,
|
||||
builder: (navigator, route) {
|
||||
return new Focus(
|
||||
key: new GlobalObjectKey(route),
|
||||
autofocus: true,
|
||||
child: builder(navigator)
|
||||
);
|
||||
}
|
||||
));
|
||||
return completer.future;
|
||||
}
|
148
packages/flutter/lib/src/fn3/drawer.dart
Normal file
148
packages/flutter/lib/src/fn3/drawer.dart
Normal file
@ -0,0 +1,148 @@
|
||||
// Copyright 2015 The Chromium Authors. 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:sky/animation.dart';
|
||||
import 'package:sky/material.dart';
|
||||
import 'package:sky/src/fn3/framework.dart';
|
||||
import 'package:sky/src/fn3/basic.dart';
|
||||
import 'package:sky/src/fn3/gesture_detector.dart';
|
||||
import 'package:sky/src/fn3/navigator.dart';
|
||||
import 'package:sky/src/fn3/scrollable.dart';
|
||||
import 'package:sky/src/fn3/theme.dart';
|
||||
import 'package:sky/src/fn3/transitions.dart';
|
||||
|
||||
// TODO(eseidel): Draw width should vary based on device size:
|
||||
// http://www.google.com/design/spec/layout/structure.html#structure-side-nav
|
||||
|
||||
// Mobile:
|
||||
// Width = Screen width − 56 dp
|
||||
// Maximum width: 320dp
|
||||
// Maximum width applies only when using a left nav. When using a right nav,
|
||||
// the panel can cover the full width of the screen.
|
||||
|
||||
// Desktop/Tablet:
|
||||
// Maximum width for a left nav is 400dp.
|
||||
// The right nav can vary depending on content.
|
||||
|
||||
const double _kWidth = 304.0;
|
||||
const double _kMinFlingVelocity = 365.0;
|
||||
const double _kFlingVelocityScale = 1.0 / 300.0;
|
||||
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);
|
||||
const Duration _kThemeChangeDuration = const Duration(milliseconds: 200);
|
||||
const Point _kOpenPosition = Point.origin;
|
||||
const Point _kClosedPosition = const Point(-_kWidth, 0.0);
|
||||
|
||||
typedef void DrawerDismissedCallback();
|
||||
|
||||
class Drawer extends StatefulComponent {
|
||||
Drawer({
|
||||
Key key,
|
||||
this.children,
|
||||
this.showing: false,
|
||||
this.level: 0,
|
||||
this.onDismissed,
|
||||
this.navigator
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Widget> children;
|
||||
final bool showing;
|
||||
final int level;
|
||||
final DrawerDismissedCallback onDismissed;
|
||||
final NavigatorState navigator;
|
||||
|
||||
DrawerState createState() => new DrawerState(this);
|
||||
}
|
||||
|
||||
class DrawerState extends ComponentState<Drawer> {
|
||||
DrawerState(Drawer config) : super(config) {
|
||||
_performance = new AnimationPerformance(duration: _kBaseSettleDuration);
|
||||
_performance.addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.dismissed)
|
||||
_handleDismissed();
|
||||
});
|
||||
// Use a spring force for animating the drawer. We can't use curves for
|
||||
// this because we need a linear curve in order to track the user's finger
|
||||
// while dragging.
|
||||
_performance.attachedForce = kDefaultSpringForce;
|
||||
if (config.navigator != null) {
|
||||
// TODO(ianh): This is crazy. We should convert drawer to use a pattern like openDialog().
|
||||
// https://github.com/domokit/sky_engine/pull/1186
|
||||
scheduleMicrotask(() {
|
||||
config.navigator.pushState(this, (_) => _performance.reverse());
|
||||
});
|
||||
}
|
||||
_performance.play(_direction);
|
||||
}
|
||||
|
||||
AnimationPerformance _performance;
|
||||
|
||||
Direction get _direction => config.showing ? Direction.forward : Direction.reverse;
|
||||
|
||||
void didUpdateConfig(Drawer oldConfig) {
|
||||
if (config.showing != oldConfig.showing)
|
||||
_performance.play(_direction);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
var mask = new GestureDetector(
|
||||
child: new ColorTransition(
|
||||
performance: _performance.view,
|
||||
color: new AnimatedColorValue(Colors.transparent, end: const Color(0x7F000000)),
|
||||
child: new Container()
|
||||
),
|
||||
onTap: () {
|
||||
_performance.reverse();
|
||||
}
|
||||
);
|
||||
|
||||
Widget content = new SlideTransition(
|
||||
performance: _performance.view,
|
||||
position: new AnimatedValue<Point>(_kClosedPosition, end: _kOpenPosition),
|
||||
// TODO(abarth): Use AnimatedContainer
|
||||
child: new Container(
|
||||
// behavior: implicitlyAnimate(const Duration(milliseconds: 200)),
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
boxShadow: shadows[config.level]),
|
||||
width: _kWidth,
|
||||
child: new Block(config.children)
|
||||
)
|
||||
);
|
||||
|
||||
return new GestureDetector(
|
||||
onHorizontalDragStart: _performance.stop,
|
||||
onHorizontalDragUpdate: _handleDragUpdate,
|
||||
onHorizontalDragEnd: _handleDragEnd,
|
||||
child: new Stack([ mask, content ])
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDismissed() {
|
||||
if (config.navigator != null &&
|
||||
config.navigator.currentRoute is RouteState &&
|
||||
(config.navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer
|
||||
config.navigator.pop();
|
||||
if (config.onDismissed != null)
|
||||
config.onDismissed();
|
||||
}
|
||||
|
||||
bool get _isMostlyClosed => _performance.progress < 0.5;
|
||||
|
||||
void _settle() { _isMostlyClosed ? _performance.reverse() : _performance.play(); }
|
||||
|
||||
void _handleDragUpdate(double delta) {
|
||||
_performance.progress += delta / _kWidth;
|
||||
}
|
||||
|
||||
void _handleDragEnd(Offset velocity) {
|
||||
if (velocity.dx.abs() >= _kMinFlingVelocity) {
|
||||
_performance.fling(velocity: velocity.dx * _kFlingVelocityScale);
|
||||
} else {
|
||||
_settle();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -90,7 +90,7 @@ class RouteState extends RouteBase {
|
||||
|
||||
Function callback;
|
||||
RouteBase route;
|
||||
StatefulComponent owner;
|
||||
ComponentState owner;
|
||||
|
||||
bool get isOpaque => false;
|
||||
|
||||
@ -160,7 +160,7 @@ class NavigatorState extends ComponentState<Navigator> {
|
||||
|
||||
RouteBase get currentRoute => config.history.currentRoute;
|
||||
|
||||
void pushState(StatefulComponent owner, Function callback) {
|
||||
void pushState(ComponentState owner, Function callback) {
|
||||
RouteBase route = new RouteState(
|
||||
owner: owner,
|
||||
callback: callback,
|
||||
|
107
packages/flutter/lib/src/fn3/snack_bar.dart
Normal file
107
packages/flutter/lib/src/fn3/snack_bar.dart
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright 2015 The Chromium Authors. 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:sky/animation.dart';
|
||||
import 'package:sky/painting.dart';
|
||||
import 'package:sky/material.dart';
|
||||
import 'package:sky/src/fn3/animated_component.dart';
|
||||
import 'package:sky/src/fn3/basic.dart';
|
||||
import 'package:sky/src/fn3/framework.dart';
|
||||
import 'package:sky/src/fn3/gesture_detector.dart';
|
||||
import 'package:sky/src/fn3/material.dart';
|
||||
import 'package:sky/src/fn3/theme.dart';
|
||||
import 'package:sky/src/fn3/transitions.dart';
|
||||
|
||||
typedef void SnackBarDismissedCallback();
|
||||
|
||||
const Duration _kSlideInDuration = const Duration(milliseconds: 200);
|
||||
// TODO(ianh): factor out some of the constants below
|
||||
|
||||
class SnackBarAction extends StatelessComponent {
|
||||
SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) {
|
||||
assert(label != null);
|
||||
}
|
||||
|
||||
final String label;
|
||||
final Function onPressed;
|
||||
|
||||
Widget build(BuildContext) {
|
||||
return new GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: new Container(
|
||||
margin: const EdgeDims.only(left: 24.0),
|
||||
padding: const EdgeDims.only(top: 14.0, bottom: 14.0),
|
||||
child: new Text(label)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SnackBar extends AnimatedComponent {
|
||||
SnackBar({
|
||||
Key key,
|
||||
this.transitionKey,
|
||||
this.content,
|
||||
this.actions,
|
||||
bool showing,
|
||||
this.onDismissed
|
||||
}) : super(key: key, direction: showing ? Direction.forward : Direction.reverse, duration: _kSlideInDuration) {
|
||||
assert(content != null);
|
||||
}
|
||||
|
||||
final Key transitionKey;
|
||||
final Widget content;
|
||||
final List<SnackBarAction> actions;
|
||||
final SnackBarDismissedCallback onDismissed;
|
||||
|
||||
SnackBarState createState() => new SnackBarState(this);
|
||||
}
|
||||
|
||||
class SnackBarState extends AnimatedComponentState<SnackBar> {
|
||||
SnackBarState(SnackBar config) : super(config);
|
||||
|
||||
void handleDismissed() {
|
||||
if (config.onDismissed != null)
|
||||
config.onDismissed();
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = [
|
||||
new Flexible(
|
||||
child: new Container(
|
||||
margin: const EdgeDims.symmetric(vertical: 14.0),
|
||||
child: new DefaultTextStyle(
|
||||
style: Typography.white.subhead,
|
||||
child: config.content
|
||||
)
|
||||
)
|
||||
)
|
||||
];
|
||||
if (config.actions != null)
|
||||
children.addAll(config.actions);
|
||||
return new SlideTransition(
|
||||
key: config.transitionKey,
|
||||
performance: performance.view,
|
||||
position: new AnimatedValue<Point>(
|
||||
Point.origin,
|
||||
end: const Point(0.0, -52.0),
|
||||
curve: easeIn,
|
||||
reverseCurve: easeOut
|
||||
),
|
||||
child: new Material(
|
||||
level: 2,
|
||||
color: const Color(0xFF323232),
|
||||
type: MaterialType.canvas,
|
||||
child: new Container(
|
||||
margin: const EdgeDims.symmetric(horizontal: 24.0),
|
||||
child: new DefaultTextStyle(
|
||||
style: new TextStyle(color: Theme.of(context).accentColor),
|
||||
child: new Row(children)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
620
packages/flutter/lib/src/fn3/tabs.dart
Normal file
620
packages/flutter/lib/src/fn3/tabs.dart
Normal file
@ -0,0 +1,620 @@
|
||||
// Copyright 2015 The Chromium Authors. 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:math' as math;
|
||||
import 'dart:sky' as sky;
|
||||
|
||||
import 'package:newton/newton.dart';
|
||||
import 'package:sky/animation.dart';
|
||||
import 'package:sky/painting.dart';
|
||||
import 'package:sky/rendering.dart';
|
||||
import 'package:sky/material.dart';
|
||||
import 'package:sky/src/fn3/basic.dart';
|
||||
import 'package:sky/src/fn3/framework.dart';
|
||||
import 'package:sky/src/fn3/gesture_detector.dart';
|
||||
import 'package:sky/src/fn3/icon.dart';
|
||||
import 'package:sky/src/fn3/ink_well.dart';
|
||||
import 'package:sky/src/fn3/scrollable.dart';
|
||||
import 'package:sky/src/fn3/theme.dart';
|
||||
import 'package:sky/src/fn3/transitions.dart';
|
||||
|
||||
typedef void SelectedIndexChanged(int selectedIndex);
|
||||
typedef void LayoutChanged(Size size, List<double> widths);
|
||||
|
||||
// See https://www.google.com/design/spec/components/tabs.html#tabs-specs
|
||||
const double _kTabHeight = 46.0;
|
||||
const double _kTextAndIconTabHeight = 72.0;
|
||||
const double _kTabIndicatorHeight = 2.0;
|
||||
const double _kMinTabWidth = 72.0;
|
||||
const double _kMaxTabWidth = 264.0;
|
||||
const EdgeDims _kTabLabelPadding = const EdgeDims.symmetric(horizontal: 12.0);
|
||||
const int _kTabIconSize = 24;
|
||||
const double _kTabBarScrollDrag = 0.025;
|
||||
const Duration _kTabBarScroll = const Duration(milliseconds: 200);
|
||||
|
||||
class TabBarParentData extends BoxParentData with
|
||||
ContainerParentDataMixin<RenderBox> { }
|
||||
|
||||
class RenderTabBar extends RenderBox with
|
||||
ContainerRenderObjectMixin<RenderBox, TabBarParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, TabBarParentData> {
|
||||
|
||||
RenderTabBar(this.onLayoutChanged);
|
||||
|
||||
int _selectedIndex;
|
||||
int get selectedIndex => _selectedIndex;
|
||||
void set selectedIndex(int value) {
|
||||
if (_selectedIndex != value) {
|
||||
_selectedIndex = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Color _backgroundColor;
|
||||
Color get backgroundColor => _backgroundColor;
|
||||
void set backgroundColor(Color value) {
|
||||
if (_backgroundColor != value) {
|
||||
_backgroundColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Color _indicatorColor;
|
||||
Color get indicatorColor => _indicatorColor;
|
||||
void set indicatorColor(Color value) {
|
||||
if (_indicatorColor != value) {
|
||||
_indicatorColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Rect _indicatorRect;
|
||||
Rect get indicatorRect => _indicatorRect;
|
||||
void set indicatorRect(Rect value) {
|
||||
if (_indicatorRect != value) {
|
||||
_indicatorRect = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
}
|
||||
|
||||
bool _textAndIcons;
|
||||
bool get textAndIcons => _textAndIcons;
|
||||
void set textAndIcons(bool value) {
|
||||
if (_textAndIcons != value) {
|
||||
_textAndIcons = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isScrollable;
|
||||
bool get isScrollable => _isScrollable;
|
||||
void set isScrollable(bool value) {
|
||||
if (_isScrollable != value) {
|
||||
_isScrollable = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! TabBarParentData)
|
||||
child.parentData = new TabBarParentData();
|
||||
}
|
||||
|
||||
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
||||
BoxConstraints widthConstraints =
|
||||
new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight);
|
||||
|
||||
double maxWidth = 0.0;
|
||||
RenderBox child = firstChild;
|
||||
while (child != null) {
|
||||
maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(widthConstraints));
|
||||
assert(child.parentData is TabBarParentData);
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
double width = isScrollable ? maxWidth : maxWidth * childCount;
|
||||
return constraints.constrainWidth(width);
|
||||
}
|
||||
|
||||
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
||||
BoxConstraints widthConstraints =
|
||||
new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight);
|
||||
|
||||
double maxWidth = 0.0;
|
||||
RenderBox child = firstChild;
|
||||
while (child != null) {
|
||||
maxWidth = math.max(maxWidth, child.getMaxIntrinsicWidth(widthConstraints));
|
||||
assert(child.parentData is TabBarParentData);
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
double width = isScrollable ? maxWidth : maxWidth * childCount;
|
||||
return constraints.constrainWidth(width);
|
||||
}
|
||||
|
||||
double get _tabHeight => textAndIcons ? _kTextAndIconTabHeight : _kTabHeight;
|
||||
double get _tabBarHeight => _tabHeight + _kTabIndicatorHeight;
|
||||
|
||||
double _getIntrinsicHeight(BoxConstraints constraints) => constraints.constrainHeight(_tabBarHeight);
|
||||
|
||||
double getMinIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints);
|
||||
|
||||
double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints);
|
||||
|
||||
void layoutFixedWidthTabs() {
|
||||
double tabWidth = size.width / childCount;
|
||||
BoxConstraints tabConstraints =
|
||||
new BoxConstraints.tightFor(width: tabWidth, height: _tabHeight);
|
||||
double x = 0.0;
|
||||
RenderBox child = firstChild;
|
||||
while (child != null) {
|
||||
child.layout(tabConstraints);
|
||||
assert(child.parentData is TabBarParentData);
|
||||
child.parentData.position = new Point(x, 0.0);
|
||||
x += tabWidth;
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
double layoutScrollableTabs() {
|
||||
BoxConstraints tabConstraints = new BoxConstraints(
|
||||
minWidth: _kMinTabWidth,
|
||||
maxWidth: _kMaxTabWidth,
|
||||
minHeight: _tabHeight,
|
||||
maxHeight: _tabHeight
|
||||
);
|
||||
double x = 0.0;
|
||||
RenderBox child = firstChild;
|
||||
while (child != null) {
|
||||
child.layout(tabConstraints, parentUsesSize: true);
|
||||
assert(child.parentData is TabBarParentData);
|
||||
child.parentData.position = new Point(x, 0.0);
|
||||
x += child.size.width;
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
Size layoutSize;
|
||||
List<double> layoutWidths;
|
||||
LayoutChanged onLayoutChanged;
|
||||
|
||||
void reportLayoutChangedIfNeeded() {
|
||||
assert(onLayoutChanged != null);
|
||||
List<double> widths = new List<double>(childCount);
|
||||
if (!isScrollable && childCount > 0) {
|
||||
double tabWidth = size.width / childCount;
|
||||
widths.fillRange(0, widths.length, tabWidth);
|
||||
} else if (isScrollable) {
|
||||
RenderBox child = firstChild;
|
||||
int childIndex = 0;
|
||||
while (child != null) {
|
||||
widths[childIndex++] = child.size.width;
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
assert(childIndex == widths.length);
|
||||
}
|
||||
if (size != layoutSize || widths != layoutWidths) {
|
||||
layoutSize = size;
|
||||
layoutWidths = widths;
|
||||
onLayoutChanged(layoutSize, layoutWidths);
|
||||
}
|
||||
}
|
||||
|
||||
void performLayout() {
|
||||
assert(constraints is BoxConstraints);
|
||||
if (childCount == 0)
|
||||
return;
|
||||
|
||||
if (isScrollable) {
|
||||
double tabBarWidth = layoutScrollableTabs();
|
||||
size = constraints.constrain(new Size(tabBarWidth, _tabBarHeight));
|
||||
} else {
|
||||
size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight));
|
||||
layoutFixedWidthTabs();
|
||||
}
|
||||
|
||||
if (onLayoutChanged != null)
|
||||
reportLayoutChangedIfNeeded();
|
||||
}
|
||||
|
||||
void hitTestChildren(HitTestResult result, { Point position }) {
|
||||
defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
void _paintIndicator(PaintingCanvas canvas, RenderBox selectedTab, Offset offset) {
|
||||
if (indicatorColor == null)
|
||||
return;
|
||||
|
||||
if (indicatorRect != null) {
|
||||
canvas.drawRect(indicatorRect.shift(offset), new Paint()..color = indicatorColor);
|
||||
return;
|
||||
}
|
||||
|
||||
var size = new Size(selectedTab.size.width, _kTabIndicatorHeight);
|
||||
var point = new Point(
|
||||
selectedTab.parentData.position.x,
|
||||
_tabBarHeight - _kTabIndicatorHeight
|
||||
);
|
||||
Rect rect = (point + offset) & size;
|
||||
canvas.drawRect(rect, new Paint()..color = indicatorColor);
|
||||
}
|
||||
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (backgroundColor != null) {
|
||||
double width = layoutWidths != null
|
||||
? layoutWidths.reduce((sum, width) => sum + width)
|
||||
: size.width;
|
||||
Rect rect = offset & new Size(width, size.height);
|
||||
context.canvas.drawRect(rect, new Paint()..color = backgroundColor);
|
||||
}
|
||||
int index = 0;
|
||||
RenderBox child = firstChild;
|
||||
while (child != null) {
|
||||
assert(child.parentData is TabBarParentData);
|
||||
context.paintChild(child, child.parentData.position + offset);
|
||||
if (index++ == selectedIndex)
|
||||
_paintIndicator(context.canvas, child, offset);
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TabBarWrapper extends MultiChildRenderObjectWidget {
|
||||
_TabBarWrapper({
|
||||
Key key,
|
||||
List<Widget> children,
|
||||
this.selectedIndex,
|
||||
this.backgroundColor,
|
||||
this.indicatorColor,
|
||||
this.indicatorRect,
|
||||
this.textAndIcons,
|
||||
this.isScrollable: false,
|
||||
this.onLayoutChanged
|
||||
}) : super(key: key, children: children);
|
||||
|
||||
final int selectedIndex;
|
||||
final Color backgroundColor;
|
||||
final Color indicatorColor;
|
||||
final Rect indicatorRect;
|
||||
final bool textAndIcons;
|
||||
final bool isScrollable;
|
||||
final LayoutChanged onLayoutChanged;
|
||||
|
||||
RenderTabBar createRenderObject() {
|
||||
RenderTabBar result = new RenderTabBar(onLayoutChanged);
|
||||
updateRenderObject(result, null);
|
||||
return result;
|
||||
}
|
||||
|
||||
void updateRenderObject(RenderTabBar renderObject, _TabBarWrapper oldWidget) {
|
||||
renderObject.selectedIndex = selectedIndex;
|
||||
renderObject.backgroundColor = backgroundColor;
|
||||
renderObject.indicatorColor = indicatorColor;
|
||||
renderObject.indicatorRect = indicatorRect;
|
||||
renderObject.textAndIcons = textAndIcons;
|
||||
renderObject.isScrollable = isScrollable;
|
||||
renderObject.onLayoutChanged = onLayoutChanged;
|
||||
}
|
||||
}
|
||||
|
||||
class TabLabel {
|
||||
const TabLabel({ this.text, this.icon });
|
||||
|
||||
final String text;
|
||||
final String icon;
|
||||
}
|
||||
|
||||
class Tab extends StatelessComponent {
|
||||
Tab({
|
||||
Key key,
|
||||
this.label,
|
||||
this.color,
|
||||
this.selected: false,
|
||||
this.selectedColor
|
||||
}) : super(key: key) {
|
||||
assert(label.text != null || label.icon != null);
|
||||
}
|
||||
|
||||
final TabLabel label;
|
||||
final Color color;
|
||||
final bool selected;
|
||||
final Color selectedColor;
|
||||
|
||||
Widget _buildLabelText() {
|
||||
assert(label.text != null);
|
||||
TextStyle style = new TextStyle(color: selected ? selectedColor : color);
|
||||
return new Text(label.text, style: style);
|
||||
}
|
||||
|
||||
Widget _buildLabelIcon() {
|
||||
assert(label.icon != null);
|
||||
Color iconColor = selected ? selectedColor : color;
|
||||
sky.ColorFilter filter = new sky.ColorFilter.mode(iconColor, sky.TransferMode.srcATop);
|
||||
return new Icon(type: label.icon, size: _kTabIconSize, colorFilter: filter);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
Widget labelContent;
|
||||
if (label.icon == null) {
|
||||
labelContent = _buildLabelText();
|
||||
} else if (label.text == null) {
|
||||
labelContent = _buildLabelIcon();
|
||||
} else {
|
||||
labelContent = new Column(
|
||||
<Widget>[
|
||||
new Container(
|
||||
child: _buildLabelIcon(),
|
||||
margin: const EdgeDims.only(bottom: 10.0)
|
||||
),
|
||||
_buildLabelText()
|
||||
],
|
||||
justifyContent: FlexJustifyContent.center,
|
||||
alignItems: FlexAlignItems.center
|
||||
);
|
||||
}
|
||||
|
||||
Container centeredLabel = new Container(
|
||||
child: new Center(child: labelContent),
|
||||
constraints: new BoxConstraints(minWidth: _kMinTabWidth),
|
||||
padding: _kTabLabelPadding
|
||||
);
|
||||
|
||||
return new InkWell(child: centeredLabel);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabsScrollBehavior extends BoundedBehavior {
|
||||
_TabsScrollBehavior();
|
||||
|
||||
bool isScrollable = true;
|
||||
|
||||
Simulation release(double position, double velocity) {
|
||||
if (!isScrollable)
|
||||
return null;
|
||||
|
||||
double velocityPerSecond = velocity * 1000.0;
|
||||
return new BoundedFrictionSimulation(
|
||||
_kTabBarScrollDrag, position, velocityPerSecond, minScrollOffset, maxScrollOffset
|
||||
);
|
||||
}
|
||||
|
||||
double applyCurve(double scrollOffset, double scrollDelta) {
|
||||
return (isScrollable) ? super.applyCurve(scrollOffset, scrollDelta) : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
class TabBar extends Scrollable {
|
||||
TabBar({
|
||||
Key key,
|
||||
this.labels,
|
||||
this.selectedIndex: 0,
|
||||
this.onChanged,
|
||||
this.isScrollable: false
|
||||
}) : super(key: key, scrollDirection: ScrollDirection.horizontal);
|
||||
|
||||
final Iterable<TabLabel> labels;
|
||||
final int selectedIndex;
|
||||
final SelectedIndexChanged onChanged;
|
||||
final bool isScrollable;
|
||||
|
||||
TabBarState createState() => new TabBarState(this);
|
||||
}
|
||||
|
||||
class TabBarState extends ScrollableState<TabBar> {
|
||||
TabBarState(TabBar config) : super(config) {
|
||||
_indicatorAnimation = new ValueAnimation<Rect>()
|
||||
..duration = _kTabBarScroll
|
||||
..variable = new AnimatedRect(null, curve: ease);
|
||||
scrollBehavior.isScrollable = config.isScrollable;
|
||||
}
|
||||
|
||||
Size _tabBarSize;
|
||||
Size _viewportSize = Size.zero;
|
||||
List<double> _tabWidths;
|
||||
ValueAnimation<Rect> _indicatorAnimation;
|
||||
|
||||
void didUpdateConfig(TabBar oldConfig) {
|
||||
super.didUpdateConfig(oldConfig);
|
||||
if (!config.isScrollable)
|
||||
scrollTo(0.0);
|
||||
}
|
||||
|
||||
AnimatedRect get _indicatorRect => _indicatorAnimation.variable;
|
||||
|
||||
void _startIndicatorAnimation(int fromTabIndex, int toTabIndex) {
|
||||
_indicatorRect
|
||||
..begin = (_indicatorRect.value == null ? _tabIndicatorRect(fromTabIndex) : _indicatorRect.value)
|
||||
..end = _tabIndicatorRect(toTabIndex);
|
||||
_indicatorAnimation
|
||||
..progress = 0.0
|
||||
..play();
|
||||
}
|
||||
|
||||
ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior();
|
||||
_TabsScrollBehavior get scrollBehavior => super.scrollBehavior;
|
||||
|
||||
Rect _tabRect(int tabIndex) {
|
||||
assert(_tabBarSize != null);
|
||||
assert(_tabWidths != null);
|
||||
assert(tabIndex >= 0 && tabIndex < _tabWidths.length);
|
||||
double tabLeft = 0.0;
|
||||
if (tabIndex > 0)
|
||||
tabLeft = _tabWidths.take(tabIndex).reduce((sum, width) => sum + width);
|
||||
double tabTop = 0.0;
|
||||
double tabBottom = _tabBarSize.height - _kTabIndicatorHeight;
|
||||
double tabRight = tabLeft + _tabWidths[tabIndex];
|
||||
return new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
|
||||
}
|
||||
|
||||
Rect _tabIndicatorRect(int tabIndex) {
|
||||
Rect r = _tabRect(tabIndex);
|
||||
return new Rect.fromLTRB(r.left, r.bottom, r.right, r.bottom + _kTabIndicatorHeight);
|
||||
}
|
||||
|
||||
double _centeredTabScrollOffset(int tabIndex) {
|
||||
double viewportWidth = scrollBehavior.containerExtent;
|
||||
return (_tabRect(tabIndex).left + _tabWidths[tabIndex] / 2.0 - viewportWidth / 2.0)
|
||||
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
|
||||
}
|
||||
|
||||
void _handleTap(int tabIndex) {
|
||||
if (tabIndex != config.selectedIndex) {
|
||||
if (_tabWidths != null) {
|
||||
if (config.isScrollable)
|
||||
scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll);
|
||||
_startIndicatorAnimation(config.selectedIndex, tabIndex);
|
||||
}
|
||||
if (config.onChanged != null)
|
||||
config.onChanged(tabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) {
|
||||
return new GestureDetector(
|
||||
onTap: () => _handleTap(tabIndex),
|
||||
child: new Tab(
|
||||
label: label,
|
||||
color: color,
|
||||
selected: tabIndex == config.selectedIndex,
|
||||
selectedColor: selectedColor
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _updateScrollBehavior() {
|
||||
scrollBehavior.updateExtents(
|
||||
containerExtent: config.scrollDirection == ScrollDirection.vertical ? _viewportSize.height : _viewportSize.width,
|
||||
contentExtent: _tabWidths.reduce((sum, width) => sum + width)
|
||||
);
|
||||
}
|
||||
|
||||
void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
|
||||
setState(() {
|
||||
_tabBarSize = tabBarSize;
|
||||
_tabWidths = tabWidths;
|
||||
_updateScrollBehavior();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleViewportSizeChanged(Size newSize) {
|
||||
_viewportSize = newSize;
|
||||
_updateScrollBehavior();
|
||||
}
|
||||
|
||||
Widget buildContent(BuildContext context) {
|
||||
assert(config.labels != null && config.labels.isNotEmpty);
|
||||
|
||||
ThemeData themeData = Theme.of(context);
|
||||
Color backgroundColor = themeData.primaryColor;
|
||||
Color indicatorColor = themeData.accentColor;
|
||||
if (indicatorColor == backgroundColor) {
|
||||
indicatorColor = Colors.white;
|
||||
}
|
||||
|
||||
TextStyle textStyle;
|
||||
IconThemeColor iconThemeColor;
|
||||
switch (themeData.primaryColorBrightness) {
|
||||
case ThemeBrightness.light:
|
||||
textStyle = Typography.black.body1;
|
||||
iconThemeColor = IconThemeColor.black;
|
||||
break;
|
||||
case ThemeBrightness.dark:
|
||||
textStyle = Typography.white.body1;
|
||||
iconThemeColor = IconThemeColor.white;
|
||||
break;
|
||||
}
|
||||
|
||||
List<Widget> tabs = <Widget>[];
|
||||
bool textAndIcons = false;
|
||||
int tabIndex = 0;
|
||||
for (TabLabel label in config.labels) {
|
||||
tabs.add(_toTab(label, tabIndex++, textStyle.color, indicatorColor));
|
||||
if (label.text != null && label.icon != null)
|
||||
textAndIcons = true;
|
||||
}
|
||||
|
||||
Widget tabBar = new IconTheme(
|
||||
data: new IconThemeData(color: iconThemeColor),
|
||||
child: new DefaultTextStyle(
|
||||
style: textStyle,
|
||||
child: new BuilderTransition(
|
||||
variables: [_indicatorRect],
|
||||
performance: _indicatorAnimation.view,
|
||||
builder: (BuildContext context) {
|
||||
return new _TabBarWrapper(
|
||||
children: tabs,
|
||||
selectedIndex: config.selectedIndex,
|
||||
backgroundColor: backgroundColor,
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorRect: _indicatorRect.value,
|
||||
textAndIcons: textAndIcons,
|
||||
isScrollable: config.isScrollable,
|
||||
onLayoutChanged: _layoutChanged
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (!config.isScrollable)
|
||||
return tabBar;
|
||||
|
||||
return new SizeObserver(
|
||||
callback: _handleViewportSizeChanged,
|
||||
child: new Viewport(
|
||||
scrollDirection: ScrollDirection.horizontal,
|
||||
scrollOffset: new Offset(scrollOffset, 0.0),
|
||||
child: tabBar
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabNavigatorView {
|
||||
TabNavigatorView({ this.label, this.builder });
|
||||
|
||||
final TabLabel label;
|
||||
final WidgetBuilder builder;
|
||||
|
||||
Widget buildContent(BuildContext context) {
|
||||
assert(builder != null);
|
||||
Widget content = builder(context);
|
||||
assert(content != null);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
class TabNavigator extends StatelessComponent {
|
||||
TabNavigator({
|
||||
Key key,
|
||||
this.views,
|
||||
this.selectedIndex: 0,
|
||||
this.onChanged,
|
||||
this.isScrollable: false
|
||||
}) : super(key: key);
|
||||
|
||||
final List<TabNavigatorView> views;
|
||||
final int selectedIndex;
|
||||
final SelectedIndexChanged onChanged;
|
||||
final bool isScrollable;
|
||||
|
||||
void _handleSelectedIndexChanged(int tabIndex) {
|
||||
if (onChanged != null)
|
||||
onChanged(tabIndex);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
assert(views != null && views.isNotEmpty);
|
||||
assert(selectedIndex >= 0 && selectedIndex < views.length);
|
||||
|
||||
TabBar tabBar = new TabBar(
|
||||
labels: views.map((view) => view.label),
|
||||
onChanged: _handleSelectedIndexChanged,
|
||||
selectedIndex: selectedIndex,
|
||||
isScrollable: isScrollable
|
||||
);
|
||||
|
||||
Widget content = views[selectedIndex].buildContent(context);
|
||||
return new Column([tabBar, new Flexible(child: content)]);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user