feat: Added scroll picker field

This commit is contained in:
Jacques 2023-11-01 11:30:31 +01:00
parent 6f9fbccfbd
commit 73ce94ba37
10 changed files with 441 additions and 26 deletions

View file

@ -41,3 +41,6 @@
## 2.4.0
* The ability to disable the onTap paramater of the DatePicker
* FlutterFormInputDateTime now also had the enabled parameter to provide to DateTimeInputField
## 2.5.0
* Addition of the ScrollPicker input field.

View file

@ -31,12 +31,14 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
var weekDayDateFormat = DateFormat('EEEE');
var monthDateFormat = DateFormat('MMMM');
var formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
}
var weekDays = TypeUtils().createWeekDays(WeekDay.monday, WeekDay.sunday);
var dates = TypeUtils().createMonthList(Month.january, Month.december, 2023);
var years = TypeUtils().createYearList(2000, 2023);
@override
Widget build(BuildContext context) {
@ -138,6 +140,42 @@ class _MyHomePageState extends State<MyHomePage> {
},
),
Container(height: 50),
const Text('FlutterFormInputScrollPicker: Weekdays'),
const SizedBox(
height: 8,
),
FlutterFormInputScrollPicker(
values: weekDays,
onChanged: (value) {},
childToString: (s) =>
s != null ? weekDayDateFormat.format(s) : '',
decoration: ScrollPickerDecoration(),
),
const Text('FlutterFormInputScrollPicker: Months'),
const SizedBox(
height: 8,
),
FlutterFormInputScrollPicker(
values: dates,
onChanged: (value) {},
childToString: (s) =>
s != null ? monthDateFormat.format(s) : '',
decoration: ScrollPickerDecoration(),
),
const Padding(
padding: EdgeInsets.all(8.0),
child: Divider(),
),
const Text('FlutterFormInputScrollPicker: Years'),
const SizedBox(
height: 8,
),
FlutterFormInputScrollPicker(
values: years,
onChanged: (value) {},
childToString: (s) => s?.year.toString() ?? '',
decoration: ScrollPickerDecoration(),
),
],
),
),

View file

@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted
version: "1.17.1"
version: "1.17.2"
cupertino_icons:
dependency: "direct main"
description:
@ -68,7 +68,7 @@ packages:
path: ".."
relative: true
source: path
version: "2.2.1"
version: "2.4.0"
flutter_lints:
dependency: "direct dev"
description:
@ -90,14 +90,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.18.1"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
lints:
dependency: transitive
description:
@ -110,18 +102,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.5.0"
meta:
dependency: transitive
description:
@ -147,10 +139,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
stack_trace:
dependency: transitive
description:
@ -187,10 +179,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.6.0"
vector_math:
dependency: transitive
description:
@ -199,6 +191,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
sdks:
dart: ">=3.0.0-0 <4.0.0"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=1.17.0"

View file

@ -5,3 +5,4 @@ export 'text/plain_text.dart';
export 'slider/slider.dart';
export 'switch/switch.dart';
export 'date_picker/date_picker.dart';
export 'scroll_picker/scroll_picker.dart';

View file

@ -0,0 +1,3 @@
export 'scroll_picker_widget.dart';
export 'scroll_picker_decoration.dart';
export 'scroll_picker_type_extensions.dart';

View file

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
class ScrollPickerDecoration {
ScrollPickerDecoration({
this.scrollItemBuilder,
this.highlightWidget,
this.scrollItemTextStyle,
this.numberOfVisibleItems = 5,
this.itemHeight = 30,
this.diameterRatio = 2.0,
this.perspective = 0.002,
this.overAndUnderCenterOpacity = 1.0,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
this.squeeze = 1.0,
this.renderChildrenOutsideViewport = false,
});
/// Ability to provide your own builder for the scroll items
final Widget Function(BuildContext context, int index, dynamic value)?
scrollItemBuilder;
/// Override the standard highlight widget. (Grey container which is placed behind the selected item).
final Widget? highlightWidget;
/// Textstyle of the scroll items. Will be overridden if the [scrollItemBuilder] is set.
final TextStyle? scrollItemTextStyle;
/// Amount of visible items in the scroll wheel. Changing this changes the height of the widget.
final int numberOfVisibleItems;
/// Height of each item in the scrollwheel. Chaging this changes the height of the widget.
final double itemHeight;
/// A ratio between the diameter of the cylinder and the viewport's size in the main axis.
///
/// A value of 1 means the cylinder has the same diameter as the viewport's size.
///
/// A value smaller than 1 means items at the edges of the cylinder are entirely contained inside the viewport.
///
/// A value larger than 1 means angles less than ±[pi] / 2 from the center of the cylinder are visible.
///
/// The same number of children will be visible in the viewport regardless of the [diameterRatio]. The number of children visible is based on the viewport's length along the main axis divided by the children's [itemExtent]. Then the children are evenly distributed along the visible angles up to ±[pi] / 2.
///
/// Just as it's impossible to stretch a paper to cover the an entire half of a cylinder's surface where the cylinder has the same diameter as the paper's length, choosing a [diameterRatio] smaller than [pi] will leave same gaps between the children.
///
/// Defaults to an arbitrary but aesthetically reasonable number of 2.0.
///
/// Must not be null and must be positive.
final double diameterRatio;
/// Size of each child in the main axis. Must not be null and must be positive.
final double perspective;
/// The opacity value that will be applied to the wheel that appears below and above the magnifier.
///
/// The default value is 1.0, which will not change anything.
///
/// Must be greater than or equal to 0, and less than or equal to 1.
final double overAndUnderCenterOpacity;
/// How much the wheel is horizontally off-center, as a fraction of its width. This property creates the visual effect of looking at a vertical wheel from its side where its vanishing points at the edge curves to one side instead of looking at the wheel head-on.
///
/// The value is horizontal distance between the wheel's center and the vertical vanishing line at the edges of the wheel, represented as a fraction of the wheel's width.
///
/// The value 0.0 means the wheel is looked at head-on and its vanishing line runs through the center of the wheel. Negative values means moving the wheel to the left of the observer, thus the edges curve to the right. Positive values means moving the wheel to the right of the observer, thus the edges curve to the left.
///
/// The visual effect causes the wheel's edges to curve rather than moving the center. So a value of 0.5 means the edges' vanishing line will touch the wheel's size's left edge.
///
/// Defaults to 0.0, which means looking at the wheel head-on. The visual effect can be unaesthetic if this value is too far from the range [-0.5, 0.5].
final double offAxisFraction;
/// Whether to use the magnifier for the center item of the wheel.
final bool useMagnifier;
/// The zoomed-in rate of the magnifier, if it is used.
///
/// The default value is 1.0, which will not change anything. If the value is > 1.0, the center item will be zoomed in by that rate, and it will also be rendered as flat, not cylindrical like the rest of the list. The item will be zoomed out if magnification < 1.0.
///
/// Must be positive.
final double magnification;
/// The angular compactness of the children on the wheel.
///
/// This denotes a ratio of the number of children on the wheel vs the number of children that would fit on a flat list of equivalent size, assuming [diameterRatio] of 1.
///
/// For instance, if this RenderListWheelViewport has a height of 100px and [itemExtent] is 20px, 5 items would fit on an equivalent flat list. With a [squeeze] of 1, 5 items would also be shown in the RenderListWheelViewport. With a [squeeze] of 2, 10 items would be shown in the RenderListWheelViewport.
///
/// Changing this value will change the number of children built and shown inside the wheel.
///
/// Must not be null and must be positive.
///
/// Defaults to 1.
final double squeeze;
/// Whether to paint children inside the viewport only.
///
/// If false, every child will be painted. However the [Scrollable] is still the size of the viewport and detects gestures inside only.
///
/// Defaults to false. Must not be null. Cannot be true if [clipBehavior] is not [Clip.none] since children outside the viewport will be clipped, and therefore cannot render children outside the viewport.
final bool renderChildrenOutsideViewport;
}

View file

@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_input_library/src/inputs/scroll_picker/scroll_picker_decoration.dart';
class ScrollPicker extends StatefulWidget {
const ScrollPicker({
required this.list,
required this.onChanged,
required this.decoration,
this.initialIndex,
Key? key,
}) : super(key: key);
final List<String> list;
final void Function(int index) onChanged;
final ScrollPickerDecoration decoration;
final int? initialIndex;
@override
State<ScrollPicker> createState() => _ScrollPickerState();
}
class _ScrollPickerState extends State<ScrollPicker> {
late FixedExtentScrollController scrollController;
late double pickerHeight;
late int selectedIndex;
@override
void initState() {
super.initState();
var initialIndex = widget.initialIndex;
if (initialIndex != null &&
initialIndex > 0 &&
initialIndex < widget.list.length) {
selectedIndex = initialIndex;
} else {
selectedIndex = (widget.list.length / 2).floor();
}
pickerHeight =
widget.decoration.itemHeight * widget.decoration.numberOfVisibleItems;
scrollController = FixedExtentScrollController(
initialItem: selectedIndex,
);
scrollController.addListener(() {
var newIndex =
(scrollController.offset / widget.decoration.itemHeight).round();
if (newIndex != selectedIndex) {
widget.onChanged.call(
(scrollController.offset / widget.decoration.itemHeight).round());
selectedIndex = newIndex;
}
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: Center(
child: widget.decoration.highlightWidget ??
Container(
height: widget.decoration.itemHeight,
decoration: ShapeDecoration(
color: Colors.grey.shade300,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
),
),
),
SizedBox(
height: pickerHeight,
child: ListWheelScrollView.useDelegate(
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.decoration.diameterRatio,
itemExtent: widget.decoration.itemHeight,
controller: scrollController,
perspective: widget.decoration.perspective,
overAndUnderCenterOpacity:
widget.decoration.overAndUnderCenterOpacity,
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) =>
widget.decoration.scrollItemBuilder
?.call(context, index, widget.list[index]) ??
Center(
child: Text(
widget.list[index],
style: widget.decoration.scrollItemTextStyle,
),
),
childCount: widget.list.length,
),
offAxisFraction: widget.decoration.offAxisFraction,
useMagnifier: widget.decoration.useMagnifier,
magnification: widget.decoration.magnification,
squeeze: widget.decoration.squeeze,
renderChildrenOutsideViewport:
widget.decoration.renderChildrenOutsideViewport,
),
),
],
);
}
}

View file

@ -0,0 +1,71 @@
enum WeekDay {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday,
}
enum Month {
january,
february,
march,
april,
may,
june,
july,
august,
september,
october,
november,
december,
}
class TypeUtils {
/// Creates list of Datetime with days. These fall on the respective week days from start to end.
List<DateTime> createWeekDays(
WeekDay start,
WeekDay end,
) {
if (start.index > end.index) {
throw ArgumentError('Start month must be before or equal to end month.');
}
List<DateTime> result = [];
for (int i = start.index; i <= end.index; i++) {
result.add(DateTime(2024, 1, WeekDay.values[i].index + 1));
}
return result;
}
/// Creates list of Datetime with the months from start to end.
List<DateTime> createMonthList(Month start, Month end, int year) {
if (start.index > end.index) {
throw ArgumentError('Start month must be before or equal to end month.');
}
List<DateTime> result = [];
for (int i = start.index; i <= end.index; i++) {
result.add(DateTime(year, Month.values[i].index + 1, 1));
}
return result;
}
/// Creates a list of Datetime with the years from start to end.
List<DateTime> createYearList(int start, int end) {
if (start > end) {
throw ArgumentError('Start year must be before or equal to year month.');
}
List<DateTime> result = [];
for (int i = 0; i <= end - start; i++) {
result.add(DateTime(start + i, 1, 1));
}
return result;
}
}

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_input_library/src/inputs/scroll_picker/scroll_picker_decoration.dart';
import 'package:flutter_input_library/src/inputs/scroll_picker/scroll_picker_field.dart';
class FlutterFormInputScrollPicker<T> extends StatelessWidget {
const FlutterFormInputScrollPicker({
required this.values,
required this.decoration,
required this.childToString,
this.onSaved,
this.onChanged,
this.initialIndex = 0,
Key? key,
}) : super(key: key);
/// Values that will be shown in the scroll picker.
final List<T> values;
/// Initial index to set the scroll picker too.
final int? initialIndex;
/// Function called when the save function is called on the parent form.
final Function(T?)? onSaved;
/// Function called when the value is changed by the user.
final Function(T?)? onChanged;
/// Converts the given value to a String.
final String Function(T?) childToString;
/// Decoration for the scroll picker.
final ScrollPickerDecoration decoration;
@override
Widget build(BuildContext context) {
return ScrollPickerFormField<T>(
values: values,
initialIndex: initialIndex,
decoration: decoration,
childToString: childToString,
onSaved: (value) => onSaved?.call(value),
onChanged: (value) => onChanged?.call(value),
);
}
}
class ScrollPickerFormField<T> extends FormField<T> {
ScrollPickerFormField({
required List<T> values,
int? initialIndex,
required ScrollPickerDecoration decoration,
required FormFieldSetter<T> onSaved,
void Function(T value)? onChanged,
required String Function(T) childToString,
Key? key,
}) : super(
key: key,
onSaved: onSaved,
initialValue: values[initialIndex ?? (values.length / 2).floor()],
builder: (FormFieldState<T> state) {
return ScrollPicker(
list: values.map((e) => childToString(e)).toList(),
decoration: decoration,
initialIndex: initialIndex,
onChanged: (int index) {
onChanged?.call(values[index]);
state.didChange(values[index]);
},
);
},
);
}

View file

@ -1,6 +1,6 @@
name: flutter_input_library
description: A new Flutter package project.
version: 2.4.0
version: 2.5.0
repository: https://github.com/Iconica-Development/flutter_input_library
environment: