wt1.1.2e NumberPicker
This commit is contained in:
@@ -0,0 +1,772 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:infinite_listview/infinite_listview.dart';
|
||||
|
||||
/// Created by Marcin Szałek
|
||||
|
||||
///Define a text mapper to transform the text displayed by the picker
|
||||
typedef String TextMapper(String numberText);
|
||||
|
||||
///NumberPicker is a widget designed to pick a number between #minValue and #maxValue
|
||||
class NumberPicker extends StatelessWidget {
|
||||
///height of every list element for normal number picker
|
||||
///width of every list element for horizontal number picker
|
||||
static const double kDefaultItemExtent = 60.0;
|
||||
|
||||
///width of list view for normal number picker
|
||||
///height of list view for horizontal number picker
|
||||
static const double kDefaultListViewCrossAxisSize = 120.0;
|
||||
|
||||
///constructor for horizontal number picker
|
||||
NumberPicker.horizontal({
|
||||
Key key,
|
||||
@required int initialValue,
|
||||
@required this.minValue,
|
||||
@required this.maxValue,
|
||||
@required this.onChanged,
|
||||
this.textMapper,
|
||||
this.itemExtent = kDefaultItemExtent,
|
||||
this.listViewHeight = kDefaultListViewCrossAxisSize,
|
||||
this.step = 1,
|
||||
this.zeroPad = false,
|
||||
this.highlightSelectedValue = true,
|
||||
this.decoration,
|
||||
this.haptics = false,
|
||||
this.textStyle,
|
||||
this.textStyleHighlighted
|
||||
}) : 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 = ScrollController(
|
||||
initialScrollOffset: (initialValue - minValue) ~/ step * itemExtent,
|
||||
),
|
||||
scrollDirection = Axis.horizontal,
|
||||
decimalScrollController = null,
|
||||
listViewWidth = 3 * itemExtent,
|
||||
infiniteLoop = false,
|
||||
integerItemCount = (maxValue - minValue) ~/ step + 1,
|
||||
super(key: key);
|
||||
|
||||
///constructor for integer number picker
|
||||
NumberPicker.integer({
|
||||
Key key,
|
||||
@required int initialValue,
|
||||
@required this.minValue,
|
||||
@required this.maxValue,
|
||||
@required this.onChanged,
|
||||
this.textMapper,
|
||||
this.itemExtent = kDefaultItemExtent,
|
||||
this.listViewWidth = kDefaultListViewCrossAxisSize,
|
||||
this.step = 1,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.infiniteLoop = false,
|
||||
this.zeroPad = false,
|
||||
this.highlightSelectedValue = true,
|
||||
this.decoration,
|
||||
this.haptics = false,
|
||||
this.textStyle,
|
||||
this.textStyleHighlighted
|
||||
}) : assert(initialValue != null),
|
||||
assert(minValue != null),
|
||||
assert(maxValue != null),
|
||||
assert(maxValue > minValue),
|
||||
assert(initialValue >= minValue && initialValue <= maxValue),
|
||||
assert(step > 0),
|
||||
assert(scrollDirection != null),
|
||||
selectedIntValue = initialValue,
|
||||
selectedDecimalValue = -1,
|
||||
decimalPlaces = 0,
|
||||
intScrollController = infiniteLoop
|
||||
? InfiniteScrollController(
|
||||
initialScrollOffset:
|
||||
(initialValue - minValue) ~/ step * itemExtent,
|
||||
)
|
||||
: 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.textMapper,
|
||||
this.decimalPlaces = 1,
|
||||
this.itemExtent = kDefaultItemExtent,
|
||||
this.listViewWidth = kDefaultListViewCrossAxisSize,
|
||||
this.highlightSelectedValue = true,
|
||||
this.decoration,
|
||||
this.haptics = false,
|
||||
this.textStyle,
|
||||
this.textStyleHighlighted
|
||||
}) : 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 = ScrollController(
|
||||
initialScrollOffset: (initialValue.floor() - minValue) * itemExtent,
|
||||
),
|
||||
decimalScrollController = ScrollController(
|
||||
initialScrollOffset: ((initialValue - initialValue.floorToDouble()) *
|
||||
math.pow(10, decimalPlaces))
|
||||
.roundToDouble() *
|
||||
itemExtent,
|
||||
),
|
||||
listViewHeight = 3 * itemExtent,
|
||||
step = 1,
|
||||
scrollDirection = Axis.vertical,
|
||||
integerItemCount = maxValue.floor() - minValue.floor() + 1,
|
||||
infiniteLoop = false,
|
||||
zeroPad = 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;
|
||||
|
||||
///build the text of each item on the picker
|
||||
final TextMapper textMapper;
|
||||
|
||||
///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;
|
||||
|
||||
///height of list view 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;
|
||||
|
||||
///If currently selected value should be highlighted
|
||||
final bool highlightSelectedValue;
|
||||
|
||||
///Decoration to apply to central box where the selected value is placed
|
||||
final Decoration decoration;
|
||||
|
||||
///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;
|
||||
|
||||
/// Direction of scrolling
|
||||
final Axis scrollDirection;
|
||||
|
||||
///Repeat values infinitely
|
||||
final bool infiniteLoop;
|
||||
|
||||
///Pads displayed integer values up to the length of maxValue
|
||||
final bool zeroPad;
|
||||
|
||||
///Amount of items
|
||||
final int integerItemCount;
|
||||
|
||||
///Whether to trigger haptic pulses or not
|
||||
final bool haptics;
|
||||
|
||||
///TextStyle of the non-highlighted numbers
|
||||
final TextStyle textStyle;
|
||||
|
||||
///TextStyle of the highlighted numbers
|
||||
final TextStyle textStyleHighlighted;
|
||||
|
||||
//
|
||||
//----------------------------- PUBLIC ------------------------------
|
||||
//
|
||||
|
||||
/// Used to animate integer number picker to new selected value
|
||||
void animateInt(int valueToSelect) {
|
||||
int diff = valueToSelect - minValue;
|
||||
int index = diff ~/ step;
|
||||
animateIntToIndex(index);
|
||||
}
|
||||
|
||||
/// Used to animate integer number picker to new selected index
|
||||
void animateIntToIndex(int index) {
|
||||
_animate(intScrollController, index * itemExtent);
|
||||
}
|
||||
|
||||
/// Used to animate decimal part of double value to new selected value
|
||||
void animateDecimal(int decimalValue) {
|
||||
_animate(decimalScrollController, decimalValue * itemExtent);
|
||||
}
|
||||
|
||||
/// Used to animate decimal number picker to selected value
|
||||
void 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 Row(
|
||||
children: <Widget>[
|
||||
_integerListView(themeData),
|
||||
_decimalListView(themeData),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _integerListView(ThemeData themeData) {
|
||||
TextStyle defaultStyle = textStyle == null ?
|
||||
themeData.textTheme.body1 : textStyle;
|
||||
TextStyle selectedStyle = textStyleHighlighted == null ?
|
||||
themeData.textTheme.headline.copyWith(color: themeData.accentColor)
|
||||
: textStyleHighlighted;
|
||||
|
||||
var listItemCount = integerItemCount + 2;
|
||||
|
||||
return Listener(
|
||||
onPointerUp: (ev) {
|
||||
///used to detect that user stopped scrolling
|
||||
if (intScrollController.position.activity is HoldScrollActivity) {
|
||||
animateInt(selectedIntValue);
|
||||
}
|
||||
},
|
||||
child: NotificationListener(
|
||||
child: Container(
|
||||
height: listViewHeight,
|
||||
width: listViewWidth,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
ListView.builder(
|
||||
scrollDirection: scrollDirection,
|
||||
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 && highlightSelectedValue
|
||||
? selectedStyle
|
||||
: defaultStyle;
|
||||
|
||||
bool isExtra = index == 0 || index == listItemCount - 1;
|
||||
|
||||
return isExtra
|
||||
? Container() //empty first and last element
|
||||
: Center(
|
||||
child: Text(
|
||||
getDisplayedValue(value),
|
||||
style: itemStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_NumberPickerSelectedItemDecoration(
|
||||
axis: scrollDirection,
|
||||
itemExtent: itemExtent,
|
||||
decoration: decoration,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onNotification: _onIntegerNotification,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _decimalListView(ThemeData themeData) {
|
||||
TextStyle defaultStyle = textStyle == null ?
|
||||
themeData.textTheme.body1 : textStyle;
|
||||
TextStyle selectedStyle = textStyleHighlighted == null ?
|
||||
themeData.textTheme.headline.copyWith(color: themeData.accentColor)
|
||||
: textStyleHighlighted;
|
||||
|
||||
|
||||
int decimalItemCount =
|
||||
selectedIntValue == maxValue ? 3 : math.pow(10, decimalPlaces) + 2;
|
||||
|
||||
return Listener(
|
||||
onPointerUp: (ev) {
|
||||
///used to detect that user stopped scrolling
|
||||
if (decimalScrollController.position.activity is HoldScrollActivity) {
|
||||
animateDecimal(selectedDecimalValue);
|
||||
}
|
||||
},
|
||||
child: NotificationListener(
|
||||
child: Container(
|
||||
height: listViewHeight,
|
||||
width: listViewWidth,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
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 && highlightSelectedValue
|
||||
? selectedStyle
|
||||
: defaultStyle;
|
||||
|
||||
bool isExtra = index == 0 || index == decimalItemCount - 1;
|
||||
|
||||
return isExtra
|
||||
? Container() //empty first and last element
|
||||
: Center(
|
||||
child: Text(
|
||||
value.toString().padLeft(decimalPlaces, '0'),
|
||||
style: itemStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_NumberPickerSelectedItemDecoration(
|
||||
axis: scrollDirection,
|
||||
itemExtent: itemExtent,
|
||||
decoration: decoration,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onNotification: _onDecimalNotification,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _integerInfiniteListView(ThemeData themeData) {
|
||||
TextStyle defaultStyle = textStyle == null ?
|
||||
themeData.textTheme.body1 : textStyle;
|
||||
TextStyle selectedStyle = textStyleHighlighted == null ?
|
||||
themeData.textTheme.headline.copyWith(color: themeData.accentColor)
|
||||
: textStyleHighlighted;
|
||||
|
||||
|
||||
return Listener(
|
||||
onPointerUp: (ev) {
|
||||
///used to detect that user stopped scrolling
|
||||
if (intScrollController.position.activity is HoldScrollActivity) {
|
||||
_animateIntWhenUserStoppedScrolling(selectedIntValue);
|
||||
}
|
||||
},
|
||||
child: NotificationListener(
|
||||
child: Container(
|
||||
height: listViewHeight,
|
||||
width: listViewWidth,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
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 && highlightSelectedValue
|
||||
? selectedStyle
|
||||
: defaultStyle;
|
||||
|
||||
return Center(
|
||||
child: Text(
|
||||
getDisplayedValue(value),
|
||||
style: itemStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_NumberPickerSelectedItemDecoration(
|
||||
axis: scrollDirection,
|
||||
itemExtent: itemExtent,
|
||||
decoration: decoration,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onNotification: _onIntegerNotification,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getDisplayedValue(int value) {
|
||||
final text = zeroPad
|
||||
? value.toString().padLeft(maxValue.toString().length, '0')
|
||||
: value.toString();
|
||||
return textMapper != null ? textMapper(text) : text;
|
||||
}
|
||||
|
||||
//
|
||||
// ----------------------------- 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());
|
||||
}
|
||||
}
|
||||
if (haptics) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
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());
|
||||
if (haptics) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
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) * kDefaultItemExtent <= cacheExtent) {
|
||||
cacheExtent = ((itemCount - 3) * kDefaultItemExtent);
|
||||
}
|
||||
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 &&
|
||||
scrollController.position.activity is! HoldScrollActivity;
|
||||
}
|
||||
|
||||
/// Allows to find currently selected element index and animate this element
|
||||
/// Use it only when user manually stops scrolling in infinite loop
|
||||
void _animateIntWhenUserStoppedScrolling(int valueToSelect) {
|
||||
// estimated index of currently selected element based on offset and item extent
|
||||
int currentlySelectedElementIndex =
|
||||
intScrollController.offset ~/ itemExtent;
|
||||
|
||||
// when more(less) than half of the top(bottom) element is hidden
|
||||
// then we should increment(decrement) index in case of positive(negative) offset
|
||||
if (intScrollController.offset > 0 &&
|
||||
intScrollController.offset % itemExtent > itemExtent / 2) {
|
||||
currentlySelectedElementIndex++;
|
||||
} else if (intScrollController.offset < 0 &&
|
||||
intScrollController.offset % itemExtent < itemExtent / 2) {
|
||||
currentlySelectedElementIndex--;
|
||||
}
|
||||
|
||||
animateIntToIndex(currentlySelectedElementIndex);
|
||||
}
|
||||
|
||||
///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: Duration(seconds: 1),
|
||||
curve: ElasticOutCurve(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NumberPickerSelectedItemDecoration extends StatelessWidget {
|
||||
final Axis axis;
|
||||
final double itemExtent;
|
||||
final Decoration decoration;
|
||||
|
||||
const _NumberPickerSelectedItemDecoration(
|
||||
{Key key,
|
||||
@required this.axis,
|
||||
@required this.itemExtent,
|
||||
@required this.decoration})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: isVertical ? double.infinity : itemExtent,
|
||||
height: isVertical ? itemExtent : double.infinity,
|
||||
decoration: decoration,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isVertical => axis == Axis.vertical;
|
||||
}
|
||||
|
||||
///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;
|
||||
final bool zeroPad;
|
||||
final bool highlightSelectedValue;
|
||||
final Decoration decoration;
|
||||
final TextMapper textMapper;
|
||||
final bool haptics;
|
||||
|
||||
///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,
|
||||
this.zeroPad = false,
|
||||
this.highlightSelectedValue = true,
|
||||
this.decoration,
|
||||
this.textMapper,
|
||||
this.haptics = false,
|
||||
Widget confirmWidget,
|
||||
Widget cancelWidget,
|
||||
}) : confirmWidget = confirmWidget ?? Text("OK"),
|
||||
cancelWidget = cancelWidget ?? 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,
|
||||
this.highlightSelectedValue = true,
|
||||
this.decoration,
|
||||
this.textMapper,
|
||||
this.haptics = false,
|
||||
Widget confirmWidget,
|
||||
Widget cancelWidget,
|
||||
}) : confirmWidget = confirmWidget ?? Text("OK"),
|
||||
cancelWidget = cancelWidget ?? Text("CANCEL"),
|
||||
initialIntegerValue = -1,
|
||||
step = 1,
|
||||
infiniteLoop = false,
|
||||
zeroPad = false;
|
||||
|
||||
@override
|
||||
State<NumberPickerDialog> createState() => _NumberPickerDialogControllerState(
|
||||
initialIntegerValue, initialDoubleValue);
|
||||
}
|
||||
|
||||
class _NumberPickerDialogControllerState extends State<NumberPickerDialog> {
|
||||
int selectedIntValue;
|
||||
double selectedDoubleValue;
|
||||
|
||||
_NumberPickerDialogControllerState(
|
||||
this.selectedIntValue, this.selectedDoubleValue);
|
||||
|
||||
void _handleValueChanged(num value) {
|
||||
if (value is int) {
|
||||
setState(() => selectedIntValue = value);
|
||||
} else {
|
||||
setState(() => selectedDoubleValue = value);
|
||||
}
|
||||
}
|
||||
|
||||
NumberPicker _buildNumberPicker() {
|
||||
if (widget.decimalPlaces > 0) {
|
||||
return NumberPicker.decimal(
|
||||
initialValue: selectedDoubleValue,
|
||||
minValue: widget.minValue,
|
||||
maxValue: widget.maxValue,
|
||||
decimalPlaces: widget.decimalPlaces,
|
||||
highlightSelectedValue: widget.highlightSelectedValue,
|
||||
decoration: widget.decoration,
|
||||
onChanged: _handleValueChanged,
|
||||
textMapper: widget.textMapper,
|
||||
haptics: widget.haptics,
|
||||
);
|
||||
} else {
|
||||
return NumberPicker.integer(
|
||||
initialValue: selectedIntValue,
|
||||
minValue: widget.minValue,
|
||||
maxValue: widget.maxValue,
|
||||
step: widget.step,
|
||||
infiniteLoop: widget.infiniteLoop,
|
||||
zeroPad: widget.zeroPad,
|
||||
highlightSelectedValue: widget.highlightSelectedValue,
|
||||
decoration: widget.decoration,
|
||||
onChanged: _handleValueChanged,
|
||||
textMapper: widget.textMapper,
|
||||
haptics: widget.haptics,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: widget.title,
|
||||
titlePadding: widget.titlePadding,
|
||||
content: _buildNumberPicker(),
|
||||
actions: [
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: widget.cancelWidget,
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(widget.decimalPlaces > 0
|
||||
? selectedDoubleValue
|
||||
: selectedIntValue),
|
||||
child: widget.confirmWidget),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+112
-7
@@ -1,8 +1,11 @@
|
||||
/// Tree view widget library
|
||||
library tree_view;
|
||||
|
||||
import 'package:aitrainer_app/util/common.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class TreeView extends InheritedWidget {
|
||||
final List<Widget> children;
|
||||
@@ -39,7 +42,7 @@ class TreeView extends InheritedWidget {
|
||||
class _TreeViewData extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const _TreeViewData({
|
||||
_TreeViewData({
|
||||
this.children,
|
||||
});
|
||||
|
||||
@@ -90,13 +93,30 @@ class TreeViewChild extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class TreeViewChildState extends State<TreeViewChild> {
|
||||
class TreeViewChildState extends State<TreeViewChild> with Common, SingleTickerProviderStateMixin {
|
||||
bool isExpanded;
|
||||
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
||||
Color _color;
|
||||
double _opacity = 0;
|
||||
|
||||
/* List<Widget> listWidgets; */
|
||||
AnimationController _controller;
|
||||
Animation<double> sizeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isExpanded = widget.startExpanded;
|
||||
_color = Colors.transparent;
|
||||
_opacity = 0;
|
||||
/* listWidgets = List.from(widget.children); */
|
||||
|
||||
/* _controller = AnimationController(
|
||||
duration: const Duration(seconds: 1),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
sizeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);*/
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -114,22 +134,107 @@ class TreeViewChildState extends State<TreeViewChild> {
|
||||
child: widget.parent,
|
||||
onTap: widget.onTap ?? () => toggleExpanded(),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 400),
|
||||
child: isExpanded
|
||||
Flexible(
|
||||
child: Container(
|
||||
/*animation: _controller,
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return Transform.scale(
|
||||
scale: _controller.value,
|
||||
child: child,
|
||||
);
|
||||
},*/
|
||||
/* color: _color,
|
||||
duration: Duration(seconds: 2),
|
||||
curve: Curves.easeInOut,*/
|
||||
//height: double.infinity,
|
||||
/*child: isExpanded //animateChildren()
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
)
|
||||
: Offstage(),
|
||||
),
|
||||
: Offstage(),*/
|
||||
child:
|
||||
/*AnimatedCrossFade(
|
||||
firstChild: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
),
|
||||
secondChild: Offstage(),
|
||||
duration: Duration(milliseconds:800),
|
||||
crossFadeState: isExpanded ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||
)*/
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds:900),
|
||||
reverseDuration: Duration(milliseconds:500),
|
||||
switchInCurve: Curves.easeIn,
|
||||
child: isExpanded ? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
) : Offstage(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/*AnimatedList animateChildren() {
|
||||
|
||||
return AnimatedList(
|
||||
shrinkWrap: true,
|
||||
key: listKey,
|
||||
initialItemCount: listWidgets.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
return slideIt(animation, listWidgets[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget slideIt(Animation<double> animation, Widget element) {
|
||||
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axis: Axis.vertical,
|
||||
child: element,
|
||||
);
|
||||
}*/
|
||||
|
||||
void toggleExpanded() {
|
||||
setState(() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
_color = isExpanded ? Colors.black12 : Colors.transparent;
|
||||
_opacity = isExpanded ? 1 : 0;
|
||||
|
||||
/* if ( isExpanded == false ) {
|
||||
_removeAllItems();
|
||||
} else {
|
||||
//_addAllItems();
|
||||
listWidgets = List.from(widget.children);
|
||||
}*/
|
||||
});
|
||||
}
|
||||
|
||||
/* void _removeAllItems() {
|
||||
final int itemCount = widget.children.length;
|
||||
|
||||
for (var i = 0; i < itemCount; i++) {
|
||||
Widget itemToRemove = listWidgets[0];
|
||||
listKey.currentState.removeItem(0,
|
||||
(BuildContext context, Animation<double> animation) => slideIt(animation, itemToRemove),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
);
|
||||
|
||||
listWidgets.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
void _addAllItems() {
|
||||
final int itemCount = widget.children.length;
|
||||
|
||||
for (var i = 0; i < itemCount; i++) {
|
||||
listKey.currentState.insertItem(0);
|
||||
listWidgets.insert(0, widget.children[i]);
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user