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;
|
Function callback;
|
||||||
RouteBase route;
|
RouteBase route;
|
||||||
StatefulComponent owner;
|
ComponentState owner;
|
||||||
|
|
||||||
bool get isOpaque => false;
|
bool get isOpaque => false;
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ class NavigatorState extends ComponentState<Navigator> {
|
|||||||
|
|
||||||
RouteBase get currentRoute => config.history.currentRoute;
|
RouteBase get currentRoute => config.history.currentRoute;
|
||||||
|
|
||||||
void pushState(StatefulComponent owner, Function callback) {
|
void pushState(ComponentState owner, Function callback) {
|
||||||
RouteBase route = new RouteState(
|
RouteBase route = new RouteState(
|
||||||
owner: owner,
|
owner: owner,
|
||||||
callback: callback,
|
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