1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 14:58:34 +00:00

Add flutter_web samples (#75)

This commit is contained in:
Kevin Moore
2019-05-07 13:32:08 -07:00
committed by Andrew Brogdon
parent 42f2dce01b
commit 3fe927cb29
697 changed files with 241026 additions and 0 deletions

View File

@@ -0,0 +1,264 @@
// Package infinite_listview:
// https://pub.dartlang.org/packages/infinite_listview
import 'dart:math' as math;
import 'package:flutter_web/rendering.dart';
import 'package:flutter_web/widgets.dart';
/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class InfiniteListView extends StatelessWidget {
/// See [ListView.builder]
InfiniteListView.builder({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
InfiniteScrollController controller,
this.physics,
this.padding,
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
this.cacheExtent,
}) : positiveChildrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
negativeChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) => itemBuilder(context, -1 - index),
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
controller = controller ?? InfiniteScrollController(),
super(key: key);
/// See [ListView.separated]
InfiniteListView.separated({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
InfiniteScrollController controller,
this.physics,
this.padding,
@required IndexedWidgetBuilder itemBuilder,
@required IndexedWidgetBuilder separatorBuilder,
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
this.cacheExtent,
}) : assert(itemBuilder != null),
assert(separatorBuilder != null),
itemExtent = null,
positiveChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
final itemIndex = index ~/ 2;
return index.isEven
? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
},
childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
negativeChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
final itemIndex = (-1 - index) ~/ 2;
return index.isOdd
? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
},
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
controller = controller ?? InfiniteScrollController(),
super(key: key);
/// See: [ScrollView.scrollDirection]
final Axis scrollDirection;
/// See: [ScrollView.reverse]
final bool reverse;
/// See: [ScrollView.controller]
final InfiniteScrollController controller;
/// See: [ScrollView.physics]
final ScrollPhysics physics;
/// See: [BoxScrollView.padding]
final EdgeInsets padding;
/// See: [ListView.itemExtent]
final double itemExtent;
/// See: [ScrollView.cacheExtent]
final double cacheExtent;
/// See: [ListView.childrenDelegate]
final SliverChildDelegate negativeChildrenDelegate;
/// See: [ListView.childrenDelegate]
final SliverChildDelegate positiveChildrenDelegate;
@override
Widget build(BuildContext context) {
final List<Widget> slivers = _buildSlivers(context, negative: false);
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
final AxisDirection axisDirection = _getDirection(context);
final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics);
return Scrollable(
axisDirection: axisDirection,
controller: controller,
physics: scrollPhysics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return Builder(builder: (BuildContext context) {
/// Build negative [ScrollPosition] for the negative scrolling [Viewport].
final state = Scrollable.of(context);
final negativeOffset = _InfiniteScrollPosition(
physics: scrollPhysics,
context: state,
initialPixels: -offset.pixels,
keepScrollOffset: controller.keepScrollOffset,
);
/// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
offset.addListener(() {
negativeOffset._forceNegativePixels(offset.pixels);
});
/// Stack the two [Viewport]s on top of each other so they move in sync.
return Stack(
children: <Widget>[
Viewport(
axisDirection: flipAxisDirection(axisDirection),
anchor: 1.0,
offset: negativeOffset,
slivers: negativeSlivers,
cacheExtent: cacheExtent,
),
Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
),
],
);
});
},
);
}
AxisDirection _getDirection(BuildContext context) {
return getAxisDirectionFromAxisReverseAndDirectionality(
context, scrollDirection, reverse);
}
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
Widget sliver;
if (itemExtent != null) {
sliver = SliverFixedExtentList(
delegate:
negative ? negativeChildrenDelegate : positiveChildrenDelegate,
itemExtent: itemExtent,
);
} else {
sliver = SliverList(
delegate:
negative ? negativeChildrenDelegate : positiveChildrenDelegate);
}
if (padding != null) {
sliver = new SliverPadding(
padding: negative
? padding - EdgeInsets.only(bottom: padding.bottom)
: padding - EdgeInsets.only(top: padding.top),
sliver: sliver,
);
}
return <Widget>[sliver];
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new EnumProperty<Axis>('scrollDirection', scrollDirection));
properties.add(new FlagProperty('reverse',
value: reverse, ifTrue: 'reversed', showName: true));
properties.add(new DiagnosticsProperty<ScrollController>(
'controller', controller,
showName: false, defaultValue: null));
properties.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics,
showName: false, defaultValue: null));
properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>(
'padding', padding,
defaultValue: null));
properties
.add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null));
properties.add(
new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null));
}
}
/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
class InfiniteScrollController extends ScrollController {
/// Creates a new [InfiniteScrollController]
InfiniteScrollController({
double initialScrollOffset = 0.0,
bool keepScrollOffset = true,
String debugLabel,
}) : super(
initialScrollOffset: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
debugLabel: debugLabel,
);
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition oldPosition) {
return new _InfiniteScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class _InfiniteScrollPosition extends ScrollPositionWithSingleContext {
_InfiniteScrollPosition({
@required ScrollPhysics physics,
@required ScrollContext context,
double initialPixels = 0.0,
bool keepScrollOffset = true,
ScrollPosition oldPosition,
String debugLabel,
}) : super(
physics: physics,
context: context,
initialPixels: initialPixels,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
void _forceNegativePixels(double value) {
super.forcePixels(-value);
}
@override
double get minScrollExtent => double.negativeInfinity;
@override
double get maxScrollExtent => double.infinity;
}

260
web/timeflow/lib/main.dart Normal file
View File

@@ -0,0 +1,260 @@
import 'dart:core';
import 'dart:math';
import 'package:flutter_web/material.dart';
import 'package:flutter_web/scheduler.dart';
import 'numberpicker.dart';
main() => runApp(MaterialApp(home: App(), debugShowCheckedModeBanner: false));
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => TM();
}
enum SI { pause, play, stop }
List<T> triangles;
var percent = 0.0, cTime = 0.0, dur = 120000.0, rng = Random(), rebuild = true;
class TM extends State<App> {
SI cState = SI.stop;
Ticker t;
var pTime = 0.0;
@override
initState() {
// Screen.keepOn(true);
t = Ticker(up);
super.initState();
}
up(Duration d) {
if (cState == SI.play) {
setState(() {
if (cTime >= dur)
stop();
else {
cTime = d.inMilliseconds.toDouble() + pTime;
percent = cTime / dur;
}
});
}
}
press() {
if (cState == SI.play)
pause();
else if (cState == SI.pause)
play();
else {
cState = SI.play;
t.start();
}
}
pause() {
setState(() {
cState = SI.pause;
t.stop();
});
}
play() {
setState(() {
cState = SI.play;
t.start();
pTime = cTime;
});
}
stop() {
setState(() {
cState = SI.stop;
t.stop();
pTime = 0.0;
cTime = 0.0;
percent = 0.0;
});
}
openDialog() {
showDialog<num>(
context: context,
builder: (BuildContext context) {
return NumberPickerDialog.integer(
initialIntegerValue: (dur + 1.0) ~/ 60000,
maxValue: 20,
minValue: 1,
title: Text('Minutes'));
}).then((num v) {
if (v != null) dur = 60000.0 * v;
});
}
@override
Widget build(BuildContext context) {
List<Widget> w = List();
if (cState == SI.pause) {
w.add(fab(Colors.green, play, Icons.play_arrow));
w.add(SizedBox(height: 10));
w.add(fab(Colors.red, stop, Icons.close));
w.add(SizedBox(height: 20));
}
if (cState == SI.stop) {
w.add(fab(Colors.lightBlue, openDialog, Icons.timer));
w.add(SizedBox(height: 10));
w.add(fab(Colors.yellow[900], () {
rebuild = true;
}, Icons.loop));
w.add(SizedBox(height: 20));
}
Column r = Column(mainAxisAlignment: MainAxisAlignment.end, children: w);
return Scaffold(
backgroundColor: Colors.black,
body: SizedBox.expand(
child: Container(
child: CustomPaint(
painter: P(),
child: FlatButton(
onPressed: press,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [r]))))));
}
}
FloatingActionButton fab(Color c, VoidCallback f, IconData ic) =>
FloatingActionButton(backgroundColor: c, onPressed: f, child: Icon(ic));
class P extends CustomPainter {
@override
paint(Canvas canvas, Size size) {
var w = size.width, h = size.height, d = 2 / 3 * w;
if (w > 0.1 && h > 0.1) {
if (rebuild) {
rebuild = false;
setupT();
for (var t in triangles) t.setupdP(w / d, h / d);
}
for (var t in triangles) {
var cP = t.cP(), p = Path();
p.moveTo(cP[0].x * d + w / 2, cP[0].y * d + h / 2);
for (i = 1; i < 3; i++)
p.lineTo(cP[i].x * d + w / 2, cP[i].y * d + h / 2);
p.close();
canvas.drawPath(p, t.p);
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
int i;
class T {
List<Point> dP = List(3), sP = List(3);
Paint p;
T(Point p1, p2, p3, var c) {
p = Paint()..style = PaintingStyle.fill;
sP[0] = p1;
sP[1] = p2;
sP[2] = p3;
p.color = c[100 * (rng.nextInt(9) + 1)];
double x = 0, y = 0;
for (i = 0; i < 3; i++) {
x += sP[i].x;
y += sP[i].y;
}
x = 2 * x / 3;
y = 2 * y / 3;
if (x * x + y * y < 1) triangles.add(this);
}
setupdP(double wR, hR) {
var x = (rng.nextDouble() - 0.5) * (wR - 0.1),
y = (rng.nextDouble() - 0.5) * (hR - 0.1);
dP[0] = Point(x, y);
for (i = 1; i < 3; i++)
dP[i] = Point(sP[i].x + x - sP[0].x, sP[i].y + y - sP[0].y);
}
List<Point> cP() {
List<Point> res = List(3);
var p, k, o = 6000, r;
if (cTime < o)
p = 1 - cTime / o;
else
p = (cTime - o) / (dur - o);
k = 2 * ((cTime.toInt() % o) - o / 2).abs() / o;
r = min(min(1, (dur - cTime) / o), cTime / o);
this.p.color = this.p.color.withAlpha(255 - (200 * k * r).toInt());
for (i = 0; i < 3; i++)
res[i] = Point(
sP[i].x * p + dP[i].x * (1 - p), sP[i].y * p + dP[i].y * (1 - p));
if (cTime > o) {
var d = res[0].distanceTo(sP[0]);
var a = acos((sP[0].x - res[0].x) / d);
if (sP[0].y > res[0].y) a = 2 * pi - a;
var b = pi - a + p * pi * dur / 120000;
var dX = cos(b) * d, dY = sin(b) * d;
for (i = 0; i < 3; i++) res[i] = Point(sP[i].x + dX, sP[i].y + dY);
}
double mx = 0, my = 0;
for (i = 0; i < 3; i++) {
mx += res[i].x;
my += res[i].y;
}
mx /= 3;
my /= 3;
for (i = 0; i < 3; i++)
res[i] = Point(res[i].x + (res[i].x - mx) * (1 - k) * r / 2,
res[i].y + (res[i].y - my) * (1 - k) * r / 2);
return res;
}
}
setupT() {
int dim = 20, x, y;
List<Point> tri = List(dim * dim);
for (x = 0; x < dim; x++) {
for (y = 0; y < dim; y++) {
var dx = rng.nextDouble() - 0.5, dy = rng.nextDouble() - 0.5, off;
if (x % 2 == 0)
off = 0;
else
off = 0.5;
tri[x * dim + y] =
Point((x + dx) / (dim - 1) - 0.5, (y + off + dy) / (dim - 1) - 0.5);
}
}
triangles = List();
var r = rng.nextInt(5), c;
if (r == 0) c = Colors.lightBlue;
if (r == 1) c = Colors.yellow;
if (r == 2) c = Colors.lightGreen;
if (r == 3) c = Colors.red;
if (r == 4) c = Colors.pink;
for (x = 0; x < dim - 1; x++) {
for (y = 0; y < dim - 1; y++) {
int off = x * dim;
T(tri[y + off], tri[y + 1 + off], tri[y + off + dim], c);
T(tri[y + off + dim], tri[y + 1 + off], tri[y + 1 + off + dim], c);
}
}
}

View File

@@ -0,0 +1,527 @@
// Package numberpicker:
// https://pub.dartlang.org/packages/numberpicker
import 'dart:math' as math;
import 'package:flutter_web/foundation.dart';
import 'package:flutter_web/material.dart';
import 'package:flutter_web/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
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),
],
);
}
}