initial commit

This commit is contained in:
Niels Gorter 2022-11-29 13:16:44 +01:00
commit a7c3b3b192
38 changed files with 3263 additions and 0 deletions

37
.gitignore vendored Normal file
View file

@ -0,0 +1,37 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
example/linux
example/macos
example/windows
example/web
example/android
example/ios

10
.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: eb6d86ee27deecba4a83536aa20f366a6044895c
channel: stable
project_type: package

3
CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 0.0.1
* Initial release, retrieved inputs from flutter_form

198
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,198 @@
# Contributing
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued.
See the [Table of Contents](#table-of-contents) for different ways to help and details about how we handle them.
Please make sure to read the relevant section before making your contribution.
It will make it a lot easier for us maintainers and smooth out the experience for all involved.
Iconica looks forward to your contributions. 🎉
## Table of contents
- [Code of conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Contributing code](#contributing-code)
## Code of conduct
### Legal notice
When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
All accepted pull requests and other additions to this project will be considered intellectual property of Iconica.
All repositories should be kept clean of jokes, easter eggs and other unnecessary additions.
## I have a question
If you want to ask a question, we assume that you have read the available documentation found within the code.
Before you ask a question, it is best to search for existing issues that might help you.
In case you have found a suitable issue but still need clarification, you can ask your question
It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an issue.
- Provide as much context as you can about what you're running into.
We will then take care of the issue as soon as possible.
## I want to contribute
### Reporting bugs
<!-- omit in toc -->
**Before submitting a bug report**
A good bug report shouldn't leave others needing to chase you up for more information.
Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report.
Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error.
- Also make sure to search the internet (including Stack Overflow) to see if users outside of Iconica have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Time and date of occurance
- Describe the expected result and actual result
- Can you reliably reproduce the issue? And can you also reproduce it with older versions? Describe all steps that lead to the bug.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps.
If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for additional information.
- If the team is able to reproduce the issue, it will be moved into the backlog, as well as marked with a priority, and the issue will be left to be [implemented by someone](#contributing-code).
### Contributing code
When you start working on your contribution, make sure you are aware of the relevant documentation and the functionality of the component you are working on.
When writing code, follow the style guidelines set by Dart: [Effective Dart](https://Dart.dev/guides/language/effective-Dart). This contains most information you will need to write clean Dart code that is well documented.
**Documentation**
As Effective Dart indicates, documenting your public methods with Dart doc comments is recommended.
Aside from Effective Dart, we require specific information in the documentation of a method:
At the very least, your documentation should first name what the code does, then followed below by requirements for calling the method, the result of the method.
Any references to internal variables or other methods should be done through [var] to indicate a reference.
If the method or class is complex enough (determined by the reviewers) an example is required.
If unsure, add an example in the docs using code blocks.
For classes and methods, document the individual parameters with their implications.
> Tip: Remember that the shortest documentation can be written by having good descriptive names in the first place.
An example:
````Dart
library iconica_utilities.bidirectional_sorter;
part 'sorter.Dart';
part 'enum.Dart';
/// Generic sort method, allow sorting of list with primitives or complex types.
/// Uses [SortDirection] to determine the direction, either Ascending or Descending,
/// Gets called on [List] toSort of type [T] which cannot be shorter than 2.
/// Optionally for complex types a [Comparable] [Function] can be given to compare complex types.
/// ```
/// List<TestObject> objects = [];
/// for (int i = 0; i < 10; i++) {
/// objects.add(TestObject(name: "name", id: i));
/// }
///
/// sort<TestObject>(
/// SortDirection.descending, objects, (object) => object.id);
///
/// ```
/// In the above example a list of TestObjects is created, and then sorted in descending order.
/// If the implementation of TestObject is as following:
/// ```
/// class TestObject {
/// final String name;
/// final int id;
///
/// TestObject({required this.name, required this.id});
/// }
/// ```
/// And the list is logged to the console, the following will appear:
/// ```
/// [name9, name8, name7, name6, name5, name4, name3, name2, name1, name0]
/// ```
void sort<T>(
/// Determines the sorting direction, can be either Ascending or Descending
SortDirection sortDirection,
/// Incoming list, which gets sorted
List<T> toSort, [
/// Optional comparable, which is only necessary for complex types
SortFieldGetter<T>? sortValueCallback,
]) {
if (toSort.length < 2) return;
assert(
toSort.whereType<Comparable>().isNotEmpty || sortValueCallback != null);
BidirectionalSorter<T>(
sortInstructions: <SortInstruction<T>>[
SortInstruction(
sortValueCallback ?? (t) => t as Comparable, sortDirection),
],
).sort(toSort);
}
/// same functionality as [sort] but with the added functionality
/// of sorting multiple values
void sortMulti<T>(
/// Incoming list, which gets sorted
List<T> toSort,
/// list of comparables to sort multiple values at once,
/// priority based on index
List<SortInstruction<T>> sortValueCallbacks,
) {
if (toSort.length < 2) return;
assert(sortValueCallbacks.isNotEmpty);
BidirectionalSorter<T>(
sortInstructions: sortValueCallbacks,
).sort(toSort);
}
````
**Tests**
For each public method that was created, excluding widgets, which contains any form of logic (e.g. Calculations, predicates or major side-effects) tests are required.
A set of tests is written for each method, covering at least each path within the method. For example:
```Dart
void foo() {
try {
var bar = doSomething();
if (bar) {
doSomethingElse();
} else {
doSomethingCool();
}
} catch (_) {
displayError();
}
}
```
The method above should result in 3 tests:
1. A test for the path leading to displayError by the cause of an exception
2. A test for if bar is true, resulting in doSomethingElse()
3. A test for if bar is false, resulting in the doSomethingCool() method being called.
This means that we require 100% coverage of each method you test.

29
LICENSE.md Normal file
View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2022, Iconica Development
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

23
README.md Normal file
View file

@ -0,0 +1,23 @@
[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart)
Generic input library for Flutter.
## Setup
Add `flutter_input_library` to your `pubspec.yaml`:
## How to use
Look at the example for a simple [example](./example/lib/main.dart)
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_input_library/issues) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_input_library/pulls).
## Author
This `flutter_input_library` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

4
analysis_options.yaml Normal file
View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

44
example/.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
example/.metadata Normal file
View file

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: eb6d86ee27deecba4a83536aa20f366a6044895c
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- platform: android
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- platform: ios
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- platform: linux
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- platform: macos
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- platform: web
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- platform: windows
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View file

@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

128
example/lib/main.dart Normal file
View file

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_input_library/flutter_input_library.dart';
import 'package:intl/intl.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
children: [
FlutterFormInputSwitch(
initialValue: true,
onChanged: (v) {
print('Switch changed to $v');
},
),
FlutterFormInputDateTime(
inputType: FlutterFormDateTimeType.dateTime,
dateFormat: DateFormat('dd/MM/yyyy HH:mm'),
onChanged: (v) {
print('Date changed to $v');
},
),
FlutterFormInputNumberPicker(
onChanged: (v) {
print('Number changed to $v');
},
),
FlutterFormInputSlider(
onChanged: (v) {
print('Slider changed to $v');
},
),
SizedBox(
height: 100,
width: 100,
child: FlutterFormInputCarousel(
onChanged: (v) {
print('Carousel changed to $v');
},
items: [
Container(
height: 100,
width: 100,
color: Colors.red,
),
Container(
height: 100,
width: 100,
color: Colors.green,
),
Container(
height: 100,
width: 100,
color: Colors.blue,
)
],
),
),
FlutterFormInputPlainText(
onChanged: (v) {
print('Plain text changed to $v');
},
),
SizedBox(
height: 50,
width: 200,
child: FlutterFormInputMultiLine(
onChanged: (v) {
print('Multi line changed to $v');
},
),
),
FlutterFormInputPassword(
onChanged: (v) {
print('Password changed to $v');
},
),
],
),
),
),
),
);
}
}

196
example/pubspec.lock Normal file
View file

@ -0,0 +1,196 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.9.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_input_library:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_riverpod:
dependency: transitive
description:
name: flutter_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.12"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
riverpod:
dependency: transitive
description:
name: riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
state_notifier:
dependency: transitive
description:
name: state_notifier
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.2+1"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.12"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
sdks:
dart: ">=2.18.2 <3.0.0"
flutter: ">=3.0.0"

96
example/pubspec.yaml Normal file
View file

@ -0,0 +1,96 @@
name: example
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=2.18.2 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
intl: any
flutter_input_library:
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View file

@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
library flutter_input_library;
export 'src/inputs/inputs.dart';
export 'src/utils/utils.dart';

View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'carousel_form.dart';
class FlutterFormInputCarousel extends ConsumerWidget {
const FlutterFormInputCarousel({
Key? key,
required this.items,
this.height = 425,
this.onSaved,
this.onChanged,
this.initialValue,
this.validator,
}) : super(
key: key,
);
final List<Widget> items;
final double height;
final Function(int?)? onSaved;
final String Function(int?)? validator;
final Function(int?)? onChanged;
final int? initialValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
return CarouselFormField(
onSaved: (value) => onSaved?.call(value),
validator: (value) => validator?.call(value),
onChanged: (value) => onChanged?.call(value),
initialValue: initialValue ?? 0,
items: items,
height: height,
);
}
}

View file

@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:async';
import 'package:flutter/material.dart';
import 'carousel_utils.dart';
import 'carousel_options.dart';
import 'carousel_state.dart';
abstract class CarouselController {
bool get ready;
Future<void> get onReady;
Future<void> nextPage({Duration? duration, Curve? curve});
Future<void> previousPage({Duration? duration, Curve? curve});
void jumpToPage(int page);
Future<void> animateToPage(int page, {Duration? duration, Curve? curve});
void startAutoPlay();
void stopAutoPlay();
factory CarouselController() => CarouselControllerImpl();
}
class CarouselControllerImpl implements CarouselController {
final Completer<void> _readyCompleter = Completer<void>();
CarouselState? _state;
set state(CarouselState? state) {
_state = state;
if (!_readyCompleter.isCompleted) {
_readyCompleter.complete();
}
}
void _setModeController() =>
_state!.changeMode(CarouselPageChangedReason.controller);
@override
bool get ready => _state != null;
@override
Future<void> get onReady => _readyCompleter.future;
/// Animates the controlled [CarouselSlider] to the next page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> nextPage(
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
_setModeController();
await _state!.pageController!.nextPage(duration: duration!, curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Animates the controlled [CarouselSlider] to the previous page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> previousPage(
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
_setModeController();
await _state!.pageController!
.previousPage(duration: duration!, curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Changes which page is displayed in the controlled [CarouselSlider].
///
/// Jumps the page position from its current value to the given value,
/// without animation, and without checking if the new value is in range.
@override
void jumpToPage(int page) {
final index = getRealIndex(_state!.pageController!.page!.toInt(),
_state!.realPage - _state!.initialPage, _state!.itemCount);
_setModeController();
final int pageToJump = _state!.pageController!.page!.toInt() + page - index;
return _state!.pageController!.jumpToPage(pageToJump);
}
/// Animates the controlled [CarouselSlider] from the current page to the
/// given page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
@override
Future<void> animateToPage(int page,
{Duration? duration = const Duration(milliseconds: 300),
Curve? curve = Curves.linear}) async {
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
if (isNeedResetTimer) {
_state!.onResetTimer();
}
final index = getRealIndex(_state!.pageController!.page!.toInt(),
_state!.realPage - _state!.initialPage, _state!.itemCount);
_setModeController();
await _state!.pageController!.animateToPage(
_state!.pageController!.page!.toInt() + page - index,
duration: duration!,
curve: curve!);
if (isNeedResetTimer) {
_state!.onResumeTimer();
}
}
/// Starts the controlled [CarouselSlider] autoplay.
///
/// The carousel will only autoPlay if the [autoPlay] parameter
/// in [CarouselOptions] is true.
@override
void startAutoPlay() {
_state!.onResumeTimer();
}
/// Stops the controlled [CarouselSlider] from autoplaying.
///
/// This is a more on-demand way of doing this. Use the [autoPlay]
/// parameter in [CarouselOptions] to specify the autoPlay behaviour of the
/// carousel.
@override
void stopAutoPlay() {
_state!.onResetTimer();
}
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'carousel_slider.dart';
class CarouselFormField extends FormField<int> {
CarouselFormField({
Key? key,
required FormFieldSetter<int> onSaved,
required FormFieldValidator<int> validator,
void Function(int value)? onChanged,
void Function(int value)? onSubmit,
int initialValue = 0,
bool autovalidate = false,
required List<Widget> items,
double height = 425,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<int> state) {
return CarouselSlider(
options: CarouselOptions(
initialPage: initialValue,
onPageChanged: (index, reason) {
onChanged?.call(index);
state.didChange(index);
},
height: height,
aspectRatio: 2.0,
enlargeCenterPage: true,
enableInfiniteScroll: false,
),
items: items.map((Widget item) {
return item;
}).toList(),
);
});
}

View file

@ -0,0 +1,220 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
enum CarouselPageChangedReason { timed, manual, controller }
enum CenterPageEnlargeStrategy { scale, height }
class CarouselOptions {
/// Set carousel height and overrides any existing [aspectRatio].
final double? height;
/// Aspect ratio is used if no height have been declared.
///
/// Defaults to 16:9 aspect ratio.
final double aspectRatio;
/// The fraction of the viewport that each page should occupy.
///
/// Defaults to 0.8, which means each page fills 80% of the carousel.
final double viewportFraction;
/// The initial page to show when first creating the [CarouselSlider].
///
/// Defaults to 0.
final int initialPage;
///Determines if carousel should loop infinitely or be limited to item length.
///
///Defaults to true, i.e. infinite loop.
final bool enableInfiniteScroll;
/// Reverse the order of items if set to true.
///
/// Defaults to false.
final bool reverse;
/// Enables auto play, sliding one page at a time.
///
/// Use [autoPlayInterval] to determine the frequency of slides.
/// Defaults to false.
final bool autoPlay;
/// Sets Duration to determine the frequency of slides when [autoPlay] is set
/// to true.
/// Defaults to 4 seconds.
final Duration autoPlayInterval;
/// The animation duration between two transitioning pages while in auto
/// playback.
///
/// Defaults to 800 ms.
final Duration autoPlayAnimationDuration;
/// Determines the animation curve physics.
///
/// Defaults to [Curves.fastOutSlowIn].
final Curve autoPlayCurve;
/// Determines if current page should be larger than the side images,
/// creating a feeling of depth in the carousel.
///
/// Defaults to false.
final bool? enlargeCenterPage;
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Called whenever the page in the center of the viewport changes.
final Function(int index, CarouselPageChangedReason reason)? onPageChanged;
/// Called whenever the carousel is scrolled
final ValueChanged<double?>? onScrolled;
/// How the carousel should respond to user input.
///
/// For example, determines how the items continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// Defaults to matching platform conventions.
final ScrollPhysics? scrollPhysics;
/// Set to false to disable page snapping, useful for custom scroll behavior.
///
/// Default to `true`.
final bool pageSnapping;
/// If `true`, the auto play function will be paused when user is interacting
/// with the carousel, and will be resumed when user finish interacting.
///
/// Default to `true`.
final bool pauseAutoPlayOnTouch;
/// If `true`, the auto play function will be paused when user is calling
/// [PageController]'s [nextPage] or [previousPage] or [animateToPage] method.
/// And after the animation complete, the auto play will be resumed.
///
/// Default to `true`.
final bool pauseAutoPlayOnManualNavigate;
/// If [enableInfiniteScroll] is `false`, and [autoPlay] is `true`, this option
/// decide the carousel should go to the first item when it reach the last item or not.
/// If set to `true`, the auto play will be paused when it reach the last item.
/// If set to `false`, the auto play function will animate to the first item
/// when it was in the last item.
final bool pauseAutoPlayInFiniteScroll;
/// Pass a [PageStorageKey] if you want to keep the pageview's position when
/// it was recreated.
final PageStorageKey? pageViewKey;
/// Use [enlargeStrategy] to determine which method to enlarge the center page.
final CenterPageEnlargeStrategy enlargeStrategy;
/// Whether or not to disable the [Center] widget for each slide.
final bool disableCenter;
/// Whether to add padding to both ends of the list.
/// If this is set to true and [viewportFraction] < 1.0, padding will be added
/// such that the first and last child slivers will be in the center of the 1
/// viewport when scrolled all the way to the start or end, respectively.
///
/// If [viewportFraction] >= 1.0, this property has no effect.
/// This property defaults to true and must not be null.
final bool padEnds;
/// Exposed [clipBehavior] of [PageView]
final Clip clipBehavior;
CarouselOptions({
this.height,
this.aspectRatio = 16 / 9,
this.viewportFraction = 0.8,
this.initialPage = 0,
this.enableInfiniteScroll = true,
this.reverse = false,
this.autoPlay = false,
this.autoPlayInterval = const Duration(seconds: 4),
this.autoPlayAnimationDuration = const Duration(milliseconds: 800),
this.autoPlayCurve = Curves.fastOutSlowIn,
this.enlargeCenterPage = false,
this.onPageChanged,
this.onScrolled,
this.scrollPhysics,
this.pageSnapping = true,
this.scrollDirection = Axis.horizontal,
this.pauseAutoPlayOnTouch = true,
this.pauseAutoPlayOnManualNavigate = true,
this.pauseAutoPlayInFiniteScroll = false,
this.pageViewKey,
this.enlargeStrategy = CenterPageEnlargeStrategy.scale,
this.disableCenter = false,
this.padEnds = true,
this.clipBehavior = Clip.hardEdge,
});
///Generate new [CarouselOptions] based on old ones.
CarouselOptions copyWith(
{double? height,
double? aspectRatio,
double? viewportFraction,
int? initialPage,
bool? enableInfiniteScroll,
bool? reverse,
bool? autoPlay,
Duration? autoPlayInterval,
Duration? autoPlayAnimationDuration,
Curve? autoPlayCurve,
bool? enlargeCenterPage,
Function(int index, CarouselPageChangedReason reason)? onPageChanged,
ValueChanged<double?>? onScrolled,
ScrollPhysics? scrollPhysics,
bool? pageSnapping,
Axis? scrollDirection,
bool? pauseAutoPlayOnTouch,
bool? pauseAutoPlayOnManualNavigate,
bool? pauseAutoPlayInFiniteScroll,
PageStorageKey? pageViewKey,
CenterPageEnlargeStrategy? enlargeStrategy,
bool? disableCenter,
Clip? clipBehavior,
bool? padEnds}) =>
CarouselOptions(
height: height ?? this.height,
aspectRatio: aspectRatio ?? this.aspectRatio,
viewportFraction: viewportFraction ?? this.viewportFraction,
initialPage: initialPage ?? this.initialPage,
enableInfiniteScroll: enableInfiniteScroll ?? this.enableInfiniteScroll,
reverse: reverse ?? this.reverse,
autoPlay: autoPlay ?? this.autoPlay,
autoPlayInterval: autoPlayInterval ?? this.autoPlayInterval,
autoPlayAnimationDuration:
autoPlayAnimationDuration ?? this.autoPlayAnimationDuration,
autoPlayCurve: autoPlayCurve ?? this.autoPlayCurve,
enlargeCenterPage: enlargeCenterPage ?? this.enlargeCenterPage,
onPageChanged: onPageChanged ?? this.onPageChanged,
onScrolled: onScrolled ?? this.onScrolled,
scrollPhysics: scrollPhysics ?? this.scrollPhysics,
pageSnapping: pageSnapping ?? this.pageSnapping,
scrollDirection: scrollDirection ?? this.scrollDirection,
pauseAutoPlayOnTouch: pauseAutoPlayOnTouch ?? this.pauseAutoPlayOnTouch,
pauseAutoPlayOnManualNavigate:
pauseAutoPlayOnManualNavigate ?? this.pauseAutoPlayOnManualNavigate,
pauseAutoPlayInFiniteScroll:
pauseAutoPlayInFiniteScroll ?? this.pauseAutoPlayInFiniteScroll,
pageViewKey: pageViewKey ?? this.pageViewKey,
enlargeStrategy: enlargeStrategy ?? this.enlargeStrategy,
disableCenter: disableCenter ?? this.disableCenter,
clipBehavior: clipBehavior ?? this.clipBehavior,
padEnds: padEnds ?? this.padEnds,
);
}

View file

@ -0,0 +1,358 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
library carousel_slider;
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'carousel_state.dart';
import 'carousel_utils.dart';
import 'carousel_controller.dart';
import 'carousel_options.dart';
export 'carousel_controller.dart';
export 'carousel_options.dart';
typedef ExtendedIndexedWidgetBuilder = Widget Function(
BuildContext context, int index, int realIndex);
class CarouselSlider extends StatefulWidget {
/// [CarouselOptions] to create a [CarouselState] with.
final CarouselOptions options;
/// The widgets to be shown in the carousel of default constructor.
final List<Widget>? items;
/// The widget item builder that will be used to build item on demand
/// The third argument is the [PageView]'s real index, can be used to cooperate
/// with Hero.
final ExtendedIndexedWidgetBuilder? itemBuilder;
/// A [MapController], used to control the map.
final CarouselControllerImpl _carouselController;
final int? itemCount;
CarouselSlider(
{required this.items,
required this.options,
CarouselController? carouselController,
Key? key})
: itemBuilder = null,
itemCount = items != null ? items.length : 0,
_carouselController = carouselController != null
? carouselController as CarouselControllerImpl
: CarouselController() as CarouselControllerImpl,
super(key: key);
/// The on demand item builder constructor/
CarouselSlider.builder(
{required this.itemCount,
required this.itemBuilder,
required this.options,
CarouselController? carouselController,
Key? key})
: items = null,
_carouselController = carouselController != null
? carouselController as CarouselControllerImpl
: CarouselController() as CarouselControllerImpl,
super(key: key);
@override
CarouselSliderState createState() => CarouselSliderState();
}
class CarouselSliderState extends State<CarouselSlider>
with TickerProviderStateMixin {
late CarouselControllerImpl carouselController;
Timer? timer;
CarouselOptions get options => widget.options;
CarouselState? carouselState;
PageController? pageController;
/// [mode] is related to why the page is being changed.
CarouselPageChangedReason mode = CarouselPageChangedReason.controller;
CarouselSliderState();
void changeMode(CarouselPageChangedReason mode) {
this.mode = mode;
}
@override
void didUpdateWidget(CarouselSlider oldWidget) {
carouselState!.options = options;
carouselState!.itemCount = widget.itemCount;
/// [pageController] needs to be re-initialized to respond to state changes.
pageController = PageController(
viewportFraction: options.viewportFraction,
initialPage: carouselState!.realPage,
);
carouselState!.pageController = pageController;
/// handle autoplay when state changes
handleAutoPlay();
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
super.initState();
carouselController = widget._carouselController;
carouselState = CarouselState(options, clearTimer, resumeTimer, changeMode);
carouselState!.itemCount = widget.itemCount;
carouselController.state = carouselState;
carouselState!.initialPage = widget.options.initialPage;
carouselState!.realPage = options.enableInfiniteScroll
? carouselState!.realPage + carouselState!.initialPage
: carouselState!.initialPage;
handleAutoPlay();
pageController = PageController(
viewportFraction: options.viewportFraction,
initialPage: carouselState!.realPage,
);
carouselState!.pageController = pageController;
}
Timer? getTimer() {
return widget.options.autoPlay
? Timer.periodic(widget.options.autoPlayInterval, (_) {
final route = ModalRoute.of(context);
if (route?.isCurrent == false) {
return;
}
CarouselPageChangedReason previousReason = mode;
changeMode(CarouselPageChangedReason.timed);
int nextPage = carouselState!.pageController!.page!.round() + 1;
int itemCount = widget.itemCount ?? widget.items!.length;
if (nextPage >= itemCount &&
widget.options.enableInfiniteScroll == false) {
if (widget.options.pauseAutoPlayInFiniteScroll) {
clearTimer();
return;
}
nextPage = 0;
}
carouselState!.pageController!
.animateToPage(nextPage,
duration: widget.options.autoPlayAnimationDuration,
curve: widget.options.autoPlayCurve)
.then((_) => changeMode(previousReason));
})
: null;
}
void clearTimer() {
if (timer != null) {
timer?.cancel();
timer = null;
}
}
void resumeTimer() {
timer ??= getTimer();
}
void handleAutoPlay() {
bool autoPlayEnabled = widget.options.autoPlay;
if (autoPlayEnabled && timer != null) return;
clearTimer();
if (autoPlayEnabled) {
resumeTimer();
}
}
Widget getGestureWrapper(Widget child) {
Widget wrapper;
if (widget.options.height != null) {
wrapper = SizedBox(height: widget.options.height, child: child);
} else {
wrapper =
AspectRatio(aspectRatio: widget.options.aspectRatio, child: child);
}
return RawGestureDetector(
gestures: {
_MultipleGestureRecognizer:
GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>(
() => _MultipleGestureRecognizer(),
(_MultipleGestureRecognizer instance) {
instance.onStart = (_) {
onStart();
};
instance.onDown = (_) {
onPanDown();
};
instance.onEnd = (_) {
onPanUp();
};
instance.onCancel = () {
onPanUp();
};
}),
},
child: NotificationListener(
onNotification: (Notification notification) {
if (widget.options.onScrolled != null &&
notification is ScrollUpdateNotification) {
widget.options.onScrolled!(carouselState!.pageController!.page);
}
return false;
},
child: wrapper,
),
);
}
Widget getCenterWrapper(Widget child) {
if (widget.options.disableCenter) {
return Container(
child: child,
);
}
return Center(child: child);
}
Widget getEnlargeWrapper(Widget? child,
{double? width, double? height, double? scale}) {
if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.height) {
return SizedBox(width: width, height: height, child: child);
}
return Transform.scale(
scale: scale!,
child: SizedBox(width: width, height: height, child: child));
}
void onStart() {
changeMode(CarouselPageChangedReason.manual);
}
void onPanDown() {
if (widget.options.pauseAutoPlayOnTouch) {
clearTimer();
}
changeMode(CarouselPageChangedReason.manual);
}
void onPanUp() {
if (widget.options.pauseAutoPlayOnTouch) {
resumeTimer();
}
}
@override
void dispose() {
super.dispose();
clearTimer();
}
@override
Widget build(BuildContext context) {
return getGestureWrapper(PageView.builder(
padEnds: widget.options.padEnds,
scrollBehavior: ScrollConfiguration.of(context).copyWith(
scrollbars: false,
overscroll: false,
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
),
clipBehavior: widget.options.clipBehavior,
physics: widget.options.scrollPhysics,
scrollDirection: widget.options.scrollDirection,
pageSnapping: widget.options.pageSnapping,
controller: carouselState!.pageController,
reverse: widget.options.reverse,
itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount,
key: widget.options.pageViewKey,
onPageChanged: (int index) {
int currentPage = getRealIndex(index + carouselState!.initialPage,
carouselState!.realPage, widget.itemCount);
if (widget.options.onPageChanged != null) {
widget.options.onPageChanged!(currentPage, mode);
}
},
itemBuilder: (BuildContext context, int idx) {
final int index = getRealIndex(idx + carouselState!.initialPage,
carouselState!.realPage, widget.itemCount);
return AnimatedBuilder(
animation: carouselState!.pageController!,
child: (widget.items != null)
? (widget.items!.isNotEmpty ? widget.items![index] : Container())
: widget.itemBuilder!(context, index, idx),
builder: (BuildContext context, child) {
double distortionValue = 1.0;
// if [enlargeCenterPage] is true, we must calculate the carousel item's height
// to display the visual effect
if (widget.options.enlargeCenterPage != null &&
widget.options.enlargeCenterPage == true) {
// [pageController.page] can only be accessed after the first build,
// so in the first build we calculate the [itemOffset] manually
double itemOffset = 0;
var position = carouselState?.pageController?.position;
if (position != null &&
position.hasPixels &&
position.hasContentDimensions) {
var page = carouselState?.pageController?.page;
if (page != null) {
itemOffset = page - idx;
}
} else {
BuildContext storageContext = carouselState!
.pageController!.position.context.storageContext;
final double? previousSavedPosition =
PageStorage.of(storageContext)?.readState(storageContext)
as double?;
if (previousSavedPosition != null) {
itemOffset = previousSavedPosition - idx.toDouble();
} else {
itemOffset =
carouselState!.realPage.toDouble() - idx.toDouble();
}
}
final num distortionRatio =
(1 - (itemOffset.abs() * 0.3)).clamp(0.0, 1.0);
distortionValue =
Curves.easeOut.transform(distortionRatio as double);
}
final double height = widget.options.height ??
MediaQuery.of(context).size.width *
(1 / widget.options.aspectRatio);
if (widget.options.scrollDirection == Axis.horizontal) {
return getCenterWrapper(getEnlargeWrapper(child,
height: distortionValue * height, scale: distortionValue));
} else {
return getCenterWrapper(getEnlargeWrapper(child,
width: distortionValue * MediaQuery.of(context).size.width,
scale: distortionValue));
}
},
);
},
));
}
}
class _MultipleGestureRecognizer extends PanGestureRecognizer {}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'carousel_slider.dart';
class CarouselState {
/// The [CarouselOptions] to create this state
CarouselOptions options;
/// [pageController] is created using the properties passed to the constructor
/// and can be used to control the [PageView] it is passed to.
PageController? pageController;
/// The actual index of the [PageView].
///
/// This value can be ignored unless you know the carousel will be scrolled
/// backwards more then 10000 pages.
/// Defaults to 10000 to simulate infinite backwards scrolling.
int realPage = 10000;
/// The initial index of the [PageView] on [CarouselSlider] init.
///
int initialPage = 0;
/// The widgets count that should be shown at carousel
int? itemCount;
/// Will be called when using [pageController] to go to next page or
/// previous page. It will clear the autoPlay timer.
/// Internal use only
Function onResetTimer;
/// Will be called when using pageController to go to next page or
/// previous page. It will restart the autoPlay timer.
/// Internal use only
Function onResumeTimer;
/// The callback to set the Reason Carousel changed
Function(CarouselPageChangedReason) changeMode;
CarouselState(
this.options, this.onResetTimer, this.onResumeTimer, this.changeMode);
}

View file

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
/// Converts an index of a set size to the corresponding index of a collection of another size
/// as if they were circular.
///
/// Takes a [position] from collection Foo, a [base] from where Foo's index originated
/// and the [length] of a second collection Baa, for which the correlating index is sought.
///
/// For example; We have a Carousel of 10000(simulating infinity) but only 6 images.
/// We need to repeat the images to give the illusion of a never ending stream.
/// By calling [getRealIndex] with position and base we get an offset.
/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image
/// to be placed in the given position.
int getRealIndex(int position, int base, int? length) {
final int offset = position - base;
return remainder(offset, length);
}
/// Returns the remainder of the modulo operation [input] % [source], and adjust it for
/// negative values.
int remainder(int input, int? source) {
if (source == 0) return 0;
final int result = input % source!;
return result < 0 ? source + result : result;
}

View file

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_input_library/src/inputs/date_picker/date_picker_field.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
enum FlutterFormDateTimeType {
date,
time,
dateTime,
range,
}
class FlutterFormInputDateTime extends ConsumerWidget {
const FlutterFormInputDateTime({
Key? key,
this.label,
this.showIcon = true,
required this.inputType,
required this.dateFormat,
this.firstDate,
this.lastDate,
this.initialDate,
this.initialDateTimeRange,
this.icon = Icons.calendar_today,
this.initialValue,
this.onChanged,
this.onSaved,
this.validator,
}) : super(
key: key,
);
final Widget? label;
final bool showIcon;
final FlutterFormDateTimeType inputType;
final DateFormat dateFormat;
final DateTime? initialDate;
final DateTimeRange? initialDateTimeRange;
final DateTime? firstDate;
final DateTime? lastDate;
final IconData icon;
final String? initialValue;
final String? Function(String?)? validator;
final void Function(String?)? onSaved;
final void Function(String?)? onChanged;
@override
Widget build(BuildContext context, WidgetRef ref) {
return DateTimeInputField(
label: label,
icon: icon,
firstDate: firstDate,
lastDate: lastDate,
inputType: inputType,
dateFormat: dateFormat,
initialDate: initialDate,
initialDateTimeRange: initialDateTimeRange,
initialValue: initialValue,
onChanged: (value) => onChanged?.call(value),
onSaved: (value) => onSaved?.call(value),
showIcon: showIcon,
);
}
}

View file

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_input_library/src/inputs/date_picker/date_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class DateTimeInputField extends ConsumerStatefulWidget {
const DateTimeInputField({
Key? key,
required this.inputType,
this.label,
this.showIcon = true,
this.icon,
required this.dateFormat,
required this.firstDate,
required this.lastDate,
this.initialDate,
this.initialDateTimeRange,
this.initialValue,
this.onChanged,
this.onSaved,
this.validator,
}) : super(
key: key,
);
final FlutterFormDateTimeType inputType;
final DateFormat dateFormat;
final bool showIcon;
final DateTime? firstDate;
final DateTime? lastDate;
final DateTime? initialDate;
final DateTimeRange? initialDateTimeRange;
final IconData? icon;
final Widget? label;
final String? initialValue;
final String? Function(String?)? validator;
final void Function(String?)? onSaved;
final void Function(String?)? onChanged;
@override
ConsumerState<DateTimeInputField> createState() => _DateInputFieldState();
}
class _DateInputFieldState extends ConsumerState<DateTimeInputField> {
late final DateTime firstDate;
late final DateTime lastDate;
late final DateTime initialDate;
late final DateTimeRange initialDateRange;
String currentValue = '';
@override
void initState() {
firstDate = widget.firstDate ??
DateTime.now().subtract(
const Duration(days: 1000),
);
lastDate = widget.lastDate ??
DateTime.now().add(
const Duration(days: 1000),
);
initialDate = widget.initialDate ?? DateTime.now();
initialDateRange = widget.initialDateTimeRange ??
DateTimeRange(
start: DateTime.now(),
end: DateTime.now().add(
const Duration(days: 7),
),
);
super.initState();
}
@override
Widget build(BuildContext context) {
Future<String> getInputFromUser(FlutterFormDateTimeType inputType) async {
String userInput = '';
switch (inputType) {
case FlutterFormDateTimeType.date:
DateTime? unformatted = await showDatePicker(
initialDate: initialDate,
context: context,
firstDate: firstDate,
lastDate: lastDate,
);
userInput = unformatted != null
? widget.dateFormat.format(unformatted)
: userInput;
break;
case FlutterFormDateTimeType.dateTime:
await getInputFromUser(FlutterFormDateTimeType.date)
.then((value) async {
if (value != '') {
String secondInput =
await getInputFromUser(FlutterFormDateTimeType.time);
if (secondInput != '') {
userInput = '$value $secondInput';
}
}
});
break;
case FlutterFormDateTimeType.range:
userInput = (await showDateRangePicker(
context: context,
firstDate: firstDate,
lastDate: lastDate,
initialDateRange: initialDateRange)
.then((value) {
return value != null
? '${widget.dateFormat.format(value.start)} - ${widget.dateFormat.format(value.end)}'
: '';
}))
.toString();
break;
case FlutterFormDateTimeType.time:
userInput = await showTimePicker(
context: context, initialTime: TimeOfDay.now())
.then((value) => value == null ? '' : value.format(context));
}
return userInput;
}
return TextFormField(
keyboardType: TextInputType.none,
readOnly: true,
key: Key(currentValue.toString()),
initialValue: widget.initialValue,
onSaved: (value) => widget.onSaved?.call(value),
onTap: () async {
String userInput = await getInputFromUser(widget.inputType);
setState(() {
currentValue = userInput != '' ? userInput : currentValue;
widget.onChanged?.call(userInput != '' ? userInput : currentValue);
});
},
validator: (value) => widget.validator?.call(value),
decoration: InputDecoration(
suffixIcon: widget.showIcon ? Icon(widget.icon) : null,
focusColor: Theme.of(context).primaryColor,
label: widget.label ?? const Text("Date"),
),
);
}
}

View file

@ -0,0 +1,7 @@
export 'carousel/carousel.dart';
export 'number_picker/number_picker.dart';
export 'text/password.dart';
export 'text/plain_text.dart';
export 'slider/slider.dart';
export 'switch/switch.dart';
export 'date_picker/date_picker.dart';

View file

@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'number_picker_field.dart';
class DecimalNumberPicker extends StatelessWidget {
final int minValue;
final int maxValue;
final double value;
final ValueChanged<double> onChanged;
final int itemCount;
final double itemHeight;
final double itemWidth;
final Axis axis;
final TextStyle? textStyle;
final TextStyle? selectedTextStyle;
final bool haptics;
final TextMapper? integerTextMapper;
final TextMapper? decimalTextMapper;
final bool integerZeroPad;
/// Decoration to apply to central box where the selected integer value is placed
final Decoration? integerDecoration;
/// Decoration to apply to central box where the selected decimal value is placed
final Decoration? decimalDecoration;
/// 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;
const DecimalNumberPicker({
Key? key,
required this.minValue,
required this.maxValue,
required this.value,
required this.onChanged,
this.itemCount = 3,
this.itemHeight = 50,
this.itemWidth = 100,
this.axis = Axis.vertical,
this.textStyle,
this.selectedTextStyle,
this.haptics = false,
this.decimalPlaces = 1,
this.integerTextMapper,
this.decimalTextMapper,
this.integerZeroPad = false,
this.integerDecoration,
this.decimalDecoration,
}) : assert(minValue <= value),
assert(value <= maxValue),
super(key: key);
@override
Widget build(BuildContext context) {
final isMax = value.floor() == maxValue;
final decimalValue = isMax
? 0
: ((value - value.floorToDouble()) * math.pow(10, decimalPlaces))
.round();
final doubleMaxValue = isMax ? 0 : math.pow(10, decimalPlaces).toInt() - 1;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
NumberPicker(
minValue: minValue,
maxValue: maxValue,
value: value.floor(),
onChanged: _onIntChanged,
itemCount: itemCount,
itemHeight: itemHeight,
itemWidth: itemWidth,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
haptics: haptics,
zeroPad: integerZeroPad,
textMapper: integerTextMapper,
decoration: integerDecoration,
),
NumberPicker(
minValue: 0,
maxValue: doubleMaxValue,
value: decimalValue,
onChanged: _onDoubleChanged,
itemCount: itemCount,
itemHeight: itemHeight,
itemWidth: itemWidth,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
haptics: haptics,
textMapper: decimalTextMapper,
decoration: decimalDecoration,
),
],
);
}
void _onIntChanged(int intValue) {
final newValue =
(value - value.floor() + intValue).clamp(minValue, maxValue);
onChanged(newValue.toDouble());
}
void _onDoubleChanged(int doubleValue) {
final decimalPart = double.parse(
(doubleValue * math.pow(10, -decimalPlaces))
.toStringAsFixed(decimalPlaces));
onChanged(value.floor() + decimalPart);
}
}

View file

@ -0,0 +1,366 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
library infinite_listview;
import 'dart:math' as math;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class InfiniteListView extends StatefulWidget {
/// See [ListView.builder]
const InfiniteListView.builder({
Key? key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.physics,
this.padding,
this.itemExtent,
required this.itemBuilder,
this.itemCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.anchor = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : separatorBuilder = null,
super(key: key);
/// See [ListView.separated]
const InfiniteListView.separated({
Key? key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.physics,
this.padding,
required this.itemBuilder,
required this.separatorBuilder,
this.itemCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.anchor = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : itemExtent = null,
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.builder]
final IndexedWidgetBuilder itemBuilder;
/// See: [ListView.separated]
final IndexedWidgetBuilder? separatorBuilder;
/// See: [SliverChildBuilderDelegate.childCount]
final int? itemCount;
/// See: [ListView.itemExtent]
final double? itemExtent;
/// See: [ScrollView.cacheExtent]
final double? cacheExtent;
/// See: [ScrollView.anchor]
final double anchor;
/// See: [SliverChildBuilderDelegate.addAutomaticKeepAlives]
final bool addAutomaticKeepAlives;
/// See: [SliverChildBuilderDelegate.addRepaintBoundaries]
final bool addRepaintBoundaries;
/// See: [SliverChildBuilderDelegate.addSemanticIndexes]
final bool addSemanticIndexes;
/// See: [ScrollView.dragStartBehavior]
final DragStartBehavior dragStartBehavior;
/// See: [ScrollView.keyboardDismissBehavior]
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// See: [ScrollView.restorationId]
final String? restorationId;
/// See: [ScrollView.clipBehavior]
final Clip clipBehavior;
@override
InfiniteListViewState createState() => InfiniteListViewState();
}
class InfiniteListViewState extends State<InfiniteListView> {
InfiniteScrollController? _controller;
InfiniteScrollController get _effectiveController =>
widget.controller ?? _controller!;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = InfiniteScrollController();
}
}
@override
void didUpdateWidget(InfiniteListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null) {
_controller = InfiniteScrollController();
} else if (widget.controller != null && oldWidget.controller == null) {
_controller!.dispose();
_controller = null;
}
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@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 =
widget.physics ?? const AlwaysScrollableScrollPhysics();
return Scrollable(
axisDirection: axisDirection,
controller: _effectiveController,
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: _effectiveController.keepScrollOffset,
negativeScroll: true,
);
/// 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 - widget.anchor,
offset: negativeOffset,
slivers: negativeSlivers,
cacheExtent: widget.cacheExtent,
),
Viewport(
axisDirection: axisDirection,
anchor: widget.anchor,
offset: offset,
slivers: slivers,
cacheExtent: widget.cacheExtent,
),
],
);
});
},
);
}
AxisDirection _getDirection(BuildContext context) {
return getAxisDirectionFromAxisReverseAndDirectionality(
context, widget.scrollDirection, widget.reverse);
}
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
final itemExtent = widget.itemExtent;
final padding = widget.padding ?? EdgeInsets.zero;
return <Widget>[
SliverPadding(
padding: negative
? padding - EdgeInsets.only(bottom: padding.bottom)
: padding - EdgeInsets.only(top: padding.top),
sliver: (itemExtent != null)
? SliverFixedExtentList(
delegate: negative
? negativeChildrenDelegate
: positiveChildrenDelegate,
itemExtent: itemExtent,
)
: SliverList(
delegate: negative
? negativeChildrenDelegate
: positiveChildrenDelegate,
),
)
];
}
SliverChildDelegate get negativeChildrenDelegate {
return SliverChildBuilderDelegate(
(BuildContext context, int index) {
final separatorBuilder = widget.separatorBuilder;
if (separatorBuilder != null) {
final itemIndex = (-1 - index) ~/ 2;
return index.isOdd
? widget.itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
} else {
return widget.itemBuilder(context, -1 - index);
}
},
childCount: widget.itemCount,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
);
}
SliverChildDelegate get positiveChildrenDelegate {
final separatorBuilder = widget.separatorBuilder;
final itemCount = widget.itemCount;
return SliverChildBuilderDelegate(
(separatorBuilder != null)
? (BuildContext context, int index) {
final itemIndex = index ~/ 2;
return index.isEven
? widget.itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
}
: widget.itemBuilder,
childCount: separatorBuilder == null
? itemCount
: (itemCount != null ? math.max(0, itemCount * 2 - 1) : null),
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
properties.add(FlagProperty('reverse',
value: widget.reverse, ifTrue: 'reversed', showName: true));
properties.add(DiagnosticsProperty<ScrollController>(
'controller', widget.controller,
showName: false, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics,
showName: false, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
'padding', widget.padding,
defaultValue: null));
properties.add(
DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null));
properties.add(
DoubleProperty('cacheExtent', widget.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 _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,
this.negativeScroll = false,
}) : super(
physics: physics,
context: context,
initialPixels: initialPixels,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
final bool negativeScroll;
void _forceNegativePixels(double value) {
super.forcePixels(-value);
}
@override
void saveScrollOffset() {
if (!negativeScroll) {
super.saveScrollOffset();
}
}
@override
void restoreScrollOffset() {
if (!negativeScroll) {
super.restoreScrollOffset();
}
}
@override
double get minScrollExtent => double.negativeInfinity;
@override
double get maxScrollExtent => double.infinity;
}

View file

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'number_picker_field.dart';
class FlutterFormInputNumberPicker extends ConsumerWidget {
const FlutterFormInputNumberPicker({
Key? key,
Widget? label,
this.minValue = 0,
this.maxValue = 100,
this.onSaved,
this.onChanged,
this.initialValue,
this.validator,
}) : assert(minValue < maxValue),
super(
key: key,
);
final int minValue;
final int maxValue;
final Function(int?)? onSaved;
final String Function(int?)? validator;
final int? initialValue;
final Function(int?)? onChanged;
@override
Widget build(BuildContext context, WidgetRef ref) {
return NumberPickerFormField(
minValue: minValue,
maxValue: maxValue,
onSaved: (value) => onSaved?.call(value),
validator: (value) => validator?.call(value),
onChanged: (value) => onChanged?.call(value),
initialValue: initialValue ?? 0,
);
}
}
class NumberPickerFormField extends FormField<int> {
NumberPickerFormField({
Key? key,
required FormFieldSetter<int> onSaved,
required FormFieldValidator<int> validator,
void Function(int value)? onChanged,
int initialValue = 0,
bool autovalidate = false,
int minValue = 0,
int maxValue = 100,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<int> state) {
return NumberPicker(
minValue: minValue,
maxValue: maxValue,
value: initialValue,
onChanged: (int value) {
onChanged?.call(value);
state.didChange(value);
},
itemHeight: 35,
itemCount: 5,
);
});
}

View file

@ -0,0 +1,309 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'infinite_listview.dart';
typedef TextMapper = String Function(String numberText);
class NumberPicker extends StatefulWidget {
/// Min value user can pick
final int minValue;
/// Max value user can pick
final int maxValue;
/// Currently selected value
final int value;
/// Called when selected value changes
final ValueChanged<int> onChanged;
/// Specifies how many items should be shown - defaults to 3
final int itemCount;
/// 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;
/// height of single item in pixels
final double itemHeight;
/// width of single item in pixels
final double itemWidth;
/// Direction of scrolling
final Axis axis;
/// Style of non-selected numbers. If null, it uses Theme's bodyText2
final TextStyle? textStyle;
/// Style of non-selected numbers. If null, it uses Theme's headline5 with accentColor
final TextStyle? selectedTextStyle;
/// Whether to trigger haptic pulses or not
final bool haptics;
/// Build the text of each item on the picker
final TextMapper? textMapper;
/// Pads displayed integer values up to the length of maxValue
final bool zeroPad;
/// Decoration to apply to central box where the selected value is placed
final Decoration? decoration;
final bool infiniteLoop;
const NumberPicker({
Key? key,
required this.minValue,
required this.maxValue,
required this.value,
required this.onChanged,
this.itemCount = 3,
this.step = 1,
this.itemHeight = 50,
this.itemWidth = 100,
this.axis = Axis.vertical,
this.textStyle,
this.selectedTextStyle,
this.haptics = false,
this.decoration,
this.zeroPad = false,
this.textMapper,
this.infiniteLoop = false,
}) : assert(minValue <= value),
assert(value <= maxValue),
super(key: key);
@override
NumberPickerState createState() => NumberPickerState();
}
class NumberPickerState extends State<NumberPicker> {
late ScrollController _scrollController;
late int value;
@override
void initState() {
super.initState();
value = widget.value;
final initialOffset = (value - widget.minValue) ~/ widget.step * itemExtent;
if (widget.infiniteLoop) {
_scrollController =
InfiniteScrollController(initialScrollOffset: initialOffset);
} else {
_scrollController = ScrollController(initialScrollOffset: initialOffset);
}
_scrollController.addListener(_scrollListener);
}
void _scrollListener() {
var indexOfMiddleElement = (_scrollController.offset / itemExtent).round();
if (widget.infiniteLoop) {
indexOfMiddleElement %= itemCount;
} else {
indexOfMiddleElement = indexOfMiddleElement.clamp(0, itemCount - 1);
}
final intValueInTheMiddle =
_intValueFromIndex(indexOfMiddleElement + additionalItemsOnEachSide);
if (value != intValueInTheMiddle) {
setState(() {
value = intValueInTheMiddle;
});
widget.onChanged(intValueInTheMiddle);
if (widget.haptics) {
HapticFeedback.selectionClick();
}
}
Future.delayed(
const Duration(milliseconds: 100),
() => _maybeCenterValue(),
);
}
@override
void didUpdateWidget(NumberPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != value) {
_maybeCenterValue();
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
bool get isScrolling => _scrollController.position.isScrollingNotifier.value;
double get itemExtent =>
widget.axis == Axis.vertical ? widget.itemHeight : widget.itemWidth;
int get itemCount => (widget.maxValue - widget.minValue) ~/ widget.step + 1;
int get listItemsCount => itemCount + 2 * additionalItemsOnEachSide;
int get additionalItemsOnEachSide => (widget.itemCount - 1) ~/ 2;
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.axis == Axis.vertical
? widget.itemWidth
: widget.itemCount * widget.itemWidth,
height: widget.axis == Axis.vertical
? widget.itemCount * widget.itemHeight
: widget.itemHeight,
child: NotificationListener<ScrollEndNotification>(
onNotification: (not) {
if (not.dragDetails?.primaryVelocity == 0) {
Future.microtask(() => _maybeCenterValue());
}
return true;
},
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: Stack(
children: [
Center(
child: Container(
width: 300,
height: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: const Color(0xFFD8D8D8).withOpacity(0.50),
),
),
),
if (widget.infiniteLoop)
InfiniteListView.builder(
scrollDirection: widget.axis,
controller: _scrollController as InfiniteScrollController,
itemExtent: itemExtent,
itemBuilder: _itemBuilder,
padding: EdgeInsets.zero,
)
else
ListView.builder(
itemCount: listItemsCount,
scrollDirection: widget.axis,
controller: _scrollController,
itemExtent: itemExtent,
itemBuilder: _itemBuilder,
padding: EdgeInsets.zero,
),
_NumberPickerSelectedItemDecoration(
axis: widget.axis,
itemExtent: itemExtent,
decoration: widget.decoration,
),
],
),
),
),
);
}
Widget _itemBuilder(BuildContext context, int index) {
final themeData = Theme.of(context);
final defaultStyle = widget.textStyle ?? themeData.textTheme.bodyText2;
final selectedStyle = widget.selectedTextStyle ??
themeData.textTheme.headline5
?.copyWith(color: themeData.highlightColor);
final valueFromIndex = _intValueFromIndex(index % itemCount);
final isExtra = !widget.infiniteLoop &&
(index < additionalItemsOnEachSide ||
index >= listItemsCount - additionalItemsOnEachSide);
final itemStyle = valueFromIndex == value ? selectedStyle : defaultStyle;
final child = isExtra
? const SizedBox.shrink()
: Text(
_getDisplayedValue(valueFromIndex),
style: itemStyle,
);
return Container(
width: widget.itemWidth,
height: widget.itemHeight,
alignment: Alignment.center,
child: child,
);
}
String _getDisplayedValue(int value) {
final text = widget.zeroPad
? value.toString().padLeft(widget.maxValue.toString().length, '0')
: value.toString();
if (widget.textMapper != null) {
return widget.textMapper!(text);
} else {
return text;
}
}
int _intValueFromIndex(int index) {
index -= additionalItemsOnEachSide;
index %= itemCount;
return widget.minValue + index * widget.step;
}
void _maybeCenterValue() {
if (_scrollController.hasClients && !isScrolling) {
int diff = value - widget.minValue;
int index = diff ~/ widget.step;
if (widget.infiniteLoop) {
final offset = _scrollController.offset + 0.5 * itemExtent;
final cycles = (offset / (itemCount * itemExtent)).floor();
index += cycles * itemCount;
}
_scrollController.animateTo(
index * itemExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
);
}
}
}
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;
}

View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_input_library/src/inputs/slider/slider_field.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FlutterFormInputSlider extends ConsumerWidget {
const FlutterFormInputSlider({
Key? key,
this.minValue = 0,
this.maxValue = 100,
this.onSaved,
this.onChanged,
this.initialValue,
this.validator,
}) : assert(minValue < maxValue),
super(
key: key,
);
final int minValue;
final int maxValue;
final Function(double?)? onSaved;
final String Function(double?)? validator;
final double? initialValue;
final Function(double?)? onChanged;
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliderFormField(
onSaved: (value) => onSaved?.call(value),
validator: (value) => validator?.call(value),
onChanged: (value) => onChanged?.call(value),
initialValue: initialValue ?? 0.5,
);
}
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
/// Creates a slider with the given input parameters
class SliderFormField extends FormField<double> {
SliderFormField({
Key? key,
required FormFieldSetter<double> onSaved,
required FormFieldValidator<double> validator,
void Function(double value)? onChanged,
double initialValue = 0.5,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<double> state) {
return Slider(
value: state.value ?? initialValue,
onChanged: (double value) {
onChanged?.call(value);
state.didChange(value);
},
);
});
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_input_library/src/inputs/switch/switch_field.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FlutterFormInputSwitch extends ConsumerWidget {
final Widget? label;
final Function(bool?)? onSaved;
final String Function(bool?)? validator;
final Function(bool?)? onChanged;
final bool? initialValue;
const FlutterFormInputSwitch({
Key? key,
this.label,
this.onSaved,
this.validator,
this.onChanged,
this.initialValue = false,
}) : super(
key: key,
);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SwitchFormField(
onSaved: (value) => onSaved?.call(value),
onChanged: (value) => onChanged?.call(value),
validator: (value) => validator?.call(value),
initialValue: initialValue ?? false,
);
}
}

View file

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
class SwitchFormField extends FormField<bool> {
SwitchFormField({
Key? key,
required FormFieldSetter<bool> onSaved,
required FormFieldValidator<bool> validator,
bool initialValue = false,
bool autovalidate = false,
void Function(bool? value)? onChanged,
}) : super(
key: key,
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<bool> state) {
return SwitchWidget(
initialValue: initialValue,
state: state,
onChanged: onChanged,
);
});
}
class SwitchWidget extends StatefulWidget {
const SwitchWidget({
this.initialValue = false,
required this.state,
this.onChanged,
super.key,
});
final bool initialValue;
final FormFieldState<bool> state;
final void Function(bool? value)? onChanged;
@override
State<SwitchWidget> createState() => _SwitchWidgetState();
}
class _SwitchWidgetState extends State<SwitchWidget> {
late bool value = widget.initialValue;
@override
Widget build(BuildContext context) {
return Switch(
value: value,
onChanged: (bool value) {
widget.onChanged?.call(value);
widget.state.didChange(value);
setState(() {
this.value = value;
});
},
);
}
}

View file

@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Generates a [TextFormField] for passwords. It requires a [FlutterFormInputController]
/// as the [controller] parameter and an optional [Widget] as [label]
class FlutterFormInputPassword extends ConsumerStatefulWidget {
final Widget? label;
final String? initialValue;
final Function(String?)? onSaved;
final String Function(String?)? validator;
final Function(String?)? onChanged;
final Function(String?)? onFieldSubmitted;
const FlutterFormInputPassword({
Key? key,
this.label,
this.initialValue,
this.onSaved,
this.validator,
this.onChanged,
this.onFieldSubmitted,
}) : super(key: key);
@override
ConsumerState<FlutterFormInputPassword> createState() =>
_PasswordTextFieldState();
}
class _PasswordTextFieldState extends ConsumerState<FlutterFormInputPassword> {
bool obscured = true;
@override
Widget build(BuildContext context) {
return TextFormField(
initialValue: widget.initialValue,
obscureText: obscured,
onSaved: (value) => widget.onSaved?.call(value),
validator: (value) => widget.validator?.call(value),
onChanged: (value) => widget.onChanged?.call(value),
onFieldSubmitted: (value) => widget.onFieldSubmitted?.call(value),
decoration: InputDecoration(
label: widget.label ?? const Text("Password"),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscured = !obscured;
});
},
icon: Icon(obscured ? Icons.visibility_off : Icons.visibility),
),
),
);
}
}

View file

@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FlutterFormInputPlainText extends ConsumerWidget {
const FlutterFormInputPlainText({
Key? key,
this.label,
this.decoration,
this.textAlignVertical,
this.expands = false,
this.maxLines = 1,
this.scrollPadding,
this.maxLength,
this.keyboardType,
this.initialValue,
this.onChanged,
this.onSaved,
this.validator,
this.onFieldSubmitted,
}) : super(
key: key,
);
final InputDecoration? decoration;
final TextAlignVertical? textAlignVertical;
final bool expands;
final int? maxLines;
final int? maxLength;
final EdgeInsets? scrollPadding;
final TextInputType? keyboardType;
final Widget? label;
final String? initialValue;
final Function(String?)? onSaved;
final String Function(String?)? validator;
final Function(String?)? onChanged;
final Function(String?)? onFieldSubmitted;
@override
Widget build(BuildContext context, WidgetRef ref) {
InputDecoration inputDecoration = decoration ??
InputDecoration(
label: label ?? const Text("Plain text"),
);
return TextFormField(
scrollPadding: scrollPadding ?? const EdgeInsets.all(20.0),
initialValue: initialValue,
onSaved: (value) => onSaved?.call(value),
validator: (value) => validator?.call(value),
onChanged: (value) => onChanged?.call(value),
onFieldSubmitted: (value) => onFieldSubmitted?.call(value),
decoration: inputDecoration,
textAlignVertical: textAlignVertical,
expands: expands,
maxLines: maxLines,
maxLength: maxLength,
keyboardType: keyboardType,
);
}
}
class FlutterFormInputMultiLine extends StatelessWidget {
const FlutterFormInputMultiLine({
Key? key,
this.label,
this.hint,
this.maxCharacters,
this.scrollPadding,
this.keyboardType,
this.initialValue,
this.decoration,
this.onChanged,
this.onSaved,
this.validator,
this.onFieldSubmitted,
}) : super(key: key);
final Widget? label;
final String? hint;
final int? maxCharacters;
final InputDecoration? decoration;
final EdgeInsets? scrollPadding;
final TextInputType? keyboardType;
final String? initialValue;
final Function(String?)? onSaved;
final String Function(String?)? validator;
final Function(String?)? onChanged;
final Function(String?)? onFieldSubmitted;
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: FlutterFormInputPlainText(
label: label,
textAlignVertical: TextAlignVertical.top,
expands: true,
maxLines: null,
maxLength: maxCharacters,
initialValue: initialValue,
scrollPadding: scrollPadding,
keyboardType: keyboardType,
onSaved: onSaved,
validator: validator,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
decoration: decoration ??
InputDecoration(
hintText: hint,
floatingLabelBehavior: FloatingLabelBehavior.never,
isDense: true,
border: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFF979797)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFF979797)),
),
filled: true,
),
),
),
],
);
}
}

5
lib/src/utils/utils.dart Normal file
View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
export 'validators/validators.dart';

View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
class EmailValidator {
static bool isValid(String? email) {
if (email == null) {
return false;
}
return RegExp(
r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""",
).hasMatch(email);
}
}

View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
export 'email/email.dart';

56
pubspec.yaml Normal file
View file

@ -0,0 +1,56 @@
name: flutter_input_library
description: A new Flutter package project.
version: 0.0.1
repository: https://github.com/Iconica-Development/flutter_input_library
environment:
sdk: '>=2.18.2 <3.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_riverpod: any
intl: any
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages