mirror of
https://github.com/Iconica-Development/flutter_input_library.git
synced 2025-05-18 08:53:45 +02:00
initial commit
This commit is contained in:
commit
a7c3b3b192
38 changed files with 3263 additions and 0 deletions
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal 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
10
.metadata
Normal 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
3
CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
* Initial release, retrieved inputs from flutter_form
|
198
CONTRIBUTING.md
Normal file
198
CONTRIBUTING.md
Normal 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
29
LICENSE.md
Normal 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
23
README.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[](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
4
analysis_options.yaml
Normal 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
44
example/.gitignore
vendored
Normal 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
45
example/.metadata
Normal 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'
|
29
example/analysis_options.yaml
Normal file
29
example/analysis_options.yaml
Normal 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
128
example/lib/main.dart
Normal 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
196
example/pubspec.lock
Normal 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
96
example/pubspec.yaml
Normal 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
|
7
lib/flutter_input_library.dart
Normal file
7
lib/flutter_input_library.dart
Normal 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';
|
41
lib/src/inputs/carousel/carousel.dart
Normal file
41
lib/src/inputs/carousel/carousel.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
150
lib/src/inputs/carousel/carousel_controller.dart
Normal file
150
lib/src/inputs/carousel/carousel_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
43
lib/src/inputs/carousel/carousel_form.dart
Normal file
43
lib/src/inputs/carousel/carousel_form.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
220
lib/src/inputs/carousel/carousel_options.dart
Normal file
220
lib/src/inputs/carousel/carousel_options.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
358
lib/src/inputs/carousel/carousel_slider.dart
Normal file
358
lib/src/inputs/carousel/carousel_slider.dart
Normal 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 {}
|
45
lib/src/inputs/carousel/carousel_state.dart
Normal file
45
lib/src/inputs/carousel/carousel_state.dart
Normal 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);
|
||||||
|
}
|
27
lib/src/inputs/carousel/carousel_utils.dart
Normal file
27
lib/src/inputs/carousel/carousel_utils.dart
Normal 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;
|
||||||
|
}
|
68
lib/src/inputs/date_picker/date_picker.dart
Normal file
68
lib/src/inputs/date_picker/date_picker.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
148
lib/src/inputs/date_picker/date_picker_field.dart
Normal file
148
lib/src/inputs/date_picker/date_picker_field.dart
Normal 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"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
7
lib/src/inputs/inputs.dart
Normal file
7
lib/src/inputs/inputs.dart
Normal 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';
|
116
lib/src/inputs/number_picker/decimal_numberpicker.dart
Normal file
116
lib/src/inputs/number_picker/decimal_numberpicker.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
366
lib/src/inputs/number_picker/infinite_listview.dart
Normal file
366
lib/src/inputs/number_picker/infinite_listview.dart
Normal 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;
|
||||||
|
}
|
74
lib/src/inputs/number_picker/number_picker.dart
Normal file
74
lib/src/inputs/number_picker/number_picker.dart
Normal 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
309
lib/src/inputs/number_picker/number_picker_field.dart
Normal file
309
lib/src/inputs/number_picker/number_picker_field.dart
Normal 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;
|
||||||
|
}
|
40
lib/src/inputs/slider/slider.dart
Normal file
40
lib/src/inputs/slider/slider.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
lib/src/inputs/slider/slider_field.dart
Normal file
30
lib/src/inputs/slider/slider_field.dart
Normal 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
38
lib/src/inputs/switch/switch.dart
Normal file
38
lib/src/inputs/switch/switch.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
lib/src/inputs/switch/switch_field.dart
Normal file
63
lib/src/inputs/switch/switch_field.dart
Normal 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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
58
lib/src/inputs/text/password.dart
Normal file
58
lib/src/inputs/text/password.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
132
lib/src/inputs/text/plain_text.dart
Normal file
132
lib/src/inputs/text/plain_text.dart
Normal 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
5
lib/src/utils/utils.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
export 'validators/validators.dart';
|
15
lib/src/utils/validators/email/email.dart
Normal file
15
lib/src/utils/validators/email/email.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
5
lib/src/utils/validators/validators.dart
Normal file
5
lib/src/utils/validators/validators.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
export 'email/email.dart';
|
56
pubspec.yaml
Normal file
56
pubspec.yaml
Normal 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
|
Loading…
Reference in a new issue