├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── new-issue.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yaml ├── packages ├── flutter_simple_treeview │ ├── example │ │ ├── screenshots │ │ │ ├── treeview_demo.png │ │ │ └── treeview_demo_JSON.png │ │ ├── web │ │ │ └── index.html │ │ ├── readme.md │ │ ├── pubspec.yaml │ │ └── lib │ │ │ ├── main.dart │ │ │ └── trees │ │ │ ├── controller_usage.dart │ │ │ └── tree_from_json.dart │ ├── lib │ │ ├── flutter_simple_treeview.dart │ │ └── src │ │ │ ├── primitives │ │ │ ├── tree_node.dart │ │ │ ├── key_provider.dart │ │ │ └── tree_controller.dart │ │ │ ├── builder.dart │ │ │ ├── copy_tree_nodes.dart │ │ │ ├── tree_view.dart │ │ │ └── node_widget.dart │ ├── pubspec.yaml │ ├── CHANGELOG.md │ ├── README.md │ ├── LICENSE │ └── test │ │ ├── primitives │ │ └── tree_controller_test.dart │ │ └── tree_view_test.dart ├── self_storing_input │ ├── example │ │ ├── readme.md │ │ ├── web │ │ │ └── index.html │ │ ├── pubspec.yaml │ │ └── lib │ │ │ └── main.dart │ ├── pubspec.yaml │ ├── lib │ │ ├── src │ │ │ ├── primitives │ │ │ │ ├── operation_result.dart │ │ │ │ ├── the_progress_indicator.dart │ │ │ │ ├── overlay.dart │ │ │ │ ├── saver.dart │ │ │ │ ├── message_overlay.dart │ │ │ │ └── overlay_builder.dart │ │ │ ├── self_storing_checkbox │ │ │ │ ├── self_storing_checkbox_style.dart │ │ │ │ ├── shared_state.dart │ │ │ │ └── custom_checkbox.dart │ │ │ ├── self_storing_radio_group │ │ │ │ ├── self_storing_radio_group_style.dart │ │ │ │ ├── shared_state.dart │ │ │ │ └── custom_radio.dart │ │ │ ├── self_storing_text │ │ │ │ ├── self_storing_text_style.dart │ │ │ │ ├── shared_state.dart │ │ │ │ ├── edit_button.dart │ │ │ │ └── overlay_box.dart │ │ │ ├── self_storing_text.dart │ │ │ ├── self_storing_checkbox.dart │ │ │ └── self_storing_radio_group.dart │ │ └── self_storing_input.dart │ ├── test │ │ ├── self_storing_checkbox_test.dart │ │ ├── testing │ │ │ └── widget_testing.dart │ │ ├── primitives │ │ │ └── message_overlay_test.dart │ │ └── self_storing_text_test.dart │ ├── CHANGELOG.md │ ├── README.md │ └── LICENSE ├── visibility_detector │ ├── example │ │ ├── pubspec.yaml │ │ └── test │ │ │ └── unit_test.dart │ ├── lib │ │ ├── visibility_detector.dart │ │ └── src │ │ │ ├── visibility_detector_controller.dart │ │ │ └── visibility_detector.dart │ ├── pubspec.yaml │ ├── LICENSE │ ├── test │ │ ├── unit_test.dart │ │ ├── text_handle_test.dart │ │ ├── render_visibility_detector_test.dart │ │ └── impression_test.dart │ ├── CHANGELOG.md │ └── README.md ├── scrollable_positioned_list │ ├── lib │ │ ├── scrollable_positioned_list.dart │ │ └── src │ │ │ ├── item_positions_notifier.dart │ │ │ ├── scroll_offset_notifier.dart │ │ │ ├── scroll_offset_listener.dart │ │ │ ├── post_mount_callback.dart │ │ │ ├── item_positions_listener.dart │ │ │ ├── scroll_view.dart │ │ │ └── element_registry.dart │ ├── example │ │ ├── pubspec.yaml │ │ └── test │ │ │ └── scrollable_positioned_list_example_test.dart │ ├── pubspec.yaml │ ├── LICENSE │ ├── README.md │ ├── CHANGELOG.md │ └── test │ │ ├── scroll_offset_controller_test.dart │ │ ├── scroll_offset_listener_test.dart │ │ ├── seperated_horizontal_scrollable_positioned_list_test.dart │ │ └── reversed_scrollable_positioned_list_test.dart └── linked_scroll_controller │ ├── pubspec.yaml │ ├── CHANGELOG.md │ ├── LICENSE │ └── README.md ├── AUTHORS ├── .gitignore ├── .gitattributes ├── tool ├── validate_actions.dart └── generate_readme.dart ├── LICENSE ├── analysis_options.yaml ├── README.md └── CONTRIBUTING.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/screenshots/treeview_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/flutter.widgets/HEAD/packages/flutter_simple_treeview/example/screenshots/treeview_demo.png -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/screenshots/treeview_demo_JSON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/flutter.widgets/HEAD/packages/flutter_simple_treeview/example/screenshots/treeview_demo_JSON.png -------------------------------------------------------------------------------- /packages/self_storing_input/example/readme.md: -------------------------------------------------------------------------------- 1 | This is demo app for [self_storing_input](https://pub.dev/packages/self_storing_input). 2 | 3 | Demo is hosted [here](http://self_storing_input_demo.surge.sh). 4 | 5 | Run the demo: 6 | ``` 7 | flutter run -d chrome 8 | ``` 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Flutter Widgets project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | Abhijeeth Padarthi 8 | Alex Li 9 | -------------------------------------------------------------------------------- /packages/self_storing_input/example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | self_storing_input demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | flutter_simple_treeview demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/readme.md: -------------------------------------------------------------------------------- 1 | This is demo app for [flutter_simple-treeview](https://pub.dev/packages/flutter_simple_treeview). 2 | 3 | Demo shows how to control the nodes and how to render json as a tree. 4 | It is hosted [here](https://flutter_simple_treeview.surge.sh/). 5 | 6 | Run demo: 7 | ``` 8 | flutter run -d chrome 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/visibility_detector/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: visibility_detector_example 2 | 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: '>=2.14.0 <3.0.0' 7 | flutter: '>=1.13.8' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | visibility_detector: 13 | path: ../ 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | -------------------------------------------------------------------------------- /packages/visibility_detector/lib/visibility_detector.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | export 'src/visibility_detector.dart'; 8 | export 'src/visibility_detector_controller.dart'; 9 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/scrollable_positioned_list.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | export 'src/item_positions_listener.dart'; 6 | export 'src/scrollable_positioned_list.dart'; 7 | export 'src/scroll_offset_listener.dart'; 8 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: scrollable_positioned_list_example 2 | 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: '>=2.12.0-0 <3.0.0' 7 | flutter: '>=1.13.8' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | scrollable_positioned_list: 13 | path: ../ 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/flutter_simple_treeview.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | export 'src/primitives/tree_controller.dart'; 8 | export 'src/primitives/tree_node.dart'; 9 | export 'src/tree_view.dart'; 10 | -------------------------------------------------------------------------------- /packages/self_storing_input/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: self_storing_input 2 | version: 4.0.2 3 | description: > 4 | A set of input widgets that automatically save and load the entered value to a data store. 5 | homepage: https://github.com/google/flutter.widgets 6 | 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | flutter: '>=1.20.0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | test: 17 | flutter_test: 18 | sdk: flutter 19 | -------------------------------------------------------------------------------- /packages/linked_scroll_controller/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: linked_scroll_controller 2 | version: 0.2.0 3 | description: > 4 | A scroll controller that allows two or more scroll views to be in sync. 5 | repository: https://github.com/google/flutter.widgets/tree/master/packages/linked_scroll_controller 6 | 7 | environment: 8 | sdk: '>=2.12.0-0 <3.0.0' 9 | flutter: '>=1.13.8' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | pedantic: ^1.10.0-0 17 | flutter_test: 18 | sdk: flutter 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Dart template 2 | # Don’t commit the following directories created by pub. 3 | .buildlog 4 | .dart_tool 5 | .pub/ 6 | build/ 7 | .packages 8 | 9 | # Or the files created by dart2js. 10 | *.dart.js 11 | *.js_ 12 | *.js.deps 13 | *.js.map 14 | 15 | # Include when developing application packages. 16 | pubspec.lock 17 | 18 | # IDE 19 | .project 20 | .settings 21 | .idea 22 | .c9 23 | 24 | # Flutter tool generated ephemeral files 25 | .flutter-plugins 26 | .flutter-plugins-dependencies 27 | generated_plugin_registrant.dart 28 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: scrollable_positioned_list 2 | version: 0.3.8+1 3 | description: > 4 | A list with helper methods to programmatically scroll to an item. 5 | homepage: https://github.com/google/flutter.widgets/tree/master/packages/scrollable_positioned_list 6 | 7 | environment: 8 | sdk: '>=2.15.0 <4.0.0' 9 | flutter: '>=3.1.0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | collection: ^1.15.0 15 | 16 | dev_dependencies: 17 | pedantic: ^1.10.0-0 18 | flutter_test: 19 | sdk: flutter 20 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/primitives/operation_result.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | /// Result of value validation. 8 | class OperationResult { 9 | bool get isSuccess => error == null; 10 | final String? error; 11 | 12 | const OperationResult.success() : error = null; 13 | 14 | const OperationResult.error(this.error); 15 | } 16 | -------------------------------------------------------------------------------- /packages/visibility_detector/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: visibility_detector 2 | version: 0.4.0+3 3 | description: > 4 | A widget that detects the visibility of its child and notifies a callback. 5 | repository: https://github.com/google/flutter.widgets/tree/master/packages/visibility_detector 6 | 7 | environment: 8 | sdk: '>=2.14.0 <3.0.0' 9 | flutter: '>=3.1.0-0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | visibility_detector_example: 19 | path: example/ 20 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/src/item_positions_notifier.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import 'item_positions_listener.dart'; 8 | 9 | /// Internal implementation of [ItemPositionsListener]. 10 | class ItemPositionsNotifier implements ItemPositionsListener { 11 | @override 12 | final ValueNotifier> itemPositions = ValueNotifier([]); 13 | } 14 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/src/primitives/tree_node.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | /// One node of a tree. 10 | class TreeNode { 11 | final List? children; 12 | final Widget content; 13 | final Key? key; 14 | 15 | TreeNode({this.key, this.children, Widget? content}) 16 | : content = content ?? Container(width: 0, height: 0); 17 | } 18 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/src/scroll_offset_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'scroll_offset_listener.dart'; 4 | 5 | class ScrollOffsetNotifier implements ScrollOffsetListener { 6 | final bool recordProgrammaticScrolls; 7 | 8 | ScrollOffsetNotifier({this.recordProgrammaticScrolls = true}); 9 | 10 | final _streamController = StreamController(); 11 | 12 | @override 13 | Stream get changes => _streamController.stream; 14 | 15 | StreamController get changeController => _streamController; 16 | 17 | void dispose() { 18 | _streamController.close(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Always perform LF normalization on these files 5 | *.dart text 6 | *.gradle text 7 | *.html text 8 | *.java text 9 | *.json text 10 | *.md text 11 | *.py text 12 | *.sh text 13 | *.txt text 14 | *.xml text 15 | *.yaml text 16 | 17 | # Make sure that these Windows files always have CRLF line endings in checkout 18 | *.bat text eol=crlf 19 | *.ps1 text eol=crlf 20 | 21 | # Never perform LF normalization on these files 22 | *.ico binary 23 | *.jar binary 24 | *.png binary 25 | *.zip binary 26 | -------------------------------------------------------------------------------- /packages/self_storing_input/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: self_storing_input_demo 2 | 3 | environment: 4 | sdk: '>=2.12.0 <3.0.0' 5 | 6 | dependencies: 7 | flutter: 8 | sdk: flutter 9 | url_launcher: ^6.1.11 10 | # Currently the example references self_storing_input by relative path. 11 | # If you copy the example to other place, you need to either update the path 12 | # or reference pub.dev: 13 | # 14 | # self_storing_input: 15 | # 16 | # Find the latest version here: https://pub.dev/packages/self_storing_input 17 | self_storing_input: 18 | path: .. 19 | 20 | flutter: 21 | uses-material-design: true 22 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_tree_demo 2 | 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | 6 | dependencies: 7 | flutter: 8 | sdk: flutter 9 | url_launcher: ^6.1.3 10 | # Currently the example references flutter_simple_treeview by relative path. 11 | # If you copy the example to other place, you need to either update the path 12 | # or reference pub.dev: 13 | # 14 | # flutter_simple_treeview: 15 | # 16 | # Find the latest version here: https://pub.dev/packages/flutter_simple_treeview 17 | flutter_simple_treeview: 18 | path: .. 19 | 20 | flutter: 21 | uses-material-design: true 22 | -------------------------------------------------------------------------------- /packages/self_storing_input/test/self_storing_checkbox_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:self_storing_input/self_storing_input.dart'; 9 | 10 | import 'testing/widget_testing.dart'; 11 | 12 | void main() { 13 | group('SelfStoringCheckbox', () { 14 | testWidgets('widget renders successfully if no parameters provided.', 15 | (WidgetTester tester) async { 16 | await wrapAndPump(tester, SelfStoringCheckbox('id')); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/linked_scroll_controller/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.0 2 | 3 | * Stable release for null safety. 4 | 5 | # 0.2.0-nullsafety.0 6 | 7 | * Update to null safety. 8 | 9 | * Make `ScrollView`s rebuild at the group's offset instead of their last 10 | known offset. 11 | 12 | # 0.1.2 13 | 14 | * Add `LinkedScrollControllerGroup.animateTo` and 15 | `LinkedScrollControllerGroup.jumpTo` methods that control the scroll 16 | position of the group. 17 | 18 | # 0.1.1 19 | 20 | * Add `LinkedScrollControllerGroup.addOffsetChangedListener` method that calls 21 | the provided callback when the scroll offset of the group changes. 22 | 23 | # 0.1.0 24 | 25 | * Add getter `offset` that returns the current scroll offset for the group. 26 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_simple_treeview 2 | version: 3.0.2 3 | description: > 4 | A widget, that visualises a tree structure, where a node can be any widget. 5 | repository: https://github.com/google/flutter.widgets/tree/master/packages/flutter_simple_treeview 6 | 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | flutter: '>=1.20.0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | test: 17 | flutter_test: 18 | sdk: flutter 19 | 20 | screenshots: 21 | - description: 'Example of tree controller usage.' 22 | path: example/screenshots/treeview_demo.png 23 | - description: 'Example of tree from JSON.' 24 | path: example/screenshots/treeview_demo_JSON.png 25 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.2 2 | 3 | * Add screenshots 4 | 5 | # 3.0.1 6 | 7 | * Minor fixes. 8 | 9 | # 3.0.0 10 | 11 | * Stable release for null safety. 12 | 13 | # 3.0.0-nullsafety.1 14 | 15 | * Avoid deprecation warnings with Flutter 1.25.0-8.1.pre per 16 | http://flutter.dev/go/material-button-migration-guide. 17 | 18 | # 3.0.0-nullsafety.0 19 | 20 | * Set nullsafety version 21 | 22 | # 2.1.0 23 | 24 | * Enable null-safety. 25 | 26 | # 2.0.2 27 | 28 | * Add example for JSON input. 29 | * Fix layout bug. 30 | 31 | # 2.0.1 32 | 33 | * Update links to demo. 34 | 35 | # 2.0.0 36 | 37 | * Add controller. 38 | 39 | # 1.0.1 40 | 41 | * Add example. 42 | 43 | # 1.0.0 44 | 45 | * Publish initial version. 46 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/self_storing_input.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | export 'src/primitives/operation_result.dart'; 8 | export 'src/primitives/overlay.dart'; 9 | export 'src/primitives/saver.dart'; 10 | export 'src/self_storing_checkbox.dart'; 11 | export 'src/self_storing_checkbox/self_storing_checkbox_style.dart'; 12 | export 'src/self_storing_radio_group.dart'; 13 | export 'src/self_storing_radio_group/self_storing_radio_group_style.dart'; 14 | export 'src/self_storing_text.dart'; 15 | export 'src/self_storing_text/self_storing_text_style.dart'; 16 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/primitives/the_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | Widget get theProgressIndicator => const _ProgressIndicator(); 10 | 11 | class _ProgressIndicator extends StatelessWidget { 12 | const _ProgressIndicator(); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Container( 17 | width: 16, 18 | height: 16, 19 | margin: EdgeInsets.all(8), 20 | child: Align(child: CircularProgressIndicator()), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_checkbox/self_storing_checkbox_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import '../primitives/overlay.dart'; 8 | 9 | /// A style for [SelfStoringCheckbox]. 10 | class SelfStoringCheckboxStyle { 11 | /// Style of the error message box. 12 | final OverlayStyle overlayStyle; 13 | 14 | /// Size of the button that closes the error message box. 15 | final double closeIconSize; 16 | 17 | const SelfStoringCheckboxStyle( 18 | {this.closeIconSize = 18, 19 | this.overlayStyle = const OverlayStyle.forMessage()}); 20 | } 21 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_radio_group/self_storing_radio_group_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import '../primitives/overlay.dart'; 8 | 9 | /// A style for [SelfStoringRadioGroup]. 10 | class SelfStoringRadioGroupStyle { 11 | /// Style of the error message box. 12 | final OverlayStyle overlayStyle; 13 | 14 | /// Size of the button that closes the error message box. 15 | final double closeIconSize; 16 | 17 | const SelfStoringRadioGroupStyle( 18 | {this.closeIconSize = 18, 19 | this.overlayStyle = const OverlayStyle.forMessage()}); 20 | } 21 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/src/scroll_offset_listener.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'scroll_offset_notifier.dart'; 4 | 5 | /// Provides an affordance for listening to scroll offset changes. 6 | /// 7 | /// This is an experimental API and is subject to change. 8 | /// Behavior may be ill-defined in some cases. Please file bugs. 9 | abstract class ScrollOffsetListener { 10 | /// Stream of scroll offset deltas. 11 | Stream get changes; 12 | 13 | /// Construct a ScrollOffsetListener. 14 | /// 15 | /// Set [recordProgrammaticScrolls] to false to prevent reporting of 16 | /// programmatic scrolls. 17 | factory ScrollOffsetListener.create( 18 | {bool recordProgrammaticScrolls = true}) => 19 | ScrollOffsetNotifier( 20 | recordProgrammaticScrolls: recordProgrammaticScrolls); 21 | } 22 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_text/self_storing_text_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:self_storing_input/self_storing_input.dart'; 9 | 10 | /// A style for [SelfStoringText]. 11 | class SelfStoringTextStyle { 12 | final OverlayStyle overlayStyle; 13 | final TextInputType? keyboardType; 14 | 15 | /// Maximum number of lines. Infinite if null. 16 | /// 17 | /// Behaves the same way as [Text.maxLines]. 18 | final int? maxLines; 19 | 20 | const SelfStoringTextStyle( 21 | {this.overlayStyle = const OverlayStyle.forTextEditor(), 22 | this.keyboardType, 23 | this.maxLines = 1}); 24 | } 25 | -------------------------------------------------------------------------------- /packages/self_storing_input/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.0.2 2 | 3 | * Migrate the example to null safety. 4 | 5 | # 4.0.1 6 | 7 | * Minor fixes. 8 | 9 | # 4.0.0 10 | 11 | * Stable release for null safety. 12 | 13 | # 4.0.0-nullsafety.1 14 | 15 | * Avoid deprecation warnings with Flutter 1.25.0-8.1.pre per 16 | http://flutter.dev/go/material-button-migration-guide. 17 | 18 | # 4.0.0-nullsafety.0 19 | 20 | * Set nullsafety version 21 | 22 | # 3.1.0 23 | 24 | * Enable null-safety. 25 | 26 | # 3.0.1 27 | 28 | * Make widgets strongly typed. 29 | 30 | # 3.0.0 31 | 32 | * Make Saver strongly typed. 33 | 34 | # 2.0.2 35 | 36 | * Replace WhitelistingTextInputFormatter with FilteringTextInputFormatter. 37 | 38 | # 2.0.1 39 | 40 | * Add self_storing_radio_group. 41 | 42 | # 2.0.0 43 | 44 | * Add self_storing_checkbox. 45 | 46 | # 1.0.1 47 | 48 | * Fix bug in self_storing_text. 49 | 50 | # 1.0.0 51 | 52 | * Publish initial version. 53 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/src/builder.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import 'node_widget.dart'; 10 | import 'primitives/tree_controller.dart'; 11 | import 'primitives/tree_node.dart'; 12 | 13 | /// Builds set of [nodes] respecting [state], [indent] and [iconSize]. 14 | Widget buildNodes(Iterable nodes, double? indent, 15 | TreeController state, double? iconSize) { 16 | return Column( 17 | crossAxisAlignment: CrossAxisAlignment.start, 18 | children: [ 19 | for (var node in nodes) 20 | NodeWidget( 21 | treeNode: node, 22 | indent: indent, 23 | state: state, 24 | iconSize: iconSize, 25 | ) 26 | ], 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New issue 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Problem description 11 | 14 | 15 | ## Steps to reproduce 16 | 20 | 21 | 1. 22 | 23 | ## Expected behavior 24 | 25 | 26 | ## Actual behavior 27 | 28 | 29 | ## Environment 30 | 31 | 32 | ## Additional details 33 | 34 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/README.md: -------------------------------------------------------------------------------- 1 | # flutter_simple_treeview 2 | This widget visualises a tree structure, where a node can be any widget. 3 | 4 | ## Demo 5 | 6 | [https://flutter_simple_treeview.surge.sh/](https://flutter_simple_treeview.surge.sh/) 7 | 8 | ## Usage 9 | 10 | ``` 11 | TreeView(nodes: [ 12 | TreeNode(content: Text("root1")), 13 | TreeNode( 14 | content: Text("root2"), 15 | children: [ 16 | TreeNode(content: Text("child21")), 17 | TreeNode(content: Text("child22")), 18 | TreeNode( 19 | content: Text("root23"), 20 | children: [ 21 | TreeNode(content: Text("child231")), 22 | ], 23 | ), 24 | ], 25 | ), 26 | ]), 27 | ``` 28 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/src/copy_tree_nodes.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'primitives/key_provider.dart'; 8 | import 'primitives/tree_node.dart'; 9 | 10 | /// Copies nodes to unmodifiable list, assigning missing keys and checking for duplicates. 11 | List copyTreeNodes(List? nodes) { 12 | return _copyNodesRecursively(nodes, KeyProvider())!; 13 | } 14 | 15 | List? _copyNodesRecursively( 16 | List? nodes, KeyProvider keyProvider) { 17 | if (nodes == null) { 18 | return null; 19 | } 20 | return List.unmodifiable(nodes.map((n) { 21 | return TreeNode( 22 | key: keyProvider.key(n.key), 23 | content: n.content, 24 | children: _copyNodesRecursively(n.children, keyProvider), 25 | ); 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /packages/self_storing_input/test/testing/widget_testing.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | /// Wraps [widget] to MaterialApp and pumps. 11 | Future wrapAndPump(WidgetTester tester, Widget widget) async { 12 | var wrapped = MaterialApp( 13 | home: SingleChildScrollView( 14 | child: SingleChildScrollView( 15 | scrollDirection: Axis.horizontal, 16 | child: Card(child: widget), 17 | )), 18 | ); 19 | await tester.pumpWidget(wrapped); 20 | } 21 | 22 | /// Wraps widget into sized and aligned container. 23 | Widget sizeAndLayout(Widget child) => SizedBox( 24 | width: 5000, 25 | height: 5000, 26 | child: Align(alignment: Alignment.topLeft, child: child), 27 | ); 28 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/src/primitives/key_provider.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | class _TreeNodeKey extends ValueKey { 10 | _TreeNodeKey(dynamic value) : super(value); 11 | } 12 | 13 | /// Provides unique keys and verifies duplicates. 14 | class KeyProvider { 15 | int _nextIndex = 0; 16 | final Set _keys = {}; 17 | 18 | /// If [originalKey] is null, generates new key, otherwise verifies the key 19 | /// was not met before. 20 | Key key(Key? originalKey) { 21 | if (originalKey == null) { 22 | return _TreeNodeKey(_nextIndex++); 23 | } 24 | if (_keys.contains(originalKey)) { 25 | throw ArgumentError('There should not be nodes with the same kays. ' 26 | 'Duplicate value found: $originalKey.'); 27 | } 28 | _keys.add(originalKey); 29 | return originalKey; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/self_storing_input/test/primitives/message_overlay_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:self_storing_input/self_storing_input.dart'; 9 | import 'package:self_storing_input/src/primitives/message_overlay.dart'; 10 | import 'package:self_storing_input/src/primitives/overlay_builder.dart'; 11 | 12 | import '../testing/widget_testing.dart'; 13 | 14 | void main() { 15 | group('MessageOverlay', () { 16 | testWidgets('renders successfully if trivial parameters provided.', 17 | (WidgetTester tester) async { 18 | var style = OverlayStyle.forMessage(); 19 | var content = MessageOverlay( 20 | message: '', 21 | style: style, 22 | overlayController: OverlayController(), 23 | ); 24 | await wrapAndPump(tester, applyOverlayStyle(style, content)); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/primitives/overlay.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | /// A controller for the overlay of a widget, 10 | /// a floating dialog box used to 11 | /// edit/save the value and/or notify about errors. 12 | class OverlayController with ChangeNotifier { 13 | /// Closes the overlay of the controlling widget. If there is no active overlay, 14 | /// nothing will happen. 15 | void close() { 16 | notifyListeners(); 17 | } 18 | } 19 | 20 | class OverlayStyle { 21 | final double elevation; 22 | final double width; 23 | final double height; 24 | final double margin; 25 | 26 | const OverlayStyle.forTextEditor({ 27 | this.elevation = 4.0, 28 | this.width = 500, 29 | this.height = 100, 30 | this.margin = 8, 31 | }); 32 | 33 | const OverlayStyle.forMessage({ 34 | this.elevation = 4.0, 35 | this.width = 200, 36 | this.height = 80, 37 | this.margin = 2, 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /tool/validate_actions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | void main(List args) { 4 | const actionsFile = '.github/workflows/build.yaml'; 5 | 6 | var packages = Directory('packages') 7 | .listSync() 8 | .whereType() 9 | .map((d) => d.path.substring(d.path.lastIndexOf('/') + 1)) 10 | .toList() 11 | ..sort(); 12 | 13 | print('Validating $actionsFile ...\n'); 14 | 15 | var failed = false; 16 | var lines = 17 | File(actionsFile).readAsLinesSync().map((line) => line.trim()).toSet(); 18 | 19 | // Here, we look for `- package-name`. This will catch a few additional 20 | // matches we don't care about, like `- stable`; that won't be an issue in 21 | // terms of validating that we're testing all packages. 22 | for (var package in packages) { 23 | if (lines.contains('- $package')) { 24 | print(" found configuration: '- $package'"); 25 | } else { 26 | print("missing configuration: '- $package'"); 27 | failed = true; 28 | } 29 | } 30 | 31 | if (failed) { 32 | exitCode = 1; 33 | print('\nPlease add missing packages to $actionsFile.'); 34 | } else { 35 | print('\nNo issues found!'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_text/shared_state.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import '../primitives/overlay.dart'; 10 | import '../primitives/saver.dart'; 11 | import 'self_storing_text_style.dart'; 12 | 13 | /// State that needs to be shared between [OverlayBox] and its 14 | /// parent. 15 | class SharedState with ChangeNotifier { 16 | String? _storedValue; 17 | final OverlayController overlayController; 18 | final Saver saver; 19 | final Object itemKey; 20 | final SelfStoringTextStyle style; 21 | 22 | String? get storedValue => _storedValue; 23 | set storedValue(String? value) { 24 | if (value == _storedValue) return; 25 | _storedValue = value; 26 | notifyListeners(); 27 | } 28 | 29 | SharedState({ 30 | storedValue, 31 | required this.overlayController, 32 | required this.saver, 33 | required this.itemKey, 34 | required this.style, 35 | }) : _storedValue = storedValue; 36 | } 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Description 9 | 10 | 13 | *TODO* 14 | 15 | ## Related Issues 16 | 17 | 20 | *TODO* 21 | 22 | ## Checklist 23 | 24 | Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (`[x]`). This will ensure a smooth and quick review process. 25 | 26 | - [ ] I signed the [CLA]. 27 | - [ ] All tests from running `flutter test` pass. 28 | - [ ] `flutter analyze` does not report any problems on my PR. 29 | - [ ] I am willing to follow-up on review comments in a timely manner. 30 | 31 | 32 | [CLA]: https://cla.developers.google.com/ 33 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/src/primitives/tree_controller.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/foundation.dart'; 8 | 9 | /// A controller for a tree state. 10 | /// 11 | /// Allows to modify the state of the tree. 12 | class TreeController { 13 | bool _allNodesExpanded; 14 | final Map _expanded = {}; 15 | 16 | TreeController({allNodesExpanded = true}) 17 | : _allNodesExpanded = allNodesExpanded; 18 | 19 | bool get allNodesExpanded => _allNodesExpanded; 20 | 21 | bool isNodeExpanded(Key key) { 22 | return _expanded[key] ?? _allNodesExpanded; 23 | } 24 | 25 | void toggleNodeExpanded(Key key) { 26 | _expanded[key] = !isNodeExpanded(key); 27 | } 28 | 29 | void expandAll() { 30 | _allNodesExpanded = true; 31 | _expanded.clear(); 32 | } 33 | 34 | void collapseAll() { 35 | _allNodesExpanded = false; 36 | _expanded.clear(); 37 | } 38 | 39 | void expandNode(Key key) { 40 | _expanded[key] = true; 41 | } 42 | 43 | void collapseNode(Key key) { 44 | _expanded[key] = false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_checkbox/shared_state.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import '../primitives/operation_result.dart'; 10 | import '../primitives/overlay.dart'; 11 | import '../primitives/saver.dart'; 12 | import 'self_storing_checkbox_style.dart'; 13 | 14 | /// State that needs to be shared between main widget and children. 15 | class SharedState with ChangeNotifier { 16 | final Saver saver; 17 | final Object itemKey; 18 | final OverlayController overlayController; 19 | final SelfStoringCheckboxStyle style; 20 | final bool tristate; 21 | 22 | bool? storedValue; 23 | OperationResult operationResult = OperationResult.success(); 24 | 25 | bool _isSaving = false; 26 | bool get isSaving => _isSaving; 27 | set isSaving(bool value) { 28 | _isSaving = value; 29 | notifyListeners(); 30 | } 31 | 32 | SharedState({ 33 | required this.saver, 34 | required this.itemKey, 35 | required this.overlayController, 36 | required this.style, 37 | this.tristate = false, 38 | this.storedValue, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/src/post_mount_callback.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// Widget whose [Element] calls a callback when the element is mounted. 8 | class PostMountCallback extends StatelessWidget { 9 | /// Creates a [PostMountCallback] widget. 10 | const PostMountCallback({required this.child, this.callback, Key? key}) 11 | : super(key: key); 12 | 13 | /// The widget below this widget in the tree. 14 | final Widget child; 15 | 16 | /// Callback to call when the element for this widget is mounted. 17 | final void Function()? callback; 18 | 19 | @override 20 | StatelessElement createElement() => _PostMountCallbackElement(this); 21 | 22 | @override 23 | Widget build(BuildContext context) => child; 24 | } 25 | 26 | class _PostMountCallbackElement extends StatelessElement { 27 | _PostMountCallbackElement(PostMountCallback widget) : super(widget); 28 | 29 | @override 30 | void mount(Element? parent, dynamic newSlot) { 31 | super.mount(parent, newSlot); 32 | final PostMountCallback postMountCallback = widget as PostMountCallback; 33 | postMountCallback.callback?.call(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tool/generate_readme.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | void main(List args) { 4 | var packages = Directory('packages') 5 | .listSync() 6 | .whereType() 7 | .map((d) => Package(d)) 8 | .toList() 9 | ..sort(); 10 | 11 | print('Package | Description | Version'); 12 | print('--- | --- | ---'); 13 | for (var package in packages) { 14 | print( 15 | '[${package.name}](${package.path}/) | ' 16 | '${package.description} | ' 17 | '[![pub package](https://img.shields.io/pub/v/${package.name}.svg)]' 18 | '(https://pub.dev/packages/${package.name})', 19 | ); 20 | } 21 | } 22 | 23 | class Package implements Comparable { 24 | final Directory dir; 25 | 26 | Package(this.dir); 27 | 28 | String get name => dir.path.substring(dir.path.lastIndexOf('/') + 1); 29 | 30 | String get path => dir.path; 31 | 32 | String get description { 33 | // An quick and dirty yaml parser (this script doesn't currently have access 34 | // to a pubspec). 35 | var pubspec = File('${dir.path}/pubspec.yaml'); 36 | var contents = pubspec.readAsStringSync(); 37 | contents = contents.replaceAll('>\n', ''); 38 | var lines = contents.split('\n'); 39 | return lines 40 | .firstWhere((line) => line.startsWith('description:')) 41 | .substring('description:'.length) 42 | .trim(); 43 | } 44 | 45 | @override 46 | int compareTo(Package other) => name.compareTo(other.name); 47 | } 48 | -------------------------------------------------------------------------------- /packages/self_storing_input/README.md: -------------------------------------------------------------------------------- 1 | # self_storing_input 2 | A set of input widgets that automatically save and load 3 | the entered value to a data store. 4 | 5 | ## Demo 6 | 7 | [https://self_storing_input_demo.surge.sh](https://self_storing_input_demo.surge.sh) 8 | 9 | ## Usage 10 | 11 | ### Define Saver 12 | 13 | Implement a Saver that loads, validates and saves data items by itemKey. 14 | The itemKey can be of any form. You can make it a resource URL string 15 | or a tuple . 16 | 17 | Find example of an in-memory Saver 18 | [here](https://github.com/google/flutter.widgets/tree/master/packages/self_storing_input/example/lib/main.dart#L16). 19 | 20 | ### Define Input 21 | 22 | Add self storing input widgets to your screen and parameterize each with the 23 | defined Saver and itemKey. The widgets will take care of loading data, 24 | validating data, saving data, and handling failure modes like poor internet 25 | connection and data storage failures. 26 | 27 | ### Close the Overlays on Tap 28 | 29 | Define an OverlayController in your screen state: 30 | 31 | ``` 32 | OverlayController _controller = OverlayController(); 33 | ``` 34 | 35 | Wrap your screen widget body with a GestureDetector to close editing overlays 36 | on tap: 37 | 38 | ``` 39 | GestureDetector( 40 | onTap: () async { 41 | _controller.close(); 42 | }, 43 | child: Scaffold( 44 | body: ... 45 | ``` 46 | 47 | Pass `_controller` to each self storing widget: 48 | 49 | ``` 50 | SelfStoringText( 51 | overlayController: _controller, 52 | ... 53 | ``` 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 the Dart project authors, Inc. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /packages/self_storing_input/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 the Dart project authors, Inc. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 the Dart project authors, Inc. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /packages/visibility_detector/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 the Dart project authors, Inc. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /packages/linked_scroll_controller/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 the Dart project authors, Inc. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 the Dart project authors, Inc. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/test/primitives/tree_controller_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_simple_treeview/src/primitives/tree_controller.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | Key k1 = UniqueKey(); 13 | Key k2 = UniqueKey(); 14 | 15 | test('Initial state is equal to default state.', () { 16 | for (var allExpanded in [true, false]) { 17 | var state = TreeController(allNodesExpanded: allExpanded); 18 | expect(state.isNodeExpanded(k1), allExpanded); 19 | } 20 | }); 21 | 22 | test('Toggle works.', () { 23 | for (var allExpanded in [true, false]) { 24 | var state = TreeController(allNodesExpanded: allExpanded); 25 | state.toggleNodeExpanded(k1); 26 | expect(state.isNodeExpanded(k1), !allExpanded); 27 | state.toggleNodeExpanded(k1); 28 | expect(state.isNodeExpanded(k1), allExpanded); 29 | } 30 | }); 31 | 32 | test('States are independant.', () { 33 | for (var allExpanded in [true, false]) { 34 | var state = TreeController(allNodesExpanded: allExpanded); 35 | state.toggleNodeExpanded(k1); 36 | expect(state.isNodeExpanded(k2), allExpanded); 37 | state.toggleNodeExpanded(k1); 38 | expect(state.isNodeExpanded(k2), allExpanded); 39 | state.toggleNodeExpanded(k2); 40 | expect(state.isNodeExpanded(k1), allExpanded); 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/src/tree_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import 'builder.dart'; 10 | import 'copy_tree_nodes.dart'; 11 | import 'primitives/tree_controller.dart'; 12 | import 'primitives/tree_node.dart'; 13 | 14 | /// Tree view with collapsible and expandable nodes. 15 | class TreeView extends StatefulWidget { 16 | /// List of root level tree nodes. 17 | final List nodes; 18 | 19 | /// Horizontal indent between levels. 20 | final double? indent; 21 | 22 | /// Size of the expand/collapse icon. 23 | final double? iconSize; 24 | 25 | /// Tree controller to manage the tree state. 26 | final TreeController? treeController; 27 | 28 | TreeView( 29 | {Key? key, 30 | required List nodes, 31 | this.indent = 40, 32 | this.iconSize, 33 | this.treeController}) 34 | : nodes = copyTreeNodes(nodes), 35 | super(key: key); 36 | 37 | @override 38 | _TreeViewState createState() => _TreeViewState(); 39 | } 40 | 41 | class _TreeViewState extends State { 42 | TreeController? _controller; 43 | 44 | @override 45 | void initState() { 46 | _controller = widget.treeController ?? TreeController(); 47 | super.initState(); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return buildNodes( 53 | widget.nodes, widget.indent, _controller!, widget.iconSize); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/primitives/saver.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'operation_result.dart'; 8 | 9 | /// Loads and saves a value of an input widget. 10 | abstract class Saver { 11 | /// Loads the value of a data item from the storage. 12 | /// 13 | /// [itemKey] identifies the data item. [itemKey] can be of any form: 14 | /// resource url, guid, tuple etc. 15 | Future load(K itemKey); 16 | 17 | /// Saves a value to the storage by [itemKey]. 18 | /// 19 | /// See [load] for [itemKey]. 20 | Future save(K itemKey, T? value); 21 | 22 | /// Validates whether [value] is allowed to be stored for [itemKey]. 23 | /// Disallowed values will not be passed to [save]. 24 | /// 25 | /// Use this method only for synchronous validation, 26 | /// as asynchronous validation should be done in [save]. 27 | /// 28 | /// See [load] for [itemKey]. 29 | OperationResult validate(K itemKey, T? value); 30 | } 31 | 32 | /// Trivial implementation of [Saver]. Always returns null value and 33 | /// successful operation result. 34 | class NoOpSaver implements Saver { 35 | const NoOpSaver(); 36 | 37 | @override 38 | Future load(K itemKey) async => null; 39 | 40 | @override 41 | OperationResult validate(K itemKey, T? value) => OperationResult.success(); 42 | 43 | @override 44 | Future save(K itemKey, T? value) async => 45 | OperationResult.success(); 46 | } 47 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_text/edit_button.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import '../primitives/overlay_builder.dart'; 10 | import 'overlay_box.dart'; 11 | import 'shared_state.dart'; 12 | 13 | /// Button that opens editing area when clicked. 14 | class EditButton extends StatefulWidget { 15 | final SharedState state; 16 | 17 | const EditButton(this.state); 18 | 19 | @override 20 | _EditButtonState createState() => _EditButtonState(); 21 | } 22 | 23 | class _EditButtonState extends State { 24 | OverlayEntry? _overlay; 25 | 26 | @override 27 | void initState() { 28 | widget.state.overlayController.addListener(_closeOverlay); 29 | super.initState(); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | widget.state.overlayController.removeListener(_closeOverlay); 35 | super.dispose(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return IconButton( 41 | icon: Icon(Icons.edit, color: Theme.of(context).colorScheme.secondary), 42 | onPressed: () { 43 | widget.state.overlayController.close(); 44 | _overlay = _buildOverlay(context); 45 | Overlay.of(context).insert(_overlay!); 46 | }, 47 | ); 48 | } 49 | 50 | void _closeOverlay() { 51 | _overlay?.remove(); 52 | _overlay = null; 53 | } 54 | 55 | OverlayEntry _buildOverlay(BuildContext context) { 56 | return createOverlayInTheMiddle( 57 | OverlayBox(widget.state), 58 | context, 59 | widget.state.style.overlayStyle, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/primitives/message_overlay.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:self_storing_input/self_storing_input.dart'; 9 | 10 | /// The panel that pops up to show a message. 11 | class MessageOverlay extends StatelessWidget { 12 | final String message; 13 | final OverlayStyle style; 14 | final double? closeIconSize; 15 | final OverlayController overlayController; 16 | 17 | const MessageOverlay( 18 | {required this.message, 19 | required this.style, 20 | this.closeIconSize, 21 | required this.overlayController}); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var iconSize = 26 | closeIconSize ?? (Theme.of(context).iconTheme.size ?? 24) / 2; 27 | return Column(children: [ 28 | _buildButton(iconSize), 29 | _buildMessage(iconSize), 30 | ]); 31 | } 32 | 33 | Widget _buildButton(double iconSize) { 34 | return Align( 35 | alignment: Alignment.topRight, 36 | child: SizedBox( 37 | height: iconSize, 38 | width: iconSize, 39 | child: IconButton( 40 | padding: EdgeInsets.all(0.0), 41 | icon: Icon(Icons.clear, size: iconSize), 42 | onPressed: overlayController.close, 43 | ), 44 | ), 45 | ); 46 | } 47 | 48 | Widget _buildMessage(double iconSize) { 49 | return Align( 50 | alignment: Alignment.topLeft, 51 | child: SizedBox( 52 | height: style.height - iconSize - style.margin, 53 | child: SingleChildScrollView( 54 | child: Text(message), 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | schedule: 5 | # “At 00:00 (UTC) on Sunday.” 6 | - cron: '0 0 * * 0' 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | 14 | # A jobs configuration to test all packages in `packages/`. 15 | build: 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | working-directory: packages/${{ matrix.package }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | # Add new packages here to ensure that they're tested on the CI. 24 | package: 25 | - flutter_simple_treeview 26 | - linked_scroll_controller 27 | - scrollable_positioned_list 28 | - self_storing_input 29 | - visibility_detector 30 | # Test against the Flutter stable SDK. 31 | version: 32 | - stable 33 | name: ${{ matrix.package }} / flutter ${{ matrix.version }} 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: subosito/flutter-action@v2.4.0 37 | with: 38 | channel: ${{ matrix.version }} 39 | 40 | - name: flutter version 41 | run: flutter --version 42 | 43 | - name: flutter pub get 44 | run: flutter pub get 45 | 46 | - name: flutter analyze 47 | run: flutter analyze 48 | 49 | - name: flutter test 50 | run: flutter test 51 | 52 | - name: dart format 53 | run: dart format -o none --set-exit-if-changed . 54 | # Only test formatting on one version of Flutter. 55 | if: ${{ matrix.version == 'stable' }} 56 | 57 | # A job configuration to do some light validation of the CI configuration. 58 | validate: 59 | runs-on: ubuntu-latest 60 | name: Validate .github/workflows/build.yaml 61 | steps: 62 | - uses: actions/checkout@v3 63 | - uses: subosito/flutter-action@v2.4.0 64 | 65 | - name: dart tool/validate_actions.dart 66 | run: dart tool/validate_actions.dart 67 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | errors: 3 | # treat missing required parameters as a warning (not a hint) 4 | missing_required_param: warning 5 | # Allow having TODOs in the code 6 | todo: ignore 7 | # TODO(jamesderlin): Temporarily disable unnecessary null comparisons during 8 | # the transition to null-safety. 9 | unnecessary_null_comparison: ignore 10 | 11 | linter: 12 | rules: 13 | # these rules are documented on and in the same order as 14 | # the Dart Lint rules page to make maintenance easier 15 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 16 | - always_declare_return_types 17 | - annotate_overrides 18 | # TODO - avoid_as 19 | - avoid_empty_else 20 | - avoid_init_to_null 21 | - avoid_return_types_on_setters 22 | - await_only_futures 23 | - camel_case_types 24 | - cancel_subscriptions 25 | - close_sinks 26 | #- comment_references # TODO 27 | - constant_identifier_names 28 | - control_flow_in_finally 29 | - empty_catches 30 | - empty_constructor_bodies 31 | - empty_statements 32 | - hash_and_equals 33 | - implementation_imports 34 | - iterable_contains_unrelated_type 35 | - library_names 36 | - library_prefixes 37 | - list_remove_unrelated_type 38 | #- literal_only_boolean_expressions # https://github.com/dart-lang/linter/issues/453 39 | - non_constant_identifier_names 40 | - one_member_abstracts 41 | - only_throw_errors 42 | - overridden_fields 43 | - package_api_docs 44 | - package_names 45 | - package_prefixed_library_names 46 | - prefer_is_not_empty 47 | - slash_for_doc_comments 48 | #- sort_constructors_first 49 | - sort_unnamed_constructors_first 50 | - test_types_in_equals 51 | - throw_in_finally 52 | #- type_annotate_public_apis # Annoying for cases with obvious types. 53 | - type_init_formals 54 | - unawaited_futures 55 | - unnecessary_getters_setters 56 | - unnecessary_new 57 | - unrelated_type_equality_checks 58 | - valid_regexps 59 | -------------------------------------------------------------------------------- /packages/visibility_detector/lib/src/visibility_detector_controller.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/foundation.dart'; 8 | 9 | import 'render_visibility_detector.dart'; 10 | 11 | /// A [VisibilityDetectorController] is a singleton object that can perform 12 | /// actions and change configuration for all [VisibilityDetector] widgets. 13 | class VisibilityDetectorController { 14 | static final _instance = VisibilityDetectorController(); 15 | static VisibilityDetectorController get instance => _instance; 16 | 17 | /// The minimum amount of time to wait between firing batches of visibility 18 | /// callbacks. 19 | /// 20 | /// If set to [Duration.zero], callbacks instead will fire at the end of every 21 | /// frame. This is useful for automated tests. 22 | /// 23 | /// Changing [updateInterval] will not affect any pending callbacks. Clients 24 | /// should call [notifyNow] explicitly to flush them if desired. 25 | Duration updateInterval = const Duration(milliseconds: 500); 26 | 27 | /// Forces firing all pending visibility callbacks immediately. 28 | /// 29 | /// This might be desirable just prior to tearing down the widget tree (such 30 | /// as when switching views or when exiting the application). 31 | void notifyNow() => RenderVisibilityDetectorBase.notifyNow(); 32 | 33 | /// Forgets any pending visibility callbacks for the [VisibilityDetector] with 34 | /// the given [key]. 35 | /// 36 | /// If the widget gets attached/detached, the callback will be rescheduled. 37 | /// 38 | /// This method can be used to cancel timers after the [VisibilityDetector] 39 | /// has been detached to avoid pending timers in tests. 40 | void forget(Key key) => RenderVisibilityDetectorBase.forget(key); 41 | 42 | int? get debugUpdateCount { 43 | if (!kDebugMode) { 44 | return null; 45 | } 46 | return RenderVisibilityDetectorBase.debugUpdateCount; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | 10 | import 'trees/controller_usage.dart'; 11 | import 'trees/tree_from_json.dart'; 12 | 13 | void main() { 14 | runApp(Demo()); 15 | } 16 | 17 | class Demo extends StatelessWidget { 18 | @override 19 | Widget build(BuildContext context) { 20 | return MaterialApp( 21 | home: DefaultTabController( 22 | length: 2, 23 | child: Scaffold( 24 | appBar: AppBar( 25 | title: Text('flutter_simple_treeview Demo'), 26 | actions: [ 27 | TextButton( 28 | child: Text( 29 | "Source Code", 30 | style: TextStyle(color: Colors.white), 31 | ), 32 | onPressed: () async => await launchUrl( 33 | Uri.https('github.com', 34 | 'google/flutter.widgets/tree/master/packages/flutter_simple_treeview/example'), 35 | )), 36 | ], 37 | bottom: TabBar( 38 | tabs: [ 39 | Tab(text: "Tree Controller Usage"), 40 | Tab(text: "Tree From JSON"), 41 | ], 42 | ), 43 | ), 44 | body: TabBarView( 45 | children: [ 46 | buildBodyFrame(ControllerUsage()), 47 | buildBodyFrame(TreeFromJson()), 48 | ], 49 | ), 50 | ), 51 | ), 52 | ); 53 | } 54 | 55 | /// Adds scrolling and padding to the [content]. 56 | Widget buildBodyFrame(Widget content) { 57 | return SingleChildScrollView( 58 | child: SingleChildScrollView( 59 | scrollDirection: Axis.horizontal, 60 | child: Padding( 61 | padding: EdgeInsets.all(40), 62 | child: content, 63 | ), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/primitives/overlay_builder.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'dart:math'; 8 | 9 | import 'package:flutter/material.dart'; 10 | 11 | import 'overlay.dart'; 12 | 13 | /// Creates an overlay with top left corner 14 | /// in the middle of the [context]'s render box, 15 | /// if area size allows. 16 | OverlayEntry createOverlayInTheMiddle( 17 | Widget content, 18 | BuildContext context, 19 | OverlayStyle style, 20 | ) { 21 | RenderBox renderBox = context.findRenderObject() as RenderBox; 22 | var offset = renderBox.localToGlobal(Offset.zero); 23 | 24 | return OverlayEntry( 25 | builder: (context) => Positioned( 26 | left: _getOverlayPosition( 27 | target: offset.dx + renderBox.size.width / 2, 28 | overlaySize: style.width, 29 | areaSize: MediaQuery.of(context).size.width, 30 | ), 31 | top: _getOverlayPosition( 32 | target: offset.dy + renderBox.size.height / 2, 33 | overlaySize: style.height, 34 | areaSize: MediaQuery.of(context).size.height, 35 | ), 36 | child: applyOverlayStyle(style, content), 37 | ), 38 | ); 39 | } 40 | 41 | /// Applies overlay style to the provided overlay content. 42 | Widget applyOverlayStyle(OverlayStyle style, Widget child) { 43 | return Material( 44 | elevation: style.elevation, 45 | child: Container( 46 | width: style.width, 47 | height: style.height, 48 | margin: EdgeInsets.symmetric(horizontal: style.margin), 49 | child: child, 50 | ), 51 | ); 52 | } 53 | 54 | /// Calculates the overlay position for one dimension. 55 | /// 56 | /// The preferred position of the overlay is to place 57 | /// its top-left corner in the [target]. 58 | /// The overlay position will be adjusted, if necessary, 59 | /// to fit on the screen if possible. 60 | double _getOverlayPosition({ 61 | required double target, 62 | required double overlaySize, 63 | required double areaSize, 64 | }) { 65 | if (target + overlaySize <= areaSize) { 66 | return target; 67 | } 68 | return max(0, areaSize - overlaySize); 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/google/flutter.widgets/actions/workflows/build.yaml/badge.svg)](https://github.com/google/flutter.widgets/actions/workflows/build.yaml) 2 | 3 | ## Flutter widgets 4 | 5 | This repository contains the source code for various Flutter widgets that are 6 | developed by Google but not by the core Flutter team. 7 | 8 | The product and source code here are neither endorsed by Google nor the Flutter 9 | team. They are mainly made available as a reference. Support is best-effort. 10 | 11 | ## Packages 12 | 13 | Package | Description | Version 14 | --- | --- | --- 15 | [flutter_simple_treeview](packages/flutter_simple_treeview/) | A widget, that visualises a tree structure, where a node can be any widget. | [![pub package](https://img.shields.io/pub/v/flutter_simple_treeview.svg)](https://pub.dev/packages/flutter_simple_treeview) 16 | [linked_scroll_controller](packages/linked_scroll_controller/) | A scroll controller that allows two or more scroll views to be in sync. | [![pub package](https://img.shields.io/pub/v/linked_scroll_controller.svg)](https://pub.dev/packages/linked_scroll_controller) 17 | [scrollable_positioned_list](packages/scrollable_positioned_list/) | A list with helper methods to programmatically scroll to an item. | [![pub package](https://img.shields.io/pub/v/scrollable_positioned_list.svg)](https://pub.dev/packages/scrollable_positioned_list) 18 | [self_storing_input](packages/self_storing_input/) | A set of input widgets that automatically save and load the entered value to a data store. | [![pub package](https://img.shields.io/pub/v/self_storing_input.svg)](https://pub.dev/packages/self_storing_input) 19 | [visibility_detector](packages/visibility_detector/) | A widget that detects the visibility of its child and notifies a callback. | [![pub package](https://img.shields.io/pub/v/visibility_detector.svg)](https://pub.dev/packages/visibility_detector) 20 | 21 | ## Issues 22 | 23 | Please file any issues, bugs, or feature requests in the [this 24 | repo](https://github.com/google/flutter.widgets/issues/new). 25 | 26 | ## Contributing 27 | 28 | If you wish to contribute a change to any of the existing widgets in this repo, 29 | please review our [contribution guide](https://github.com/google/flutter.widgets/blob/master/CONTRIBUTING.md), 30 | and send a [pull request](https://github.com/google/flutter.widgets/pulls). 31 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/lib/src/node_widget.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import 'builder.dart'; 10 | import 'primitives/tree_controller.dart'; 11 | import 'primitives/tree_node.dart'; 12 | 13 | /// Widget that displays one [TreeNode] and its children. 14 | class NodeWidget extends StatefulWidget { 15 | final TreeNode treeNode; 16 | final double? indent; 17 | final double? iconSize; 18 | final TreeController state; 19 | 20 | const NodeWidget( 21 | {Key? key, 22 | required this.treeNode, 23 | this.indent, 24 | required this.state, 25 | this.iconSize}) 26 | : super(key: key); 27 | 28 | @override 29 | _NodeWidgetState createState() => _NodeWidgetState(); 30 | } 31 | 32 | class _NodeWidgetState extends State { 33 | bool get _isLeaf { 34 | return widget.treeNode.children == null || 35 | widget.treeNode.children!.isEmpty; 36 | } 37 | 38 | bool get _isExpanded { 39 | return widget.state.isNodeExpanded(widget.treeNode.key!); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | var icon = _isLeaf 45 | ? null 46 | : _isExpanded 47 | ? Icons.expand_more 48 | : Icons.chevron_right; 49 | 50 | var onIconPressed = _isLeaf 51 | ? null 52 | : () => setState( 53 | () => widget.state.toggleNodeExpanded(widget.treeNode.key!)); 54 | 55 | return Column( 56 | crossAxisAlignment: CrossAxisAlignment.start, 57 | children: [ 58 | Row( 59 | children: [ 60 | IconButton( 61 | iconSize: widget.iconSize ?? 24.0, 62 | icon: Icon(icon), 63 | onPressed: onIconPressed, 64 | ), 65 | widget.treeNode.content, 66 | ], 67 | ), 68 | if (_isExpanded && !_isLeaf) 69 | Padding( 70 | padding: EdgeInsets.only(left: widget.indent!), 71 | child: buildNodes(widget.treeNode.children!, widget.indent, 72 | widget.state, widget.iconSize), 73 | ) 74 | ], 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_radio_group/shared_state.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:self_storing_input/self_storing_input.dart'; 9 | 10 | /// State that needs to be shared between main widget and children. 11 | class SharedState with ChangeNotifier { 12 | final Saver saver; 13 | final Object itemKey; 14 | final SelfStoringRadioGroupStyle style; 15 | final OverlayController overlayController; 16 | final Object? defaultValue; 17 | 18 | /// If true, the radio buttons in the group can be unselected, 19 | /// returning to the state when user did not enter value yet. 20 | final bool isUnselectable; 21 | 22 | OverlayEntry? overlay; 23 | OperationResult operationResult = OperationResult.success(); 24 | Object? storedValue; 25 | 26 | Object? get selectedValue => _selectedValue; 27 | Object? _selectedValue; 28 | 29 | /// Value of the radio button, that caused the change of the value and 30 | /// triggered the saving operation. 31 | /// We need it to show the spinning wheel. 32 | Object? get pendingValue => _pendingValue; 33 | Object? _pendingValue; 34 | 35 | Object get isSaving => _isSaving; 36 | Object _isSaving = false; 37 | 38 | SharedState( 39 | this.defaultValue, 40 | this.overlayController, 41 | this.saver, 42 | this.itemKey, 43 | this.style, 44 | this.storedValue, 45 | this.isUnselectable, 46 | ) : _selectedValue = storedValue; 47 | 48 | /// Tries to save the [value], showing spinning wheal near [newPendingValue] 49 | /// while saving. 50 | Future select(Object value, bool selected) async { 51 | _selectedValue = selected ? value : defaultValue; 52 | _pendingValue = value; 53 | _isSaving = true; 54 | notifyListeners(); 55 | operationResult = await saver.save(itemKey, _selectedValue); 56 | if (operationResult.isSuccess) { 57 | storedValue = _selectedValue; 58 | } else { 59 | _selectedValue = storedValue; 60 | } 61 | _isSaving = false; 62 | notifyListeners(); 63 | } 64 | 65 | void closeOverlay() { 66 | overlay?.remove(); 67 | overlay = null; 68 | notifyListeners(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/src/item_positions_listener.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import 'item_positions_notifier.dart'; 8 | import 'scrollable_positioned_list.dart'; 9 | 10 | /// Provides a listenable iterable of [itemPositions] of items that are on 11 | /// screen and their locations. 12 | abstract class ItemPositionsListener { 13 | /// Creates an [ItemPositionsListener] that can be used by a 14 | /// [ScrollablePositionedList] to return the current position of items. 15 | factory ItemPositionsListener.create() => ItemPositionsNotifier(); 16 | 17 | /// The position of items that are at least partially visible in the viewport. 18 | ValueListenable> get itemPositions; 19 | } 20 | 21 | /// Position information for an item in the list. 22 | class ItemPosition { 23 | /// Create an [ItemPosition]. 24 | const ItemPosition( 25 | {required this.index, 26 | required this.itemLeadingEdge, 27 | required this.itemTrailingEdge}); 28 | 29 | /// Index of the item. 30 | final int index; 31 | 32 | /// Distance in proportion of the viewport's main axis length from the leading 33 | /// edge of the viewport to the leading edge of the item. 34 | /// 35 | /// May be negative if the item is partially visible. 36 | final double itemLeadingEdge; 37 | 38 | /// Distance in proportion of the viewport's main axis length from the leading 39 | /// edge of the viewport to the trailing edge of the item. 40 | /// 41 | /// May be greater than one if the item is partially visible. 42 | final double itemTrailingEdge; 43 | 44 | @override 45 | bool operator ==(dynamic other) { 46 | if (other.runtimeType != runtimeType) return false; 47 | final ItemPosition otherPosition = other; 48 | return otherPosition.index == index && 49 | otherPosition.itemLeadingEdge == itemLeadingEdge && 50 | otherPosition.itemTrailingEdge == itemTrailingEdge; 51 | } 52 | 53 | @override 54 | int get hashCode => 55 | 31 * (31 * (7 + index.hashCode) + itemLeadingEdge.hashCode) + 56 | itemTrailingEdge.hashCode; 57 | 58 | @override 59 | String toString() => 60 | 'ItemPosition(index: $index, itemLeadingEdge: $itemLeadingEdge, itemTrailingEdge: $itemTrailingEdge)'; 61 | } 62 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/README.md: -------------------------------------------------------------------------------- 1 | # scrollable_positioned_list 2 | 3 | A flutter list that allows scrolling to a specific item in the list. 4 | 5 | Also allows determining what items are currently visible. 6 | 7 | ## Usage 8 | 9 | A `ScrollablePositionedList` works much like the builder version of `ListView` 10 | except that the list can be scrolled or jumped to a specific item. 11 | 12 | ### Example 13 | 14 | A `ScrollablePositionedList` can be created with: 15 | 16 | ```dart 17 | final ItemScrollController itemScrollController = ItemScrollController(); 18 | final ScrollOffsetController scrollOffsetController = ScrollOffsetController(); 19 | final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); 20 | final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create() 21 | 22 | ScrollablePositionedList.builder( 23 | itemCount: 500, 24 | itemBuilder: (context, index) => Text('Item $index'), 25 | itemScrollController: itemScrollController, 26 | scrollOffsetController: scrollOffsetController, 27 | itemPositionsListener: itemPositionsListener, 28 | scrollOffsetListener: scrollOffsetListener, 29 | ); 30 | ``` 31 | 32 | One then can scroll to a particular item with: 33 | 34 | ```dart 35 | itemScrollController.scrollTo( 36 | index: 150, 37 | duration: Duration(seconds: 2), 38 | curve: Curves.easeInOutCubic); 39 | ``` 40 | 41 | or jump to a particular item with: 42 | 43 | ```dart 44 | itemScrollController.jumpTo(index: 150); 45 | ``` 46 | 47 | One can monitor what items are visible on screen with: 48 | 49 | ```dart 50 | itemPositionsListener.itemPositions.addListener(() => ...); 51 | ``` 52 | 53 | ### Experimental APIs (subject to bugs and changes) 54 | 55 | Changes in scroll position can be monitored with: 56 | 57 | ```dart 58 | scrollOffsetListener.changes.listen((event) => ...) 59 | ``` 60 | 61 | see `ScrollSum` in [this test](test/scroll_offset_listener_test.dart) for an example of how the current offset can be 62 | calculated from the stream of scroll change deltas. This feature is new and experimental. 63 | 64 | Changes in scroll position in pixels, relative to the current scroll position, can be made with: 65 | 66 | ```dart 67 | scrollOffsetController.animateScroll(offset: 100, duration: Duration(seconds: 1)); 68 | ``` 69 | 70 | A full example can be found in the example folder. 71 | 72 | -------------------------------------------------------------------------------- 73 | 74 | This is not an officially supported Google product. 75 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_radio_group/custom_radio.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:self_storing_input/src/primitives/message_overlay.dart'; 9 | 10 | import '../primitives/overlay_builder.dart'; 11 | import 'shared_state.dart'; 12 | 13 | /// Radio button that saves the radio group value using [state.saver] 14 | /// and can be unchecked. 15 | class CustomRadio extends StatelessWidget { 16 | final SharedState? state; 17 | final Object value; 18 | 19 | const CustomRadio(this.state, this.value); 20 | 21 | bool get isSelected => state!.selectedValue == value; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var isEnabled = !(state!.isSaving as bool) && state!.overlay == null; 26 | var iconSize = (Theme.of(context).iconTheme.size ?? 24); 27 | var icon = Icons.radio_button_unchecked; 28 | if (isSelected) { 29 | icon = state!.isUnselectable 30 | ? Icons.check_circle 31 | : Icons.radio_button_checked; 32 | } 33 | 34 | return GestureDetector( 35 | onTap: isEnabled ? () => _onTap(context) : null, 36 | child: Padding( 37 | padding: EdgeInsets.all(iconSize / 2), 38 | child: Icon( 39 | icon, 40 | color: isSelected && isEnabled 41 | ? Theme.of(context).colorScheme.secondary 42 | : Theme.of(context).disabledColor, 43 | ), 44 | ), 45 | ); 46 | } 47 | 48 | Future _onTap(BuildContext context) async { 49 | if (!state!.isUnselectable && isSelected) return; 50 | await state!.select(value, !isSelected); 51 | if (!state!.operationResult.isSuccess) _showOverlay(context); 52 | } 53 | 54 | void _showOverlay(BuildContext context) { 55 | state!.closeOverlay(); 56 | state!.overlay = _buildOverlay(context); 57 | Overlay.of(context).insert(state!.overlay!); 58 | } 59 | 60 | OverlayEntry _buildOverlay(BuildContext context) { 61 | return createOverlayInTheMiddle( 62 | MessageOverlay( 63 | message: state!.operationResult.error!, 64 | style: state!.style.overlayStyle, 65 | closeIconSize: state!.style.closeIconSize, 66 | overlayController: state!.overlayController, 67 | ), 68 | context, 69 | state!.style.overlayStyle, 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/src/scroll_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/gestures.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | 9 | import 'wrapping.dart'; 10 | import 'viewport.dart'; 11 | 12 | /// A version of [CustomScrollView] that allows does not constrict the extents 13 | /// to be within 0 and 1. See [CustomScrollView] for more information. 14 | class UnboundedCustomScrollView extends CustomScrollView { 15 | final bool _shrinkWrap; 16 | 17 | const UnboundedCustomScrollView({ 18 | Key? key, 19 | Axis scrollDirection = Axis.vertical, 20 | bool reverse = false, 21 | ScrollController? controller, 22 | bool? primary, 23 | ScrollPhysics? physics, 24 | bool shrinkWrap = false, 25 | Key? center, 26 | double anchor = 0.0, 27 | double? cacheExtent, 28 | List slivers = const [], 29 | int? semanticChildCount, 30 | DragStartBehavior dragStartBehavior = DragStartBehavior.start, 31 | }) : _shrinkWrap = shrinkWrap, 32 | _anchor = anchor, 33 | super( 34 | key: key, 35 | scrollDirection: scrollDirection, 36 | reverse: reverse, 37 | controller: controller, 38 | primary: primary, 39 | physics: physics, 40 | shrinkWrap: false, 41 | center: center, 42 | cacheExtent: cacheExtent, 43 | semanticChildCount: semanticChildCount, 44 | dragStartBehavior: dragStartBehavior, 45 | slivers: slivers, 46 | ); 47 | 48 | // [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so 49 | // we need our own version. 50 | final double _anchor; 51 | 52 | @override 53 | double get anchor => _anchor; 54 | 55 | /// Build the viewport. 56 | @override 57 | @protected 58 | Widget buildViewport( 59 | BuildContext context, 60 | ViewportOffset offset, 61 | AxisDirection axisDirection, 62 | List slivers, 63 | ) { 64 | if (_shrinkWrap) { 65 | return CustomShrinkWrappingViewport( 66 | axisDirection: axisDirection, 67 | offset: offset, 68 | slivers: slivers, 69 | cacheExtent: cacheExtent, 70 | center: center, 71 | anchor: anchor, 72 | ); 73 | } 74 | return UnboundedViewport( 75 | axisDirection: axisDirection, 76 | offset: offset, 77 | slivers: slivers, 78 | cacheExtent: cacheExtent, 79 | center: center, 80 | anchor: anchor, 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_text.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:self_storing_input/src/self_storing_text/edit_button.dart'; 9 | 10 | import 'primitives/overlay.dart'; 11 | import 'primitives/saver.dart'; 12 | import 'primitives/the_progress_indicator.dart'; 13 | import 'self_storing_text/self_storing_text_style.dart'; 14 | import 'self_storing_text/shared_state.dart'; 15 | 16 | /// A widget to enter and store single or multiline text. 17 | class SelfStoringText extends StatefulWidget { 18 | final Saver saver; 19 | 20 | /// Key of the item to be provided to [saver]. 21 | final K itemKey; 22 | final String emptyText; 23 | final OverlayController overlayController; 24 | final SelfStoringTextStyle style; 25 | 26 | SelfStoringText( 27 | this.itemKey, { 28 | Key? key, 29 | saver, 30 | this.emptyText = '--', 31 | overlayController, 32 | this.style = const SelfStoringTextStyle(), 33 | }) : overlayController = overlayController ?? OverlayController(), 34 | this.saver = saver ?? NoOpSaver(), 35 | super(key: key); 36 | 37 | @override 38 | _SelfStoringTextState createState() => _SelfStoringTextState(); 39 | } 40 | 41 | class _SelfStoringTextState extends State { 42 | late SharedState _state; 43 | bool _isLoading = true; 44 | 45 | @override 46 | void initState() { 47 | _loadValue(); 48 | super.initState(); 49 | } 50 | 51 | @override 52 | void dispose() { 53 | _state.removeListener(_onSharedStateChange); 54 | super.dispose(); 55 | } 56 | 57 | void _onSharedStateChange() => setState(() {}); 58 | 59 | Future _loadValue() async { 60 | var storedValue = await widget.saver.load(widget.itemKey); 61 | _state = SharedState( 62 | storedValue: storedValue, 63 | overlayController: widget.overlayController, 64 | saver: widget.saver, 65 | itemKey: widget.itemKey, 66 | style: widget.style, 67 | )..addListener(_onSharedStateChange); 68 | 69 | setState(() => _isLoading = false); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | if (_isLoading) { 75 | return theProgressIndicator; 76 | } 77 | 78 | var text = _state.storedValue; 79 | if (text == null || text.isEmpty) text = widget.emptyText; 80 | 81 | return Row( 82 | children: [ 83 | Flexible(child: Text(text)), 84 | EditButton(_state), 85 | ], 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/lib/trees/controller_usage.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'; 9 | 10 | /// Demonstrates how to change state of the tree in external event handlers, 11 | /// like button taps. 12 | class ControllerUsage extends StatefulWidget { 13 | @override 14 | _ControllerUsageState createState() => _ControllerUsageState(); 15 | } 16 | 17 | class _ControllerUsageState extends State { 18 | final Key _key = ValueKey(22); 19 | final TreeController _controller = TreeController(allNodesExpanded: true); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Column( 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | SizedBox( 27 | height: 300, 28 | width: 300, 29 | child: buildTree(), 30 | ), 31 | ElevatedButton( 32 | child: Text("Expand All"), 33 | onPressed: () => setState(() { 34 | _controller.expandAll(); 35 | }), 36 | ), 37 | ElevatedButton( 38 | child: Text("Collapse All"), 39 | onPressed: () => setState(() { 40 | _controller.collapseAll(); 41 | }), 42 | ), 43 | ElevatedButton( 44 | child: Text("Expand node 22"), 45 | onPressed: () => setState(() { 46 | _controller.expandNode(_key); 47 | }), 48 | ), 49 | ElevatedButton( 50 | child: Text("Collapse node 22"), 51 | onPressed: () => setState(() { 52 | _controller.collapseNode(_key); 53 | }), 54 | ), 55 | ], 56 | ); 57 | } 58 | 59 | Widget buildTree() { 60 | return TreeView( 61 | treeController: _controller, 62 | nodes: [ 63 | TreeNode(content: Text("node 1")), 64 | TreeNode( 65 | content: Icon(Icons.audiotrack), 66 | children: [ 67 | TreeNode(content: Text("node 21")), 68 | TreeNode( 69 | content: Text("node 22"), 70 | key: _key, 71 | children: [ 72 | TreeNode( 73 | content: Icon(Icons.sentiment_very_satisfied), 74 | ), 75 | ], 76 | ), 77 | TreeNode( 78 | content: Text("node 23"), 79 | ), 80 | ], 81 | ), 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/linked_scroll_controller/README.md: -------------------------------------------------------------------------------- 1 | # linked_scroll_controller 2 | 3 | This package provides a way to set up a set of scrollable widgets whose 4 | scrolling is synchronized. The set can be stable across the lifetime of the 5 | containing screen, or can change dynamically (for example, a vertically 6 | scrolling `ListView.builder()` whose items are Scrollables that scroll 7 | horizontally in unison). 8 | 9 | **If you add controllers dynamically, the corresponding scrollables must be 10 | given unique keys to avoid the scroll offset going out of sync.** 11 | 12 | # Example usage 13 | 14 | The code below sets up two side-by-side `ListView`s that scroll in unison. 15 | 16 | ```dart 17 | class LinkedScrollables extends StatefulWidget { 18 | @override 19 | _LinkedScrollablesState createState() => _LinkedScrollablesState(); 20 | } 21 | 22 | class _LinkedScrollablesState extends State { 23 | LinkedScrollControllerGroup _controllers; 24 | ScrollController _letters; 25 | ScrollController _numbers; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _controllers = LinkedScrollControllerGroup(); 31 | _letters = _controllers.addAndGet(); 32 | _numbers = _controllers.addAndGet(); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | _letters.dispose(); 38 | _numbers.dispose(); 39 | super.dispose(); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Directionality( 45 | textDirection: TextDirection.ltr, 46 | child: Row( 47 | children: [ 48 | Expanded( 49 | child: ListView( 50 | controller: _letters, 51 | children: [ 52 | _Tile('Hello A'), 53 | _Tile('Hello B'), 54 | _Tile('Hello C'), 55 | _Tile('Hello D'), 56 | _Tile('Hello E'), 57 | ], 58 | ), 59 | ), 60 | Expanded( 61 | child: ListView( 62 | controller: _numbers, 63 | children: [ 64 | _Tile('Hello 1'), 65 | _Tile('Hello 2'), 66 | _Tile('Hello 3'), 67 | _Tile('Hello 4'), 68 | _Tile('Hello 5'), 69 | ], 70 | ), 71 | ), 72 | ], 73 | ), 74 | ); 75 | } 76 | } 77 | 78 | class _Tile extends StatelessWidget { 79 | final String caption; 80 | 81 | _Tile(this.caption); 82 | 83 | @override 84 | Widget build(_) => Container( 85 | margin: const EdgeInsets.all(8.0), 86 | padding: const EdgeInsets.all(8.0), 87 | height: 250.0, 88 | child: Center(child: Text(caption)), 89 | ); 90 | } 91 | 92 | ``` 93 | -------------------------------------------------------------------------------- /packages/visibility_detector/example/test/unit_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | import 'package:visibility_detector_example/main.dart'; 10 | 11 | void main() { 12 | test('collate works', () { 13 | expect(collate(>[]).toList(), []); 14 | expect(collate([[]]).toList(), []); 15 | expect( 16 | collate([ 17 | [1] 18 | ]).toList(), 19 | [1]); 20 | expect( 21 | collate([ 22 | [1], 23 | [] 24 | ]).toList(), 25 | [1]); 26 | expect( 27 | collate([ 28 | [], 29 | [1] 30 | ]).toList(), 31 | [1]); 32 | expect( 33 | collate([ 34 | [1, 2] 35 | ]).toList(), 36 | [1, 2]); 37 | expect( 38 | collate([ 39 | [1, 2], 40 | [] 41 | ]).toList(), 42 | [1, 2]); 43 | expect( 44 | collate([ 45 | [], 46 | [1, 2] 47 | ]).toList(), 48 | [1, 2]); 49 | expect( 50 | collate([ 51 | [1], 52 | [2] 53 | ]).toList(), 54 | [1, 2]); 55 | expect( 56 | collate([ 57 | [], 58 | [1], 59 | [2] 60 | ]).toList(), 61 | [1, 2]); 62 | expect( 63 | collate([ 64 | [1], 65 | [], 66 | [2] 67 | ]).toList(), 68 | [1, 2]); 69 | expect( 70 | collate([ 71 | [1], 72 | [2], 73 | [] 74 | ]).toList(), 75 | [1, 2]); 76 | expect( 77 | collate([ 78 | [1], 79 | [2], 80 | [3] 81 | ]).toList(), 82 | [1, 2, 3]); 83 | expect( 84 | collate([ 85 | [1, 4], 86 | [2], 87 | [3] 88 | ]).toList(), 89 | [1, 2, 3, 4]); 90 | expect( 91 | collate([ 92 | [1], 93 | [2, 4], 94 | [3] 95 | ]).toList(), 96 | [1, 2, 3, 4]); 97 | expect( 98 | collate([ 99 | [1], 100 | [2], 101 | [3, 4] 102 | ]).toList(), 103 | [1, 2, 3, 4]); 104 | 105 | expect( 106 | collate([ 107 | [1, 4, 7], 108 | [2, 5, 8, 9], 109 | [3, 6] 110 | ]).toList(), 111 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 112 | ); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_checkbox/custom_checkbox.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import '../primitives/message_overlay.dart'; 10 | import '../primitives/overlay_builder.dart'; 11 | import 'shared_state.dart'; 12 | 13 | /// The checkbox that saves entered value and 14 | /// shows error message in case of failure. 15 | class CustomCheckbox extends StatefulWidget { 16 | final SharedState state; 17 | 18 | const CustomCheckbox(this.state); 19 | 20 | @override 21 | _CustomCheckboxState createState() => _CustomCheckboxState(); 22 | } 23 | 24 | class _CustomCheckboxState extends State { 25 | bool? _localValue; 26 | OverlayEntry? _overlay; 27 | 28 | @override 29 | void initState() { 30 | _localValue = widget.state.storedValue; 31 | widget.state.overlayController.addListener(_closeOverlay); 32 | super.initState(); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | widget.state.overlayController.removeListener(_closeOverlay); 38 | super.dispose(); 39 | } 40 | 41 | void _showOverlay() { 42 | widget.state.overlayController.close(); 43 | _overlay = _buildOverlay(context); 44 | Overlay.of(context).insert(_overlay!); 45 | } 46 | 47 | void _closeOverlay() { 48 | _overlay?.remove(); 49 | _overlay = null; 50 | setState(() {}); 51 | } 52 | 53 | OverlayEntry _buildOverlay(BuildContext context) { 54 | return createOverlayInTheMiddle( 55 | MessageOverlay( 56 | message: widget.state.operationResult.error!, 57 | style: widget.state.style.overlayStyle, 58 | closeIconSize: widget.state.style.closeIconSize, 59 | overlayController: widget.state.overlayController, 60 | ), 61 | context, 62 | widget.state.style.overlayStyle, 63 | ); 64 | } 65 | 66 | void _onValueChanged(bool? value) async { 67 | _localValue = value; 68 | widget.state.isSaving = true; 69 | setState(() {}); 70 | widget.state.operationResult = 71 | await widget.state.saver.save(widget.state.itemKey, _localValue); 72 | if (widget.state.operationResult.isSuccess) { 73 | widget.state.storedValue = _localValue; 74 | } else { 75 | _localValue = widget.state.storedValue; 76 | _showOverlay(); 77 | } 78 | widget.state.isSaving = false; 79 | setState(() {}); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | var isEnabled = !widget.state.isSaving && _overlay == null; 85 | 86 | return Checkbox( 87 | onChanged: isEnabled ? _onValueChanged : null, 88 | value: _localValue, 89 | tristate: widget.state.tristate, 90 | activeColor: Theme.of(context).colorScheme.secondary, 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/test/tree_view_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | /// Wraps widget to MaterialApp and pumps. 12 | Future _wrapAndPump(WidgetTester tester, Widget widget) async { 13 | var wrapped = MaterialApp( 14 | home: SingleChildScrollView( 15 | child: SingleChildScrollView( 16 | scrollDirection: Axis.horizontal, 17 | child: Card(child: widget), 18 | )), 19 | ); 20 | await tester.pumpWidget(wrapped); 21 | } 22 | 23 | void main() { 24 | group('TreeView', () { 25 | testWidgets('renders all nodes when expanded.', 26 | (WidgetTester tester) async { 27 | var widget = TreeView( 28 | nodes: [ 29 | TreeNode(content: Text('n0')), 30 | TreeNode(content: Text('n1')), 31 | TreeNode( 32 | content: Text('n2'), 33 | children: [ 34 | TreeNode(content: Text('n3')), 35 | TreeNode(content: Text('n4')), 36 | TreeNode( 37 | content: Text('n5'), 38 | children: [ 39 | TreeNode(content: Text('n6')), 40 | ], 41 | ), 42 | ], 43 | ), 44 | ], 45 | ); 46 | await _wrapAndPump(tester, widget); 47 | 48 | for (var i = 0; i < 7; i++) { 49 | expect( 50 | find.byWidgetPredicate( 51 | (widget) => widget is Text && widget.data == 'n$i'), 52 | findsOneWidget, 53 | ); 54 | } 55 | }); 56 | 57 | test('generates unique key if the key is null.', () { 58 | var tree = TreeView(nodes: [ 59 | TreeNode(), 60 | TreeNode( 61 | key: ValueKey(1), 62 | children: [TreeNode()], 63 | ) 64 | ]); 65 | 66 | expect(tree.nodes[0].key, isNotNull); 67 | expect(tree.nodes[1].key, ValueKey(1)); 68 | expect(tree.nodes[0].key == tree.nodes[1].children![0].key, false); 69 | }); 70 | 71 | test('throws if the key is duplicate.', () { 72 | var nodes = [TreeNode(key: ValueKey(1)), TreeNode(key: ValueKey(1))]; 73 | 74 | expect(() => TreeView(nodes: nodes), throwsA(isA())); 75 | }); 76 | 77 | test('has immutable node list.', () { 78 | try { 79 | TreeView(nodes: []).nodes.add(TreeNode()); 80 | } catch (e) { 81 | print(e.runtimeType); 82 | } 83 | expect( 84 | () => TreeView(nodes: []).nodes.add(TreeNode()), 85 | throwsA( 86 | predicate((dynamic e) => e.toString().contains('unmodifiable')))); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /packages/flutter_simple_treeview/example/lib/trees/tree_from_json.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'dart:convert'; 8 | 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'; 11 | 12 | /// Demonstrates how to convert a json content to tree, allowing user to 13 | /// modify the content and see how it affects the tree. 14 | class TreeFromJson extends StatefulWidget { 15 | @override 16 | _TreeFromJsonState createState() => _TreeFromJsonState(); 17 | } 18 | 19 | class _TreeFromJsonState extends State { 20 | final TreeController _treeController = 21 | TreeController(allNodesExpanded: false); 22 | final TextEditingController _textController = TextEditingController(text: ''' 23 | { 24 | "employee": { 25 | "name": "sonoo", 26 | "level": 56, 27 | "married": true, 28 | "hobby": null 29 | }, 30 | "week": [ 31 | "Sunday", 32 | "Monday", 33 | "Tuesday", 34 | "Wednesday", 35 | "Thursday", 36 | "Friday", 37 | "Saturday" 38 | ] 39 | } 40 | '''); 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Row( 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | SizedBox( 48 | height: 600, 49 | width: 400, 50 | child: TextField( 51 | maxLines: 10000, 52 | controller: _textController, 53 | decoration: InputDecoration(border: OutlineInputBorder()), 54 | style: TextStyle(fontFamily: "courier"), 55 | )), 56 | IconButton( 57 | icon: Icon(Icons.arrow_right), 58 | iconSize: 40, 59 | onPressed: () => setState(() {}), 60 | ), 61 | buildTree(), 62 | ], 63 | ); 64 | } 65 | 66 | /// Builds tree or error message out of the entered content. 67 | Widget buildTree() { 68 | try { 69 | var parsedJson = json.decode(_textController.text); 70 | return TreeView( 71 | nodes: toTreeNodes(parsedJson), 72 | treeController: _treeController, 73 | ); 74 | } on FormatException catch (e) { 75 | return Text(e.message); 76 | } 77 | } 78 | 79 | List toTreeNodes(dynamic parsedJson) { 80 | if (parsedJson is Map) { 81 | return parsedJson.keys 82 | .map((k) => TreeNode( 83 | content: Text('$k:'), children: toTreeNodes(parsedJson[k]))) 84 | .toList(); 85 | } 86 | if (parsedJson is List) { 87 | return parsedJson 88 | .asMap() 89 | .map((i, element) => MapEntry(i, 90 | TreeNode(content: Text('[$i]:'), children: toTreeNodes(element)))) 91 | .values 92 | .toList(); 93 | } 94 | return [TreeNode(content: Text(parsedJson.toString()))]; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Flutter Widgets 2 | =============================== 3 | 4 | _See also: [Flutter's code of conduct](https://flutter.io/design-principles/#code-of-conduct)_ 5 | 6 | Things you will need 7 | -------------------- 8 | 9 | * Linux, Mac OS X, or Windows. 10 | * git (used for source version control). 11 | * An ssh client (used to authenticate with GitHub). 12 | 13 | Getting the code and configuring your environment 14 | ------------------------------------------------- 15 | 16 | * Ensure all the dependencies described in the previous section are installed. 17 | * Fork `https://github.com/google/flutter.widgets` into your own GitHub account. 18 | If you already have a fork and are now installing a development environment on 19 | a new machine, make sure you've updated your fork so that you don't use stale 20 | configuration options from long ago. 21 | * If you haven't configured your machine with an SSH key that's known to github, then 22 | follow [GitHub's directions](https://help.github.com/articles/generating-ssh-keys/) 23 | to generate an SSH key. 24 | * `git clone git@github.com:/flutter.widgets.git` 25 | * `cd widgets` 26 | * `git remote add upstream git@github.com:google/flutter.widgets.git` 27 | (So that you fetch from the master repository, not your clone, when running 28 | `git fetch` et al.) 29 | 30 | Contributing code 31 | ----------------- 32 | 33 | We gladly accept contributions via GitHub pull requests. 34 | 35 | Please peruse Flutter's 36 | [style guide](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo) and 37 | [design principles](https://flutter.io/design-principles/) before 38 | working on anything non-trivial. These guidelines are intended to 39 | keep the code consistent and avoid common pitfalls. 40 | 41 | You must complete the 42 | [Contributor License Agreement](https://cla.developers.google.com/clas). You can 43 | do this online, and it takes only a minute. If you've never submitted code 44 | before, you must add your (or your organization's) name and contact info to the 45 | [AUTHORS](AUTHORS) file. 46 | 47 | To start working on a patch: 48 | 49 | * `git fetch upstream` 50 | * `git checkout upstream/master -b ` 51 | * Hack away. 52 | * `git commit -a -m ""` 53 | * `git push origin ` 54 | 55 | To send us a pull request: 56 | 57 | * `git pull-request` (if you are using [Hub](http://github.com/github/hub/)) or 58 | go to `https://github.com/google/flutter.widgets` and click the 59 | "Compare & pull request" button 60 | 61 | Please make sure all your checkins have detailed commit messages explaining 62 | what the patch does and *why*. **Changes to code behavior should include unit 63 | tests** that would fail without the change. 64 | 65 | Once you've gotten an LGTM from a project maintainer and once your PR has 66 | received the green light from all our automated testing (Travis, Appveyor, etc), 67 | one of the project maintainers will test the changes to our internal repo. This 68 | might cause test failures that need to be debugged internally so we might make 69 | further suggestions on your PR. 70 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/example/test/scrollable_positioned_list_example_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; 8 | 9 | import 'package:scrollable_positioned_list_example/main.dart'; 10 | 11 | void main() { 12 | setUp(() { 13 | WidgetsBinding.instance.renderView.configuration = 14 | TestViewConfiguration.fromView( 15 | size: const Size(800, 900), 16 | view: WidgetsBinding.instance.platformDispatcher.views.single, 17 | ); 18 | }); 19 | 20 | testWidgets('Start at 0', (WidgetTester tester) async { 21 | await tester.pumpWidget(MaterialApp(home: ScrollablePositionedListPage())); 22 | await tester.pump(); 23 | expect( 24 | tester.getTopLeft(item(0)).dy - 25 | tester.getTopLeft(find.byType(ScrollablePositionedList)).dy, 26 | 0); 27 | expect(find.text('First Item: 0'), findsOneWidget); 28 | }); 29 | 30 | testWidgets('Scroll Up a little', (WidgetTester tester) async { 31 | await tester.pumpWidget(MaterialApp(home: ScrollablePositionedListPage())); 32 | await tester.drag(find.byType(ScrollablePositionedListPage), Offset(0, -5)); 33 | await tester.pump(); 34 | await tester.pump(); 35 | expect(find.text('First Item: 0'), findsOneWidget); 36 | }); 37 | 38 | testWidgets('Scroll to 100', (WidgetTester tester) async { 39 | await tester.pumpWidget(MaterialApp(home: ScrollablePositionedListPage())); 40 | await tester.tap(find.byKey(const ValueKey('Scroll100'))); 41 | await tester.pumpAndSettle(); 42 | expect( 43 | tester.getTopLeft(item(100)).dy - 44 | tester.getTopLeft(find.byType(ScrollablePositionedList)).dy, 45 | 0); 46 | expect(find.text('First Item: 100'), findsOneWidget); 47 | }); 48 | 49 | testWidgets('Jump to 1000', (WidgetTester tester) async { 50 | await tester.pumpWidget(MaterialApp(home: ScrollablePositionedListPage())); 51 | await tester.tap(find.byKey(const ValueKey('Jump1000'))); 52 | await tester.pump(); 53 | await tester.pump(); 54 | expect( 55 | tester.getTopLeft(item(1000)).dy - 56 | tester.getTopLeft(find.byType(ScrollablePositionedList)).dy, 57 | 0); 58 | expect(find.text('First Item: 1000'), findsOneWidget); 59 | }); 60 | 61 | testWidgets('Scroll to 1000', (WidgetTester tester) async { 62 | await tester.pumpWidget(MaterialApp(home: ScrollablePositionedListPage())); 63 | await tester.tap(find.byKey(const ValueKey('Scroll1000'))); 64 | await tester.pumpAndSettle(); 65 | expect( 66 | tester.getTopLeft(item(1000)).dy - 67 | tester.getTopLeft(find.byType(ScrollablePositionedList)).dy, 68 | 0); 69 | expect(find.text('First Item: 1000'), findsOneWidget); 70 | }); 71 | } 72 | 73 | Finder item(int index) => find.ancestor( 74 | of: find.text('Item $index'), matching: find.byType(SizedBox)); 75 | -------------------------------------------------------------------------------- /packages/visibility_detector/test/unit_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/widgets.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import 'package:visibility_detector/visibility_detector.dart'; 11 | 12 | void _expectVisibility(Rect widgetBounds, Rect clipRect, 13 | Rect expectedVisibleBounds, double expectedVisibleFraction) { 14 | final info = VisibilityInfo.fromRects( 15 | key: UniqueKey(), widgetBounds: widgetBounds, clipRect: clipRect); 16 | expect(info.size, widgetBounds.size); 17 | expect(info.visibleBounds, expectedVisibleBounds); 18 | expect(info.visibleFraction, expectedVisibleFraction); 19 | } 20 | 21 | void main() { 22 | const clipRect = Rect.fromLTWH(100, 200, 300, 400); 23 | 24 | group('VisibilityInfo', () { 25 | test('Computes not visible', () { 26 | const widgetBounds = Rect.fromLTWH(15, 25, 10, 20); 27 | const expectedVisibleBounds = Rect.zero; 28 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 0); 29 | }); 30 | 31 | test('Computes fully visible', () { 32 | const widgetBounds = Rect.fromLTWH(115, 225, 10, 20); 33 | const expectedVisibleBounds = Rect.fromLTWH(0, 0, 10, 20); 34 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 1); 35 | }); 36 | 37 | test('Computes partially visible (1 edge offscreen)', () { 38 | const widgetBounds = Rect.fromLTWH(115, 195, 10, 20); 39 | const expectedVisibleBounds = Rect.fromLTWH(0, 5, 10, 15); 40 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 0.75); 41 | }); 42 | 43 | test('Computes partially visible (2 edges offscreen)', () { 44 | const widgetBounds = Rect.fromLTWH(99, 195, 10, 20); 45 | const expectedVisibleBounds = Rect.fromLTWH(1, 5, 9, 15); 46 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 0.675); 47 | }); 48 | 49 | test('Computes partially visible (3 edges offscreen)', () { 50 | const widgetBounds = Rect.fromLTWH(99, 195, 500, 20); 51 | const expectedVisibleBounds = Rect.fromLTWH(1, 5, 300, 15); 52 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 0.45); 53 | }); 54 | 55 | test('Computes partially visible (4 edges offscreen)', () { 56 | const widgetBounds = Rect.fromLTWH(99, 195, 500, 600); 57 | const expectedVisibleBounds = Rect.fromLTWH(1, 5, 300, 400); 58 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 0.4); 59 | }); 60 | 61 | test('Computes ~0% visibility as 0%', () { 62 | const widgetBounds = Rect.fromLTWH(100, 599, 300, 400); 63 | const expectedVisibleBounds = Rect.fromLTWH(0, 0, 300, 1); 64 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 0); 65 | }); 66 | 67 | test('Computes ~100% visibility as 100%', () { 68 | const widgetBounds = Rect.fromLTWH(100, 200, 300, 399); 69 | const expectedVisibleBounds = Rect.fromLTWH(0, 0, 300, 399); 70 | _expectVisibility(widgetBounds, clipRect, expectedVisibleBounds, 1); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /packages/visibility_detector/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.4.0+3 4 | 5 | * Correct minimum Dart SDK constraints to 2.14.0. 6 | * Add ignores for unnecessary casts due to upcoming changes in Flutter. 7 | 8 | ## 0.4.0+2 9 | 10 | * Fix a bug for updates to render objects that have not been laid out yet. 11 | 12 | ## 0.4.0+1 13 | 14 | * Correct Flutter SDK version dependency to 3.1.0. 15 | * Replace use of deprecated APIs in the example for compatibility with Flutter v3.1.0. 16 | 17 | ## 0.4.0 18 | 19 | * Refactor to avoid forcing composition in the layer/render trees. 20 | * Remove `VisibilityDetectorLayer`. 21 | * Add `RenderVisibilityDetectorBase` as a mixin that mostly takes over 22 | functionality from the old layer. 23 | * Remove the lookup map/method for getting former screen rects and instead add 24 | the rect to `VisibilityInfo`. 25 | 26 | ## 0.3.3 27 | 28 | * Re-apply Flutter framework bindings' null safety calls but set SDK 29 | constraints correctly to 2.12.0 instead. 30 | 31 | ## 0.3.2 32 | 33 | * Reverts change from 0.3.0 where the Flutter version constraint should have 34 | been set to 2.12.0 instead of 2.10.5. 35 | 36 | ## 0.3.1-dev 37 | 38 | * Populate the pubspec `repository` field. 39 | 40 | ## 0.3.0 41 | 42 | * Move to Flutter version 2.10.5 and update dependencies' null safety calls. 43 | 44 | ## 0.2.2 45 | 46 | * Minor internal changes to maintain forward-compatibility with [flutter#91753](https://github.com/flutter/flutter/pull/91753). 47 | 48 | ## 0.2.1 49 | 50 | * Bug fix for using VisibilityDetector with FittedBox and Transform.scale [issue #285](https://github.com/google/flutter.widgets/issues/285). 51 | 52 | ## 0.2.0 53 | 54 | * Added `SliverVisibilityDetector` to report visibility of `RenderSliver`-based 55 | widgets. Fixes [issue #174](https://github.com/google/flutter.widgets/issues/174). 56 | 57 | ## 0.2.0-nullsafety.1 58 | 59 | * Revert change to add `VisibilityDetectorController.scheduleNotification`, 60 | which introduced unexpected memory usage. 61 | 62 | ## 0.2.0-nullsafety.0 63 | 64 | * Update to null safety. 65 | 66 | * Try to fix the link to the example on pub.dev. 67 | 68 | * Revert tests to again use `RenderView` instead of `TestWindow`. 69 | 70 | * Add `VisibilityDetectorController.scheduleNotification` to force firing a 71 | visibility callback. 72 | 73 | ## 0.1.5 74 | 75 | * Compatibility fixes to `demo.dart` for Flutter 1.13.8. 76 | 77 | * Moved `demo.dart` to an `examples/` directory, renamed it, and added 78 | instructions to `README.md`. 79 | 80 | * Adjusted tests to use `TestWindow` instead of `RenderView`. 81 | 82 | * Added a "Known limitations" section to `README.md`. 83 | 84 | ## 0.1.4 85 | 86 | * Style and comment adjustments. 87 | 88 | * Fix a potential infinite loop in the demo app and add tests for it. 89 | 90 | ## 0.1.3 91 | 92 | * Fixed positioning of text selection handles for `EditableText`-based 93 | widgets (e.g. `TextField`, `CupertinoTextField`) when used within a 94 | `VisibilityDetector`. 95 | 96 | * Added `VisibilityDetectorController.widgetBoundsFor`. 97 | 98 | ## 0.1.2 99 | 100 | * Compatibility fixes for Flutter 1.3.0. 101 | 102 | ## 0.1.1 103 | 104 | * Added `VisibilityDetectorController.forget`. 105 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_checkbox.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:self_storing_input/src/self_storing_checkbox/shared_state.dart'; 9 | 10 | import 'primitives/overlay.dart'; 11 | import 'primitives/saver.dart'; 12 | import 'primitives/the_progress_indicator.dart'; 13 | import 'self_storing_checkbox/custom_checkbox.dart'; 14 | import 'self_storing_checkbox/self_storing_checkbox_style.dart'; 15 | 16 | /// A widget to enter and store a boolean value. 17 | class SelfStoringCheckbox extends StatefulWidget { 18 | /// [Saver.validate] will not be invoked for [SelfStoringCheckbox]. 19 | final Saver saver; 20 | 21 | /// Key of the item to be provided to [saver]. 22 | final K itemKey; 23 | final OverlayController overlayController; 24 | final SelfStoringCheckboxStyle style; 25 | 26 | /// If true this checkbox's value can be any one of true, false, or null. 27 | /// 28 | /// If tristate is false, a null value for this checkbox 29 | /// will be interpreted as false. 30 | /// See [Checkbox.tristate] for more. 31 | final bool tristate; 32 | 33 | /// The same as [CheckboxListTile.title]. 34 | final Widget title; 35 | 36 | SelfStoringCheckbox( 37 | this.itemKey, { 38 | Key? key, 39 | saver, 40 | overlayController, 41 | this.tristate = true, 42 | Widget? title, 43 | this.style = const SelfStoringCheckboxStyle(), 44 | }) : overlayController = overlayController ?? OverlayController(), 45 | this.title = title ?? Container(width: 0, height: 0), 46 | this.saver = saver ?? NoOpSaver(), 47 | super(key: key); 48 | 49 | @override 50 | _SelfStoringCheckboxState createState() => _SelfStoringCheckboxState(); 51 | } 52 | 53 | class _SelfStoringCheckboxState extends State { 54 | bool _isLoading = true; 55 | late SharedState _state; 56 | 57 | @override 58 | void initState() { 59 | _loadValue(); 60 | super.initState(); 61 | } 62 | 63 | @override 64 | void dispose() { 65 | _state.removeListener(_onSharedStateChange); 66 | super.dispose(); 67 | } 68 | 69 | void _onSharedStateChange() { 70 | setState(() {}); 71 | } 72 | 73 | Future _loadValue() async { 74 | var storedValue = await widget.saver.load(widget.itemKey); 75 | if (storedValue == null && !widget.tristate) storedValue = false; 76 | _state = SharedState( 77 | saver: widget.saver, 78 | itemKey: widget.itemKey, 79 | overlayController: widget.overlayController, 80 | style: widget.style, 81 | tristate: widget.tristate, 82 | storedValue: storedValue, 83 | )..addListener(_onSharedStateChange); 84 | _isLoading = false; 85 | setState(() => {}); 86 | } 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | if (_isLoading) return theProgressIndicator; 91 | 92 | return Row( 93 | children: [ 94 | CustomCheckbox(_state), 95 | widget.title, 96 | if (_state.isSaving) theProgressIndicator, 97 | ], 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/visibility_detector/test/text_handle_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:visibility_detector/visibility_detector.dart'; 4 | 5 | void main() { 6 | final visibilityDetectorKey = UniqueKey(); 7 | 8 | // Disable VisibilityDetector timers. 9 | VisibilityDetectorController.instance.updateInterval = Duration.zero; 10 | 11 | testWidgets( 12 | 'VisibilityDetector does not affect the positioning of text selection ' 13 | 'handles', 14 | (WidgetTester tester) async { 15 | final expectedSelectionHandles = await _setUpSelectionHandles( 16 | tester, 17 | null, 18 | ); 19 | 20 | final actualSelectionHandles = await _setUpSelectionHandles( 21 | tester, 22 | (textField) => VisibilityDetector( 23 | key: visibilityDetectorKey, 24 | onVisibilityChanged: (visibilityInfo) {}, 25 | child: textField, 26 | ), 27 | ); 28 | 29 | expect(actualSelectionHandles, expectedSelectionHandles); 30 | }, 31 | ); 32 | } 33 | 34 | /// Sets up a [TextField] with sample text and a sample selection. 35 | /// 36 | /// [wrapTextField] is a function that takes the [TextField] as an argument and 37 | /// returns it wrapped in another widget. If [wrapTextField] is null, the 38 | /// [TextField] will be added to the widget tree directly. 39 | /// 40 | /// Returns the bounding rectangles (in global coordinates). The returned 41 | /// [List] will have 3 elements: 42 | /// 0: The [Rect] of the [TextField]. 43 | /// 1: The [Rect] of the starting selection handle. 44 | /// 2: The [Rect] of the ending selection handle. 45 | Future> _setUpSelectionHandles( 46 | WidgetTester tester, 47 | Widget Function(Widget)? wrapTextField, 48 | ) async { 49 | final textController = TextEditingController() 50 | ..text = 'The five boxing wizards jump quickly'; 51 | final textField = TextField(controller: textController); 52 | 53 | await tester.pumpWidget( 54 | MaterialApp( 55 | home: Scaffold( 56 | appBar: AppBar(), 57 | body: (wrapTextField ?? (x) => x)(textField), 58 | ), 59 | ), 60 | ); 61 | 62 | // The [TextField] must be focused first. Otherwise any changes we make 63 | // to the selection will be lost when it gains focus. 64 | final textFieldFinder = find.byType(TextField); 65 | await tester.tap(textFieldFinder); 66 | 67 | const selection = TextSelection(baseOffset: 4, extentOffset: 10); 68 | textController.selection = selection; 69 | await tester.pumpAndSettle(); 70 | 71 | final state = tester.state(find.byType(EditableText)); 72 | expect(state.selectionOverlay!.handlesAreVisible, true); 73 | 74 | // Find the text selection handles (via [CustomPaint] widgets within 75 | // [CompositedTransformFollower] parents). 76 | const handleCount = 2; 77 | final selectionHandles = find.descendant( 78 | of: find.byType(CompositedTransformFollower), 79 | matching: find.byType(CustomPaint), 80 | ); 81 | expect(selectionHandles, findsNWidgets(handleCount)); 82 | 83 | final selectionHandleRects = [ 84 | for (var i = 0; i < handleCount; i += 1) 85 | tester.getRect(selectionHandles.at(i)), 86 | ]; 87 | 88 | expect(selectionHandleRects[0], isNot(equals(selectionHandleRects[1]))); 89 | return [tester.getRect(textFieldFinder), ...selectionHandleRects]; 90 | } 91 | -------------------------------------------------------------------------------- /packages/visibility_detector/README.md: -------------------------------------------------------------------------------- 1 | [![pub package](https://img.shields.io/pub/v/visibility_detector.svg)](https://pub.dev/packages/visibility_detector) 2 | 3 | ## VisibilityDetector 4 | 5 | A `VisibilityDetector` widget wraps an existing Flutter widget and fires a 6 | callback when the widget's visibility changes. (It actually reports when the 7 | visibility of the `VisibilityDetector` itself changes, and its visibility is 8 | expected to be identical to that of its child.) 9 | 10 | Callbacks are not fired immediately on visibility changes. Instead, callbacks 11 | are deferred and coalesced such that the callback for each `VisibilityDetector` 12 | will be invoked at most once per `VisibilityDetectorController.updateInterval` 13 | (unless forced by `VisibilityDetectorController.notifyNow()`). Callbacks for 14 | *all* `VisibilityDetector` widgets are fired together synchronously between 15 | frames. 16 | 17 | `VisibilityDetectorController.notifyNow()` may be used to force triggering 18 | pending visibility callbacks; this might be desirable just prior to tearing down 19 | the widget tree (such as when switching views or when exiting the application). 20 | 21 | For more details, see the documentation to the `VisibilityDetector`, 22 | `VisibilityInfo`, and `VisibilityDetectorController` classes. 23 | 24 | 25 | ## Example usage 26 | 27 | ```dart 28 | @override 29 | Widget build(BuildContext context) { 30 | return VisibilityDetector( 31 | key: Key('my-widget-key'), 32 | onVisibilityChanged: (visibilityInfo) { 33 | var visiblePercentage = visibilityInfo.visibleFraction * 100; 34 | debugPrint( 35 | 'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible'); 36 | }, 37 | child: someOtherWidget, 38 | ); 39 | } 40 | ``` 41 | 42 | See the `example/` directory for a sample application. To build it, first 43 | create the default Flutter project files: 44 | 45 | ```shell 46 | cd example 47 | flutter create . 48 | ``` 49 | and then it can be run with `flutter run`. 50 | 51 | 52 | ## Widget tests 53 | 54 | Widget tests that use `VisibilityDetector`s usually should set: 55 | 56 | ```dart 57 | VisibilityDetectorController.instance.updateInterval = Duration.zero; 58 | ``` 59 | 60 | This will have two effects: 61 | 62 | 1. Visibility changes will be reported immediately, which can be less surprising 63 | for automated tests. 64 | 65 | 2. It avoids the following assertion when tearing down the widget tree: 66 | 67 | > The following assertion was thrown running a test: \ 68 | > A Timer is still pending even after the widget tree was disposed. 69 | 70 | See https://github.com/flutter/flutter/issues/24166 for details. 71 | 72 | If setting `updateInterval = Duration.zero` is undesirable, to address each of 73 | the corresponding issues above, tests alternatively can: 74 | 75 | 1. Wait sufficiently long for callbacks to fire: 76 | 77 | ```dart 78 | await tester.pump(VisibilityDetectorController.instance.updateInterval); 79 | ``` 80 | 81 | 2. Avoid the "Timer is still pending..." assertion by explicitly destroying the 82 | widget tree before the test completes: 83 | 84 | ```dart 85 | await tester.pumpWidget(Placeholder()); 86 | ``` 87 | 88 | See `test/widget_test.dart` for examples. 89 | 90 | 91 | ## Known limitations 92 | 93 | * `VisibilityDetector` considers only its bounding box. It does not take 94 | widget opacity into account. 95 | 96 | * The reported `visibleFraction` might not account for overlapping widgets that 97 | obscure the `VisbilityDetector`. 98 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/lib/src/element_registry.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// A registry to track some [Element]s in the tree. 8 | class RegistryWidget extends StatefulWidget { 9 | /// Creates a [RegistryWidget]. 10 | const RegistryWidget({Key? key, this.elementNotifier, required this.child}) 11 | : super(key: key); 12 | 13 | /// The widget below this widget in the tree. 14 | final Widget child; 15 | 16 | /// Contains the current set of all [Element]s created by 17 | /// [RegisteredElementWidget]s in the tree below this widget. 18 | /// 19 | /// Note that if there is another [RegistryWidget] in this widget's subtree 20 | /// that registry, and not this one, will collect elements in its subtree. 21 | final ValueNotifier?>? elementNotifier; 22 | 23 | @override 24 | State createState() => _RegistryWidgetState(); 25 | } 26 | 27 | /// A widget whose [Element] will be added its nearest ancestor 28 | /// [RegistryWidget]. 29 | class RegisteredElementWidget extends ProxyWidget { 30 | /// Creates a [RegisteredElementWidget]. 31 | const RegisteredElementWidget({Key? key, required Widget child}) 32 | : super(key: key, child: child); 33 | 34 | @override 35 | Element createElement() => _RegisteredElement(this); 36 | } 37 | 38 | class _RegistryWidgetState extends State { 39 | final Set registeredElements = {}; 40 | 41 | @override 42 | Widget build(BuildContext context) => _InheritedRegistryWidget( 43 | state: this, 44 | child: widget.child, 45 | ); 46 | } 47 | 48 | class _InheritedRegistryWidget extends InheritedWidget { 49 | final _RegistryWidgetState state; 50 | 51 | const _InheritedRegistryWidget( 52 | {Key? key, required this.state, required Widget child}) 53 | : super(key: key, child: child); 54 | 55 | @override 56 | bool updateShouldNotify(InheritedWidget oldWidget) => true; 57 | } 58 | 59 | class _RegisteredElement extends ProxyElement { 60 | _RegisteredElement(ProxyWidget widget) : super(widget); 61 | 62 | @override 63 | void notifyClients(ProxyWidget oldWidget) {} 64 | 65 | late _RegistryWidgetState _registryWidgetState; 66 | 67 | @override 68 | void mount(Element? parent, dynamic newSlot) { 69 | super.mount(parent, newSlot); 70 | final _inheritedRegistryWidget = 71 | dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; 72 | _registryWidgetState = _inheritedRegistryWidget.state; 73 | _registryWidgetState.registeredElements.add(this); 74 | _registryWidgetState.widget.elementNotifier?.value = 75 | _registryWidgetState.registeredElements; 76 | } 77 | 78 | @override 79 | void didChangeDependencies() { 80 | super.didChangeDependencies(); 81 | final _inheritedRegistryWidget = 82 | dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; 83 | _registryWidgetState = _inheritedRegistryWidget.state; 84 | _registryWidgetState.registeredElements.add(this); 85 | _registryWidgetState.widget.elementNotifier?.value = 86 | _registryWidgetState.registeredElements; 87 | } 88 | 89 | @override 90 | void unmount() { 91 | _registryWidgetState.registeredElements.remove(this); 92 | _registryWidgetState.widget.elementNotifier?.value = 93 | _registryWidgetState.registeredElements; 94 | super.unmount(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_radio_group.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:self_storing_input/src/self_storing_radio_group/custom_radio.dart'; 3 | import 'package:self_storing_input/src/self_storing_radio_group/shared_state.dart'; 4 | 5 | import 'primitives/overlay.dart'; 6 | import 'primitives/saver.dart'; 7 | import 'primitives/the_progress_indicator.dart'; 8 | import 'self_storing_radio_group/self_storing_radio_group_style.dart'; 9 | 10 | /// A widget to enter and store a boolean value. 11 | class SelfStoringRadioGroup extends StatefulWidget { 12 | /// [Saver.validate] will not be invoked for [SelfStoringRadioGroup]. 13 | final Saver saver; 14 | 15 | /// Key of the item to be provided to [saver]. 16 | final K itemKey; 17 | final OverlayController overlayController; 18 | final SelfStoringRadioGroupStyle style; 19 | 20 | /// Value to use if the loaded value is not in the list of values or 21 | /// if all values are unchecked. 22 | final Object? defaultValue; 23 | 24 | /// If true, the radio buttons in the group can be unselected, 25 | /// returning to the state when user did not enter a value yet. 26 | final bool isUnselectable; 27 | 28 | /// Map . 29 | final Map items; 30 | 31 | SelfStoringRadioGroup( 32 | this.itemKey, { 33 | Key? key, 34 | saver, 35 | overlayController, 36 | this.style = const SelfStoringRadioGroupStyle(), 37 | required this.items, 38 | this.defaultValue, 39 | this.isUnselectable = false, 40 | }) : overlayController = overlayController ?? OverlayController(), 41 | this.saver = saver ?? NoOpSaver(), 42 | super(key: key); 43 | 44 | @override 45 | _SelfStoringRadioGroupState createState() => _SelfStoringRadioGroupState(); 46 | } 47 | 48 | class _SelfStoringRadioGroupState extends State { 49 | bool _isLoading = true; 50 | late SharedState _state; 51 | 52 | @override 53 | void initState() { 54 | _loadValue(); 55 | super.initState(); 56 | } 57 | 58 | @override 59 | void dispose() { 60 | _state.removeListener(_emptySetState); 61 | widget.overlayController.removeListener(_state.closeOverlay); 62 | super.dispose(); 63 | } 64 | 65 | void _emptySetState() => setState(() {}); 66 | 67 | Future _loadValue() async { 68 | Object? storedValue = await widget.saver.load(widget.itemKey); 69 | if (!widget.items.containsKey(storedValue)) 70 | storedValue = widget.defaultValue; 71 | 72 | _isLoading = false; 73 | _state = SharedState( 74 | widget.defaultValue, 75 | widget.overlayController, 76 | widget.saver, 77 | widget.itemKey, 78 | widget.style, 79 | storedValue, 80 | widget.isUnselectable, 81 | )..addListener(_emptySetState); 82 | widget.overlayController.addListener(_state.closeOverlay); 83 | 84 | setState(() => {}); 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | if (_isLoading) return theProgressIndicator; 90 | 91 | List elements = widget.items.keys 92 | .map((v) => _buildRadioButton(v, widget.items[v] ?? '')) 93 | .toList(growable: false); 94 | return Column( 95 | children: elements, 96 | ); 97 | } 98 | 99 | Widget _buildRadioButton(Object value, String name) { 100 | return Row( 101 | children: [ 102 | CustomRadio(_state, value), 103 | Flexible(child: Text(name)), 104 | if (_state.isSaving as bool && _state.pendingValue == value) 105 | theProgressIndicator, 106 | ], 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.8+1 2 | * Migrate tests off deprecated APIs. 3 | * Bump min Flutter version to 3.1.0. 4 | 5 | # 0.3.8 6 | * Add ScrollOffsetController to allow pixel-based changes in offset. 7 | * Bump min sdk version to 2.15.0 8 | 9 | # 0.3.7 10 | * Add ScrollOffsetListener to allow listening to changes in scroll offset. 11 | 12 | # 0.3.6 13 | * Fix cache extents for horizontal lists 14 | * scrollTo future doesn't complete until scrolling is done. 15 | 16 | # 0.3.5 17 | * Fix extraneous animation controller declaration in 0.3.4. 18 | 19 | # 0.3.4 20 | * Disposed the animation controller when disposing the scrollable list. 21 | 22 | # 0.3.3 23 | * Fix potential crash when reading from RenderBox.size. 24 | 25 | # 0.3.2 26 | * Re-apply Flutter framework bindings' null safety calls but set SDK 27 | constraints correctly to 2.12.0 instead. 28 | 29 | # 0.3.1 30 | * Reverts change from 0.3.0 where the Flutter version constraint should have 31 | been set to 2.12.0 instead of 2.10.5. 32 | 33 | # 0.3.0 34 | * Move to Flutter version 2.10.5 and update dependencies' null safety calls. 35 | 36 | # 0.2.3 37 | * Support shrink wrap. 38 | # 0.2.2 39 | * Move dependencies from pre-release versions to released versions. 40 | 41 | # 0.2.1 42 | * Fix crash on NaN or infinite offset. 43 | 44 | # 0.2.0-nullsafety.0 45 | * Update to null safety. 46 | 47 | # 0.1.10 48 | * Update the home page URL to fix 49 | [issue #190](https://github.com/google/flutter.widgets/issues/190). 50 | * Miscellaneous tweaks to the example. 51 | * Added documentation to address 52 | [issue #96](https://github.com/google/flutter.widgets/issues/96). 53 | * Miscellaneous other cleanup. 54 | * Restructured `_ScrollablePositionedListState` to try to simplify logic. 55 | * Fixed an issue with `ItemScrollController.scrollTo` where it could scroll to 56 | the wrong item if a non-zero `alignment` was specified and if the list was 57 | manually scrolled by dragging. 58 | 59 | # 0.1.9 60 | * Fixed the example in `README.md`. Fixes 61 | [issue #191](https://github.com/google/flutter.widgets/issues/191). 62 | * Made the example runnable with `flutter run`. Fixes 63 | [issue #211](https://github.com/google/flutter.widgets/issues/211). 64 | * Updates to computation of semantic clip. 65 | * Smoother transition between views on long scrolls. 66 | * New controls over transition between views on long scrolls. 67 | 68 | # 0.1.8 69 | * Set updateScheduled to false when short circuiting due to empty list. 70 | To fix https://github.com/google/flutter.widgets/issues/182. 71 | 72 | # 0.1.7 73 | * Apply viewport dimensions in UnboundedRenderedViewport.performResize. 74 | To work around change in https://github.com/flutter/flutter/pull/61973 75 | causing breakage 76 | 77 | # 0.1.6 78 | * Change to do local scroll (without a fade) whenever target item is found 79 | within the cache. 80 | * Added sdk constraints to the example. 81 | * Moved `itemScrollControllerDetachment` to 82 | `_ScrollablePositionedListState.deactivate`. 83 | 84 | # 0.1.5 85 | 86 | * Added minCacheExtent to ScrollablePositionedList 87 | * Fixes the issue when item count updated from zero to one and `index` in 88 | `itemBuilder` becomes `-1`. Fixes 89 | [issue #104](https://github.com/google/flutter.widgets/issues/104). 90 | 91 | # 0.1.4 92 | 93 | * itemBuilders should not be called with indices > itemCount - 1. Fixes 94 | [issue #42](https://github.com/google/flutter.widgets/issues/42) and 95 | [issue #77](https://github.com/google/flutter.widgets/issues/77). 96 | 97 | # 0.1.3 98 | 99 | * Don't build items when `itemCount` is 0. Fixes 100 | [issue #78](https://github.com/google/flutter.widgets/issues/78). 101 | 102 | * Fix typos in `README.md`. 103 | 104 | # 0.1.2 105 | 106 | * Store scroll state in page storage to fix 107 | [issue #43](https://github.com/google/flutter.widgets/issues/43). 108 | 109 | # 0.1.1 110 | 111 | * Fix padding for horizontal lists. 112 | 113 | * Add `ScrollablePositionedList.separated` constructor to complete 114 | [issue #34](https://github.com/google/flutter.widgets/issues/34). 115 | 116 | * Add `isAttached` method to `ItemScrollController`. 117 | 118 | # 0.1.0 119 | 120 | * Properly bound `ScrollablePositionedList` to fix 121 | [issue #23](https://github.com/google/flutter.widgets/issues/23). 122 | 123 | * Allow `ScrollablePositionedList` alignment outside `[0..1]` to fix 124 | [issue #31](https://github.com/google/flutter.widgets/issues/31). 125 | 126 | * Moved `ScrollablePositionedList` example into `example` subdirectory. 127 | 128 | # 0.0.1 129 | 130 | * Added `ScrollablePositionedList`. 131 | -------------------------------------------------------------------------------- /packages/self_storing_input/lib/src/self_storing_text/overlay_box.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import '../primitives/operation_result.dart'; 10 | import '../primitives/the_progress_indicator.dart'; 11 | import 'shared_state.dart'; 12 | 13 | const Key okButtonKey = ValueKey('okButton'); 14 | const Key cancelButtonKey = ValueKey('cancelButton'); 15 | const Key clearButtonKey = ValueKey('clearButton'); 16 | 17 | /// The panel that pops up, when user clicks 'Edit'. 18 | class OverlayBox extends StatefulWidget { 19 | final SharedState sharedState; 20 | 21 | const OverlayBox(this.sharedState); 22 | 23 | @override 24 | _OverlayBoxState createState() => _OverlayBoxState(); 25 | } 26 | 27 | class _OverlayBoxState extends State { 28 | final TextEditingController _textController = TextEditingController(); 29 | final FocusNode _focusNode = FocusNode()..requestFocus(); 30 | bool _isSaving = false; 31 | String? _validationError; 32 | String? _savingError; 33 | 34 | @override 35 | void initState() { 36 | _textController.text = widget.sharedState.storedValue ?? ''; 37 | _textController.addListener(_onTextChange); 38 | super.initState(); 39 | } 40 | 41 | @override 42 | void dispose() { 43 | _textController.removeListener(_onTextChange); 44 | super.dispose(); 45 | } 46 | 47 | @override 48 | void setState(fn) { 49 | // This check takes care of "setState() called after dispose()" exception. 50 | // See "https://github.com/Norbert515/flutter_villains/issues/8". 51 | if (mounted) { 52 | super.setState(fn); 53 | } 54 | } 55 | 56 | /// This method is invoked when user types. 57 | void _onTextChange() { 58 | _savingError = null; 59 | _validationError = widget.sharedState.saver 60 | .validate(widget.sharedState.itemKey, _textController.text) 61 | .error; 62 | setState(() {}); 63 | } 64 | 65 | /// This method is invoked when user clicked 'Save'. 66 | Future _saveEnteredValue() async { 67 | String? value = _textController.text; 68 | // We cannot differentiate empty string and null, 69 | // so we always save null for consistency. 70 | if (value == '') value = null; 71 | 72 | if (value == widget.sharedState.storedValue) { 73 | return OperationResult.success(); 74 | } 75 | 76 | var operationResult = 77 | await widget.sharedState.saver.save(widget.sharedState.itemKey, value); 78 | 79 | if (operationResult.isSuccess) { 80 | widget.sharedState.storedValue = value; 81 | } 82 | 83 | return operationResult; 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return _isSaving ? theProgressIndicator : buildContent(context); 89 | } 90 | 91 | Widget buildContent(BuildContext context) { 92 | var error = _validationError ?? _savingError; 93 | 94 | return Column( 95 | mainAxisAlignment: MainAxisAlignment.center, 96 | children: [ 97 | Flexible( 98 | child: Align( 99 | alignment: Alignment.centerLeft, 100 | child: TextFormField( 101 | controller: _textController, 102 | focusNode: _focusNode, 103 | maxLines: widget.sharedState.style.maxLines, 104 | keyboardType: widget.sharedState.style.keyboardType, 105 | decoration: InputDecoration( 106 | suffixIcon: IconButton( 107 | key: clearButtonKey, 108 | icon: Icon(Icons.clear), 109 | onPressed: () { 110 | _textController.text = ''; 111 | }, 112 | ), 113 | ), 114 | ), 115 | ), 116 | ), 117 | Row( 118 | children: [ 119 | _buildOkButton(), 120 | _buildCancelButton(), 121 | if (error != null) _buildErrorWidget(error, context), 122 | ], 123 | ), 124 | ], 125 | ); 126 | } 127 | 128 | Widget _buildCancelButton() { 129 | return TextButton( 130 | key: cancelButtonKey, 131 | onPressed: () { 132 | widget.sharedState.overlayController.close(); 133 | _textController.text = widget.sharedState.storedValue ?? ''; 134 | }, 135 | child: Icon(Icons.close), 136 | ); 137 | } 138 | 139 | Widget _buildOkButton() { 140 | return TextButton( 141 | key: okButtonKey, 142 | onPressed: _validationError != null 143 | ? null 144 | : () async { 145 | _savingError = null; 146 | _isSaving = true; 147 | setState(() {}); 148 | var savingResult = await _saveEnteredValue(); 149 | _savingError = savingResult.error; 150 | if (savingResult.isSuccess) { 151 | widget.sharedState.overlayController.close(); 152 | } 153 | _isSaving = false; 154 | setState(() => {}); 155 | }, 156 | child: Icon(Icons.check), 157 | ); 158 | } 159 | 160 | Widget _buildErrorWidget(String text, BuildContext context) { 161 | return SizedBox( 162 | height: Theme.of(context).buttonTheme.height, 163 | width: widget.sharedState.style.overlayStyle.width - 164 | widget.sharedState.style.overlayStyle.margin * 2 - 165 | Theme.of(context).buttonTheme.minWidth * 2, 166 | child: SingleChildScrollView( 167 | child: Text(text), 168 | ), 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /packages/visibility_detector/test/render_visibility_detector_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'dart:ui'; 8 | 9 | import 'package:flutter/rendering.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | import 'package:visibility_detector/src/render_visibility_detector.dart'; 12 | import 'package:visibility_detector/visibility_detector.dart'; 13 | 14 | void main() { 15 | VisibilityDetectorController.instance.updateInterval = Duration.zero; 16 | 17 | testWidgets('RVS (box) unregisters its callback on paint', 18 | (WidgetTester tester) async { 19 | final RenderVisibilityDetector detector = RenderVisibilityDetector( 20 | key: Key('test'), 21 | onVisibilityChanged: (_) {}, 22 | ); 23 | 24 | final ContainerLayer layer = ContainerLayer(); 25 | final PaintingContext context = PaintingContext(layer, Rect.largest); 26 | expect(layer.subtreeHasCompositionCallbacks, false); 27 | 28 | detector.layout(BoxConstraints.tight(const Size(200, 200))); 29 | detector.paint(context, Offset.zero); 30 | detector.paint(context, Offset.zero); 31 | 32 | context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member 33 | 34 | expect(layer.subtreeHasCompositionCallbacks, true); 35 | 36 | expect(detector.debugScheduleUpdateCount, 0); 37 | layer.buildScene(SceneBuilder()).dispose(); 38 | 39 | expect(detector.debugScheduleUpdateCount, 1); 40 | }); 41 | 42 | testWidgets('RVS (sliver) unregisters its callback on paint', 43 | (WidgetTester tester) async { 44 | final RenderSliverVisibilityDetector detector = 45 | RenderSliverVisibilityDetector( 46 | key: Key('test'), 47 | onVisibilityChanged: (_) {}, 48 | sliver: RenderSliverToBoxAdapter(child: RenderLimitedBox()), 49 | ); 50 | 51 | final ContainerLayer layer = ContainerLayer(); 52 | final PaintingContext context = PaintingContext(layer, Rect.largest); 53 | expect(layer.subtreeHasCompositionCallbacks, false); 54 | 55 | detector.layout(SliverConstraints( 56 | axisDirection: AxisDirection.down, 57 | growthDirection: GrowthDirection.forward, 58 | userScrollDirection: ScrollDirection.forward, 59 | scrollOffset: 0, 60 | precedingScrollExtent: 0, 61 | overlap: 0, 62 | remainingPaintExtent: 0, 63 | crossAxisExtent: 0, 64 | crossAxisDirection: AxisDirection.left, 65 | viewportMainAxisExtent: 0, 66 | remainingCacheExtent: 0, 67 | cacheOrigin: 0, 68 | )); 69 | 70 | final owner = PipelineOwner(); 71 | detector.attach(owner); 72 | owner.flushCompositingBits(); 73 | 74 | detector.paint(context, Offset.zero); 75 | detector.paint(context, Offset.zero); 76 | expect(layer.subtreeHasCompositionCallbacks, true); 77 | 78 | expect(detector.debugScheduleUpdateCount, 0); 79 | context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member 80 | layer.buildScene(SceneBuilder()).dispose(); 81 | 82 | expect(detector.debugScheduleUpdateCount, 1); 83 | }); 84 | 85 | testWidgets('RVS unregisters its callback on dispose', 86 | (WidgetTester tester) async { 87 | final RenderVisibilityDetector detector = RenderVisibilityDetector( 88 | key: Key('test'), 89 | onVisibilityChanged: (_) {}, 90 | ); 91 | 92 | final ContainerLayer layer = ContainerLayer(); 93 | final PaintingContext context = PaintingContext(layer, Rect.largest); 94 | expect(layer.subtreeHasCompositionCallbacks, false); 95 | 96 | detector.layout(BoxConstraints.tight(const Size(200, 200))); 97 | 98 | detector.paint(context, Offset.zero); 99 | expect(layer.subtreeHasCompositionCallbacks, true); 100 | 101 | detector.dispose(); 102 | expect(layer.subtreeHasCompositionCallbacks, false); 103 | 104 | expect(detector.debugScheduleUpdateCount, 0); 105 | context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member 106 | layer.buildScene(SceneBuilder()).dispose(); 107 | 108 | expect(detector.debugScheduleUpdateCount, 0); 109 | }); 110 | 111 | testWidgets('RVS unregisters its callback when callback changes', 112 | (WidgetTester tester) async { 113 | final RenderVisibilityDetector detector = RenderVisibilityDetector( 114 | key: Key('test'), 115 | onVisibilityChanged: (_) {}, 116 | ); 117 | 118 | final ContainerLayer layer = ContainerLayer(); 119 | final PaintingContext context = PaintingContext(layer, Rect.largest); 120 | expect(layer.subtreeHasCompositionCallbacks, false); 121 | 122 | detector.layout(BoxConstraints.tight(const Size(200, 200))); 123 | 124 | detector.paint(context, Offset.zero); 125 | expect(layer.subtreeHasCompositionCallbacks, true); 126 | 127 | detector.onVisibilityChanged = null; 128 | 129 | expect(layer.subtreeHasCompositionCallbacks, false); 130 | 131 | expect(detector.debugScheduleUpdateCount, 0); 132 | context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member 133 | layer.buildScene(SceneBuilder()).dispose(); 134 | 135 | expect(detector.debugScheduleUpdateCount, 0); 136 | }); 137 | 138 | testWidgets('RVS can schedule an update for a RO that is not laid out', 139 | (WidgetTester tester) async { 140 | final RenderVisibilityDetector detector = RenderVisibilityDetector( 141 | key: Key('test'), 142 | onVisibilityChanged: (_) { 143 | fail('should not get called'); 144 | }, 145 | ); 146 | 147 | // Force an out of band update to get scheduled without laying out. 148 | detector.onVisibilityChanged = (_) { 149 | fail('This should also not get called'); 150 | }; 151 | 152 | expect(detector.debugScheduleUpdateCount, 1); 153 | 154 | detector.dispose(); 155 | }); 156 | 157 | testWidgets( 158 | 'RVS (Sliver) can schedule an update for a RO that is not laid out', 159 | (WidgetTester tester) async { 160 | final RenderSliverVisibilityDetector detector = 161 | RenderSliverVisibilityDetector( 162 | key: Key('test'), 163 | onVisibilityChanged: (_) { 164 | fail('should not get called'); 165 | }, 166 | ); 167 | 168 | // Force an out of band update to get scheduled without laying out. 169 | detector.onVisibilityChanged = (_) { 170 | fail('This should also not get called'); 171 | }; 172 | 173 | expect(detector.debugScheduleUpdateCount, 1); 174 | 175 | detector.dispose(); 176 | }); 177 | } 178 | -------------------------------------------------------------------------------- /packages/self_storing_input/test/self_storing_text_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | import 'package:self_storing_input/self_storing_input.dart'; 10 | import 'package:self_storing_input/src/self_storing_text/overlay_box.dart'; 11 | 12 | import 'testing/widget_testing.dart'; 13 | 14 | /// Saves and loads values fast and successfully. 15 | /// Validation always returns true. 16 | class _HappyTestSaver implements Saver { 17 | Map storage = {}; 18 | 19 | @override 20 | Future load(String itemKey) async { 21 | return storage[itemKey]; 22 | } 23 | 24 | @override 25 | OperationResult validate(String itemKey, T? value) { 26 | return OperationResult.success(); 27 | } 28 | 29 | @override 30 | Future save(String itemKey, T? value) async { 31 | storage[itemKey] = value; 32 | return OperationResult.success(); 33 | } 34 | } 35 | 36 | void main() { 37 | group('SelfStoringText', () { 38 | testWidgets('respects empty value replacer', (WidgetTester tester) async { 39 | var saver = _HappyTestSaver(); 40 | SelfStoringText textWidget = SelfStoringText( 41 | 'itemKey', 42 | saver: saver, 43 | ); 44 | await wrapAndPump(tester, sizeAndLayout(textWidget)); 45 | await tester.pumpAndSettle(); 46 | 47 | // Check the text value. 48 | expect( 49 | find.byWidgetPredicate( 50 | (widget) => widget is Text && widget.data == textWidget.emptyText), 51 | findsOneWidget, 52 | ); 53 | }); 54 | 55 | testWidgets('loads value', (WidgetTester tester) async { 56 | // Prepare the widget. 57 | var itemKey = 'itemKey'; 58 | var value = 'value'; 59 | var saver = _HappyTestSaver()..storage[itemKey] = value; 60 | SelfStoringText textWidget = SelfStoringText( 61 | itemKey, 62 | saver: saver, 63 | ); 64 | await wrapAndPump(tester, sizeAndLayout(textWidget)); 65 | await tester.pumpAndSettle(); 66 | 67 | // Check the text value. 68 | expect( 69 | find.byWidgetPredicate( 70 | (widget) => widget is Text && widget.data == value), 71 | findsOneWidget, 72 | ); 73 | }); 74 | 75 | testWidgets('clear value', (WidgetTester tester) async { 76 | // Prepare the widget. 77 | var itemKey = 'itemKey'; 78 | var value = 'value'; 79 | var saver = _HappyTestSaver()..storage[itemKey] = value; 80 | SelfStoringText textWidget = SelfStoringText( 81 | itemKey, 82 | saver: saver, 83 | ); 84 | await wrapAndPump(tester, sizeAndLayout(textWidget)); 85 | await tester.pumpAndSettle(); 86 | 87 | // Click the edit button. 88 | await tester.tap(find.byType(IconButton)); 89 | await tester.pumpAndSettle(); 90 | await tester.pumpAndSettle(); 91 | 92 | // Check the text value. 93 | expect( 94 | find.byWidgetPredicate((widget) => 95 | widget is TextFormField && widget.controller!.text == value), 96 | findsOneWidget, 97 | ); 98 | 99 | // Clear value. 100 | await tester.tap(find.byKey(clearButtonKey)); 101 | await tester.pumpAndSettle(); 102 | 103 | // Click OK button. 104 | await tester.tap(find.byKey(okButtonKey)); 105 | await tester.pumpAndSettle(); 106 | 107 | // Check the text value. 108 | expect( 109 | find.byWidgetPredicate( 110 | (widget) => widget is Text && widget.data == textWidget.emptyText), 111 | findsOneWidget, 112 | ); 113 | 114 | expect(saver.storage[itemKey], null); 115 | }); 116 | 117 | testWidgets('happy path', (WidgetTester tester) async { 118 | // Prepare the widget. 119 | var itemKey = 'itemKey'; 120 | var value = 'value'; 121 | var saver = _HappyTestSaver(); 122 | SelfStoringText textWidget = SelfStoringText( 123 | itemKey, 124 | saver: saver, 125 | ); 126 | await wrapAndPump(tester, sizeAndLayout(textWidget)); 127 | await tester.pumpAndSettle(); 128 | 129 | // Check the text value. 130 | expect( 131 | find.byWidgetPredicate( 132 | (widget) => widget is Text && widget.data == textWidget.emptyText), 133 | findsOneWidget, 134 | ); 135 | 136 | // Click the edit button. 137 | await tester.tap(find.byType(IconButton)); 138 | await tester.pumpAndSettle(); 139 | await tester.pumpAndSettle(); 140 | 141 | // Enter 'hi' into the TextField. 142 | await tester.enterText(find.byType(TextFormField), value); 143 | 144 | // Click OK button. 145 | await tester.tap(find.byKey(okButtonKey)); 146 | await tester.pumpAndSettle(); 147 | await tester.pumpAndSettle(); 148 | 149 | // Check the entered value is applied in UI. 150 | expect( 151 | find.byWidgetPredicate( 152 | (widget) => widget is Text && widget.data == value), 153 | findsOneWidget, 154 | ); 155 | // And in storage. 156 | expect(saver.storage[itemKey], 'value'); 157 | // And overlay is closed. 158 | expect(find.byType(TextFormField), findsNothing); 159 | }); 160 | 161 | testWidgets('cancel editing', (WidgetTester tester) async { 162 | var itemKey = 'itemKey'; 163 | var saver = _HappyTestSaver(); 164 | SelfStoringText textWidget = SelfStoringText( 165 | itemKey, 166 | saver: saver, 167 | ); 168 | await wrapAndPump(tester, sizeAndLayout(textWidget)); 169 | await tester.pumpAndSettle(); 170 | 171 | // Click the edit button. 172 | await tester.tap(find.byType(IconButton)); 173 | await tester.pumpAndSettle(); 174 | await tester.pumpAndSettle(); 175 | 176 | // Enter 'hi' into the TextField. 177 | await tester.enterText(find.byType(TextFormField), 'hi'); 178 | 179 | // Click cancel button. 180 | await tester.tap(find.byKey(cancelButtonKey)); 181 | await tester.pumpAndSettle(); 182 | await tester.pumpAndSettle(); 183 | 184 | // Check the entered value is not applied in UI. 185 | expect( 186 | find.byWidgetPredicate( 187 | (widget) => widget is Text && widget.data == textWidget.emptyText), 188 | findsOneWidget, 189 | ); 190 | // And in storage. 191 | expect(saver.storage.containsKey(itemKey), false); 192 | // And overlay is closed. 193 | expect(find.byType(TextFormField), findsNothing); 194 | }); 195 | }); 196 | } 197 | -------------------------------------------------------------------------------- /packages/visibility_detector/test/impression_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:visibility_detector/visibility_detector.dart'; 4 | 5 | void main() { 6 | testWidgets('Material clip', (tester) async { 7 | VisibilityDetectorController.instance.updateInterval = Duration.zero; 8 | final Key listKey = UniqueKey(); 9 | int onFirstVis = 0; 10 | int onEnterVis = 0; 11 | int onExitVis = 0; 12 | bool inView = false; 13 | await tester.pumpWidget( 14 | MaterialApp( 15 | home: Material( 16 | child: SingleChildScrollView( 17 | child: Column( 18 | key: listKey, 19 | children: [ 20 | SizedBox.fromSize(size: Size(200, 1000)), 21 | VisibilityDetector( 22 | key: UniqueKey(), 23 | onVisibilityChanged: (info) { 24 | if (info.visibleFraction > .6) { 25 | inView = true; 26 | onFirstVis = 1; 27 | onEnterVis += 1; 28 | } else if (inView && info.visibleFraction < .4) { 29 | onExitVis += 1; 30 | inView = false; 31 | } 32 | }, 33 | child: Container( 34 | height: 200, 35 | width: 300, 36 | color: Colors.red, 37 | ), 38 | ), 39 | ], 40 | ), 41 | ), 42 | ), 43 | ), 44 | ); 45 | await tester.pumpAndSettle(); 46 | 47 | expect(onFirstVis, 0); 48 | expect(onEnterVis, 0); 49 | expect(onExitVis, 0); 50 | 51 | final drags = [-1000.0, 1000.0, -1000.0, 1000.0, -1000.0]; 52 | for (final dragAmount in drags) { 53 | await tester.drag( 54 | find.byType(SingleChildScrollView), Offset(0.0, dragAmount)); 55 | await tester.pumpAndSettle(); 56 | } 57 | 58 | expect(onFirstVis, 1); 59 | expect(onEnterVis, 3); 60 | expect(onExitVis, 2); 61 | }); 62 | 63 | testWidgets('Material clip with intermediate ROs', (tester) async { 64 | VisibilityDetectorController.instance.updateInterval = Duration.zero; 65 | final Key listKey = UniqueKey(); 66 | int onFirstVis = 0; 67 | int onEnterVis = 0; 68 | int onExitVis = 0; 69 | bool inView = false; 70 | await tester.pumpWidget( 71 | MaterialApp( 72 | home: Material( 73 | child: SingleChildScrollView( 74 | child: Column( 75 | key: listKey, 76 | children: [ 77 | SizedBox.fromSize(size: Size(200, 1000)), 78 | CustomPaint( 79 | child: VisibilityDetector( 80 | key: UniqueKey(), 81 | onVisibilityChanged: (info) { 82 | if (info.visibleFraction > .6) { 83 | inView = true; 84 | onFirstVis = 1; 85 | onEnterVis += 1; 86 | } else if (inView && info.visibleFraction < .4) { 87 | onExitVis += 1; 88 | inView = false; 89 | } 90 | }, 91 | child: Container( 92 | height: 200, 93 | width: 300, 94 | color: Colors.red, 95 | ), 96 | ), 97 | ), 98 | ], 99 | ), 100 | ), 101 | ), 102 | ), 103 | ); 104 | await tester.pumpAndSettle(); 105 | 106 | expect(onFirstVis, 0); 107 | expect(onEnterVis, 0); 108 | expect(onExitVis, 0); 109 | 110 | final drags = [-1000.0, 1000.0, -1000.0, 1000.0, -1000.0]; 111 | for (final dragAmount in drags) { 112 | await tester.drag( 113 | find.byType(SingleChildScrollView), Offset(0.0, dragAmount)); 114 | await tester.pumpAndSettle(); 115 | } 116 | 117 | expect(onFirstVis, 1); 118 | expect(onEnterVis, 3); 119 | expect(onExitVis, 2); 120 | }); 121 | 122 | testWidgets('Programmatic visibility change', (WidgetTester tester) async { 123 | VisibilityDetectorController.instance.updateInterval = Duration.zero; 124 | final List infos = []; 125 | await tester.pumpWidget( 126 | VisibilityDetector( 127 | key: Key('app_widget'), 128 | onVisibilityChanged: (info) { 129 | infos.add(info); 130 | }, 131 | child: Visibility( 132 | maintainState: true, 133 | visible: true, 134 | child: VisibilityDetector( 135 | key: Key('weatherCard'), 136 | onVisibilityChanged: (info) { 137 | infos.add(info); 138 | }, 139 | child: Placeholder(), 140 | ), 141 | ), 142 | ), 143 | ); 144 | await tester.pumpAndSettle(); 145 | await tester.pumpWidget( 146 | VisibilityDetector( 147 | key: Key('app_widget'), 148 | onVisibilityChanged: (info) { 149 | infos.add(info); 150 | }, 151 | child: Visibility( 152 | maintainState: true, 153 | visible: false, 154 | child: VisibilityDetector( 155 | key: Key('weatherCard'), 156 | onVisibilityChanged: (info) { 157 | infos.add(info); 158 | }, 159 | child: Placeholder(), 160 | ), 161 | ), 162 | ), 163 | ); 164 | await tester.pumpAndSettle(); 165 | await tester.pumpWidget( 166 | VisibilityDetector( 167 | key: Key('app_widget'), 168 | onVisibilityChanged: (info) { 169 | infos.add(info); 170 | }, 171 | child: Visibility( 172 | maintainState: true, 173 | visible: true, 174 | child: VisibilityDetector( 175 | key: Key('weatherCard'), 176 | onVisibilityChanged: (info) { 177 | infos.add(info); 178 | }, 179 | child: Placeholder(), 180 | ), 181 | ), 182 | ), 183 | ); 184 | await tester.pumpAndSettle(); 185 | await tester.pumpWidget(Placeholder()); 186 | await tester.pumpAndSettle(); 187 | 188 | expect(infos, const [ 189 | VisibilityInfo( 190 | key: Key('app_widget'), 191 | size: Size(800, 600), 192 | visibleBounds: Rect.fromLTRB(0, 0, 800, 600), 193 | ), 194 | VisibilityInfo( 195 | key: Key('weatherCard'), 196 | size: Size(800, 600), 197 | visibleBounds: Rect.fromLTRB(0, 0, 800, 600), 198 | ), 199 | VisibilityInfo( 200 | key: Key('weatherCard'), 201 | size: Size(800, 600), 202 | ), 203 | VisibilityInfo( 204 | key: Key('weatherCard'), 205 | size: Size(800, 600), 206 | visibleBounds: Rect.fromLTRB(0, 0, 800, 600), 207 | ), 208 | VisibilityInfo( 209 | key: Key('app_widget'), 210 | size: Size(800, 600), 211 | ), 212 | VisibilityInfo( 213 | key: Key('weatherCard'), 214 | size: Size(800, 600), 215 | ), 216 | ]); 217 | }); 218 | } 219 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/test/scroll_offset_controller_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; 10 | 11 | const screenHeight = 400.0; 12 | const screenWidth = 400.0; 13 | const itemHeight = screenHeight / 10.0; 14 | const defaultItemCount = 500; 15 | const scrollDuration = Duration(seconds: 1); 16 | const scrollDurationTolerance = Duration(milliseconds: 1); 17 | const tolerance = 1e-3; 18 | 19 | void main() { 20 | Future setUpWidgetTest( 21 | WidgetTester tester, { 22 | Key? key, 23 | ItemScrollController? itemScrollController, 24 | ItemPositionsListener? itemPositionsListener, 25 | ScrollOffsetListener? scrollOffsetListener, 26 | ScrollOffsetController? scrollOffsetController, 27 | Axis? scrollDirection, 28 | int initialIndex = 0, 29 | double initialAlignment = 0.0, 30 | int itemCount = defaultItemCount, 31 | ScrollPhysics? physics, 32 | bool addSemanticIndexes = true, 33 | int? semanticChildCount, 34 | EdgeInsets? padding, 35 | bool addRepaintBoundaries = true, 36 | bool addAutomaticKeepAlives = true, 37 | double? minCacheExtent, 38 | bool variableHeight = false, 39 | }) async { 40 | tester.view.devicePixelRatio = 1.0; 41 | tester.view.physicalSize = const Size(screenWidth, screenHeight); 42 | 43 | await tester.pumpWidget( 44 | MaterialApp( 45 | home: ScrollablePositionedList.builder( 46 | key: key, 47 | itemCount: itemCount, 48 | itemScrollController: itemScrollController, 49 | scrollOffsetListener: scrollOffsetListener, 50 | scrollOffsetController: scrollOffsetController, 51 | scrollDirection: scrollDirection ?? Axis.vertical, 52 | itemBuilder: (context, index) { 53 | assert(index >= 0 && index <= itemCount - 1); 54 | return SizedBox( 55 | height: 56 | variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight, 57 | child: Text('Item $index'), 58 | ); 59 | }, 60 | itemPositionsListener: itemPositionsListener, 61 | initialScrollIndex: initialIndex, 62 | initialAlignment: initialAlignment, 63 | physics: physics, 64 | addSemanticIndexes: addSemanticIndexes, 65 | semanticChildCount: semanticChildCount, 66 | padding: padding, 67 | addAutomaticKeepAlives: addAutomaticKeepAlives, 68 | addRepaintBoundaries: addRepaintBoundaries, 69 | minCacheExtent: minCacheExtent, 70 | ), 71 | ), 72 | ); 73 | } 74 | 75 | testWidgets('Programtically scroll down 50 pixels', 76 | (WidgetTester tester) async { 77 | final scrollDistance = 50.0; 78 | 79 | ScrollOffsetController scrollOffsetController = ScrollOffsetController(); 80 | 81 | await setUpWidgetTest( 82 | tester, 83 | scrollOffsetController: scrollOffsetController, 84 | initialIndex: 5, 85 | ); 86 | 87 | final originalOffest = tester.getTopLeft(find.text('Item 5')).dy; 88 | 89 | unawaited(scrollOffsetController.animateScroll( 90 | offset: -scrollDistance, 91 | duration: scrollDuration, 92 | )); 93 | await tester.pumpAndSettle(); 94 | 95 | final newOffset = tester.getTopLeft(find.text('Item 5')).dy; 96 | 97 | expect(newOffset - originalOffest, scrollDistance); 98 | }); 99 | 100 | testWidgets('Programtically scroll left 50 pixels', 101 | (WidgetTester tester) async { 102 | final scrollDistance = 50.0; 103 | 104 | ScrollOffsetController scrollOffsetController = ScrollOffsetController(); 105 | 106 | await setUpWidgetTest( 107 | tester, 108 | scrollOffsetController: scrollOffsetController, 109 | initialIndex: 5, 110 | scrollDirection: Axis.horizontal, 111 | ); 112 | 113 | final originalOffest = tester.getTopLeft(find.text('Item 5')).dx; 114 | 115 | unawaited(scrollOffsetController.animateScroll( 116 | offset: -scrollDistance, 117 | duration: scrollDuration, 118 | )); 119 | await tester.pumpAndSettle(); 120 | 121 | final newOffset = tester.getTopLeft(find.text('Item 5')).dx; 122 | 123 | expect(newOffset - originalOffest, scrollDistance); 124 | }); 125 | 126 | testWidgets('Programtically scroll down 50 pixels, stop half way', 127 | (WidgetTester tester) async { 128 | final scrollDistance = 50.0; 129 | 130 | ScrollOffsetController scrollOffsetController = ScrollOffsetController(); 131 | 132 | await setUpWidgetTest( 133 | tester, 134 | scrollOffsetController: scrollOffsetController, 135 | initialIndex: 5, 136 | ); 137 | 138 | final originalOffest = tester.getTopLeft(find.text('Item 5')).dy; 139 | 140 | unawaited(scrollOffsetController.animateScroll( 141 | offset: -scrollDistance, 142 | duration: scrollDuration, 143 | )); 144 | await tester.pump(); 145 | await tester.pump(); 146 | await tester.pump(scrollDuration ~/ 2); 147 | 148 | await tester.tap(find.byType(ScrollablePositionedList)); 149 | await tester.pumpAndSettle(); 150 | 151 | final newOffset = tester.getTopLeft(find.text('Item 5')).dy; 152 | 153 | expect(newOffset - originalOffest, scrollDistance ~/ 2); 154 | 155 | await tester.pumpAndSettle(); 156 | }); 157 | 158 | testWidgets( 159 | 'Programtically scroll down 50 pixels, stop half way and go back 12', 160 | (WidgetTester tester) async { 161 | final scrollDistance = 50.0; 162 | final scrollBack = 12.0; 163 | 164 | ScrollOffsetController scrollOffsetController = ScrollOffsetController(); 165 | 166 | await setUpWidgetTest( 167 | tester, 168 | scrollOffsetController: scrollOffsetController, 169 | initialIndex: 5, 170 | ); 171 | 172 | final originalOffest = tester.getTopLeft(find.text('Item 5')).dy; 173 | 174 | unawaited(scrollOffsetController.animateScroll( 175 | offset: -scrollDistance, 176 | duration: scrollDuration, 177 | )); 178 | await tester.pump(); 179 | await tester.pump(); 180 | await tester.pump(scrollDuration ~/ 2); 181 | 182 | unawaited(scrollOffsetController.animateScroll( 183 | offset: scrollBack, 184 | duration: scrollDuration, 185 | )); 186 | await tester.pumpAndSettle(); 187 | 188 | final newOffset = tester.getTopLeft(find.text('Item 5')).dy; 189 | 190 | expect(newOffset - originalOffest, (scrollDistance ~/ 2) - scrollBack); 191 | 192 | await tester.pumpAndSettle(); 193 | }); 194 | 195 | testWidgets( 196 | 'Programtically scroll down 50 pixels, stop half way and then programtically scroll to iten 100', 197 | (WidgetTester tester) async { 198 | final scrollDistance = 50.0; 199 | 200 | ScrollOffsetController scrollOffsetController = ScrollOffsetController(); 201 | ItemScrollController itemScrollController = ItemScrollController(); 202 | 203 | await setUpWidgetTest( 204 | tester, 205 | scrollOffsetController: scrollOffsetController, 206 | itemScrollController: itemScrollController, 207 | initialIndex: 5, 208 | ); 209 | 210 | unawaited(scrollOffsetController.animateScroll( 211 | offset: -scrollDistance, 212 | duration: scrollDuration, 213 | )); 214 | await tester.pump(); 215 | await tester.pump(); 216 | await tester.pump(scrollDuration ~/ 2); 217 | 218 | unawaited(itemScrollController.scrollTo( 219 | index: 100, 220 | duration: scrollDuration, 221 | )); 222 | await tester.pumpAndSettle(); 223 | 224 | expect(tester.getTopLeft(find.text('Item 100')).dy, 0); 225 | 226 | await tester.pumpAndSettle(); 227 | }); 228 | } 229 | -------------------------------------------------------------------------------- /packages/self_storing_input/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/services.dart'; 9 | import 'package:self_storing_input/self_storing_input.dart'; 10 | import 'package:url_launcher/url_launcher.dart'; 11 | 12 | class Fields { 13 | static const String phrase = 'phrase'; 14 | static const String paragraph = 'paragraph'; 15 | static const String tristate = 'tristate'; 16 | static const String twostate = 'twostate'; 17 | static const String unselectableRadioGroup = 'unselectableRadioGroup'; 18 | static const String radioGroup = 'radioGroup'; 19 | } 20 | 21 | class _DemoSaver with ChangeNotifier implements Saver { 22 | Map storage = {}; 23 | bool failSaving = false; 24 | Duration delay = const Duration(milliseconds: 100); 25 | 26 | @override 27 | Future load(String itemKey) async { 28 | // This delay is for demo purposes. 29 | await Future.delayed(delay); 30 | return storage[itemKey]; 31 | } 32 | 33 | @override 34 | OperationResult validate(String itemKey, T? value) { 35 | if (itemKey == Fields.phrase && (value?.toString().length ?? 0) % 2 == 1) { 36 | return OperationResult.error('Value should have even number of letters.'); 37 | } 38 | return OperationResult.success(); 39 | } 40 | 41 | @override 42 | Future save(String itemKey, T? value) async { 43 | // This delay is for demo purposes. 44 | await Future.delayed(delay); 45 | if (failSaving) { 46 | return OperationResult.error( 47 | 'Failed to save the value, for demo purposes.'); 48 | } 49 | storage[itemKey] = value; 50 | notifyListeners(); 51 | return OperationResult.success(); 52 | } 53 | } 54 | 55 | void main() { 56 | runApp(Demo()); 57 | } 58 | 59 | class Demo extends StatefulWidget { 60 | @override 61 | _DemoState createState() => _DemoState(); 62 | } 63 | 64 | class _DemoState extends State { 65 | OverlayController _controller = OverlayController(); 66 | final _DemoSaver _saver = _DemoSaver(); 67 | 68 | @override 69 | void initState() { 70 | _saver.addListener(() => setState(() {})); 71 | super.initState(); 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return MaterialApp( 77 | home: GestureDetector( 78 | onTap: () async { 79 | _controller.close(); 80 | }, 81 | child: Scaffold( 82 | body: SingleChildScrollView( 83 | child: ListTileTheme( 84 | contentPadding: EdgeInsets.all(0), 85 | child: Padding( 86 | padding: EdgeInsets.all(40), 87 | child: Column( 88 | crossAxisAlignment: CrossAxisAlignment.start, 89 | children: [ 90 | _section('SELF_STORING_INPUT\nDEMO', buildDemoHeader()), 91 | _section('Demo Parameters', buildDemoParameters()), 92 | Row( 93 | crossAxisAlignment: CrossAxisAlignment.start, 94 | children: [ 95 | _section('Self Storing Input Widgets', buildDemo()), 96 | if (_saver.storage.isNotEmpty) 97 | _section('Storage Content', buildStorageObserver()), 98 | ], 99 | ) 100 | ], 101 | ), 102 | ), 103 | ), 104 | ), 105 | ), 106 | ), 107 | ); 108 | } 109 | 110 | static Widget _right(List widgets) { 111 | return Padding( 112 | child: Column( 113 | crossAxisAlignment: CrossAxisAlignment.start, children: widgets), 114 | padding: EdgeInsets.only(left: 20), 115 | ); 116 | } 117 | 118 | Widget _section(String header, List children) { 119 | return Container( 120 | width: 400, 121 | child: Column( 122 | crossAxisAlignment: CrossAxisAlignment.start, 123 | children: [ 124 | Text( 125 | header, 126 | style: TextStyle( 127 | color: Theme.of(context).colorScheme.secondary, fontSize: 20), 128 | ), 129 | SizedBox(height: 20), 130 | _right(children), 131 | SizedBox(height: 40), 132 | ], 133 | ), 134 | ); 135 | } 136 | 137 | List buildDemoHeader() { 138 | return [ 139 | TextButton( 140 | onPressed: () async => await launchUrl( 141 | Uri.parse( 142 | 'https://github.com/google/flutter.widgets/tree/master/packages/self_storing_input/example'), 143 | ), 144 | child: Text( 145 | 'Source Code', 146 | style: TextStyle( 147 | color: Theme.of(context).colorScheme.secondary, 148 | decoration: TextDecoration.underline, 149 | ), 150 | ), 151 | ), 152 | ]; 153 | } 154 | 155 | List buildDemoParameters() { 156 | return [ 157 | CheckboxListTile( 158 | controlAffinity: ListTileControlAffinity.leading, 159 | title: Text('Fail the save operation'), 160 | value: _saver.failSaving, 161 | onChanged: (v) => setState(() => _saver.failSaving = v!), 162 | ), 163 | SizedBox(height: 10), 164 | TextFormField( 165 | onChanged: (v) => 166 | setState(() => _saver.delay = Duration(milliseconds: int.parse(v))), 167 | initialValue: _saver.delay.inMilliseconds.toString(), 168 | keyboardType: TextInputType.number, 169 | decoration: 170 | InputDecoration(labelText: 'Delay time for the save operation, ms'), 171 | inputFormatters: [FilteringTextInputFormatter.digitsOnly], 172 | ) 173 | ]; 174 | } 175 | 176 | List buildDemo() { 177 | return [ 178 | Text('${Fields.phrase}:'), 179 | SelfStoringText( 180 | Fields.phrase, 181 | overlayController: _controller, 182 | saver: _saver, 183 | ), 184 | SizedBox(height: 20), 185 | Text('${Fields.paragraph}:'), 186 | SelfStoringText( 187 | Fields.paragraph, 188 | overlayController: _controller, 189 | saver: _saver, 190 | style: SelfStoringTextStyle( 191 | overlayStyle: OverlayStyle.forTextEditor(height: 130), 192 | keyboardType: TextInputType.multiline, 193 | maxLines: null, 194 | ), 195 | ), 196 | SizedBox(height: 20), 197 | Text('checkboxes:'), 198 | SelfStoringCheckbox( 199 | Fields.tristate, 200 | saver: _saver, 201 | overlayController: _controller, 202 | title: Text(Fields.tristate), 203 | ), 204 | SizedBox(height: 20), 205 | SelfStoringCheckbox( 206 | Fields.twostate, 207 | saver: _saver, 208 | overlayController: _controller, 209 | title: Text(Fields.twostate), 210 | tristate: false, 211 | ), 212 | SizedBox(height: 40), 213 | Text('${Fields.unselectableRadioGroup}:'), 214 | SelfStoringRadioGroup( 215 | Fields.unselectableRadioGroup, 216 | saver: _saver, 217 | isUnselectable: true, 218 | overlayController: _controller, 219 | items: {1: 'One', 2: 'Two', 3: 'Three'}, 220 | ), 221 | SizedBox(height: 40), 222 | Text('${Fields.radioGroup}:'), 223 | SelfStoringRadioGroup( 224 | Fields.radioGroup, 225 | saver: _saver, 226 | overlayController: _controller, 227 | items: {1: 'One', 2: 'Two', 3: 'Three'}, 228 | ), 229 | ]; 230 | } 231 | 232 | List buildStorageObserver() { 233 | return [ 234 | for (var id in _saver.storage.keys) Text('$id: ${_saver.storage[id]}'), 235 | ]; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/test/scroll_offset_listener_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; 10 | 11 | const screenHeight = 400.0; 12 | const screenWidth = 400.0; 13 | const itemHeight = screenHeight / 10.0; 14 | const defaultItemCount = 500; 15 | const scrollDuration = Duration(seconds: 1); 16 | const scrollDurationTolerance = Duration(milliseconds: 1); 17 | const tolerance = 1e-3; 18 | 19 | void main() { 20 | Future setUpWidgetTest( 21 | WidgetTester tester, { 22 | Key? key, 23 | ItemScrollController? itemScrollController, 24 | ItemPositionsListener? itemPositionsListener, 25 | ScrollOffsetListener? scrollOffsetListener, 26 | Axis? scrollDirection, 27 | int initialIndex = 0, 28 | double initialAlignment = 0.0, 29 | int itemCount = defaultItemCount, 30 | ScrollPhysics? physics, 31 | bool addSemanticIndexes = true, 32 | int? semanticChildCount, 33 | EdgeInsets? padding, 34 | bool addRepaintBoundaries = true, 35 | bool addAutomaticKeepAlives = true, 36 | double? minCacheExtent, 37 | bool variableHeight = false, 38 | }) async { 39 | tester.view.devicePixelRatio = 1.0; 40 | tester.view.physicalSize = const Size(screenWidth, screenHeight); 41 | 42 | await tester.pumpWidget( 43 | MaterialApp( 44 | home: ScrollablePositionedList.builder( 45 | key: key, 46 | itemCount: itemCount, 47 | itemScrollController: itemScrollController, 48 | scrollOffsetListener: scrollOffsetListener, 49 | scrollDirection: scrollDirection ?? Axis.vertical, 50 | itemBuilder: (context, index) { 51 | assert(index >= 0 && index <= itemCount - 1); 52 | return SizedBox( 53 | height: 54 | variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight, 55 | child: Text('Item $index'), 56 | ); 57 | }, 58 | itemPositionsListener: itemPositionsListener, 59 | initialScrollIndex: initialIndex, 60 | initialAlignment: initialAlignment, 61 | physics: physics, 62 | addSemanticIndexes: addSemanticIndexes, 63 | semanticChildCount: semanticChildCount, 64 | padding: padding, 65 | addAutomaticKeepAlives: addAutomaticKeepAlives, 66 | addRepaintBoundaries: addRepaintBoundaries, 67 | minCacheExtent: minCacheExtent, 68 | ), 69 | ), 70 | ); 71 | } 72 | 73 | testWidgets('Manual scroll up 10 pixels', (WidgetTester tester) async { 74 | final scrollDistance = 50.0; 75 | 76 | final itemScrollController = ItemScrollController(); 77 | final itemPositionsListener = ItemPositionsListener.create(); 78 | final ScrollSum scrollSummer = ScrollSum(); 79 | 80 | await setUpWidgetTest(tester, 81 | itemScrollController: itemScrollController, 82 | itemPositionsListener: itemPositionsListener, 83 | scrollOffsetListener: scrollSummer.scrollOffsetListener, 84 | initialIndex: 5); 85 | 86 | expect( 87 | itemPositionsListener.itemPositions.value 88 | .firstWhere((position) => position.index == 5) 89 | .itemLeadingEdge, 90 | 0); 91 | 92 | await tester.drag( 93 | find.byType(ScrollablePositionedList), Offset(0, -scrollDistance)); 94 | await tester.pumpAndSettle(); 95 | 96 | expect(scrollSummer.totalScroll, scrollDistance); 97 | }); 98 | 99 | testWidgets('Manual scroll left 10 pixels', (WidgetTester tester) async { 100 | final scrollDistance = 50.0; 101 | 102 | final itemScrollController = ItemScrollController(); 103 | final itemPositionsListener = ItemPositionsListener.create(); 104 | final ScrollSum scrollSummer = ScrollSum(); 105 | 106 | await setUpWidgetTest(tester, 107 | itemScrollController: itemScrollController, 108 | itemPositionsListener: itemPositionsListener, 109 | scrollOffsetListener: scrollSummer.scrollOffsetListener, 110 | scrollDirection: Axis.horizontal, 111 | initialIndex: 5); 112 | 113 | expect( 114 | itemPositionsListener.itemPositions.value 115 | .firstWhere((position) => position.index == 5) 116 | .itemLeadingEdge, 117 | 0); 118 | 119 | await tester.drag( 120 | find.byType(ScrollablePositionedList), Offset(-scrollDistance, 0)); 121 | await tester.pumpAndSettle(); 122 | 123 | expect(scrollSummer.totalScroll, scrollDistance); 124 | }); 125 | 126 | testWidgets('Programmatic scroll to item 100 with programmatic recording on', 127 | (WidgetTester tester) async { 128 | final itemScrollController = ItemScrollController(); 129 | final itemPositionsListener = ItemPositionsListener.create(); 130 | final ScrollSum scrollSummer = ScrollSum(); 131 | 132 | await setUpWidgetTest(tester, 133 | itemScrollController: itemScrollController, 134 | itemPositionsListener: itemPositionsListener, 135 | scrollOffsetListener: scrollSummer.scrollOffsetListener, 136 | scrollDirection: Axis.horizontal, 137 | initialIndex: 5); 138 | 139 | unawaited( 140 | itemScrollController.scrollTo(index: 100, duration: scrollDuration)); 141 | 142 | await tester.pumpAndSettle(); 143 | 144 | expect(scrollSummer.totalScroll, 2 * screenHeight); 145 | }); 146 | 147 | testWidgets('Programmatic scroll to item 100 with programmatic recording off', 148 | (WidgetTester tester) async { 149 | final itemScrollController = ItemScrollController(); 150 | final itemPositionsListener = ItemPositionsListener.create(); 151 | final ScrollSum scrollSummer = ScrollSum(recordProgrammaticScrolls: false); 152 | 153 | await setUpWidgetTest(tester, 154 | itemScrollController: itemScrollController, 155 | itemPositionsListener: itemPositionsListener, 156 | scrollOffsetListener: scrollSummer.scrollOffsetListener, 157 | scrollDirection: Axis.horizontal, 158 | initialIndex: 5); 159 | 160 | unawaited( 161 | itemScrollController.scrollTo(index: 100, duration: scrollDuration)); 162 | 163 | await tester.pumpAndSettle(); 164 | 165 | expect(scrollSummer.totalScroll, 0); 166 | }); 167 | 168 | testWidgets('Manual scroll up 10 pixels with programmatic recording off', 169 | (WidgetTester tester) async { 170 | final scrollDistance = 50.0; 171 | 172 | final itemScrollController = ItemScrollController(); 173 | final itemPositionsListener = ItemPositionsListener.create(); 174 | final ScrollSum scrollSummer = ScrollSum(recordProgrammaticScrolls: false); 175 | 176 | await setUpWidgetTest(tester, 177 | itemScrollController: itemScrollController, 178 | itemPositionsListener: itemPositionsListener, 179 | scrollOffsetListener: scrollSummer.scrollOffsetListener, 180 | initialIndex: 5); 181 | 182 | expect( 183 | itemPositionsListener.itemPositions.value 184 | .firstWhere((position) => position.index == 5) 185 | .itemLeadingEdge, 186 | 0); 187 | 188 | await tester.drag( 189 | find.byType(ScrollablePositionedList), Offset(0, -scrollDistance)); 190 | await tester.pumpAndSettle(); 191 | 192 | expect(scrollSummer.totalScroll, scrollDistance); 193 | }); 194 | } 195 | 196 | class ScrollSum { 197 | final bool recordProgrammaticScrolls; 198 | double totalScroll = 0.0; 199 | final scrollOffsetListener; 200 | 201 | ScrollSum({this.recordProgrammaticScrolls = true}) 202 | : scrollOffsetListener = ScrollOffsetListener.create( 203 | recordProgrammaticScrolls: recordProgrammaticScrolls) { 204 | scrollOffsetListener.changes.listen((event) { 205 | totalScroll += event; 206 | }); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /packages/visibility_detector/lib/src/visibility_detector.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Dart project authors. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | import 'dart:math' show max; 8 | 9 | import 'package:flutter/widgets.dart'; 10 | 11 | import 'render_visibility_detector.dart'; 12 | 13 | /// A [VisibilityDetector] widget fires a specified callback when the widget 14 | /// changes visibility. 15 | /// 16 | /// Callbacks are not fired immediately on visibility changes. Instead, 17 | /// callbacks are deferred and coalesced such that the callback for each 18 | /// [VisibilityDetector] will be invoked at most once per 19 | /// [VisibilityDetectorController.updateInterval] (unless forced by 20 | /// [VisibilityDetectorController.notifyNow]). Callbacks for *all* 21 | /// [VisibilityDetector] widgets are fired together synchronously between 22 | /// frames. 23 | class VisibilityDetector extends SingleChildRenderObjectWidget { 24 | /// Constructor. 25 | /// 26 | /// `key` is required to properly identify this widget; it must be unique 27 | /// among all [VisibilityDetector] and [SliverVisibilityDetector] widgets. 28 | /// 29 | /// `onVisibilityChanged` may be `null` to disable this [VisibilityDetector]. 30 | const VisibilityDetector({ 31 | required Key key, 32 | required Widget child, 33 | required this.onVisibilityChanged, 34 | }) : assert(key != null), 35 | assert(child != null), 36 | super(key: key, child: child); 37 | 38 | /// The callback to invoke when this widget's visibility changes. 39 | final VisibilityChangedCallback? onVisibilityChanged; 40 | 41 | /// See [RenderObjectWidget.createRenderObject]. 42 | @override 43 | RenderVisibilityDetector createRenderObject(BuildContext context) { 44 | return RenderVisibilityDetector( 45 | key: key!, 46 | onVisibilityChanged: onVisibilityChanged, 47 | ); 48 | } 49 | 50 | /// See [RenderObjectWidget.updateRenderObject]. 51 | @override 52 | void updateRenderObject( 53 | BuildContext context, RenderVisibilityDetector renderObject) { 54 | assert(renderObject.key == key); 55 | renderObject.onVisibilityChanged = onVisibilityChanged; 56 | } 57 | } 58 | 59 | class SliverVisibilityDetector extends SingleChildRenderObjectWidget { 60 | /// Constructor. 61 | /// 62 | /// `key` is required to properly identify this widget; it must be unique 63 | /// among all [VisibilityDetector] and [SliverVisibilityDetector] widgets. 64 | /// 65 | /// `onVisibilityChanged` may be `null` to disable this 66 | /// [SliverVisibilityDetector]. 67 | const SliverVisibilityDetector({ 68 | required Key key, 69 | required Widget sliver, 70 | required this.onVisibilityChanged, 71 | }) : assert(key != null), 72 | assert(sliver != null), 73 | super(key: key, child: sliver); 74 | 75 | /// The callback to invoke when this widget's visibility changes. 76 | final VisibilityChangedCallback? onVisibilityChanged; 77 | 78 | /// See [RenderObjectWidget.createRenderObject]. 79 | @override 80 | RenderSliverVisibilityDetector createRenderObject(BuildContext context) { 81 | return RenderSliverVisibilityDetector( 82 | key: key!, 83 | onVisibilityChanged: onVisibilityChanged, 84 | ); 85 | } 86 | 87 | /// See [RenderObjectWidget.updateRenderObject]. 88 | @override 89 | void updateRenderObject( 90 | BuildContext context, RenderSliverVisibilityDetector renderObject) { 91 | assert(renderObject.key == key); 92 | renderObject.onVisibilityChanged = onVisibilityChanged; 93 | } 94 | } 95 | 96 | typedef VisibilityChangedCallback = void Function(VisibilityInfo info); 97 | 98 | /// Data passed to the [VisibilityDetector.onVisibilityChanged] callback. 99 | @immutable 100 | class VisibilityInfo { 101 | /// Constructor. 102 | /// 103 | /// `key` corresponds to the [Key] used to construct the corresponding 104 | /// [VisibilityDetector] widget. Must not be null. 105 | /// 106 | /// If `size` or `visibleBounds` are omitted, the [VisibilityInfo] 107 | /// will be initialized to [Offset.zero] or [Rect.zero] respectively. This 108 | /// will indicate that the corresponding widget is competely hidden. 109 | const VisibilityInfo({ 110 | required this.key, 111 | this.size = Size.zero, 112 | this.visibleBounds = Rect.zero, 113 | }) : assert(key != null); 114 | 115 | /// Constructs a [VisibilityInfo] from widget bounds and a corresponding 116 | /// clipping rectangle. 117 | /// 118 | /// [widgetBounds] and [clipRect] are expected to be in the same coordinate 119 | /// system. 120 | factory VisibilityInfo.fromRects({ 121 | required Key key, 122 | required Rect widgetBounds, 123 | required Rect clipRect, 124 | }) { 125 | assert(widgetBounds != null); 126 | assert(clipRect != null); 127 | 128 | final bool overlaps = widgetBounds.overlaps(clipRect); 129 | // Compute the intersection in the widget's local coordinates. 130 | final visibleBounds = overlaps 131 | ? widgetBounds.intersect(clipRect).shift(-widgetBounds.topLeft) 132 | : Rect.zero; 133 | 134 | return VisibilityInfo( 135 | key: key, 136 | size: widgetBounds.size, 137 | visibleBounds: visibleBounds, 138 | ); 139 | } 140 | 141 | /// The key for the corresponding [VisibilityDetector] widget. 142 | final Key key; 143 | 144 | /// The size of the widget. 145 | final Size size; 146 | 147 | /// The visible portion of the widget, in the widget's local coordinates. 148 | /// 149 | /// The bounds are reported using the widget's local coordinates to avoid 150 | /// expectations for the [VisibilityChangedCallback] to fire if the widget's 151 | /// position changes but retains the same visibility. 152 | final Rect visibleBounds; 153 | 154 | /// A fraction in the range \[0, 1\] that represents what proportion of the 155 | /// widget is visible (assuming rectangular bounding boxes). 156 | /// 157 | /// 0 means not visible; 1 means fully visible. 158 | double get visibleFraction { 159 | final visibleArea = _area(visibleBounds.size); 160 | final maxVisibleArea = _area(size); 161 | 162 | if (_floatNear(maxVisibleArea, 0)) { 163 | // Avoid division-by-zero. 164 | return 0; 165 | } 166 | 167 | var visibleFraction = visibleArea / maxVisibleArea; 168 | 169 | if (_floatNear(visibleFraction, 0)) { 170 | visibleFraction = 0; 171 | } else if (_floatNear(visibleFraction, 1)) { 172 | // The inexact nature of floating-point arithmetic means that sometimes 173 | // the visible area might never equal the maximum area (or could even 174 | // be slightly larger than the maximum). Snap to the maximum. 175 | visibleFraction = 1; 176 | } 177 | 178 | assert(visibleFraction >= 0); 179 | assert(visibleFraction <= 1); 180 | return visibleFraction; 181 | } 182 | 183 | /// Returns true if the specified [VisibilityInfo] object has equivalent 184 | /// visibility to this one. 185 | bool matchesVisibility(VisibilityInfo info) { 186 | // We don't override `operator ==` so that object equality can be separate 187 | // from whether two [VisibilityInfo] objects are sufficiently similar 188 | // that we don't need to fire callbacks for both. This could be pertinent 189 | // if other properties are added. 190 | assert(info != null); 191 | return size == info.size && visibleBounds == info.visibleBounds; 192 | } 193 | 194 | @override 195 | String toString() { 196 | return 'VisibilityInfo(key: $key, size: $size visibleBounds: $visibleBounds)'; 197 | } 198 | 199 | @override 200 | int get hashCode => Object.hash(key, size, visibleBounds); 201 | 202 | @override 203 | bool operator ==(Object other) { 204 | return other is VisibilityInfo && 205 | other.key == key && 206 | other.size == size && 207 | other.visibleBounds == visibleBounds; 208 | } 209 | } 210 | 211 | /// The tolerance used to determine whether two floating-point values are 212 | /// approximately equal. 213 | const _kDefaultTolerance = 0.01; 214 | 215 | /// Computes the area of a rectangle of the specified dimensions. 216 | double _area(Size size) { 217 | assert(size != null); 218 | assert(size.width >= 0); 219 | assert(size.height >= 0); 220 | return size.width * size.height; 221 | } 222 | 223 | /// Returns whether two floating-point values are approximately equal. 224 | bool _floatNear(double f1, double f2) { 225 | final absDiff = (f1 - f2).abs(); 226 | return absDiff <= _kDefaultTolerance || 227 | (absDiff / max(f1.abs(), f2.abs()) <= _kDefaultTolerance); 228 | } 229 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/test/seperated_horizontal_scrollable_positioned_list_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:pedantic/pedantic.dart'; 8 | import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; 9 | 10 | const screenHeight = 400.0; 11 | const screenWidth = 400.0; 12 | const itemWidth = screenWidth / 10.0; 13 | const separatorWidth = screenWidth / 20.0; 14 | const itemCount = 500; 15 | const scrollDuration = Duration(seconds: 1); 16 | const tolerance = 10e-5; 17 | 18 | void main() { 19 | Future setUpWidgetTest( 20 | WidgetTester tester, { 21 | ItemScrollController? itemScrollController, 22 | ItemPositionsListener? itemPositionsListener, 23 | bool reverse = false, 24 | EdgeInsets? padding, 25 | int initialScrollIndex = 0, 26 | }) async { 27 | tester.view.devicePixelRatio = 1.0; 28 | tester.view.physicalSize = const Size(screenWidth, screenHeight); 29 | 30 | await tester.pumpWidget( 31 | MaterialApp( 32 | home: ScrollablePositionedList.separated( 33 | itemCount: itemCount, 34 | itemScrollController: itemScrollController, 35 | itemBuilder: (context, index) => SizedBox( 36 | width: itemWidth, 37 | child: Text('Item $index'), 38 | ), 39 | separatorBuilder: (context, index) => SizedBox( 40 | width: separatorWidth, 41 | child: Text('Separator $index'), 42 | ), 43 | itemPositionsListener: itemPositionsListener, 44 | scrollDirection: Axis.horizontal, 45 | reverse: reverse, 46 | padding: padding, 47 | initialScrollIndex: initialScrollIndex, 48 | ), 49 | ), 50 | ); 51 | } 52 | 53 | testWidgets('List positioned with 0 at left', (WidgetTester tester) async { 54 | final itemPositionsListener = ItemPositionsListener.create(); 55 | await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); 56 | 57 | expect(tester.getTopLeft(find.text('Item 0')).dx, 0); 58 | expect(tester.getBottomLeft(find.text('Item 1')).dx, 59 | itemWidth + separatorWidth); 60 | 61 | expect( 62 | itemPositionsListener.itemPositions.value 63 | .firstWhere((position) => position.index == 0) 64 | .itemLeadingEdge, 65 | 0); 66 | expect( 67 | itemPositionsListener.itemPositions.value 68 | .firstWhere((position) => position.index == 1) 69 | .itemLeadingEdge, 70 | _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); 71 | }); 72 | 73 | testWidgets('Scroll to 2 (already on screen)', (WidgetTester tester) async { 74 | final itemScrollController = ItemScrollController(); 75 | final itemPositionsListener = ItemPositionsListener.create(); 76 | await setUpWidgetTest(tester, 77 | itemScrollController: itemScrollController, 78 | itemPositionsListener: itemPositionsListener); 79 | 80 | unawaited( 81 | itemScrollController.scrollTo(index: 2, duration: scrollDuration)); 82 | await tester.pump(); 83 | await tester.pump(scrollDuration); 84 | 85 | expect(find.text('Item 1'), findsNothing); 86 | expect(tester.getTopLeft(find.text('Item 2')).dx, 0); 87 | expect( 88 | tester.getTopLeft(find.text('Item 3')).dx, itemWidth + separatorWidth); 89 | 90 | expect( 91 | itemPositionsListener.itemPositions.value 92 | .firstWhere((position) => position.index == 2) 93 | .itemLeadingEdge, 94 | 0); 95 | expect( 96 | itemPositionsListener.itemPositions.value 97 | .firstWhere((position) => position.index == 3) 98 | .itemLeadingEdge, 99 | _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); 100 | }); 101 | 102 | testWidgets('Scroll to 100 (not already on screen)', 103 | (WidgetTester tester) async { 104 | final itemScrollController = ItemScrollController(); 105 | final itemPositionsListener = ItemPositionsListener.create(); 106 | await setUpWidgetTest(tester, 107 | itemScrollController: itemScrollController, 108 | itemPositionsListener: itemPositionsListener); 109 | 110 | unawaited( 111 | itemScrollController.scrollTo(index: 100, duration: scrollDuration)); 112 | await tester.pumpAndSettle(); 113 | 114 | expect(find.text('Item 99'), findsNothing); 115 | expect(find.text('Item 100'), findsOneWidget); 116 | 117 | expect( 118 | itemPositionsListener.itemPositions.value 119 | .firstWhere((position) => position.index == 100) 120 | .itemLeadingEdge, 121 | 0); 122 | expect( 123 | itemPositionsListener.itemPositions.value 124 | .firstWhere((position) => position.index == 101) 125 | .itemLeadingEdge, 126 | _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); 127 | }); 128 | 129 | testWidgets('Jump to 100', (WidgetTester tester) async { 130 | final itemScrollController = ItemScrollController(); 131 | final itemPositionsListener = ItemPositionsListener.create(); 132 | await setUpWidgetTest(tester, 133 | itemScrollController: itemScrollController, 134 | itemPositionsListener: itemPositionsListener); 135 | 136 | itemScrollController.jumpTo(index: 100); 137 | await tester.pumpAndSettle(); 138 | 139 | expect(tester.getTopLeft(find.text('Item 100')).dx, 0); 140 | expect(tester.getTopLeft(find.text('Item 101')).dx, 141 | itemWidth + separatorWidth); 142 | 143 | expect( 144 | itemPositionsListener.itemPositions.value 145 | .firstWhere((position) => position.index == 100) 146 | .itemLeadingEdge, 147 | 0); 148 | expect( 149 | itemPositionsListener.itemPositions.value 150 | .firstWhere((position) => position.index == 101) 151 | .itemLeadingEdge, 152 | _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); 153 | }); 154 | 155 | testWidgets('padding test - centered sliver at left', 156 | (WidgetTester tester) async { 157 | final itemScrollController = ItemScrollController(); 158 | await setUpWidgetTest( 159 | tester, 160 | itemScrollController: itemScrollController, 161 | padding: const EdgeInsets.all(10), 162 | ); 163 | 164 | expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); 165 | expect(tester.getTopLeft(find.text('Item 1')), 166 | const Offset(10 + itemWidth + separatorWidth, 10)); 167 | expect(tester.getBottomRight(find.text('Item 1')), 168 | const Offset(10 + 2 * itemWidth + separatorWidth, screenHeight - 10)); 169 | 170 | unawaited( 171 | itemScrollController.scrollTo(index: 494, duration: scrollDuration)); 172 | await tester.pumpAndSettle(); 173 | 174 | await tester.drag( 175 | find.byType(ScrollablePositionedList), const Offset(-500, 0)); 176 | await tester.pumpAndSettle(); 177 | 178 | expect(tester.getBottomRight(find.text('Item 499')), 179 | const Offset(screenWidth - 10, screenHeight - 10)); 180 | }); 181 | 182 | testWidgets('padding test - centered sliver not at left', 183 | (WidgetTester tester) async { 184 | final itemScrollController = ItemScrollController(); 185 | final itemPositionsListener = ItemPositionsListener.create(); 186 | await setUpWidgetTest( 187 | tester, 188 | itemScrollController: itemScrollController, 189 | itemPositionsListener: itemPositionsListener, 190 | initialScrollIndex: 2, 191 | padding: const EdgeInsets.all(10), 192 | ); 193 | 194 | await tester.drag( 195 | find.byType(ScrollablePositionedList), const Offset(300, 0)); 196 | await tester.pumpAndSettle(); 197 | 198 | expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); 199 | expect(tester.getTopLeft(find.text('Item 2')), 200 | const Offset(10 + 2 * (itemWidth + separatorWidth), 10)); 201 | expect(tester.getTopLeft(find.text('Item 3')), 202 | const Offset(10 + 3 * (itemWidth + separatorWidth), 10)); 203 | 204 | expect( 205 | itemPositionsListener.itemPositions.value 206 | .firstWhere((position) => position.index == 2) 207 | .itemLeadingEdge, 208 | closeTo( 209 | 10 / screenWidth + 2 * ((itemWidth + separatorWidth) / screenWidth), 210 | tolerance)); 211 | }); 212 | } 213 | 214 | double _screenProportion( 215 | {required double numberOfItems, required double numberOfSeparators}) => 216 | (numberOfItems * itemWidth + numberOfSeparators * separatorWidth) / 217 | screenHeight; 218 | -------------------------------------------------------------------------------- /packages/scrollable_positioned_list/test/reversed_scrollable_positioned_list_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:pedantic/pedantic.dart'; 8 | import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; 9 | 10 | const screenHeight = 400.0; 11 | const screenWidth = 400.0; 12 | const itemHeight = screenHeight / 10.0; 13 | const itemCount = 500; 14 | const scrollDuration = Duration(seconds: 1); 15 | 16 | void main() { 17 | Future setUpWidgetTest( 18 | WidgetTester tester, { 19 | ItemScrollController? itemScrollController, 20 | ItemPositionsListener? itemPositionsListener, 21 | EdgeInsets? padding, 22 | int initialIndex = 0, 23 | }) async { 24 | tester.view.devicePixelRatio = 1.0; 25 | tester.view.physicalSize = const Size(screenWidth, screenHeight); 26 | 27 | await tester.pumpWidget( 28 | MaterialApp( 29 | home: ScrollablePositionedList.builder( 30 | itemCount: itemCount, 31 | initialScrollIndex: initialIndex, 32 | itemScrollController: itemScrollController, 33 | itemBuilder: (context, index) => SizedBox( 34 | height: itemHeight, 35 | child: Text('Item $index'), 36 | ), 37 | itemPositionsListener: itemPositionsListener, 38 | reverse: true, 39 | padding: padding, 40 | ), 41 | ), 42 | ); 43 | } 44 | 45 | testWidgets('List positioned with 0 at bottom', (WidgetTester tester) async { 46 | final itemPositionsListener = ItemPositionsListener.create(); 47 | await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); 48 | 49 | expect(tester.getBottomRight(find.text('Item 0')).dy, screenHeight); 50 | expect(tester.getTopLeft(find.text('Item 9')).dy, 0); 51 | expect(find.text('Item 10'), findsNothing); 52 | 53 | expect( 54 | itemPositionsListener.itemPositions.value 55 | .firstWhere((position) => position.index == 0) 56 | .itemLeadingEdge, 57 | 0); 58 | expect( 59 | itemPositionsListener.itemPositions.value 60 | .firstWhere((position) => position.index == 9) 61 | .itemTrailingEdge, 62 | 1); 63 | }); 64 | 65 | testWidgets('Scroll to 1 then 2 (both already on screen)', 66 | (WidgetTester tester) async { 67 | final itemScrollController = ItemScrollController(); 68 | final itemPositionsListener = ItemPositionsListener.create(); 69 | await setUpWidgetTest(tester, 70 | itemScrollController: itemScrollController, 71 | itemPositionsListener: itemPositionsListener); 72 | 73 | unawaited( 74 | itemScrollController.scrollTo(index: 1, duration: scrollDuration)); 75 | await tester.pump(); 76 | await tester.pump(scrollDuration); 77 | expect(find.text('Item 0'), findsNothing); 78 | expect( 79 | itemPositionsListener.itemPositions.value 80 | .firstWhere((position) => position.index == 1) 81 | .itemLeadingEdge, 82 | 0); 83 | expect(tester.getBottomRight(find.text('Item 1')).dy, screenHeight); 84 | 85 | unawaited( 86 | itemScrollController.scrollTo(index: 2, duration: scrollDuration)); 87 | await tester.pump(); 88 | await tester.pump(scrollDuration); 89 | 90 | expect(find.text('Item 1'), findsNothing); 91 | expect(tester.getBottomRight(find.text('Item 2')).dy, screenHeight); 92 | 93 | expect( 94 | itemPositionsListener.itemPositions.value 95 | .firstWhere((position) => position.index == 2) 96 | .itemLeadingEdge, 97 | 0); 98 | expect( 99 | itemPositionsListener.itemPositions.value 100 | .firstWhere((position) => position.index == 11) 101 | .itemTrailingEdge, 102 | 1); 103 | }); 104 | 105 | testWidgets('Scroll to 5 (already on screen) and then back to 0', 106 | (WidgetTester tester) async { 107 | final itemScrollController = ItemScrollController(); 108 | final itemPositionsListener = ItemPositionsListener.create(); 109 | await setUpWidgetTest(tester, 110 | itemScrollController: itemScrollController, 111 | itemPositionsListener: itemPositionsListener); 112 | 113 | unawaited( 114 | itemScrollController.scrollTo(index: 5, duration: scrollDuration)); 115 | await tester.pumpAndSettle(); 116 | unawaited( 117 | itemScrollController.scrollTo(index: 0, duration: scrollDuration)); 118 | await tester.pumpAndSettle(); 119 | 120 | expect(find.text('Item 0'), findsOneWidget); 121 | expect(find.text('Item 9'), findsOneWidget); 122 | expect(find.text('Item 10'), findsNothing); 123 | 124 | expect( 125 | itemPositionsListener.itemPositions.value 126 | .firstWhere((position) => position.index == 0) 127 | .itemLeadingEdge, 128 | 0); 129 | expect( 130 | itemPositionsListener.itemPositions.value 131 | .firstWhere((position) => position.index == 9) 132 | .itemTrailingEdge, 133 | 1); 134 | }); 135 | 136 | testWidgets('Scroll to 100 (not already on screen)', 137 | (WidgetTester tester) async { 138 | final itemScrollController = ItemScrollController(); 139 | final itemPositionsListener = ItemPositionsListener.create(); 140 | await setUpWidgetTest(tester, 141 | itemScrollController: itemScrollController, 142 | itemPositionsListener: itemPositionsListener); 143 | 144 | unawaited( 145 | itemScrollController.scrollTo(index: 100, duration: scrollDuration)); 146 | await tester.pumpAndSettle(); 147 | 148 | expect(find.text('Item 99'), findsNothing); 149 | expect(find.text('Item 100'), findsOneWidget); 150 | 151 | expect( 152 | itemPositionsListener.itemPositions.value 153 | .firstWhere((position) => position.index == 100) 154 | .itemLeadingEdge, 155 | 0); 156 | expect( 157 | itemPositionsListener.itemPositions.value 158 | .firstWhere((position) => position.index == 109) 159 | .itemTrailingEdge, 160 | 1); 161 | }); 162 | 163 | testWidgets('Jump to 100', (WidgetTester tester) async { 164 | final itemScrollController = ItemScrollController(); 165 | final itemPositionsListener = ItemPositionsListener.create(); 166 | await setUpWidgetTest(tester, 167 | itemScrollController: itemScrollController, 168 | itemPositionsListener: itemPositionsListener); 169 | 170 | itemScrollController.jumpTo(index: 100); 171 | await tester.pumpAndSettle(); 172 | 173 | expect(tester.getBottomRight(find.text('Item 100')).dy, screenHeight); 174 | expect(tester.getTopLeft(find.text('Item 109')).dy, 0); 175 | 176 | expect( 177 | itemPositionsListener.itemPositions.value 178 | .firstWhere((position) => position.index == 100) 179 | .itemLeadingEdge, 180 | 0); 181 | expect( 182 | itemPositionsListener.itemPositions.value 183 | .firstWhere((position) => position.index == 109) 184 | .itemTrailingEdge, 185 | 1); 186 | }); 187 | 188 | testWidgets('padding test - centered sliver at bottom', 189 | (WidgetTester tester) async { 190 | final itemScrollController = ItemScrollController(); 191 | await setUpWidgetTest( 192 | tester, 193 | itemScrollController: itemScrollController, 194 | padding: const EdgeInsets.all(10), 195 | ); 196 | 197 | expect(tester.getBottomLeft(find.text('Item 0')), 198 | const Offset(10, screenHeight - 10)); 199 | expect(tester.getBottomLeft(find.text('Item 1')), 200 | const Offset(10, screenHeight - (10 + itemHeight))); 201 | expect(tester.getTopRight(find.text('Item 1')), 202 | const Offset(screenWidth - 10, screenHeight - (10 + 2 * itemHeight))); 203 | 204 | unawaited( 205 | itemScrollController.scrollTo(index: 490, duration: scrollDuration)); 206 | await tester.pumpAndSettle(); 207 | 208 | await tester.drag( 209 | find.byType(ScrollablePositionedList), const Offset(0, 100)); 210 | await tester.pumpAndSettle(); 211 | 212 | expect(tester.getTopLeft(find.text('Item 499')), const Offset(10, 10)); 213 | }); 214 | 215 | testWidgets('padding test - centered sliver not at bottom', 216 | (WidgetTester tester) async { 217 | final itemScrollController = ItemScrollController(); 218 | await setUpWidgetTest( 219 | tester, 220 | itemScrollController: itemScrollController, 221 | initialIndex: 2, 222 | padding: const EdgeInsets.all(10), 223 | ); 224 | 225 | await tester.drag( 226 | find.byType(ScrollablePositionedList), const Offset(0, -200)); 227 | await tester.pumpAndSettle(); 228 | 229 | expect(tester.getBottomLeft(find.text('Item 0')), 230 | const Offset(10, screenHeight - 10)); 231 | expect(tester.getBottomLeft(find.text('Item 2')), 232 | const Offset(10, screenHeight - (10 + 2 * itemHeight))); 233 | expect(tester.getBottomLeft(find.text('Item 3')), 234 | const Offset(10, screenHeight - (10 + 3 * itemHeight))); 235 | }); 236 | } 237 | --------------------------------------------------------------------------------