You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
samples/web/timeflow/lib/numberpicker.dart

528 lines
17 KiB

// Package numberpicker:
// https://pub.dartlang.org/packages/numberpicker
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'infinite_listview.dart';
/// Created by Marcin Szałek
///NumberPicker is a widget designed to pick a number between #minValue and #maxValue
class NumberPicker extends StatelessWidget {
///height of every list element
static const double DEFAULT_ITEM_EXTENT = 50.0;
///width of list view
static const double DEFAULT_LISTVIEW_WIDTH = 100.0;
///constructor for integer number picker
NumberPicker.integer({
Key key,
@required int initialValue,
@required this.minValue,
@required this.maxValue,
@required this.onChanged,
this.itemExtent = DEFAULT_ITEM_EXTENT,
this.listViewWidth = DEFAULT_LISTVIEW_WIDTH,
this.step = 1,
this.infiniteLoop = false,
}) : assert(initialValue != null),
assert(minValue != null),
assert(maxValue != null),
assert(maxValue > minValue),
assert(initialValue >= minValue && initialValue <= maxValue),
assert(step > 0),
selectedIntValue = initialValue,
selectedDecimalValue = -1,
decimalPlaces = 0,
intScrollController = infiniteLoop
? new InfiniteScrollController(
initialScrollOffset:
(initialValue - minValue) ~/ step * itemExtent,
)
: new ScrollController(
initialScrollOffset:
(initialValue - minValue) ~/ step * itemExtent,
),
decimalScrollController = null,
_listViewHeight = 3 * itemExtent,
integerItemCount = (maxValue - minValue) ~/ step + 1,
super(key: key);
///constructor for decimal number picker
NumberPicker.decimal({
Key key,
@required double initialValue,
@required this.minValue,
@required this.maxValue,
@required this.onChanged,
this.decimalPlaces = 1,
this.itemExtent = DEFAULT_ITEM_EXTENT,
this.listViewWidth = DEFAULT_LISTVIEW_WIDTH,
}) : assert(initialValue != null),
assert(minValue != null),
assert(maxValue != null),
assert(decimalPlaces != null && decimalPlaces > 0),
assert(maxValue > minValue),
assert(initialValue >= minValue && initialValue <= maxValue),
selectedIntValue = initialValue.floor(),
selectedDecimalValue = ((initialValue - initialValue.floorToDouble()) *
math.pow(10, decimalPlaces))
.round(),
intScrollController = new ScrollController(
initialScrollOffset: (initialValue.floor() - minValue) * itemExtent,
),
decimalScrollController = new ScrollController(
initialScrollOffset: ((initialValue - initialValue.floorToDouble()) *
math.pow(10, decimalPlaces))
.roundToDouble() *
itemExtent,
),
_listViewHeight = 3 * itemExtent,
step = 1,
integerItemCount = maxValue.floor() - minValue.floor() + 1,
infiniteLoop = false,
super(key: key);
///called when selected value changes
final ValueChanged<num> onChanged;
///min value user can pick
final int minValue;
///max value user can pick
final int maxValue;
///inidcates how many decimal places to show
/// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...]
final int decimalPlaces;
///height of every list element in pixels
final double itemExtent;
///view will always contain only 3 elements of list in pixels
final double _listViewHeight;
///width of list view in pixels
final double listViewWidth;
///ScrollController used for integer list
final ScrollController intScrollController;
///ScrollController used for decimal list
final ScrollController decimalScrollController;
///Currently selected integer value
final int selectedIntValue;
///Currently selected decimal value
final int selectedDecimalValue;
///Step between elements. Only for integer datePicker
///Examples:
/// if step is 100 the following elements may be 100, 200, 300...
/// if min=0, max=6, step=3, then items will be 0, 3 and 6
/// if min=0, max=5, step=3, then items will be 0 and 3.
final int step;
///Repeat values infinitely
final bool infiniteLoop;
///Amount of items
final int integerItemCount;
//
//----------------------------- PUBLIC ------------------------------
//
animateInt(int valueToSelect) {
int diff = valueToSelect - minValue;
int index = diff ~/ step;
animateIntToIndex(index);
}
animateIntToIndex(int index) {
_animate(intScrollController, index * itemExtent);
}
animateDecimal(int decimalValue) {
_animate(decimalScrollController, decimalValue * itemExtent);
}
animateDecimalAndInteger(double valueToSelect) {
animateInt(valueToSelect.floor());
animateDecimal(((valueToSelect - valueToSelect.floorToDouble()) *
math.pow(10, decimalPlaces))
.round());
}
//
//----------------------------- VIEWS -----------------------------
//
///main widget
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
if (infiniteLoop) {
return _integerInfiniteListView(themeData);
}
if (decimalPlaces == 0) {
return _integerListView(themeData);
} else {
return new Row(
children: <Widget>[
_integerListView(themeData),
_decimalListView(themeData),
],
mainAxisAlignment: MainAxisAlignment.center,
);
}
}
Widget _integerListView(ThemeData themeData) {
TextStyle defaultStyle = themeData.textTheme.body1;
TextStyle selectedStyle =
themeData.textTheme.headline.copyWith(color: themeData.accentColor);
var listItemCount = integerItemCount + 2;
return new NotificationListener(
child: new Container(
height: _listViewHeight,
width: listViewWidth,
child: new ListView.builder(
controller: intScrollController,
itemExtent: itemExtent,
itemCount: listItemCount,
cacheExtent: _calculateCacheExtent(listItemCount),
itemBuilder: (BuildContext context, int index) {
final int value = _intValueFromIndex(index);
//define special style for selected (middle) element
final TextStyle itemStyle =
value == selectedIntValue ? selectedStyle : defaultStyle;
bool isExtra = index == 0 || index == listItemCount - 1;
return isExtra
? new Container() //empty first and last element
: new Center(
child: new Text(value.toString(), style: itemStyle),
);
},
),
),
onNotification: _onIntegerNotification,
);
}
Widget _decimalListView(ThemeData themeData) {
TextStyle defaultStyle = themeData.textTheme.body1;
TextStyle selectedStyle =
themeData.textTheme.headline.copyWith(color: themeData.accentColor);
int decimalItemCount =
selectedIntValue == maxValue ? 3 : math.pow(10, decimalPlaces) + 2;
return new NotificationListener(
child: new Container(
height: _listViewHeight,
width: listViewWidth,
child: new ListView.builder(
controller: decimalScrollController,
itemExtent: itemExtent,
itemCount: decimalItemCount,
itemBuilder: (BuildContext context, int index) {
final int value = index - 1;
//define special style for selected (middle) element
final TextStyle itemStyle =
value == selectedDecimalValue ? selectedStyle : defaultStyle;
bool isExtra = index == 0 || index == decimalItemCount - 1;
return isExtra
? new Container() //empty first and last element
: new Center(
child: new Text(
value.toString().padLeft(decimalPlaces, '0'),
style: itemStyle),
);
},
),
),
onNotification: _onDecimalNotification,
);
}
Widget _integerInfiniteListView(ThemeData themeData) {
TextStyle defaultStyle = themeData.textTheme.body1;
TextStyle selectedStyle =
themeData.textTheme.headline.copyWith(color: themeData.accentColor);
return new NotificationListener(
child: new Container(
height: _listViewHeight,
width: listViewWidth,
child: new InfiniteListView.builder(
controller: intScrollController,
itemExtent: itemExtent,
itemBuilder: (BuildContext context, int index) {
final int value = _intValueFromIndex(index);
//define special style for selected (middle) element
final TextStyle itemStyle =
value == selectedIntValue ? selectedStyle : defaultStyle;
return new Center(
child: new Text(value.toString(), style: itemStyle),
);
},
),
),
onNotification: _onIntegerNotification,
);
}
//
// ----------------------------- LOGIC -----------------------------
//
int _intValueFromIndex(int index) {
index--;
index %= integerItemCount;
return minValue + index * step;
}
bool _onIntegerNotification(Notification notification) {
if (notification is ScrollNotification) {
//calculate
int intIndexOfMiddleElement =
(notification.metrics.pixels / itemExtent).round();
if (!infiniteLoop) {
intIndexOfMiddleElement =
intIndexOfMiddleElement.clamp(0, integerItemCount - 1);
}
int intValueInTheMiddle = _intValueFromIndex(intIndexOfMiddleElement + 1);
intValueInTheMiddle = _normalizeIntegerMiddleValue(intValueInTheMiddle);
if (_userStoppedScrolling(notification, intScrollController)) {
//center selected value
animateIntToIndex(intIndexOfMiddleElement);
}
//update selection
if (intValueInTheMiddle != selectedIntValue) {
num newValue;
if (decimalPlaces == 0) {
//return integer value
newValue = (intValueInTheMiddle);
} else {
if (intValueInTheMiddle == maxValue) {
//if new value is maxValue, then return that value and ignore decimal
newValue = (intValueInTheMiddle.toDouble());
animateDecimal(0);
} else {
//return integer+decimal
double decimalPart = _toDecimal(selectedDecimalValue);
newValue = ((intValueInTheMiddle + decimalPart).toDouble());
}
}
onChanged(newValue);
}
}
return true;
}
bool _onDecimalNotification(Notification notification) {
if (notification is ScrollNotification) {
//calculate middle value
int indexOfMiddleElement =
(notification.metrics.pixels + _listViewHeight / 2) ~/ itemExtent;
int decimalValueInTheMiddle = indexOfMiddleElement - 1;
decimalValueInTheMiddle =
_normalizeDecimalMiddleValue(decimalValueInTheMiddle);
if (_userStoppedScrolling(notification, decimalScrollController)) {
//center selected value
animateDecimal(decimalValueInTheMiddle);
}
//update selection
if (selectedIntValue != maxValue &&
decimalValueInTheMiddle != selectedDecimalValue) {
double decimalPart = _toDecimal(decimalValueInTheMiddle);
double newValue = ((selectedIntValue + decimalPart).toDouble());
onChanged(newValue);
}
}
return true;
}
///There was a bug, when if there was small integer range, e.g. from 1 to 5,
///When user scrolled to the top, whole listview got displayed.
///To prevent this we are calculating cacheExtent by our own so it gets smaller if number of items is smaller
double _calculateCacheExtent(int itemCount) {
double cacheExtent = 250.0; //default cache extent
if ((itemCount - 2) * DEFAULT_ITEM_EXTENT <= cacheExtent) {
cacheExtent = ((itemCount - 3) * DEFAULT_ITEM_EXTENT);
}
return cacheExtent;
}
///When overscroll occurs on iOS,
///we can end up with value not in the range between [minValue] and [maxValue]
///To avoid going out of range, we change values out of range to border values.
int _normalizeMiddleValue(int valueInTheMiddle, int min, int max) {
return math.max(math.min(valueInTheMiddle, max), min);
}
int _normalizeIntegerMiddleValue(int integerValueInTheMiddle) {
//make sure that max is a multiple of step
int max = (maxValue ~/ step) * step;
return _normalizeMiddleValue(integerValueInTheMiddle, minValue, max);
}
int _normalizeDecimalMiddleValue(int decimalValueInTheMiddle) {
return _normalizeMiddleValue(
decimalValueInTheMiddle, 0, math.pow(10, decimalPlaces) - 1);
}
///indicates if user has stopped scrolling so we can center value in the middle
bool _userStoppedScrolling(
Notification notification, ScrollController scrollController) {
return notification is UserScrollNotification &&
notification.direction == ScrollDirection.idle &&
// ignore: invalid_use_of_protected_member,invalid_use_of_visible_for_testing_member
scrollController.position.activity is! HoldScrollActivity;
}
///converts integer indicator of decimal value to double
///e.g. decimalPlaces = 1, value = 4 >>> result = 0.4
/// decimalPlaces = 2, value = 12 >>> result = 0.12
double _toDecimal(int decimalValueAsInteger) {
return double.parse((decimalValueAsInteger * math.pow(10, -decimalPlaces))
.toStringAsFixed(decimalPlaces));
}
///scroll to selected value
_animate(ScrollController scrollController, double value) {
scrollController.animateTo(value,
duration: new Duration(seconds: 1), curve: new ElasticOutCurve());
}
}
///Returns AlertDialog as a Widget so it is designed to be used in showDialog method
class NumberPickerDialog extends StatefulWidget {
final int minValue;
final int maxValue;
final int initialIntegerValue;
final double initialDoubleValue;
final int decimalPlaces;
final Widget title;
final EdgeInsets titlePadding;
final Widget confirmWidget;
final Widget cancelWidget;
final int step;
final bool infiniteLoop;
///constructor for integer values
NumberPickerDialog.integer({
@required this.minValue,
@required this.maxValue,
@required this.initialIntegerValue,
this.title,
this.titlePadding,
this.step = 1,
this.infiniteLoop = false,
Widget confirmWidget,
Widget cancelWidget,
}) : confirmWidget = confirmWidget ?? new Text("OK"),
cancelWidget = cancelWidget ?? new Text("CANCEL"),
decimalPlaces = 0,
initialDoubleValue = -1.0;
///constructor for decimal values
NumberPickerDialog.decimal({
@required this.minValue,
@required this.maxValue,
@required this.initialDoubleValue,
this.decimalPlaces = 1,
this.title,
this.titlePadding,
Widget confirmWidget,
Widget cancelWidget,
}) : confirmWidget = confirmWidget ?? new Text("OK"),
cancelWidget = cancelWidget ?? new Text("CANCEL"),
initialIntegerValue = -1,
step = 1,
infiniteLoop = false;
@override
State<NumberPickerDialog> createState() =>
new _NumberPickerDialogControllerState(
initialIntegerValue, initialDoubleValue);
}
class _NumberPickerDialogControllerState extends State<NumberPickerDialog> {
int selectedIntValue;
double selectedDoubleValue;
_NumberPickerDialogControllerState(
this.selectedIntValue, this.selectedDoubleValue);
_handleValueChanged(num value) {
if (value is int) {
setState(() => selectedIntValue = value);
} else {
setState(() => selectedDoubleValue = value);
}
}
NumberPicker _buildNumberPicker() {
if (widget.decimalPlaces > 0) {
return new NumberPicker.decimal(
initialValue: selectedDoubleValue,
minValue: widget.minValue,
maxValue: widget.maxValue,
decimalPlaces: widget.decimalPlaces,
onChanged: _handleValueChanged);
} else {
return new NumberPicker.integer(
initialValue: selectedIntValue,
minValue: widget.minValue,
maxValue: widget.maxValue,
step: widget.step,
infiniteLoop: widget.infiniteLoop,
onChanged: _handleValueChanged,
);
}
}
@override
Widget build(BuildContext context) {
return new AlertDialog(
title: widget.title,
titlePadding: widget.titlePadding,
content: _buildNumberPicker(),
actions: [
new FlatButton(
onPressed: () => Navigator.of(context).pop(),
child: widget.cancelWidget,
),
new FlatButton(
onPressed: () => Navigator.of(context).pop(widget.decimalPlaces > 0
? selectedDoubleValue
: selectedIntValue),
child: widget.confirmWidget),
],
);
}
}