mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Add workflow to deploy the sample index (#791)
* Add workflow to build and deploy the sample index * update gh-pages action * fix yaml * create web/ directory in build * grammar * add ignored directories * revert pubspec.lock files * add job to run _tool/verify_samples.dart * Update filipino_cuisine for Flutter 2 * remove timeflow demo. The unnamed List constructor is now deprecated, refactoring this code to use add() requires more knowledge about the code for this demo. https://dart.dev/null-safety/understanding-null-safety#no-unnamed-list-constructor * update slide_puzzle * ensure stable channel is used to verify * move verify web demos action into separate yaml file - avoid running on each flutter version. * add on: pull_request * update slide_puzzle * Update gh-pages.yml * Add copyright header
This commit is contained in:
31
.github/workflows/gh-pages.yml
vendored
Normal file
31
.github/workflows/gh-pages.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Deploy to GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: subosito/flutter-action@v1
|
||||
|
||||
- name: Init scripts
|
||||
run: dart pub get
|
||||
working-directory: web/_tool
|
||||
|
||||
- name: Build
|
||||
run: dart _tool/build_ci.dart
|
||||
working-directory: web
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: web/samples_index/public
|
||||
20
.github/workflows/verify-web-demos.yml
vendored
Normal file
20
.github/workflows/verify-web-demos.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Verify web demos
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
verify-web-demos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
channel: stable
|
||||
- name: Init scripts
|
||||
run: dart pub get
|
||||
working-directory: web/_tool
|
||||
- name: Verify packages
|
||||
run: dart _tool/verify_packages.dart
|
||||
working-directory: web
|
||||
43
web/_tool/build_ci.dart
Normal file
43
web/_tool/build_ci.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2021 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'common.dart';
|
||||
|
||||
final ignoredDirectories = ['_tool', 'samples_index'];
|
||||
|
||||
main() async {
|
||||
final packageDirs = [
|
||||
...listPackageDirs(Directory.current)
|
||||
.map((path) => p.relative(path, from: Directory.current.path))
|
||||
.where((path) => !ignoredDirectories.contains(path))
|
||||
];
|
||||
|
||||
print('Building the sample index...');
|
||||
await run('samples_index', 'pub', ['get']);
|
||||
await run('samples_index', 'pub', ['run', 'grinder', 'deploy']);
|
||||
|
||||
// Create the directory each Flutter Web sample lives in
|
||||
Directory(p.join(Directory.current.path, 'samples_index', 'public', 'web'))
|
||||
.createSync(recursive: true);
|
||||
|
||||
for (var i = 0; i < packageDirs.length; i++) {
|
||||
var directory = packageDirs[i];
|
||||
|
||||
logWrapped(ansiMagenta, '\n$directory (${i + 1} of ${packageDirs.length})');
|
||||
|
||||
// Create the target directory
|
||||
var directoryName = p.basename(directory);
|
||||
var sourceBuildDir =
|
||||
p.join(Directory.current.path, directory, 'build', 'web');
|
||||
var targetDirectory = p.join(Directory.current.path, 'samples_index',
|
||||
'public', 'web', directoryName);
|
||||
|
||||
// Build the sample and copy the files
|
||||
await run(directory, 'flutter', ['pub', 'get']);
|
||||
await run(directory, 'flutter', ['build', 'web']);
|
||||
await run(directory, 'mv', [sourceBuildDir, targetDirectory]);
|
||||
}
|
||||
}
|
||||
@@ -36,3 +36,16 @@ Future<bool> run(
|
||||
void logWrapped(int code, String message) {
|
||||
print('\x1B[${code}m$message\x1B[0m');
|
||||
}
|
||||
|
||||
Iterable<String> listPackageDirs(Directory dir) sync* {
|
||||
if (File('${dir.path}/pubspec.yaml').existsSync()) {
|
||||
yield dir.path;
|
||||
} else {
|
||||
for (var subDir in dir
|
||||
.listSync(followLinks: true)
|
||||
.whereType<Directory>()
|
||||
.where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) {
|
||||
yield* listPackageDirs(subDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'common.dart';
|
||||
|
||||
const _ansiGreen = 32;
|
||||
const _ansiRed = 31;
|
||||
const _ansiMagenta = 35;
|
||||
|
||||
void main() async {
|
||||
final packageDirs = _listPackageDirs(Directory.current)
|
||||
final packageDirs = listPackageDirs(Directory.current)
|
||||
.map((path) => p.relative(path, from: Directory.current.path))
|
||||
.toList();
|
||||
|
||||
@@ -21,7 +18,7 @@ void main() async {
|
||||
final results = <bool>[];
|
||||
for (var i = 0; i < packageDirs.length; i++) {
|
||||
final dir = packageDirs[i];
|
||||
logWrapped(_ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
|
||||
logWrapped(ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
|
||||
results.add(await run(dir, 'flutter', [
|
||||
'pub',
|
||||
'pub',
|
||||
@@ -46,19 +43,6 @@ void _printStatus(List<bool> results) {
|
||||
var success = (successCount == results.length);
|
||||
var pct = 100 * successCount / results.length;
|
||||
|
||||
logWrapped(success ? _ansiGreen : _ansiRed,
|
||||
logWrapped(success ? ansiGreen : ansiRed,
|
||||
'$successCount of ${results.length} (${pct.toStringAsFixed(2)}%)');
|
||||
}
|
||||
|
||||
Iterable<String> _listPackageDirs(Directory dir) sync* {
|
||||
if (File('${dir.path}/pubspec.yaml').existsSync()) {
|
||||
yield dir.path;
|
||||
} else {
|
||||
for (var subDir in dir
|
||||
.listSync(followLinks: true)
|
||||
.whereType<Directory>()
|
||||
.where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) {
|
||||
yield* _listPackageDirs(subDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class CState extends State<Cook> {
|
||||
|
||||
initState() {
|
||||
super.initState();
|
||||
cb = List();
|
||||
cb = [];
|
||||
}
|
||||
|
||||
Widget build(ct) {
|
||||
|
||||
@@ -60,7 +60,7 @@ class CatmullInterpolator implements Interpolator {
|
||||
}
|
||||
|
||||
static void test() {
|
||||
List<Point2D> controlPoints = List<Point2D>();
|
||||
List<Point2D> controlPoints = <Point2D>[];
|
||||
controlPoints.add(Point2D(-1, 1));
|
||||
controlPoints.add(Point2D(0, 1));
|
||||
controlPoints.add(Point2D(1, -1));
|
||||
|
||||
@@ -47,12 +47,12 @@ class LayeredChartState extends State<LayeredChart> {
|
||||
graphHeight = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 150);
|
||||
|
||||
int m = dataToPlot.length;
|
||||
paths = List<Path>(m);
|
||||
capPaths = List<Path>(m);
|
||||
maxValues = List<double>(m);
|
||||
paths = <Path>[];
|
||||
capPaths = <Path>[];
|
||||
maxValues = <double>[];
|
||||
for (int i = 0; i < m; i++) {
|
||||
int n = dataToPlot[i].series.length;
|
||||
maxValues[i] = 0;
|
||||
maxValues.add(0);
|
||||
for (int j = 0; j < n; j++) {
|
||||
double v = dataToPlot[i].series[j].toDouble();
|
||||
if (v > maxValues[i]) {
|
||||
@@ -69,11 +69,11 @@ class LayeredChartState extends State<LayeredChart> {
|
||||
double xWidth = (endX - startX) / numPoints;
|
||||
double capRangeX = capSize * cos(capTheta);
|
||||
double tanCapTheta = tan(capTheta);
|
||||
List<double> curvePoints = List<double>(numPoints);
|
||||
List<double> curvePoints = <double>[];
|
||||
for (int i = 0; i < m; i++) {
|
||||
List<int> series = dataToPlot[i].series;
|
||||
int n = series.length;
|
||||
List<Point2D> controlPoints = List<Point2D>();
|
||||
List<Point2D> controlPoints = <Point2D>[];
|
||||
controlPoints.add(Point2D(-1, 0));
|
||||
double last = 0;
|
||||
for (int j = 0; j < n; j++) {
|
||||
@@ -88,11 +88,11 @@ class LayeredChartState extends State<LayeredChart> {
|
||||
cpv.value = MathUtils.map(
|
||||
j.toDouble(), 0, (numPoints - 1).toDouble(), 0, (n - 1).toDouble());
|
||||
curve.progressiveGet(cpv);
|
||||
curvePoints[j] = MathUtils.map(
|
||||
max(0, cpv.value), 0, maxValues[i].toDouble(), 0, graphHeight);
|
||||
curvePoints.add(MathUtils.map(
|
||||
max(0, cpv.value), 0, maxValues[i].toDouble(), 0, graphHeight));
|
||||
}
|
||||
paths[i] = Path();
|
||||
capPaths[i] = Path();
|
||||
paths.add(Path());
|
||||
capPaths.add(Path());
|
||||
paths[i].moveTo(startX, startY);
|
||||
capPaths[i].moveTo(startX, startY);
|
||||
for (int j = 0; j < numPoints; j++) {
|
||||
@@ -133,7 +133,7 @@ class LayeredChartState extends State<LayeredChart> {
|
||||
capPaths[i].lineTo(startX, startY + 1);
|
||||
capPaths[i].close();
|
||||
}
|
||||
labelPainter = List<TextPainter>();
|
||||
labelPainter = <TextPainter>[];
|
||||
for (int i = 0; i < dataToPlot.length; i++) {
|
||||
TextSpan span = TextSpan(
|
||||
style: TextStyle(
|
||||
@@ -146,7 +146,7 @@ class LayeredChartState extends State<LayeredChart> {
|
||||
tp.layout();
|
||||
labelPainter.add(tp);
|
||||
}
|
||||
milestonePainter = List<TextPainter>();
|
||||
milestonePainter = <TextPainter>[];
|
||||
for (int i = 0; i < milestones.length; i++) {
|
||||
TextSpan span = TextSpan(
|
||||
style: TextStyle(
|
||||
|
||||
@@ -46,7 +46,7 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
|
||||
|
||||
createAnimation(0);
|
||||
|
||||
weekLabels = List();
|
||||
weekLabels = <WeekLabel>[];
|
||||
weekLabels.add(WeekLabel.forDate(DateTime(2019, 2, 26), "v1.2"));
|
||||
weekLabels.add(WeekLabel.forDate(DateTime(2018, 12, 4), "v1.0"));
|
||||
// weekLabels.add(WeekLabel.forDate(new DateTime(2018, 9, 19), "Preview 2"));
|
||||
@@ -79,9 +79,9 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Combined contributions data
|
||||
List<DataSeries> dataToPlot = List();
|
||||
List<DataSeries> dataToPlot = [];
|
||||
if (contributions != null) {
|
||||
List<int> series = List();
|
||||
List<int> series = [];
|
||||
for (UserContribution userContrib in contributions) {
|
||||
for (int i = 0; i < userContrib.contributions.length; i++) {
|
||||
ContributionData data = userContrib.contributions[i];
|
||||
@@ -228,7 +228,7 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
|
||||
|
||||
List<StatForWeek> summarizeWeeksFromTSV(
|
||||
String statByWeekStr, int numWeeksTotal) {
|
||||
List<StatForWeek> loadedStats = List();
|
||||
List<StatForWeek> loadedStats = [];
|
||||
HashMap<int, StatForWeek> statMap = HashMap();
|
||||
statByWeekStr.split("\n").forEach((s) {
|
||||
List<String> split = s.split("\t");
|
||||
@@ -237,7 +237,8 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
|
||||
statMap[weekNum] = StatForWeek(weekNum, int.parse(split[1]));
|
||||
}
|
||||
});
|
||||
print("Laoded ${statMap.length} weeks.");
|
||||
print("Loaded ${statMap.length} weeks.");
|
||||
|
||||
// Convert into a list by week, but fill in empty weeks with 0
|
||||
for (int i = 0; i < numWeeksTotal; i++) {
|
||||
StatForWeek starsForWeek = statMap[i];
|
||||
|
||||
@@ -7,7 +7,7 @@ packages:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.5"
|
||||
version: "1.1.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -21,7 +21,7 @@ packages:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0-nullsafety.5"
|
||||
version: "1.15.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -54,7 +54,7 @@ packages:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.6"
|
||||
version: "1.3.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -101,13 +101,13 @@ packages:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.5"
|
||||
version: "1.3.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.5"
|
||||
version: "2.1.0"
|
||||
sdks:
|
||||
dart: ">=2.12.0-0 <3.0.0"
|
||||
|
||||
@@ -8,7 +8,6 @@ directories:
|
||||
- github_dataviz/web
|
||||
- particle_background/web
|
||||
- slide_puzzle/web
|
||||
- timeflow/web
|
||||
- form_app/web
|
||||
- web_dashboard/web
|
||||
- place_tracker/web
|
||||
|
||||
@@ -22,10 +22,8 @@ You should see a message printing the URL to access: `http://localhost:8080`
|
||||
|
||||
## Deploying to GitHub Pages
|
||||
|
||||
This project uses [peanut][peanut] to build the samples and commit the output
|
||||
to the gh-pages branch. To deploy, run these commands in the `web/` directory:
|
||||
|
||||
Install the peanut command:
|
||||
This project uses a GitHub action to deploy update the `gh-pages` branch. To
|
||||
do this manually, you can also use `package:peanut`:
|
||||
|
||||
```console
|
||||
$ flutter pub global activate peanut
|
||||
|
||||
@@ -508,22 +508,6 @@ samples:
|
||||
web: web/slide_puzzle
|
||||
type: demo
|
||||
|
||||
- name: Timeflow
|
||||
author: Fabian Stein
|
||||
screenshots:
|
||||
- url: images/timeflow1.png
|
||||
alt: Timeflow screenshot
|
||||
source: https://github.com/Fabian-Stein/timeflow
|
||||
description: >
|
||||
A gentle animation that provides a calming experience to stressed developers.
|
||||
difficulty: advanced
|
||||
widgets: []
|
||||
packages: []
|
||||
platforms: ['web']
|
||||
tags: ['demo', 'animation']
|
||||
web: web/timeflow
|
||||
type: demo
|
||||
|
||||
- name: Dice
|
||||
author: Jaime Blasco
|
||||
screenshots:
|
||||
|
||||
@@ -19,7 +19,7 @@ void testCli() async => await TestRunner().testAsync(platformSelector: 'vm');
|
||||
|
||||
@Task()
|
||||
void analyze() {
|
||||
PubApp.local('tuneup')..run(['check']);
|
||||
PubApp.local('tuneup').run(['check']);
|
||||
}
|
||||
|
||||
@Task('deploy')
|
||||
|
||||
Submodule web/slide_puzzle updated: 5ef5526acb...5c590d0b02
@@ -1,13 +0,0 @@
|
||||
Copyright 2019 Fabian Stein
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
|
||||
(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,103 +0,0 @@
|
||||
A gentle animation that provides a calming experience to stressed developers.
|
||||
|
||||
Contributed as part of the Flutter Create 5K challenge by Fabian Stein.
|
||||
Original source at
|
||||
[github.com/Fabian-Stein/timeflow](https://github.com/Fabian-Stein/timeflow).
|
||||
|
||||
Timers and stopwatches aren’t the most peaceful gadgets, often reminding us of
|
||||
urgent tasks, deadlines and unpleasant appointments. Not in this case, Timeflow
|
||||
is the epitome of pure tranquility, ideal for mindful activities: mediation,
|
||||
yoga or exercise. The slow, breath like animation is free of sudden, abrupt
|
||||
jumps and builds up to a Zen finish.
|
||||
|
||||
## Use
|
||||
|
||||
Tap the screen to start/pause the timer
|
||||
|
||||
when paused:
|
||||
|
||||
1. red button reset the timer and the animation
|
||||
|
||||
2. green button: resume the timer
|
||||
|
||||
when finished/startscreen:
|
||||
|
||||
1. blue button choose the desired timeframe
|
||||
|
||||
2. orange button randomize a new triangle mesh/color scheme
|
||||
|
||||
## Code description
|
||||
|
||||
Please run dartfmt for readability.
|
||||
|
||||
Some of the variable names are short and I have not used comments, because of the character limit, so here is an explanation.
|
||||
|
||||
### globals
|
||||
|
||||
triangles: the list of triangles that are animated
|
||||
|
||||
percent: how much of the timer is completed (from 0.0 to 1.0)
|
||||
|
||||
cTime: the time that is already gone by since the start of the timer (paused time is excluded)
|
||||
|
||||
dur: how long is the timer in Milliseconds
|
||||
|
||||
rng: the random number generator that is used throughout the program
|
||||
|
||||
rebuild: is an indicator that the triangles destination points should be rebuild
|
||||
|
||||
### class TM
|
||||
The timer class that manages the state of the app
|
||||
|
||||
SI cState: tracks the change of the apps state: is the timer stopped, playing or paused
|
||||
pTime: tracks when the ticker was paused
|
||||
Ticker t: the ticker that calls the update function up every frame
|
||||
up: function that updates the current time or stops the timer, when the duration is reached
|
||||
|
||||
press, pause, play, stop: callback functions, for the button presses
|
||||
openDialog: callback function, opens the numberPickerDialog, which is used to pick the timer duration
|
||||
build: returns the app, mainly the custom painter P is called
|
||||
|
||||
### class P
|
||||
The custom painter, which draws the triangles
|
||||
|
||||
paint:
|
||||
d = diameter of the circle is 2/3 of the width of the screen
|
||||
1. if the triangles are not setup completely (rebuild == true) calculate the outer points of for every triangle setupdP this happens here, because the ratio of the screens has got be known
|
||||
2. paint all triangles
|
||||
shouldRepaint: every frame should be repainted
|
||||
|
||||
### class T
|
||||
The triangle class
|
||||
|
||||
sP: the list of the starting points of the triangle (these are the points you see at the start of the animation)
|
||||
dP: the list of destination points (the outer points, where the triangles wander to first, before they circle back to the starting point)
|
||||
|
||||
constructor: p1,p2,p3 are the starting points, c is the overall color scheme (blue, red, green etc.)
|
||||
for the triangle a random color out of the color scheme is chosen: p.color = c[100 * (rng.nextInt(9) + 1)];
|
||||
the rest of the function determines, if the triangle is in the circle, if it is, it is added to the triangles list, otherwise it is forgotten and should be freed by the garbage collector
|
||||
|
||||
setupdP: setup the destination points, choose a random x and y position on the screen
|
||||
|
||||
cP: gives back the current points of the triangle with respect to the timerstate, some trigonometry and interpolations happen here
|
||||
this is responsible for the animations
|
||||
1. alter the alpha repetitively:
|
||||
2. alter the distance to the starting points, use a linear interpolation between the starting points sP and the destination points dP with respect to the percentage already done
|
||||
3. alter the angle with respect to the starting points
|
||||
4. alter the size of the triangles repetitively
|
||||
|
||||
### function setupT
|
||||
setup the Triangles (starting positions + color scheme)
|
||||
|
||||
dim: dimensions of the “net”
|
||||
1. make a net of points in the following manner:
|
||||
. . . . .
|
||||
. . . .
|
||||
. . . . .
|
||||
. . . .
|
||||
. . . . .
|
||||
. . . .
|
||||
2. alter the points a little bit by randomization, so that the net is a little more intresting
|
||||
3. connect the points to form triangles
|
||||
4. randomize a color scheme for the triangles
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,264 +0,0 @@
|
||||
// Package infinite_listview:
|
||||
// https://pub.dartlang.org/packages/infinite_listview
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
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 StatelessWidget {
|
||||
/// See [ListView.builder]
|
||||
InfiniteListView.builder({
|
||||
Key key,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.reverse = false,
|
||||
InfiniteScrollController controller,
|
||||
this.physics,
|
||||
this.padding,
|
||||
this.itemExtent,
|
||||
@required IndexedWidgetBuilder itemBuilder,
|
||||
int itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
bool addRepaintBoundaries = true,
|
||||
this.cacheExtent,
|
||||
}) : positiveChildrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
childCount: itemCount,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
),
|
||||
negativeChildrenDelegate = SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) => itemBuilder(context, -1 - index),
|
||||
childCount: itemCount,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
),
|
||||
controller = controller ?? InfiniteScrollController(),
|
||||
super(key: key);
|
||||
|
||||
/// See [ListView.separated]
|
||||
InfiniteListView.separated({
|
||||
Key key,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.reverse = false,
|
||||
InfiniteScrollController controller,
|
||||
this.physics,
|
||||
this.padding,
|
||||
@required IndexedWidgetBuilder itemBuilder,
|
||||
@required IndexedWidgetBuilder separatorBuilder,
|
||||
int itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
bool addRepaintBoundaries = true,
|
||||
this.cacheExtent,
|
||||
}) : assert(itemBuilder != null),
|
||||
assert(separatorBuilder != null),
|
||||
itemExtent = null,
|
||||
positiveChildrenDelegate = SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final itemIndex = index ~/ 2;
|
||||
return index.isEven
|
||||
? itemBuilder(context, itemIndex)
|
||||
: separatorBuilder(context, itemIndex);
|
||||
},
|
||||
childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
),
|
||||
negativeChildrenDelegate = SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final itemIndex = (-1 - index) ~/ 2;
|
||||
return index.isOdd
|
||||
? itemBuilder(context, itemIndex)
|
||||
: separatorBuilder(context, itemIndex);
|
||||
},
|
||||
childCount: itemCount,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
),
|
||||
controller = controller ?? InfiniteScrollController(),
|
||||
super(key: key);
|
||||
|
||||
/// See: [ScrollView.scrollDirection]
|
||||
final Axis scrollDirection;
|
||||
|
||||
/// See: [ScrollView.reverse]
|
||||
final bool reverse;
|
||||
|
||||
/// See: [ScrollView.controller]
|
||||
final InfiniteScrollController controller;
|
||||
|
||||
/// See: [ScrollView.physics]
|
||||
final ScrollPhysics physics;
|
||||
|
||||
/// See: [BoxScrollView.padding]
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// See: [ListView.itemExtent]
|
||||
final double itemExtent;
|
||||
|
||||
/// See: [ScrollView.cacheExtent]
|
||||
final double cacheExtent;
|
||||
|
||||
/// See: [ListView.childrenDelegate]
|
||||
final SliverChildDelegate negativeChildrenDelegate;
|
||||
|
||||
/// See: [ListView.childrenDelegate]
|
||||
final SliverChildDelegate positiveChildrenDelegate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> slivers = _buildSlivers(context, negative: false);
|
||||
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
|
||||
final AxisDirection axisDirection = _getDirection(context);
|
||||
final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics);
|
||||
return Scrollable(
|
||||
axisDirection: axisDirection,
|
||||
controller: controller,
|
||||
physics: scrollPhysics,
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
/// Build negative [ScrollPosition] for the negative scrolling [Viewport].
|
||||
final state = Scrollable.of(context);
|
||||
final negativeOffset = _InfiniteScrollPosition(
|
||||
physics: scrollPhysics,
|
||||
context: state,
|
||||
initialPixels: -offset.pixels,
|
||||
keepScrollOffset: controller.keepScrollOffset,
|
||||
);
|
||||
|
||||
/// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
|
||||
offset.addListener(() {
|
||||
negativeOffset._forceNegativePixels(offset.pixels);
|
||||
});
|
||||
|
||||
/// Stack the two [Viewport]s on top of each other so they move in sync.
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Viewport(
|
||||
axisDirection: flipAxisDirection(axisDirection),
|
||||
anchor: 1.0,
|
||||
offset: negativeOffset,
|
||||
slivers: negativeSlivers,
|
||||
cacheExtent: cacheExtent,
|
||||
),
|
||||
Viewport(
|
||||
axisDirection: axisDirection,
|
||||
offset: offset,
|
||||
slivers: slivers,
|
||||
cacheExtent: cacheExtent,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
AxisDirection _getDirection(BuildContext context) {
|
||||
return getAxisDirectionFromAxisReverseAndDirectionality(
|
||||
context, scrollDirection, reverse);
|
||||
}
|
||||
|
||||
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
|
||||
Widget sliver;
|
||||
if (itemExtent != null) {
|
||||
sliver = SliverFixedExtentList(
|
||||
delegate:
|
||||
negative ? negativeChildrenDelegate : positiveChildrenDelegate,
|
||||
itemExtent: itemExtent,
|
||||
);
|
||||
} else {
|
||||
sliver = SliverList(
|
||||
delegate:
|
||||
negative ? negativeChildrenDelegate : positiveChildrenDelegate);
|
||||
}
|
||||
if (padding != null) {
|
||||
sliver = new SliverPadding(
|
||||
padding: negative
|
||||
? padding - EdgeInsets.only(bottom: padding.bottom)
|
||||
: padding - EdgeInsets.only(top: padding.top),
|
||||
sliver: sliver,
|
||||
);
|
||||
}
|
||||
return <Widget>[sliver];
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(new EnumProperty<Axis>('scrollDirection', scrollDirection));
|
||||
properties.add(new FlagProperty('reverse',
|
||||
value: reverse, ifTrue: 'reversed', showName: true));
|
||||
properties.add(new DiagnosticsProperty<ScrollController>(
|
||||
'controller', controller,
|
||||
showName: false, defaultValue: null));
|
||||
properties.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics,
|
||||
showName: false, defaultValue: null));
|
||||
properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>(
|
||||
'padding', padding,
|
||||
defaultValue: null));
|
||||
properties
|
||||
.add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null));
|
||||
properties.add(
|
||||
new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null));
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
|
||||
class InfiniteScrollController extends ScrollController {
|
||||
/// Creates a new [InfiniteScrollController]
|
||||
InfiniteScrollController({
|
||||
double initialScrollOffset = 0.0,
|
||||
bool keepScrollOffset = true,
|
||||
String debugLabel,
|
||||
}) : super(
|
||||
initialScrollOffset: initialScrollOffset,
|
||||
keepScrollOffset: keepScrollOffset,
|
||||
debugLabel: debugLabel,
|
||||
);
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics,
|
||||
ScrollContext context, ScrollPosition oldPosition) {
|
||||
return new _InfiniteScrollPosition(
|
||||
physics: physics,
|
||||
context: context,
|
||||
initialPixels: initialScrollOffset,
|
||||
keepScrollOffset: keepScrollOffset,
|
||||
oldPosition: oldPosition,
|
||||
debugLabel: debugLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfiniteScrollPosition extends ScrollPositionWithSingleContext {
|
||||
_InfiniteScrollPosition({
|
||||
@required ScrollPhysics physics,
|
||||
@required ScrollContext context,
|
||||
double initialPixels = 0.0,
|
||||
bool keepScrollOffset = true,
|
||||
ScrollPosition oldPosition,
|
||||
String debugLabel,
|
||||
}) : super(
|
||||
physics: physics,
|
||||
context: context,
|
||||
initialPixels: initialPixels,
|
||||
keepScrollOffset: keepScrollOffset,
|
||||
oldPosition: oldPosition,
|
||||
debugLabel: debugLabel,
|
||||
);
|
||||
|
||||
void _forceNegativePixels(double value) {
|
||||
super.forcePixels(-value);
|
||||
}
|
||||
|
||||
@override
|
||||
double get minScrollExtent => double.negativeInfinity;
|
||||
|
||||
@override
|
||||
double get maxScrollExtent => double.infinity;
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import 'dart:core';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'numberpicker.dart';
|
||||
|
||||
main() => runApp(MaterialApp(home: App(), debugShowCheckedModeBanner: false));
|
||||
|
||||
class App extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() => TM();
|
||||
}
|
||||
|
||||
enum SI { pause, play, stop }
|
||||
List<T> triangles;
|
||||
var percent = 0.0, cTime = 0.0, dur = 120000.0, rng = Random(), rebuild = true;
|
||||
|
||||
class TM extends State<App> {
|
||||
SI cState = SI.stop;
|
||||
Ticker t;
|
||||
var pTime = 0.0;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
// Screen.keepOn(true);
|
||||
t = Ticker(up);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
up(Duration d) {
|
||||
if (cState == SI.play) {
|
||||
setState(() {
|
||||
if (cTime >= dur)
|
||||
stop();
|
||||
else {
|
||||
cTime = d.inMilliseconds.toDouble() + pTime;
|
||||
percent = cTime / dur;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
press() {
|
||||
if (cState == SI.play)
|
||||
pause();
|
||||
else if (cState == SI.pause)
|
||||
play();
|
||||
else {
|
||||
cState = SI.play;
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
setState(() {
|
||||
cState = SI.pause;
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
|
||||
play() {
|
||||
setState(() {
|
||||
cState = SI.play;
|
||||
t.start();
|
||||
pTime = cTime;
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
setState(() {
|
||||
cState = SI.stop;
|
||||
t.stop();
|
||||
pTime = 0.0;
|
||||
cTime = 0.0;
|
||||
percent = 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
openDialog() {
|
||||
showDialog<num>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return NumberPickerDialog.integer(
|
||||
initialIntegerValue: (dur + 1.0) ~/ 60000,
|
||||
maxValue: 20,
|
||||
minValue: 1,
|
||||
title: Text('Minutes'));
|
||||
}).then((num v) {
|
||||
if (v != null) dur = 60000.0 * v;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> w = List();
|
||||
|
||||
if (cState == SI.pause) {
|
||||
w.add(fab(Colors.green, play, Icons.play_arrow));
|
||||
w.add(SizedBox(height: 10));
|
||||
w.add(fab(Colors.red, stop, Icons.close));
|
||||
w.add(SizedBox(height: 20));
|
||||
}
|
||||
|
||||
if (cState == SI.stop) {
|
||||
w.add(fab(Colors.lightBlue, openDialog, Icons.timer));
|
||||
w.add(SizedBox(height: 10));
|
||||
w.add(fab(Colors.yellow[900], () {
|
||||
rebuild = true;
|
||||
}, Icons.loop));
|
||||
w.add(SizedBox(height: 20));
|
||||
}
|
||||
|
||||
Column r = Column(mainAxisAlignment: MainAxisAlignment.end, children: w);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: SizedBox.expand(
|
||||
child: Container(
|
||||
child: CustomPaint(
|
||||
painter: P(),
|
||||
child: TextButton(
|
||||
onPressed: press,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [r]))))));
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton fab(Color c, VoidCallback f, IconData ic) =>
|
||||
FloatingActionButton(backgroundColor: c, onPressed: f, child: Icon(ic));
|
||||
|
||||
class P extends CustomPainter {
|
||||
@override
|
||||
paint(Canvas canvas, Size size) {
|
||||
var w = size.width, h = size.height, d = 2 / 3 * w;
|
||||
if (w > 0.1 && h > 0.1) {
|
||||
if (rebuild) {
|
||||
rebuild = false;
|
||||
setupT();
|
||||
for (var t in triangles) t.setupdP(w / d, h / d);
|
||||
}
|
||||
|
||||
for (var t in triangles) {
|
||||
var cP = t.cP(), p = Path();
|
||||
p.moveTo(cP[0].x * d + w / 2, cP[0].y * d + h / 2);
|
||||
for (i = 1; i < 3; i++)
|
||||
p.lineTo(cP[i].x * d + w / 2, cP[i].y * d + h / 2);
|
||||
p.close();
|
||||
canvas.drawPath(p, t.p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
int i;
|
||||
|
||||
class T {
|
||||
List<Point> dP = List(3), sP = List(3);
|
||||
Paint p;
|
||||
|
||||
T(Point p1, p2, p3, var c) {
|
||||
p = Paint()..style = PaintingStyle.fill;
|
||||
sP[0] = p1;
|
||||
sP[1] = p2;
|
||||
sP[2] = p3;
|
||||
p.color = c[100 * (rng.nextInt(9) + 1)];
|
||||
|
||||
double x = 0, y = 0;
|
||||
for (i = 0; i < 3; i++) {
|
||||
x += sP[i].x;
|
||||
y += sP[i].y;
|
||||
}
|
||||
|
||||
x = 2 * x / 3;
|
||||
y = 2 * y / 3;
|
||||
if (x * x + y * y < 1) triangles.add(this);
|
||||
}
|
||||
|
||||
setupdP(double wR, hR) {
|
||||
var x = (rng.nextDouble() - 0.5) * (wR - 0.1),
|
||||
y = (rng.nextDouble() - 0.5) * (hR - 0.1);
|
||||
dP[0] = Point(x, y);
|
||||
for (i = 1; i < 3; i++)
|
||||
dP[i] = Point(sP[i].x + x - sP[0].x, sP[i].y + y - sP[0].y);
|
||||
}
|
||||
|
||||
List<Point> cP() {
|
||||
List<Point> res = List(3);
|
||||
var p, k, o = 6000, r;
|
||||
if (cTime < o)
|
||||
p = 1 - cTime / o;
|
||||
else
|
||||
p = (cTime - o) / (dur - o);
|
||||
k = 2 * ((cTime.toInt() % o) - o / 2).abs() / o;
|
||||
r = min(min(1, (dur - cTime) / o), cTime / o);
|
||||
this.p.color = this.p.color.withAlpha(255 - (200 * k * r).toInt());
|
||||
|
||||
for (i = 0; i < 3; i++)
|
||||
res[i] = Point(
|
||||
sP[i].x * p + dP[i].x * (1 - p), sP[i].y * p + dP[i].y * (1 - p));
|
||||
|
||||
if (cTime > o) {
|
||||
var d = res[0].distanceTo(sP[0]);
|
||||
var a = acos((sP[0].x - res[0].x) / d);
|
||||
if (sP[0].y > res[0].y) a = 2 * pi - a;
|
||||
var b = pi - a + p * pi * dur / 120000;
|
||||
var dX = cos(b) * d, dY = sin(b) * d;
|
||||
for (i = 0; i < 3; i++) res[i] = Point(sP[i].x + dX, sP[i].y + dY);
|
||||
}
|
||||
|
||||
double mx = 0, my = 0;
|
||||
for (i = 0; i < 3; i++) {
|
||||
mx += res[i].x;
|
||||
my += res[i].y;
|
||||
}
|
||||
mx /= 3;
|
||||
my /= 3;
|
||||
for (i = 0; i < 3; i++)
|
||||
res[i] = Point(res[i].x + (res[i].x - mx) * (1 - k) * r / 2,
|
||||
res[i].y + (res[i].y - my) * (1 - k) * r / 2);
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
setupT() {
|
||||
int dim = 20, x, y;
|
||||
List<Point> tri = List(dim * dim);
|
||||
|
||||
for (x = 0; x < dim; x++) {
|
||||
for (y = 0; y < dim; y++) {
|
||||
var dx = rng.nextDouble() - 0.5, dy = rng.nextDouble() - 0.5, off;
|
||||
if (x % 2 == 0)
|
||||
off = 0;
|
||||
else
|
||||
off = 0.5;
|
||||
tri[x * dim + y] =
|
||||
Point((x + dx) / (dim - 1) - 0.5, (y + off + dy) / (dim - 1) - 0.5);
|
||||
}
|
||||
}
|
||||
triangles = List();
|
||||
var r = rng.nextInt(5), c;
|
||||
if (r == 0) c = Colors.lightBlue;
|
||||
if (r == 1) c = Colors.yellow;
|
||||
if (r == 2) c = Colors.lightGreen;
|
||||
if (r == 3) c = Colors.red;
|
||||
if (r == 4) c = Colors.pink;
|
||||
|
||||
for (x = 0; x < dim - 1; x++) {
|
||||
for (y = 0; y < dim - 1; y++) {
|
||||
int off = x * dim;
|
||||
T(tri[y + off], tri[y + 1 + off], tri[y + off + dim], c);
|
||||
T(tri[y + off + dim], tri[y + 1 + off], tri[y + 1 + off + dim], c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,527 +0,0 @@
|
||||
// Package numberpicker:
|
||||
// https://pub.dartlang.org/packages/numberpicker
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'infinite_listview.dart';
|
||||
|
||||
/// Created by Marcin Szałek
|
||||
|
||||
///NumberPicker is a widget designed to pick a number between #minValue and #maxValue
|
||||
class NumberPicker extends StatelessWidget {
|
||||
///height of every list element
|
||||
static const double DEFAULT_ITEM_EXTENT = 50.0;
|
||||
|
||||
///width of list view
|
||||
static const double DEFAULT_LISTVIEW_WIDTH = 100.0;
|
||||
|
||||
///constructor for integer number picker
|
||||
NumberPicker.integer({
|
||||
Key key,
|
||||
@required int initialValue,
|
||||
@required this.minValue,
|
||||
@required this.maxValue,
|
||||
@required this.onChanged,
|
||||
this.itemExtent = DEFAULT_ITEM_EXTENT,
|
||||
this.listViewWidth = DEFAULT_LISTVIEW_WIDTH,
|
||||
this.step = 1,
|
||||
this.infiniteLoop = false,
|
||||
}) : assert(initialValue != null),
|
||||
assert(minValue != null),
|
||||
assert(maxValue != null),
|
||||
assert(maxValue > minValue),
|
||||
assert(initialValue >= minValue && initialValue <= maxValue),
|
||||
assert(step > 0),
|
||||
selectedIntValue = initialValue,
|
||||
selectedDecimalValue = -1,
|
||||
decimalPlaces = 0,
|
||||
intScrollController = infiniteLoop
|
||||
? new InfiniteScrollController(
|
||||
initialScrollOffset:
|
||||
(initialValue - minValue) ~/ step * itemExtent,
|
||||
)
|
||||
: new ScrollController(
|
||||
initialScrollOffset:
|
||||
(initialValue - minValue) ~/ step * itemExtent,
|
||||
),
|
||||
decimalScrollController = null,
|
||||
_listViewHeight = 3 * itemExtent,
|
||||
integerItemCount = (maxValue - minValue) ~/ step + 1,
|
||||
super(key: key);
|
||||
|
||||
///constructor for decimal number picker
|
||||
NumberPicker.decimal({
|
||||
Key key,
|
||||
@required double initialValue,
|
||||
@required this.minValue,
|
||||
@required this.maxValue,
|
||||
@required this.onChanged,
|
||||
this.decimalPlaces = 1,
|
||||
this.itemExtent = DEFAULT_ITEM_EXTENT,
|
||||
this.listViewWidth = DEFAULT_LISTVIEW_WIDTH,
|
||||
}) : assert(initialValue != null),
|
||||
assert(minValue != null),
|
||||
assert(maxValue != null),
|
||||
assert(decimalPlaces != null && decimalPlaces > 0),
|
||||
assert(maxValue > minValue),
|
||||
assert(initialValue >= minValue && initialValue <= maxValue),
|
||||
selectedIntValue = initialValue.floor(),
|
||||
selectedDecimalValue = ((initialValue - initialValue.floorToDouble()) *
|
||||
math.pow(10, decimalPlaces))
|
||||
.round(),
|
||||
intScrollController = new ScrollController(
|
||||
initialScrollOffset: (initialValue.floor() - minValue) * itemExtent,
|
||||
),
|
||||
decimalScrollController = new ScrollController(
|
||||
initialScrollOffset: ((initialValue - initialValue.floorToDouble()) *
|
||||
math.pow(10, decimalPlaces))
|
||||
.roundToDouble() *
|
||||
itemExtent,
|
||||
),
|
||||
_listViewHeight = 3 * itemExtent,
|
||||
step = 1,
|
||||
integerItemCount = maxValue.floor() - minValue.floor() + 1,
|
||||
infiniteLoop = false,
|
||||
super(key: key);
|
||||
|
||||
///called when selected value changes
|
||||
final ValueChanged<num> onChanged;
|
||||
|
||||
///min value user can pick
|
||||
final int minValue;
|
||||
|
||||
///max value user can pick
|
||||
final int maxValue;
|
||||
|
||||
///inidcates how many decimal places to show
|
||||
/// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...]
|
||||
final int decimalPlaces;
|
||||
|
||||
///height of every list element in pixels
|
||||
final double itemExtent;
|
||||
|
||||
///view will always contain only 3 elements of list in pixels
|
||||
final double _listViewHeight;
|
||||
|
||||
///width of list view in pixels
|
||||
final double listViewWidth;
|
||||
|
||||
///ScrollController used for integer list
|
||||
final ScrollController intScrollController;
|
||||
|
||||
///ScrollController used for decimal list
|
||||
final ScrollController decimalScrollController;
|
||||
|
||||
///Currently selected integer value
|
||||
final int selectedIntValue;
|
||||
|
||||
///Currently selected decimal value
|
||||
final int selectedDecimalValue;
|
||||
|
||||
///Step between elements. Only for integer datePicker
|
||||
///Examples:
|
||||
/// if step is 100 the following elements may be 100, 200, 300...
|
||||
/// if min=0, max=6, step=3, then items will be 0, 3 and 6
|
||||
/// if min=0, max=5, step=3, then items will be 0 and 3.
|
||||
final int step;
|
||||
|
||||
///Repeat values infinitely
|
||||
final bool infiniteLoop;
|
||||
|
||||
///Amount of items
|
||||
final int integerItemCount;
|
||||
|
||||
//
|
||||
//----------------------------- PUBLIC ------------------------------
|
||||
//
|
||||
|
||||
animateInt(int valueToSelect) {
|
||||
int diff = valueToSelect - minValue;
|
||||
int index = diff ~/ step;
|
||||
animateIntToIndex(index);
|
||||
}
|
||||
|
||||
animateIntToIndex(int index) {
|
||||
_animate(intScrollController, index * itemExtent);
|
||||
}
|
||||
|
||||
animateDecimal(int decimalValue) {
|
||||
_animate(decimalScrollController, decimalValue * itemExtent);
|
||||
}
|
||||
|
||||
animateDecimalAndInteger(double valueToSelect) {
|
||||
animateInt(valueToSelect.floor());
|
||||
animateDecimal(((valueToSelect - valueToSelect.floorToDouble()) *
|
||||
math.pow(10, decimalPlaces))
|
||||
.round());
|
||||
}
|
||||
|
||||
//
|
||||
//----------------------------- VIEWS -----------------------------
|
||||
//
|
||||
|
||||
///main widget
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
|
||||
if (infiniteLoop) {
|
||||
return _integerInfiniteListView(themeData);
|
||||
}
|
||||
if (decimalPlaces == 0) {
|
||||
return _integerListView(themeData);
|
||||
} else {
|
||||
return new Row(
|
||||
children: <Widget>[
|
||||
_integerListView(themeData),
|
||||
_decimalListView(themeData),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _integerListView(ThemeData themeData) {
|
||||
TextStyle defaultStyle = themeData.textTheme.bodyText2;
|
||||
TextStyle selectedStyle =
|
||||
themeData.textTheme.headline5.copyWith(color: themeData.accentColor);
|
||||
|
||||
var listItemCount = integerItemCount + 2;
|
||||
|
||||
return new NotificationListener(
|
||||
child: new Container(
|
||||
height: _listViewHeight,
|
||||
width: listViewWidth,
|
||||
child: new ListView.builder(
|
||||
controller: intScrollController,
|
||||
itemExtent: itemExtent,
|
||||
itemCount: listItemCount,
|
||||
cacheExtent: _calculateCacheExtent(listItemCount),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final int value = _intValueFromIndex(index);
|
||||
|
||||
//define special style for selected (middle) element
|
||||
final TextStyle itemStyle =
|
||||
value == selectedIntValue ? selectedStyle : defaultStyle;
|
||||
|
||||
bool isExtra = index == 0 || index == listItemCount - 1;
|
||||
|
||||
return isExtra
|
||||
? new Container() //empty first and last element
|
||||
: new Center(
|
||||
child: new Text(value.toString(), style: itemStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onNotification: _onIntegerNotification,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _decimalListView(ThemeData themeData) {
|
||||
TextStyle defaultStyle = themeData.textTheme.bodyText2;
|
||||
TextStyle selectedStyle =
|
||||
themeData.textTheme.headline5.copyWith(color: themeData.accentColor);
|
||||
|
||||
int decimalItemCount =
|
||||
selectedIntValue == maxValue ? 3 : math.pow(10, decimalPlaces) + 2;
|
||||
|
||||
return new NotificationListener(
|
||||
child: new Container(
|
||||
height: _listViewHeight,
|
||||
width: listViewWidth,
|
||||
child: new ListView.builder(
|
||||
controller: decimalScrollController,
|
||||
itemExtent: itemExtent,
|
||||
itemCount: decimalItemCount,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final int value = index - 1;
|
||||
|
||||
//define special style for selected (middle) element
|
||||
final TextStyle itemStyle =
|
||||
value == selectedDecimalValue ? selectedStyle : defaultStyle;
|
||||
|
||||
bool isExtra = index == 0 || index == decimalItemCount - 1;
|
||||
|
||||
return isExtra
|
||||
? new Container() //empty first and last element
|
||||
: new Center(
|
||||
child: new Text(
|
||||
value.toString().padLeft(decimalPlaces, '0'),
|
||||
style: itemStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onNotification: _onDecimalNotification,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _integerInfiniteListView(ThemeData themeData) {
|
||||
TextStyle defaultStyle = themeData.textTheme.bodyText2;
|
||||
TextStyle selectedStyle =
|
||||
themeData.textTheme.headline5.copyWith(color: themeData.accentColor);
|
||||
|
||||
return new NotificationListener(
|
||||
child: new Container(
|
||||
height: _listViewHeight,
|
||||
width: listViewWidth,
|
||||
child: new InfiniteListView.builder(
|
||||
controller: intScrollController,
|
||||
itemExtent: itemExtent,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final int value = _intValueFromIndex(index);
|
||||
|
||||
//define special style for selected (middle) element
|
||||
final TextStyle itemStyle =
|
||||
value == selectedIntValue ? selectedStyle : defaultStyle;
|
||||
|
||||
return new Center(
|
||||
child: new Text(value.toString(), style: itemStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onNotification: _onIntegerNotification,
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// ----------------------------- LOGIC -----------------------------
|
||||
//
|
||||
|
||||
int _intValueFromIndex(int index) {
|
||||
index--;
|
||||
index %= integerItemCount;
|
||||
return minValue + index * step;
|
||||
}
|
||||
|
||||
bool _onIntegerNotification(Notification notification) {
|
||||
if (notification is ScrollNotification) {
|
||||
//calculate
|
||||
int intIndexOfMiddleElement =
|
||||
(notification.metrics.pixels / itemExtent).round();
|
||||
if (!infiniteLoop) {
|
||||
intIndexOfMiddleElement =
|
||||
intIndexOfMiddleElement.clamp(0, integerItemCount - 1);
|
||||
}
|
||||
int intValueInTheMiddle = _intValueFromIndex(intIndexOfMiddleElement + 1);
|
||||
intValueInTheMiddle = _normalizeIntegerMiddleValue(intValueInTheMiddle);
|
||||
|
||||
if (_userStoppedScrolling(notification, intScrollController)) {
|
||||
//center selected value
|
||||
animateIntToIndex(intIndexOfMiddleElement);
|
||||
}
|
||||
|
||||
//update selection
|
||||
if (intValueInTheMiddle != selectedIntValue) {
|
||||
num newValue;
|
||||
if (decimalPlaces == 0) {
|
||||
//return integer value
|
||||
newValue = (intValueInTheMiddle);
|
||||
} else {
|
||||
if (intValueInTheMiddle == maxValue) {
|
||||
//if new value is maxValue, then return that value and ignore decimal
|
||||
newValue = (intValueInTheMiddle.toDouble());
|
||||
animateDecimal(0);
|
||||
} else {
|
||||
//return integer+decimal
|
||||
double decimalPart = _toDecimal(selectedDecimalValue);
|
||||
newValue = ((intValueInTheMiddle + decimalPart).toDouble());
|
||||
}
|
||||
}
|
||||
onChanged(newValue);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _onDecimalNotification(Notification notification) {
|
||||
if (notification is ScrollNotification) {
|
||||
//calculate middle value
|
||||
int indexOfMiddleElement =
|
||||
(notification.metrics.pixels + _listViewHeight / 2) ~/ itemExtent;
|
||||
int decimalValueInTheMiddle = indexOfMiddleElement - 1;
|
||||
decimalValueInTheMiddle =
|
||||
_normalizeDecimalMiddleValue(decimalValueInTheMiddle);
|
||||
|
||||
if (_userStoppedScrolling(notification, decimalScrollController)) {
|
||||
//center selected value
|
||||
animateDecimal(decimalValueInTheMiddle);
|
||||
}
|
||||
|
||||
//update selection
|
||||
if (selectedIntValue != maxValue &&
|
||||
decimalValueInTheMiddle != selectedDecimalValue) {
|
||||
double decimalPart = _toDecimal(decimalValueInTheMiddle);
|
||||
double newValue = ((selectedIntValue + decimalPart).toDouble());
|
||||
onChanged(newValue);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
///There was a bug, when if there was small integer range, e.g. from 1 to 5,
|
||||
///When user scrolled to the top, whole listview got displayed.
|
||||
///To prevent this we are calculating cacheExtent by our own so it gets smaller if number of items is smaller
|
||||
double _calculateCacheExtent(int itemCount) {
|
||||
double cacheExtent = 250.0; //default cache extent
|
||||
if ((itemCount - 2) * DEFAULT_ITEM_EXTENT <= cacheExtent) {
|
||||
cacheExtent = ((itemCount - 3) * DEFAULT_ITEM_EXTENT);
|
||||
}
|
||||
return cacheExtent;
|
||||
}
|
||||
|
||||
///When overscroll occurs on iOS,
|
||||
///we can end up with value not in the range between [minValue] and [maxValue]
|
||||
///To avoid going out of range, we change values out of range to border values.
|
||||
int _normalizeMiddleValue(int valueInTheMiddle, int min, int max) {
|
||||
return math.max(math.min(valueInTheMiddle, max), min);
|
||||
}
|
||||
|
||||
int _normalizeIntegerMiddleValue(int integerValueInTheMiddle) {
|
||||
//make sure that max is a multiple of step
|
||||
int max = (maxValue ~/ step) * step;
|
||||
return _normalizeMiddleValue(integerValueInTheMiddle, minValue, max);
|
||||
}
|
||||
|
||||
int _normalizeDecimalMiddleValue(int decimalValueInTheMiddle) {
|
||||
return _normalizeMiddleValue(
|
||||
decimalValueInTheMiddle, 0, math.pow(10, decimalPlaces) - 1);
|
||||
}
|
||||
|
||||
///indicates if user has stopped scrolling so we can center value in the middle
|
||||
bool _userStoppedScrolling(
|
||||
Notification notification, ScrollController scrollController) {
|
||||
return notification is UserScrollNotification &&
|
||||
notification.direction == ScrollDirection.idle &&
|
||||
// ignore: invalid_use_of_protected_member,invalid_use_of_visible_for_testing_member
|
||||
scrollController.position.activity is! HoldScrollActivity;
|
||||
}
|
||||
|
||||
///converts integer indicator of decimal value to double
|
||||
///e.g. decimalPlaces = 1, value = 4 >>> result = 0.4
|
||||
/// decimalPlaces = 2, value = 12 >>> result = 0.12
|
||||
double _toDecimal(int decimalValueAsInteger) {
|
||||
return double.parse((decimalValueAsInteger * math.pow(10, -decimalPlaces))
|
||||
.toStringAsFixed(decimalPlaces));
|
||||
}
|
||||
|
||||
///scroll to selected value
|
||||
_animate(ScrollController scrollController, double value) {
|
||||
scrollController.animateTo(value,
|
||||
duration: new Duration(seconds: 1), curve: new ElasticOutCurve());
|
||||
}
|
||||
}
|
||||
|
||||
///Returns AlertDialog as a Widget so it is designed to be used in showDialog method
|
||||
class NumberPickerDialog extends StatefulWidget {
|
||||
final int minValue;
|
||||
final int maxValue;
|
||||
final int initialIntegerValue;
|
||||
final double initialDoubleValue;
|
||||
final int decimalPlaces;
|
||||
final Widget title;
|
||||
final EdgeInsets titlePadding;
|
||||
final Widget confirmWidget;
|
||||
final Widget cancelWidget;
|
||||
final int step;
|
||||
final bool infiniteLoop;
|
||||
|
||||
///constructor for integer values
|
||||
NumberPickerDialog.integer({
|
||||
@required this.minValue,
|
||||
@required this.maxValue,
|
||||
@required this.initialIntegerValue,
|
||||
this.title,
|
||||
this.titlePadding,
|
||||
this.step = 1,
|
||||
this.infiniteLoop = false,
|
||||
Widget confirmWidget,
|
||||
Widget cancelWidget,
|
||||
}) : confirmWidget = confirmWidget ?? new Text("OK"),
|
||||
cancelWidget = cancelWidget ?? new Text("CANCEL"),
|
||||
decimalPlaces = 0,
|
||||
initialDoubleValue = -1.0;
|
||||
|
||||
///constructor for decimal values
|
||||
NumberPickerDialog.decimal({
|
||||
@required this.minValue,
|
||||
@required this.maxValue,
|
||||
@required this.initialDoubleValue,
|
||||
this.decimalPlaces = 1,
|
||||
this.title,
|
||||
this.titlePadding,
|
||||
Widget confirmWidget,
|
||||
Widget cancelWidget,
|
||||
}) : confirmWidget = confirmWidget ?? new Text("OK"),
|
||||
cancelWidget = cancelWidget ?? new Text("CANCEL"),
|
||||
initialIntegerValue = -1,
|
||||
step = 1,
|
||||
infiniteLoop = false;
|
||||
|
||||
@override
|
||||
State<NumberPickerDialog> createState() =>
|
||||
new _NumberPickerDialogControllerState(
|
||||
initialIntegerValue, initialDoubleValue);
|
||||
}
|
||||
|
||||
class _NumberPickerDialogControllerState extends State<NumberPickerDialog> {
|
||||
int selectedIntValue;
|
||||
double selectedDoubleValue;
|
||||
|
||||
_NumberPickerDialogControllerState(
|
||||
this.selectedIntValue, this.selectedDoubleValue);
|
||||
|
||||
_handleValueChanged(num value) {
|
||||
if (value is int) {
|
||||
setState(() => selectedIntValue = value);
|
||||
} else {
|
||||
setState(() => selectedDoubleValue = value);
|
||||
}
|
||||
}
|
||||
|
||||
NumberPicker _buildNumberPicker() {
|
||||
if (widget.decimalPlaces > 0) {
|
||||
return new NumberPicker.decimal(
|
||||
initialValue: selectedDoubleValue,
|
||||
minValue: widget.minValue,
|
||||
maxValue: widget.maxValue,
|
||||
decimalPlaces: widget.decimalPlaces,
|
||||
onChanged: _handleValueChanged);
|
||||
} else {
|
||||
return new NumberPicker.integer(
|
||||
initialValue: selectedIntValue,
|
||||
minValue: widget.minValue,
|
||||
maxValue: widget.maxValue,
|
||||
step: widget.step,
|
||||
infiniteLoop: widget.infiniteLoop,
|
||||
onChanged: _handleValueChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new AlertDialog(
|
||||
title: widget.title,
|
||||
titlePadding: widget.titlePadding,
|
||||
content: _buildNumberPicker(),
|
||||
actions: [
|
||||
new TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: widget.cancelWidget,
|
||||
),
|
||||
new TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(widget.decimalPlaces > 0
|
||||
? selectedDoubleValue
|
||||
: selectedIntValue),
|
||||
child: widget.confirmWidget),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.5"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0-nullsafety.5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.6"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.5"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.5"
|
||||
sdks:
|
||||
dart: ">=2.12.0-0 <3.0.0"
|
||||
@@ -1,12 +0,0 @@
|
||||
name: timeflow
|
||||
|
||||
environment:
|
||||
sdk: ">=2.2.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- preview.png
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title></title>
|
||||
<script defer src="main.dart.js" type="application/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user