Merge pull request #23 from Iconica-Development/feature/scroll_picker

feat: Added scroll picker field
This commit is contained in:
Gorter-dev 2023-11-01 16:41:13 +01:00 committed by GitHub
commit d0c0d49d66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 467 additions and 55 deletions

View file

@ -1,32 +1,12 @@
name: CI name: Iconica Standard Component CI Workflow
# Workflow Caller version: 2.0.0
on: on:
push:
branches: [ master ]
pull_request: pull_request:
branches: workflow_dispatch:
- master
- feature/*
- bugfix/*
- hotfix/*
jobs: jobs:
lint: call-global-iconica-workflow:
runs-on: ubuntu-latest uses: Iconica-Development/.github/.github/workflows/component-ci.yml@master
steps: secrets: inherit
- uses: actions/checkout@v3 permissions: write-all
- uses: actions/cache@v3 with:
with: subfolder: '.' # add optional subfolder to run workflow in
path: |
~/.gradle/wrapper
/opt/hostedtoolcache/flutter
key: ${{ runner.OS }}-flutter-install-cache
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
- name: Flutter pub get
run: flutter pub get
- name: Dart format
run: dart format -o none --set-exit-if-changed .
- name: Flutter analyze
run: flutter analyze

View file

@ -40,4 +40,7 @@
## 2.4.0 ## 2.4.0
* The ability to disable the onTap paramater of the DatePicker * The ability to disable the onTap paramater of the DatePicker
* FlutterFormInputDateTime now also had the enabled parameter to provide to DateTimeInputField * 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,15 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
var weekDayDateFormat = DateFormat('EEEE');
var monthDateFormat = DateFormat('MMMM');
var formKey = GlobalKey<FormState>(); var formKey = GlobalKey<FormState>();
@override var weekDays = TypeUtils().createWeekDays(WeekDay.monday, WeekDay.sunday);
void initState() { var dates =
super.initState(); TypeUtils().createMonthList(Month.january, Month.december, year: 2023);
} var years = TypeUtils().createYearList(2000, 2023);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -138,6 +141,42 @@ class _MyHomePageState extends State<MyHomePage> {
}, },
), ),
Container(height: 50), Container(height: 50),
const Text('FlutterFormInputScrollPicker: Weekdays'),
const SizedBox(
height: 8,
),
FlutterFormInputScrollPicker(
values: weekDays,
onChanged: (value) {},
childToString: (s) =>
s != null ? weekDayDateFormat.format(s) : '',
decoration: const ScrollPickerDecoration(),
),
const Text('FlutterFormInputScrollPicker: Months'),
const SizedBox(
height: 8,
),
FlutterFormInputScrollPicker(
values: dates,
onChanged: (value) {},
childToString: (s) =>
s != null ? monthDateFormat.format(s) : '',
decoration: const 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: const ScrollPickerDecoration(),
),
], ],
), ),
), ),

View file

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

View file

@ -5,3 +5,4 @@ export 'text/plain_text.dart';
export 'slider/slider.dart'; export 'slider/slider.dart';
export 'switch/switch.dart'; export 'switch/switch.dart';
export 'date_picker/date_picker.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,107 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
class ScrollPickerDecoration {
const 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,126 @@
// SPDX-FileCopyrightText: 2023 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,
super.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
void dispose() {
scrollController.dispose();
super.dispose();
}
@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,76 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
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 ?? DateTime.now().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: 2023 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 name: flutter_input_library
description: A new Flutter package project. description: A new Flutter package project.
version: 2.4.0 version: 2.5.0
repository: https://github.com/Iconica-Development/flutter_input_library repository: https://github.com/Iconica-Development/flutter_input_library
environment: environment: