├── .github └── workflows │ └── main.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── assets ├── demo.gif └── min_demo.gif ├── benchmark └── benchmark.dart ├── example ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── examples.dart │ ├── main.dart │ ├── ui │ │ ├── lang_page.dart │ │ ├── search_page.dart │ │ ├── test_page.dart │ │ └── ui.dart │ └── util │ │ ├── box.dart │ │ ├── highlight_text.dart │ │ ├── languages.dart │ │ └── util.dart ├── pubspec.yaml ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ └── Icon-512.png │ ├── index.html │ └── manifest.json └── windows │ ├── CMakeLists.txt │ ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake │ └── runner │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── resources │ └── app_icon.ico │ ├── run_loop.cpp │ ├── run_loop.h │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── lib ├── animated_list_plus.dart ├── src │ ├── custom_sliver_animated_list.dart │ ├── diff │ │ ├── diff.dart │ │ ├── diff_callback.dart │ │ ├── diff_delegate.dart │ │ ├── diff_model.dart │ │ ├── myers_diff.dart │ │ └── path_node.dart │ ├── handle.dart │ ├── implicitly_animated_list.dart │ ├── implicitly_animated_list_base.dart │ ├── implicitly_animated_reorderable_list.dart │ ├── reorderable.dart │ ├── src.dart │ ├── transitions │ │ ├── size_fade_transition.dart │ │ └── transitions.dart │ └── util │ │ ├── handler.dart │ │ ├── invisible.dart │ │ ├── key_extensions.dart │ │ ├── sliver_child_separated_builder_delegate.dart │ │ └── util.dart └── transitions.dart ├── pubspec.yaml └── test └── diff_test.dart /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | version: [3.7.0, 3.7.3] 15 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 16 | with: 17 | flutter_channel: "stable" 18 | flutter_version: ${{ matrix.version }} 19 | test_recursion: true 20 | min_coverage: 0 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/* 17 | 18 | # Lock files 19 | pubspec.lock 20 | 21 | # Visual Studio Code related 22 | .classpath 23 | .project 24 | .settings/ 25 | .vscode/* 26 | version 27 | 28 | # packages file containing multi-root paths 29 | .packages.generated 30 | 31 | # Flutter/Dart/Pub related 32 | **/doc/api/ 33 | **/ios/Flutter/.last_build_id 34 | .dart_tool/ 35 | .flutter-plugins 36 | .flutter-plugins-dependencies 37 | .packages 38 | .pub-cache/ 39 | .pub/ 40 | build/ 41 | flutter_*.png 42 | linked_*.ds 43 | unlinked.ds 44 | unlinked_spec.ds 45 | .fvm/ 46 | 47 | # Android related 48 | **/android/**/gradle-wrapper.jar 49 | **/android/.gradle 50 | **/android/captures/ 51 | **/android/gradlew 52 | **/android/gradlew.bat 53 | **/android/local.properties 54 | **/android/**/GeneratedPluginRegistrant.java 55 | **/android/key.properties 56 | **/android/.idea/ 57 | *.jks 58 | 59 | # iOS/XCode related 60 | **/ios/**/*.mode1v3 61 | **/ios/**/*.mode2v3 62 | **/ios/**/*.moved-aside 63 | **/ios/**/*.pbxuser 64 | **/ios/**/*.perspectivev3 65 | **/ios/**/*sync/ 66 | **/ios/**/.sconsign.dblite 67 | **/ios/**/.tags* 68 | **/ios/**/.vagrant/ 69 | **/ios/**/DerivedData/ 70 | **/ios/**/Icon? 71 | **/ios/**/Pods/ 72 | **/ios/**/.symlinks/ 73 | **/ios/**/profile 74 | **/ios/**/xcuserdata 75 | **/ios/.generated/ 76 | **/ios/Flutter/App.framework 77 | **/ios/Flutter/Flutter.framework 78 | **/ios/Flutter/Flutter.podspec 79 | **/ios/Flutter/Generated.xcconfig 80 | **/ios/Flutter/app.flx 81 | **/ios/Flutter/app.zip 82 | **/ios/Flutter/.last_build_id 83 | **/ios/Flutter/flutter_assets/ 84 | **/ios/Flutter/flutter_export_environment.sh 85 | **/ios/ServiceDefinitions.json 86 | **/ios/Runner/GeneratedPluginRegistrant.* 87 | ios/Podfile.lock 88 | macos/Podfile.lock 89 | 90 | # Coverage 91 | coverage/ 92 | 93 | # Web related 94 | lib/generated_plugin_registrant.dart 95 | 96 | # Symbolication related 97 | app.*.symbols 98 | 99 | # Obfuscation related 100 | app.*.map.json 101 | 102 | # Exceptions to the above rules. 103 | !**/ios/**/default.mode1v3 104 | !**/ios/**/default.mode2v3 105 | !**/ios/**/default.pbxuser 106 | !**/ios/**/default.perspectivev3 107 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 108 | !/dev/ci/**/Gemfile.lock 109 | !.vscode/extensions.json 110 | !.vscode/launch.json 111 | !.idea/codeStyles/ 112 | !.idea/dictionaries/ 113 | !.idea/runConfigurations/ 114 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.2 2 | 3 | - Add `clipBehavior` argument 4 | 5 | ## 0.5.1 6 | 7 | - Add support for separator builder. You can now use `SliverImplicitlyAnimatedList.separated` or `ImplicitlyAnimatedReorderableList.separated`. 8 | 9 | ## 0.5.0 10 | 11 | - Minim supported Flutter version is 3.7.0 12 | - Use `.maybeOf` because `.of` is throwing a Exception in 3.7.0 13 | 14 | ## 0.4.5 15 | 16 | - Fix compatibility with new Flutter 3.7.0. They now also have an `AnimatedItemBuilder` type. 17 | 18 | ## 0.4.4 19 | 20 | - Fix README 21 | 22 | ## 0.4.3 23 | 24 | - Saved the old `implicitly_animated_reorderable_list` 25 | - Updated to Flutter 3 26 | 27 | - Saved the old `implicitly_animated_reorderable_list` 28 | - Updated to Flutter 3 29 | 30 | ## 0.4.2 31 | 32 | - **Fixed** #54, #72 33 | 34 | ## 0.4.0 35 | 36 | - **Added** NNBD support 37 | - **Fixed** #19, #49, #50, #52 38 | - **Improved** `Handle` is now able to capture pointer events which allows an `ImplicitlyAnimatedReorderableList` to be placed inside another scrollable without any workarounds. 39 | - **Breaking** Renamed `dragDuration` to `reorderDuration` 40 | - **Added** Field `liftDuration` 41 | - **Added** Field `settleDuration` 42 | 43 | ## 0.3.2 44 | 45 | - **Fixed** #47 46 | 47 | ## 0.3.1 48 | 49 | - **Fixed** #43 50 | - **Fixed** Changelog 51 | 52 | ## 0.3.0 53 | 54 | - **Fixed** #23 55 | 56 | ## 0.2.5 57 | 58 | - **Fixed** #14 59 | 60 | ## 0.2.1 61 | 62 | - **Improved** `ImplicitlyAnimatedList` now always uses the latest items, even if `listEquals()` is `true`. 63 | 64 | ## 0.2.0 65 | 66 | - **Added** support for headers and footers on the `ImplicitlyAnimatedReorderableList`. 67 | - **Added** `child` property on `Reorderable` that can be used instead off the `builder` that will use a default elevation animation instead of being forced to specify your own custom animation. 68 | 69 | ## 0.1.10 70 | 71 | - **Fixed** Bugs 72 | 73 | ## 0.1.4 74 | 75 | - **Improved** `Handle` is now scroll aware and only initiates a drag when the scroll position didn't change. 76 | - **Added** horizontal scrollDirection support for `ImplicitlyAnimatedReorderableList` 77 | 78 | ## 0.1.0 79 | 80 | - Initial release 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 bxqm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Animated List Plus 2 | 3 | Resurrection of the discontinued `implicitly_animated_reorderable_list` Plugin. 4 | 5 | A Flutter `ListView` that implicitly calculates the changes between two lists using the `MyersDiff` algorithm and animates between them for you. The `ImplicitlyAnimatedReorderableList` adds reordering support to its items with fully custom animations. 6 | 7 |

8 | Demo 9 |

10 | 11 | Click [here](https://github.com/wwwdata/implicitly_animated_reorderable_list/blob/master/example/lib/ui/) to view the full example. 12 | 13 | ## Installing 14 | 15 | Add it to your `pubspec.yaml` file: 16 | 17 | ```yaml 18 | dependencies: 19 | animated_list_plus: ^0.4.3 20 | ``` 21 | 22 | Install packages from the command line 23 | 24 | ``` 25 | flutter packages get 26 | ``` 27 | 28 | If you like this package, consider supporting it by giving it a star on [GitHub](https://github.com/wwwdata/implicitly_animated_reorderable_list) and a like on [pub.dev](https://pub.dev/packages/implicitly_animated_reorderable_list) :heart: 29 | 30 | ## Usage 31 | 32 | The package contains two `ListViews`: `ImplicitlyAnimatedList` which is the base class and offers implicit animation for item `insertions`, `removals` as well as `updates`, and `ImplicitlyAnimatedReorderableList` which extends the `ImplicitlyAnimatedList` and adds reordering support to its items. See examples below on how to use them. 33 | 34 | ### ImplicitlyAnimatedList 35 | 36 | `ImplicitlyAnimatedList` is based on `AnimatedList` and uses the `MyersDiff` algorithm to calculate the difference between two lists and calls `insertItem` and `removeItem` on the `AnimatedListState` for you. 37 | 38 | #### Example 39 | 40 | ```dart 41 | // Specify the generic type of the data in the list. 42 | ImplicitlyAnimatedList( 43 | // The current items in the list. 44 | items: items, 45 | // Called by the DiffUtil to decide whether two object represent the same item. 46 | // For example, if your items have unique ids, this method should check their id equality. 47 | areItemsTheSame: (a, b) => a.id == b.id, 48 | // Called, as needed, to build list item widgets. 49 | // List items are only built when they're scrolled into view. 50 | itemBuilder: (context, animation, item, index) { 51 | // Specifiy a transition to be used by the ImplicitlyAnimatedList. 52 | // See the Transitions section on how to import this transition. 53 | return SizeFadeTransition( 54 | sizeFraction: 0.7, 55 | curve: Curves.easeInOut, 56 | animation: animation, 57 | child: Text(item.name), 58 | ); 59 | }, 60 | // An optional builder when an item was removed from the list. 61 | // If not specified, the List uses the itemBuilder with 62 | // the animation reversed. 63 | removeItemBuilder: (context, animation, oldItem) { 64 | return FadeTransition( 65 | opacity: animation, 66 | child: Text(oldItem.name), 67 | ); 68 | }, 69 | ); 70 | ``` 71 | 72 | > If you have a `CustomScrollView` you can use the `SliverImplicitlyAnimatedList`. 73 | 74 | > As `AnimatedList` doesn't support item moves, a move is handled by removing the item from the old index and inserting it at the new index. 75 | 76 | ### ImplicitlyAnimatedReorderableList 77 | 78 | `ImplicitlyAnimatedReorderableList` is based on `ImplicitlyAnimatedList` and adds reordering support to the list. 79 | 80 | #### Example 81 | 82 | ```dart 83 | ImplicitlyAnimatedReorderableList( 84 | items: items, 85 | areItemsTheSame: (oldItem, newItem) => oldItem.id == newItem.id, 86 | onReorderFinished: (item, from, to, newItems) { 87 | // Remember to update the underlying data when the list has been 88 | // reordered. 89 | setState(() { 90 | items 91 | ..clear() 92 | ..addAll(newItems); 93 | }); 94 | }, 95 | itemBuilder: (context, itemAnimation, item, index) { 96 | // Each item must be wrapped in a Reorderable widget. 97 | return Reorderable( 98 | // Each item must have an unique key. 99 | key: ValueKey(item), 100 | // The animation of the Reorderable builder can be used to 101 | // change to appearance of the item between dragged and normal 102 | // state. For example to add elevation when the item is being dragged. 103 | // This is not to be confused with the animation of the itemBuilder. 104 | // Implicit animations (like AnimatedContainer) are sadly not yet supported. 105 | builder: (context, dragAnimation, inDrag) { 106 | final t = dragAnimation.value; 107 | final elevation = lerpDouble(0, 8, t); 108 | final color = Color.lerp(Colors.white, Colors.white.withOpacity(0.8), t); 109 | 110 | return SizeFadeTransition( 111 | sizeFraction: 0.7, 112 | curve: Curves.easeInOut, 113 | animation: itemAnimation, 114 | child: Material( 115 | color: color, 116 | elevation: elevation, 117 | type: MaterialType.transparency, 118 | child: ListTile( 119 | title: Text(item.name), 120 | // The child of a Handle can initialize a drag/reorder. 121 | // This could for example be an Icon or the whole item itself. You can 122 | // use the delay parameter to specify the duration for how long a pointer 123 | // must press the child, until it can be dragged. 124 | trailing: Handle( 125 | delay: const Duration(milliseconds: 100), 126 | child: Icon( 127 | Icons.list, 128 | color: Colors.grey, 129 | ), 130 | ), 131 | ), 132 | ), 133 | ); 134 | }, 135 | ); 136 | }, 137 | // Since version 0.2.0 you can also display a widget 138 | // before the reorderable items... 139 | header: Container( 140 | height: 200, 141 | color: Colors.red, 142 | ), 143 | // ...and after. Note that this feature - as the list itself - is still in beta! 144 | footer: Container( 145 | height: 200, 146 | color: Colors.green, 147 | ), 148 | // If you want to use headers or footers, you should set shrinkWrap to true 149 | shrinkWrap: true, 150 | ); 151 | ``` 152 | 153 | > For a more in depth example click [here](https://github.com/wwwdata/implicitly_animated_reorderable_list/blob/master/example/lib/ui/lang_page.dart). 154 | 155 | ### Transitions 156 | 157 | You can use some custom transitions (such as the `SizeFadeTransition`) for item animations by importing the transitions pack: 158 | 159 | ```dart 160 | import 'package:implicitly_animated_reorderable_list/transitions.dart'; 161 | ``` 162 | 163 | If you want to contribute your own custom transitions, feel free to make a pull request. 164 | 165 | ### Caveats 166 | 167 | Note that this package is still in its very early phase and not enough testing has been done to guarantee stability. 168 | 169 | Also note that computing the diff between two very large lists may take a significant amount of time (the computation is done on a background isolate though unless `spawnIsolate` is set to `false`). 170 | 171 | ### Acknowledgements 172 | 173 | The diff algorithm that `ImplicitlyAnimatedList` uses was written by [Dawid Bota](https://gitlab.com/otsoaUnLoco) at [GitLab](https://gitlab.com/otsoaUnLoco/animated-stream-list). 174 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/assets/demo.gif -------------------------------------------------------------------------------- /assets/min_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/assets/min_demo.gif -------------------------------------------------------------------------------- /benchmark/benchmark.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:animated_list_plus/animated_list_plus.dart'; 4 | 5 | class Item { 6 | Item(this.id, this.value); 7 | 8 | int id; 9 | int value; 10 | 11 | @override 12 | bool operator ==(Object other) => 13 | identical(this, other) || 14 | other is Item && runtimeType == other.runtimeType && id == other.id; 15 | 16 | @override 17 | int get hashCode => id.hashCode; 18 | } 19 | 20 | Future main() async { 21 | final List list = List.generate(1000, (index) => Item(index, index)); 22 | final List newList = List.from(list)..shuffle(); 23 | 24 | final start = DateTime.now(); 25 | await MyersDiff.diff( 26 | newList, 27 | list, 28 | areItemsTheSame: (a, b) => a.id == b.id, 29 | ); 30 | 31 | final millis = DateTime.now().difference(start).inMilliseconds; 32 | print('Diffing ${newList.length} elements took $millis milliseconds.'); 33 | } 34 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 31 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | applicationId "com.example.example" 41 | minSdkVersion 16 42 | targetSdkVersion 31 43 | versionCode flutterVersionCode.toInteger() 44 | versionName flutterVersionName 45 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | testImplementation 'junit:junit:4.12' 64 | androidTestImplementation 'androidx.test:runner:1.1.1' 65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 66 | } 67 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import androidx.annotation.NonNull; 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 10 | GeneratedPluginRegistrant.registerWith(flutterEngine); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.0' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/examples.dart: -------------------------------------------------------------------------------- 1 | /* import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart'; 5 | 6 | import 'util/box.dart'; 7 | 8 | class Examples extends StatelessWidget { 9 | const Examples({Key key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | // * ImplicitlyAnimatedList 14 | // Specify the generic type of the data in the list. 15 | ImplicitlyAnimatedList( 16 | // The current items in the list. 17 | data: items, 18 | // Called by the DiffUtil to decide whether two object represent the same item. 19 | // For example, if your items have unique ids, this method should check their id equality. 20 | areItemsTheSame: (a, b) => a.id == b.id, 21 | // Called, as needed, to build list item widgets. 22 | // List items are only built when they're scrolled into view. 23 | itemBuilder: (context, animation, item, index) { 24 | // Specifiy a transition to be used by the ImplicitlyAnimatedList. 25 | // In this case a custom transition. 26 | return SizeFadeTranstion( 27 | sizeFraction: 0.7, 28 | curve: Curves.easeInOut, 29 | animation: animation, 30 | child: Text(item.name), 31 | ); 32 | }, 33 | // An optional builder when an item was removed from the list. 34 | // If not specified, the List uses the itemBuilder with 35 | // the animation reversed. 36 | removedItemBuilder: (context, animation, oldItem) { 37 | return FadeTransition( 38 | opacity: animation, 39 | child: Text(oldItem.name), 40 | ); 41 | }, 42 | ); 43 | 44 | // * ImplicitlyAnimatedReorderableList 45 | ImplicitlyAnimatedReorderableList( 46 | data: items, 47 | areItemsTheSame: (a, b) => a.id == b.id, 48 | itemBuilder: (context, itemAnimation, item, index) { 49 | // Each item must be wrapped in a Reorderable widget. 50 | return Reorderable( 51 | // Each item must have a unique key. 52 | key: ValueKey(item), 53 | // The animation of the Reorderable builder can be used to 54 | // change to appearance of the item between dragged and normal 55 | // state. For example to add elevation. Implicit animation are 56 | // not yet supported. 57 | builder: (context, dragAnimation, inDrag) { 58 | final t = dragAnimation.value; 59 | 60 | final elevation = lerpDouble(0, 8, t); 61 | final color = Color.lerp(Colors.white, Colors.white.withOpacity(0.8), t); 62 | 63 | return SizeFadeTranstion( 64 | sizeFraction: 0.7, 65 | curve: Curves.easeInOut, 66 | animation: itemAnimation, 67 | child: Box( 68 | color: color, 69 | elevation: elevation, 70 | child: ListTile( 71 | title: Text(item.name), 72 | // The child of a Handle can initialize a drag/reorder. 73 | // This could for example be an Icon or the whole item itself. You can 74 | // use the delay parameter to specify the duration for how long a pointer 75 | // must press the child, until it can be dragged. 76 | trailing: Handle( 77 | delay: const Duration(milliseconds: 100), 78 | child: Icon( 79 | Icons.list, 80 | color: Colors.grey, 81 | ), 82 | ), 83 | ), 84 | ), 85 | ); 86 | }, 87 | ); 88 | }, 89 | ); 90 | } 91 | } 92 | */ 93 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import './ui/lang_page.dart'; 4 | 5 | void main() => runApp(MyApp()); 6 | 7 | class MyApp extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | SystemChrome.setSystemUIOverlayStyle( 11 | const SystemUiOverlayStyle( 12 | statusBarColor: Colors.transparent, 13 | ), 14 | ); 15 | 16 | return MaterialApp( 17 | title: 'Implicitly Animated Reorderable List Example', 18 | theme: ThemeData.light().copyWith( 19 | dividerTheme: DividerThemeData( 20 | thickness: 1, 21 | color: Colors.grey.shade300, 22 | ), 23 | ), 24 | debugShowCheckedModeBanner: false, 25 | home: const LanguagePage(), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/lib/ui/lang_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:animated_list_plus/animated_list_plus.dart'; 4 | import 'package:animated_list_plus/transitions.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_slidable/flutter_slidable.dart'; 7 | 8 | import '../util/util.dart'; 9 | import 'ui.dart'; 10 | 11 | class LanguagePage extends StatefulWidget { 12 | const LanguagePage({ 13 | Key? key, 14 | }) : super(key: key); 15 | 16 | @override 17 | _LanguagePageState createState() => _LanguagePageState(); 18 | } 19 | 20 | class _LanguagePageState extends State 21 | with SingleTickerProviderStateMixin { 22 | static const double _horizontalHeight = 96; 23 | static const List options = [ 24 | 'Shuffle', 25 | 'Test', 26 | ]; 27 | 28 | final List selectedLanguages = [ 29 | english, 30 | german, 31 | spanish, 32 | french, 33 | ]; 34 | 35 | bool inReorder = false; 36 | 37 | ScrollController scrollController = ScrollController(); 38 | 39 | void onReorderFinished(List newItems) { 40 | scrollController.jumpTo(scrollController.offset); 41 | setState(() { 42 | inReorder = false; 43 | 44 | selectedLanguages 45 | ..clear() 46 | ..addAll(newItems); 47 | }); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | final theme = Theme.of(context); 53 | final textTheme = theme.textTheme; 54 | 55 | return Scaffold( 56 | appBar: AppBar( 57 | title: const Text('Examples'), 58 | actions: [ 59 | _buildPopupMenuButton(textTheme), 60 | ], 61 | ), 62 | body: ListView( 63 | controller: scrollController, 64 | // Prevent the ListView from scrolling when an item is 65 | // currently being dragged. 66 | padding: const EdgeInsets.only(bottom: 24), 67 | children: [ 68 | _buildHeadline('Vertically'), 69 | const Divider(height: 0), 70 | _buildVerticalLanguageList(), 71 | _buildHeadline('Horizontally'), 72 | _buildHorizontalLanguageList(), 73 | const SizedBox(height: 500), 74 | ], 75 | ), 76 | ); 77 | } 78 | 79 | // * An example of a vertically reorderable list. 80 | Widget _buildVerticalLanguageList() { 81 | final theme = Theme.of(context); 82 | 83 | Reorderable buildReorderable( 84 | Language lang, 85 | Widget Function(Widget tile) transition, 86 | ) { 87 | return Reorderable( 88 | key: ValueKey(lang), 89 | builder: (context, dragAnimation, inDrag) { 90 | final tile = Column( 91 | mainAxisSize: MainAxisSize.min, 92 | children: [ 93 | _buildTile(lang), 94 | const Divider(height: 0), 95 | ], 96 | ); 97 | 98 | return AnimatedBuilder( 99 | animation: dragAnimation, 100 | builder: (context, _) { 101 | final t = dragAnimation.value; 102 | final color = Color.lerp(Colors.white, Colors.grey.shade100, t); 103 | 104 | return Material( 105 | color: color, 106 | elevation: lerpDouble(0, 8, t)!, 107 | child: transition(tile), 108 | ); 109 | }, 110 | ); 111 | }, 112 | ); 113 | } 114 | 115 | return ImplicitlyAnimatedReorderableList( 116 | items: selectedLanguages, 117 | shrinkWrap: true, 118 | reorderDuration: const Duration(milliseconds: 200), 119 | liftDuration: const Duration(milliseconds: 300), 120 | physics: const NeverScrollableScrollPhysics(), 121 | padding: EdgeInsets.zero, 122 | areItemsTheSame: (oldItem, newItem) => oldItem == newItem, 123 | onReorderStarted: (item, index) => setState(() => inReorder = true), 124 | onReorderFinished: (movedLanguage, from, to, newItems) { 125 | // Update the underlying data when the item has been reordered! 126 | onReorderFinished(newItems); 127 | }, 128 | itemBuilder: (context, itemAnimation, lang, index) { 129 | return buildReorderable(lang, (tile) { 130 | return SizeFadeTransition( 131 | sizeFraction: 0.7, 132 | curve: Curves.easeInOut, 133 | animation: itemAnimation, 134 | child: tile, 135 | ); 136 | }); 137 | }, 138 | updateItemBuilder: (context, itemAnimation, lang) { 139 | return buildReorderable(lang, (tile) { 140 | return FadeTransition( 141 | opacity: itemAnimation, 142 | child: tile, 143 | ); 144 | }); 145 | }, 146 | footer: _buildFooter(context, theme.textTheme), 147 | ); 148 | } 149 | 150 | Widget _buildHorizontalLanguageList() { 151 | return Container( 152 | height: _horizontalHeight, 153 | alignment: Alignment.center, 154 | child: ImplicitlyAnimatedReorderableList( 155 | items: selectedLanguages, 156 | padding: const EdgeInsets.symmetric(horizontal: 24), 157 | scrollDirection: Axis.horizontal, 158 | areItemsTheSame: (oldItem, newItem) => oldItem == newItem, 159 | onReorderStarted: (item, index) => setState(() => inReorder = true), 160 | onReorderFinished: (item, from, to, newItems) => 161 | onReorderFinished(newItems), 162 | itemBuilder: (context, itemAnimation, item, index) { 163 | return Reorderable( 164 | key: ValueKey(item.toString()), 165 | builder: (context, dragAnimation, inDrag) { 166 | final t = dragAnimation.value; 167 | final box = _buildBox(item, t); 168 | 169 | return SizeFadeTransition( 170 | animation: itemAnimation, 171 | axis: Axis.horizontal, 172 | axisAlignment: 1.0, 173 | curve: Curves.ease, 174 | child: box, 175 | ); 176 | }, 177 | ); 178 | }, 179 | updateItemBuilder: (context, itemAnimation, item) { 180 | return Reorderable( 181 | key: ValueKey(item.toString()), 182 | child: FadeTransition( 183 | opacity: itemAnimation, 184 | child: _buildBox(item, 0), 185 | ), 186 | ); 187 | }, 188 | ), 189 | ); 190 | } 191 | 192 | Widget _buildTile(Language lang) { 193 | final theme = Theme.of(context); 194 | final textTheme = theme.textTheme; 195 | 196 | final List actions = [ 197 | SlideAction( 198 | closeOnTap: true, 199 | color: Colors.redAccent, 200 | onTap: () => setState(() => selectedLanguages.remove(lang)), 201 | child: Center( 202 | child: Column( 203 | mainAxisSize: MainAxisSize.min, 204 | children: [ 205 | const Icon( 206 | Icons.delete, 207 | color: Colors.white, 208 | ), 209 | const SizedBox(height: 4), 210 | Text( 211 | 'Delete', 212 | style: textTheme.bodyText2?.copyWith( 213 | color: Colors.white, 214 | ), 215 | ), 216 | ], 217 | ), 218 | ), 219 | ), 220 | ]; 221 | 222 | return Slidable( 223 | actionPane: const SlidableBehindActionPane(), 224 | actions: actions, 225 | secondaryActions: actions, 226 | child: Container( 227 | alignment: Alignment.center, 228 | // For testing different size item. You can comment this line 229 | padding: lang.englishName == 'English' 230 | ? const EdgeInsets.symmetric(vertical: 16.0) 231 | : EdgeInsets.zero, 232 | child: ListTile( 233 | title: Text( 234 | lang.nativeName, 235 | style: textTheme.bodyText2?.copyWith( 236 | fontSize: 16, 237 | ), 238 | ), 239 | subtitle: Text( 240 | lang.englishName, 241 | style: textTheme.bodyText1?.copyWith( 242 | fontSize: 15, 243 | ), 244 | ), 245 | leading: SizedBox( 246 | width: 36, 247 | height: 36, 248 | child: Center( 249 | child: Text( 250 | '${selectedLanguages.indexOf(lang) + 1}', 251 | style: textTheme.bodyText2?.copyWith( 252 | color: theme.highlightColor, 253 | fontSize: 16, 254 | ), 255 | ), 256 | ), 257 | ), 258 | trailing: const Handle( 259 | delay: Duration(milliseconds: 0), 260 | capturePointer: true, 261 | child: Icon( 262 | Icons.drag_handle, 263 | color: Colors.grey, 264 | ), 265 | ), 266 | ), 267 | ), 268 | ); 269 | } 270 | 271 | Widget _buildBox(Language item, double t) { 272 | final theme = Theme.of(context); 273 | final textTheme = theme.textTheme; 274 | 275 | final elevation = lerpDouble(0, 8, t)!; 276 | 277 | return Handle( 278 | delay: const Duration(milliseconds: 500), 279 | child: Box( 280 | height: _horizontalHeight, 281 | borderRadius: 8, 282 | border: Border.all( 283 | color: Colors.grey.shade300, 284 | width: 2, 285 | ), 286 | elevation: elevation, 287 | padding: const EdgeInsets.all(16), 288 | color: Colors.white, 289 | margin: const EdgeInsets.only(right: 8), 290 | child: Center( 291 | child: Column( 292 | mainAxisSize: MainAxisSize.min, 293 | children: [ 294 | Text( 295 | item.nativeName, 296 | style: textTheme.bodyText2, 297 | ), 298 | const SizedBox(height: 8), 299 | Text( 300 | item.englishName, 301 | style: textTheme.bodyText1, 302 | ), 303 | ], 304 | ), 305 | ), 306 | ), 307 | ); 308 | } 309 | 310 | Widget _buildFooter(BuildContext context, TextTheme textTheme) { 311 | return Box( 312 | color: Colors.white, 313 | onTap: () async { 314 | final result = await Navigator.push( 315 | context, 316 | MaterialPageRoute( 317 | builder: (context) => const LanguageSearchPage(), 318 | ), 319 | ); 320 | 321 | if (result != null && !selectedLanguages.contains(result)) { 322 | setState(() { 323 | selectedLanguages.add(result); 324 | }); 325 | } 326 | }, 327 | child: Column( 328 | mainAxisSize: MainAxisSize.min, 329 | children: [ 330 | ListTile( 331 | leading: const SizedBox( 332 | height: 36, 333 | width: 36, 334 | child: Center( 335 | child: Icon( 336 | Icons.add, 337 | color: Colors.grey, 338 | ), 339 | ), 340 | ), 341 | title: Text( 342 | 'Add a language', 343 | style: textTheme.bodyText1?.copyWith( 344 | fontSize: 16, 345 | ), 346 | ), 347 | ), 348 | const Divider(height: 0), 349 | ], 350 | ), 351 | ); 352 | } 353 | 354 | Widget _buildHeadline(String headline) { 355 | final theme = Theme.of(context); 356 | final textTheme = theme.textTheme; 357 | 358 | Widget buildDivider() => Container( 359 | height: 2, 360 | color: Colors.grey.shade300, 361 | ); 362 | 363 | return Column( 364 | mainAxisSize: MainAxisSize.min, 365 | crossAxisAlignment: CrossAxisAlignment.start, 366 | children: [ 367 | const SizedBox(height: 16), 368 | buildDivider(), 369 | Padding( 370 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), 371 | child: Text( 372 | headline, 373 | style: textTheme.bodyText1?.copyWith( 374 | fontWeight: FontWeight.bold, 375 | ), 376 | ), 377 | ), 378 | buildDivider(), 379 | const SizedBox(height: 16), 380 | ], 381 | ); 382 | } 383 | 384 | Widget _buildPopupMenuButton(TextTheme textTheme) { 385 | return PopupMenuButton( 386 | padding: const EdgeInsets.all(0), 387 | shape: RoundedRectangleBorder( 388 | borderRadius: BorderRadius.circular(8), 389 | ), 390 | onSelected: (value) { 391 | switch (value) { 392 | case 'Shuffle': 393 | setState(selectedLanguages.shuffle); 394 | break; 395 | case 'Test': 396 | Navigator.push( 397 | context, 398 | MaterialPageRoute( 399 | builder: (_) => const TestPage(), 400 | ), 401 | ); 402 | break; 403 | } 404 | }, 405 | itemBuilder: (context) => options.map((option) { 406 | return PopupMenuItem( 407 | value: option, 408 | child: Text( 409 | option, 410 | style: textTheme.bodyText1, 411 | ), 412 | ); 413 | }).toList(), 414 | ); 415 | } 416 | 417 | @override 418 | void dispose() { 419 | scrollController.dispose(); 420 | super.dispose(); 421 | } 422 | } 423 | 424 | class Pair { 425 | final A first; 426 | final B second; 427 | Pair( 428 | this.first, 429 | this.second, 430 | ); 431 | } 432 | -------------------------------------------------------------------------------- /example/lib/ui/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:animated_list_plus/animated_list_plus.dart'; 2 | import 'package:animated_list_plus/transitions.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../util/util.dart'; 6 | 7 | class LanguageSearchPage extends StatefulWidget { 8 | const LanguageSearchPage({Key? key}) : super(key: key); 9 | 10 | @override 11 | _LanguageSearchPageState createState() => _LanguageSearchPageState(); 12 | } 13 | 14 | class _LanguageSearchPageState extends State { 15 | final List filteredLanguages = List.from(languages); 16 | 17 | late final _controller = TextEditingController() 18 | ..addListener( 19 | _onQueryChanged, 20 | ); 21 | 22 | String get text => _controller.text.trim(); 23 | 24 | void _onQueryChanged() { 25 | filteredLanguages.clear(); 26 | 27 | if (text.isEmpty) { 28 | filteredLanguages 29 | ..clear() 30 | ..addAll(languages); 31 | 32 | setState(() {}); 33 | 34 | return; 35 | } 36 | 37 | final query = text.toLowerCase(); 38 | for (final lang in languages) { 39 | final englishName = lang.englishName.toLowerCase(); 40 | final nativeName = lang.nativeName.toLowerCase(); 41 | final startsWith = 42 | englishName.startsWith(query) || nativeName.startsWith(query); 43 | 44 | if (startsWith) { 45 | filteredLanguages.add(lang); 46 | } 47 | } 48 | 49 | for (final lang in languages) { 50 | final englishName = lang.englishName.toLowerCase(); 51 | final nativeName = lang.nativeName.toLowerCase(); 52 | final contains = 53 | englishName.contains(query) || nativeName.contains(query); 54 | 55 | if (contains && !filteredLanguages.contains(lang)) { 56 | filteredLanguages.add(lang); 57 | } 58 | } 59 | 60 | setState(() {}); 61 | } 62 | 63 | Widget _buildItem(Language lang) { 64 | final theme = Theme.of(context); 65 | final textTheme = theme.textTheme; 66 | return Box( 67 | border: Border( 68 | bottom: BorderSide( 69 | color: Colors.grey.shade200, 70 | ), 71 | ), 72 | color: Colors.white, 73 | onTap: () => Navigator.pop(context, lang), 74 | child: ListTile( 75 | title: HighlightText( 76 | query: text, 77 | text: lang.nativeName, 78 | style: textTheme.bodyText2?.copyWith( 79 | fontSize: 16, 80 | ), 81 | activeStyle: textTheme.bodyText2?.copyWith( 82 | fontSize: 16, 83 | fontWeight: FontWeight.w900, 84 | ), 85 | ), 86 | subtitle: HighlightText( 87 | query: text, 88 | text: lang.englishName, 89 | style: textTheme.bodyText1?.copyWith( 90 | fontSize: 15, 91 | ), 92 | activeStyle: textTheme.bodyText1?.copyWith( 93 | fontSize: 15, 94 | fontWeight: FontWeight.bold, 95 | ), 96 | ), 97 | ), 98 | ); 99 | } 100 | 101 | @override 102 | Widget build(BuildContext context) { 103 | final theme = Theme.of(context); 104 | final textTheme = theme.textTheme; 105 | final padding = MediaQuery.of(context).viewPadding.top; 106 | 107 | return Scaffold( 108 | appBar: _buildAppBar(padding, theme, textTheme), 109 | body: AnimatedSwitcher( 110 | duration: const Duration(milliseconds: 300), 111 | child: filteredLanguages.isNotEmpty 112 | ? _buildList() 113 | : _buildNoLanguagesPlaceholder(), 114 | ), 115 | ); 116 | } 117 | 118 | Widget _buildList() { 119 | return ImplicitlyAnimatedList( 120 | items: filteredLanguages, 121 | updateDuration: const Duration(milliseconds: 400), 122 | areItemsTheSame: (a, b) => a == b, 123 | itemBuilder: (context, animation, lang, _) { 124 | return SizeFadeTransition( 125 | sizeFraction: 0.7, 126 | curve: Curves.easeInOut, 127 | animation: animation, 128 | child: _buildItem(lang), 129 | ); 130 | }, 131 | updateItemBuilder: (context, animation, lang) { 132 | return FadeTransition( 133 | opacity: animation, 134 | child: _buildItem(lang), 135 | ); 136 | }, 137 | ); 138 | } 139 | 140 | PreferredSize _buildAppBar( 141 | double padding, ThemeData theme, TextTheme textTheme) { 142 | return PreferredSize( 143 | preferredSize: Size.fromHeight(56 + padding), 144 | child: Box( 145 | height: 56 + padding, 146 | width: double.infinity, 147 | color: theme.highlightColor, 148 | elevation: 4, 149 | shadowColor: Colors.black.withOpacity(0.2), 150 | child: Column( 151 | children: [ 152 | SizedBox(height: padding), 153 | Expanded( 154 | child: Row( 155 | children: [ 156 | const BackButton( 157 | color: Colors.white, 158 | ), 159 | Expanded( 160 | child: TextField( 161 | autofocus: true, 162 | controller: _controller, 163 | textInputAction: TextInputAction.search, 164 | style: textTheme.bodyText2?.copyWith( 165 | color: Colors.white, 166 | fontSize: 18, 167 | ), 168 | decoration: InputDecoration( 169 | border: InputBorder.none, 170 | contentPadding: 171 | const EdgeInsets.symmetric(horizontal: 16), 172 | hintText: 'Search for a language', 173 | hintStyle: textTheme.bodyText2?.copyWith( 174 | color: Colors.grey.shade200, 175 | fontSize: 16, 176 | ), 177 | ), 178 | ), 179 | ), 180 | AnimatedOpacity( 181 | duration: const Duration(milliseconds: 350), 182 | opacity: text.isEmpty ? 0.0 : 1.0, 183 | child: IconButton( 184 | icon: const Icon( 185 | Icons.clear, 186 | color: Colors.white, 187 | ), 188 | onPressed: () => _controller.text = '', 189 | ), 190 | ), 191 | ], 192 | ), 193 | ), 194 | ], 195 | ), 196 | ), 197 | ); 198 | } 199 | 200 | Widget _buildNoLanguagesPlaceholder() { 201 | return Center( 202 | child: Column( 203 | mainAxisSize: MainAxisSize.min, 204 | children: const [ 205 | Icon( 206 | Icons.translate, 207 | color: Colors.grey, 208 | ), 209 | SizedBox(height: 16), 210 | Text( 211 | 'No languages found!', 212 | style: TextStyle( 213 | color: Colors.black, 214 | fontWeight: FontWeight.bold, 215 | ), 216 | ), 217 | ], 218 | ), 219 | ); 220 | } 221 | 222 | @override 223 | void dispose() { 224 | _controller.dispose(); 225 | super.dispose(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /example/lib/ui/test_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:animated_list_plus/animated_list_plus.dart'; 5 | import 'package:animated_list_plus/transitions.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class TestPage extends StatefulWidget { 9 | const TestPage(); 10 | 11 | @override 12 | State createState() => TestPageState(); 13 | } 14 | 15 | class TestPageState extends State { 16 | static const maxLength = 1000; 17 | final _controller = ScrollController(); 18 | 19 | List nestedList = List.generate(maxLength, (i) => Test(i)); 20 | Timer? _timer; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | 26 | // crazyListOperationMadness(); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | _controller.dispose(); 32 | super.dispose(); 33 | } 34 | 35 | void crazyListOperationMadness() { 36 | void assignNewList() { 37 | nestedList = List.generate(Random().nextInt(maxLength), (i) => Test(i)) 38 | ..shuffle(); 39 | 40 | setState(() {}); 41 | } 42 | 43 | _timer = Timer.periodic( 44 | const Duration(milliseconds: 10), 45 | (_) async { 46 | assignNewList(); 47 | assignNewList(); 48 | nestedList = List.generate(Random().nextInt(maxLength), (i) => Test(i)) 49 | ..shuffle(); 50 | setState(() {}); 51 | }, 52 | ); 53 | } 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | final theme = Theme.of(context); 58 | final textTheme = theme.textTheme; 59 | 60 | return Scaffold( 61 | appBar: AppBar(backgroundColor: Colors.amber), 62 | body: Scrollbar( 63 | controller: _controller, 64 | child: ImplicitlyAnimatedReorderableList( 65 | controller: _controller, 66 | padding: const EdgeInsets.all(24), 67 | items: nestedList, 68 | areItemsTheSame: (oldItem, newItem) => oldItem == newItem, 69 | onReorderFinished: (item, from, to, newList) { 70 | setState(() { 71 | nestedList 72 | ..clear() 73 | ..addAll(newList); 74 | }); 75 | }, 76 | header: InkWell( 77 | onTap: () { 78 | if (_timer == null) { 79 | crazyListOperationMadness(); 80 | } else { 81 | _timer?.cancel(); 82 | _timer = null; 83 | } 84 | }, 85 | child: Container( 86 | height: 120, 87 | color: _timer == null ? Colors.red : Colors.yellow, 88 | child: Center( 89 | child: Text( 90 | 'Header', 91 | style: textTheme.headline6?.copyWith(color: Colors.white), 92 | ), 93 | ), 94 | ), 95 | ), 96 | footer: Container( 97 | height: 120, 98 | color: Colors.red, 99 | child: Center( 100 | child: Text( 101 | 'Footer', 102 | style: textTheme.headline6?.copyWith(color: Colors.white), 103 | ), 104 | ), 105 | ), 106 | itemBuilder: (context, itemAnimation, item, index) { 107 | return Reorderable( 108 | key: ValueKey(item), 109 | builder: (context, dragAnimation, inDrag) => AnimatedBuilder( 110 | animation: dragAnimation, 111 | builder: (context, child) => Card( 112 | elevation: 4, 113 | // SizeFadeTransition clips, so use the 114 | // Card as a parent to avoid the box shadow 115 | // to be clipped. 116 | child: SizeFadeTransition( 117 | animation: itemAnimation, 118 | child: Handle( 119 | delay: const Duration(milliseconds: 600), 120 | child: Container( 121 | height: 120, 122 | padding: const EdgeInsets.symmetric(horizontal: 10), 123 | child: Row( 124 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 125 | children: [ 126 | Text('${item.key}'), 127 | const Icon(Icons.menu), 128 | ], 129 | ), 130 | ), 131 | ), 132 | ), 133 | ), 134 | ), 135 | ); 136 | }, 137 | ), 138 | ), 139 | ); 140 | } 141 | } 142 | 143 | class Test { 144 | final int key; 145 | Test(this.key); 146 | 147 | @override 148 | bool operator ==(Object o) { 149 | if (identical(this, o)) return true; 150 | 151 | return o is Test && o.key == key; 152 | } 153 | 154 | @override 155 | int get hashCode => key.hashCode; 156 | } 157 | -------------------------------------------------------------------------------- /example/lib/ui/ui.dart: -------------------------------------------------------------------------------- 1 | export 'lang_page.dart'; 2 | export 'search_page.dart'; 3 | export 'test_page.dart'; 4 | -------------------------------------------------------------------------------- /example/lib/util/box.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | enum ShadowDirection { 6 | topLeft, 7 | top, 8 | topRight, 9 | right, 10 | bottomRight, 11 | bottom, 12 | bottomLeft, 13 | left, 14 | center, 15 | } 16 | 17 | class Box extends StatelessWidget { 18 | final double borderRadius; 19 | final double elevation; 20 | final double? height; 21 | final double? width; 22 | final Border? border; 23 | final BorderRadius? customBorders; 24 | final EdgeInsets? margin; 25 | final EdgeInsets? padding; 26 | final Widget? child; 27 | final Color color; 28 | final Color shadowColor; 29 | final List? boxShadows; 30 | final VoidCallback? onTap; 31 | final VoidCallback? onLongPress; 32 | final VoidCallback? onDoubleTap; 33 | final BoxShape boxShape; 34 | final AlignmentGeometry? alignment; 35 | final ShadowDirection shadowDirection; 36 | final Color? splashColor; 37 | final Duration? duration; 38 | final BoxConstraints? constraints; 39 | const Box({ 40 | Key? key, 41 | this.child, 42 | this.border, 43 | this.color = Colors.transparent, 44 | this.borderRadius = 0.0, 45 | this.elevation = 0.0, 46 | this.splashColor, 47 | this.shadowColor = Colors.black12, 48 | this.onTap, 49 | this.onDoubleTap, 50 | this.onLongPress, 51 | this.height, 52 | this.width, 53 | this.margin, 54 | this.customBorders, 55 | this.alignment, 56 | this.boxShadows, 57 | this.constraints, 58 | this.duration, 59 | this.boxShape = BoxShape.rectangle, 60 | this.shadowDirection = ShadowDirection.bottomRight, 61 | this.padding = const EdgeInsets.all(0), 62 | }) : super(key: key); 63 | 64 | static const wrap = -1; 65 | 66 | bool get circle => boxShape == BoxShape.circle; 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | final theme = Theme.of(context); 71 | final w = width; 72 | final h = height; 73 | final br = customBorders ?? 74 | BorderRadius.circular( 75 | boxShape == BoxShape.rectangle 76 | ? borderRadius 77 | : w != null 78 | ? w / 2.0 79 | : h != null 80 | ? h / 2.0 81 | : 0, 82 | ); 83 | 84 | Widget content = Padding( 85 | padding: padding ?? EdgeInsets.zero, 86 | child: child, 87 | ); 88 | 89 | if (boxShape == BoxShape.circle || 90 | (customBorders != null || borderRadius > 0.0)) { 91 | content = ClipRRect( 92 | borderRadius: br, 93 | child: content, 94 | ); 95 | } 96 | 97 | if (onTap != null || onLongPress != null || onDoubleTap != null) { 98 | content = Material( 99 | color: Colors.transparent, 100 | type: MaterialType.transparency, 101 | shape: circle 102 | ? const CircleBorder() 103 | : RoundedRectangleBorder(borderRadius: br), 104 | child: InkWell( 105 | splashColor: splashColor ?? theme.splashColor, 106 | highlightColor: theme.highlightColor, 107 | hoverColor: theme.hoverColor, 108 | focusColor: theme.focusColor, 109 | customBorder: circle 110 | ? const CircleBorder() 111 | : RoundedRectangleBorder(borderRadius: br), 112 | onTap: onTap, 113 | onLongPress: onLongPress, 114 | onDoubleTap: onDoubleTap, 115 | child: content, 116 | ), 117 | ); 118 | } 119 | 120 | final List? boxShadow = boxShadows ?? 121 | ((elevation > 0 && (shadowColor.opacity > 0)) 122 | ? [ 123 | BoxShadow( 124 | color: shadowColor, 125 | offset: _getShadowOffset(min(elevation / 5.0, 1.0)), 126 | blurRadius: elevation, 127 | spreadRadius: 0, 128 | ), 129 | ] 130 | : null); 131 | 132 | final boxDecoration = BoxDecoration( 133 | color: color, 134 | borderRadius: circle || br == BorderRadius.zero ? null : br, 135 | shape: boxShape, 136 | boxShadow: boxShadow, 137 | border: border, 138 | ); 139 | 140 | return duration != null 141 | ? AnimatedContainer( 142 | height: h, 143 | width: w, 144 | margin: margin, 145 | alignment: alignment, 146 | duration: duration!, 147 | decoration: boxDecoration, 148 | constraints: constraints, 149 | child: content, 150 | ) 151 | : Container( 152 | height: h, 153 | width: w, 154 | margin: margin, 155 | alignment: alignment, 156 | decoration: boxDecoration, 157 | constraints: constraints, 158 | child: content, 159 | ); 160 | } 161 | 162 | Offset _getShadowOffset(double ele) { 163 | final ym = 5 * ele; 164 | final xm = 2 * ele; 165 | switch (shadowDirection) { 166 | case ShadowDirection.topLeft: 167 | return Offset(-1 * xm, -1 * ym); 168 | case ShadowDirection.top: 169 | return Offset(0, -1 * ym); 170 | case ShadowDirection.topRight: 171 | return Offset(xm, -1 * ym); 172 | case ShadowDirection.right: 173 | return Offset(xm, 0); 174 | case ShadowDirection.bottomRight: 175 | return Offset(xm, ym); 176 | case ShadowDirection.bottom: 177 | return Offset(0, ym); 178 | case ShadowDirection.bottomLeft: 179 | return Offset(-1 * xm, ym); 180 | case ShadowDirection.left: 181 | return Offset(-1 * xm, 0); 182 | default: 183 | return Offset.zero; 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /example/lib/util/highlight_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HighlightText extends StatefulWidget { 4 | final TextStyle? activeStyle; 5 | final TextStyle? style; 6 | final String query; 7 | final String text; 8 | final TextAlign textAlign; 9 | final TextDirection? textDirection; 10 | final bool softWrap; 11 | final TextOverflow overflow; 12 | final double textScaleFactor; 13 | final int? maxLines; 14 | const HighlightText({ 15 | Key? key, 16 | this.activeStyle, 17 | this.style, 18 | this.query = '', 19 | this.text = '', 20 | this.textAlign = TextAlign.start, 21 | this.textDirection, 22 | this.softWrap = true, 23 | this.overflow = TextOverflow.ellipsis, 24 | this.textScaleFactor = 1.0, 25 | this.maxLines, 26 | }) : super(key: key); 27 | 28 | @override 29 | _HighlightTextState createState() => _HighlightTextState(); 30 | } 31 | 32 | class _HighlightTextState extends State { 33 | TextStyle get style => widget.style ?? Theme.of(context).textTheme.bodyText2!; 34 | TextStyle get activeStyle => 35 | widget.activeStyle ?? style.copyWith(fontWeight: FontWeight.bold); 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | final idxs = getQueryHighlights(widget.text, widget.query); 40 | 41 | return RichText( 42 | textAlign: widget.textAlign, 43 | maxLines: widget.maxLines, 44 | overflow: widget.overflow, 45 | softWrap: widget.softWrap, 46 | textDirection: widget.textDirection, 47 | textScaleFactor: widget.textScaleFactor, 48 | text: TextSpan( 49 | children: idxs.map((idx) { 50 | return TextSpan( 51 | text: widget.text.substring(idx.first, idx.second), 52 | style: idx.third ? activeStyle : style, 53 | ); 54 | }).toList(), 55 | ), 56 | ); 57 | } 58 | } 59 | 60 | String replaceLast(String source, String matcher, String replacement) { 61 | final index = source.lastIndexOf(matcher); 62 | return source.replaceRange(index, index + matcher.length, replacement); 63 | } 64 | 65 | List> getQueryHighlights(String text, String query) { 66 | final t = text.toLowerCase(); 67 | final q = query.toLowerCase(); 68 | 69 | if (t.isEmpty || q.isEmpty || !t.contains(q)) 70 | return [Triplet(0, t.length, false)]; 71 | 72 | List> idxs = []; 73 | 74 | var w = t; 75 | do { 76 | final i = w.lastIndexOf(q); 77 | final e = i + q.length; 78 | if (i != -1) { 79 | w = replaceLast(w, q, ''); 80 | idxs.insert(0, Triplet(i, e, true)); 81 | } 82 | } while (w.contains(q)); 83 | 84 | if (idxs.isEmpty) { 85 | idxs.add(Triplet(0, t.length, false)); 86 | } else { 87 | final List> result = []; 88 | Triplet? last; 89 | 90 | for (final idx in idxs) { 91 | final isLast = idx == idxs.last; 92 | if (last == null) { 93 | if (idx.first == 0) { 94 | result.add(idx); 95 | } else { 96 | result 97 | ..add(Triplet(0, idx.first, false)) 98 | ..add(idx); 99 | } 100 | } else if (last.second == idx.first) { 101 | result.add(idx); 102 | } else { 103 | result 104 | ..add(Triplet(last.second, idx.first, false)) 105 | ..add(idx); 106 | } 107 | 108 | if (isLast && idx.second != t.length) { 109 | result.add(Triplet(idx.second, t.length, false)); 110 | } 111 | 112 | last = idx; 113 | } 114 | 115 | idxs = result; 116 | } 117 | 118 | return idxs; 119 | } 120 | 121 | class Triplet { 122 | A first; 123 | B second; 124 | C third; 125 | Triplet( 126 | this.first, 127 | this.second, 128 | this.third, 129 | ); 130 | 131 | Triplet copyWith({ 132 | A? first, 133 | B? second, 134 | C? third, 135 | }) { 136 | return Triplet( 137 | first ?? this.first, 138 | second ?? this.second, 139 | third ?? this.third, 140 | ); 141 | } 142 | 143 | @override 144 | String toString() => 'Triple first: $first, second: $second, third: $third'; 145 | 146 | @override 147 | bool operator ==(Object o) { 148 | return o is Triplet && 149 | o.first == first && 150 | o.second == second && 151 | o.third == third; 152 | } 153 | 154 | @override 155 | int get hashCode { 156 | return hashList([ 157 | first, 158 | second, 159 | third, 160 | ]); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /example/lib/util/languages.dart: -------------------------------------------------------------------------------- 1 | const english = Language( 2 | englishName: 'English', 3 | nativeName: 'English', 4 | ); 5 | 6 | const french = Language( 7 | englishName: 'French', 8 | nativeName: 'Français', 9 | ); 10 | 11 | const german = Language( 12 | englishName: 'German', 13 | nativeName: 'Deutsch', 14 | ); 15 | 16 | const spanish = Language( 17 | englishName: 'Spanish', 18 | nativeName: 'Español', 19 | ); 20 | 21 | const chinese = Language( 22 | englishName: 'Chinese', 23 | nativeName: '中文', 24 | ); 25 | 26 | const danish = Language( 27 | englishName: 'Danish', 28 | nativeName: 'Dansk', 29 | ); 30 | 31 | const hindi = Language( 32 | englishName: 'Hindi', 33 | nativeName: 'हिंदी', 34 | ); 35 | 36 | const afrikaans = Language( 37 | englishName: 'Afrikaans', 38 | nativeName: 'Afrikaans', 39 | ); 40 | 41 | const portuguese = Language( 42 | englishName: 'Portuguese', 43 | nativeName: 'Português', 44 | ); 45 | 46 | const List languages = [ 47 | english, 48 | french, 49 | german, 50 | spanish, 51 | chinese, 52 | danish, 53 | hindi, 54 | afrikaans, 55 | portuguese, 56 | ]; 57 | 58 | class Language { 59 | final String englishName; 60 | final String nativeName; 61 | const Language({ 62 | required this.englishName, 63 | required this.nativeName, 64 | }); 65 | 66 | @override 67 | String toString() => 68 | 'Language englishName: $englishName, nativeName: $nativeName'; 69 | 70 | @override 71 | bool operator ==(Object o) { 72 | if (identical(this, o)) return true; 73 | 74 | return o is Language && 75 | o.englishName == englishName && 76 | o.nativeName == nativeName; 77 | } 78 | 79 | @override 80 | int get hashCode => englishName.hashCode ^ nativeName.hashCode; 81 | } 82 | -------------------------------------------------------------------------------- /example/lib/util/util.dart: -------------------------------------------------------------------------------- 1 | export 'box.dart'; 2 | export 'highlight_text.dart'; 3 | export 'languages.dart'; 4 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: Example for how to use an ImplicitlyAnimatedReorderableList 3 | publish_to: none 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | 10 | dependencies: 11 | animated_list_plus: 12 | path: ../ 13 | flutter: 14 | sdk: flutter 15 | flutter_slidable: ^0.6.0-nullsafety.0 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | 21 | flutter: 22 | uses-material-design: true 23 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | example 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /example/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(example LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "example") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | 54 | # === Installation === 55 | # Support files are copied into place next to the executable, so that it can 56 | # run in place. This is done instead of making a separate bundle (as on Linux) 57 | # so that building and running from within Visual Studio will work. 58 | set(BUILD_BUNDLE_DIR "$") 59 | # Make the "install" step default, as it's required to run. 60 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 61 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 62 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 63 | endif() 64 | 65 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 66 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 67 | 68 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 69 | COMPONENT Runtime) 70 | 71 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 72 | COMPONENT Runtime) 73 | 74 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 75 | COMPONENT Runtime) 76 | 77 | if(PLUGIN_BUNDLED_LIBRARIES) 78 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 79 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 80 | COMPONENT Runtime) 81 | endif() 82 | 83 | # Fully re-copy the assets directory on each build to avoid having stale files 84 | # from a previous install. 85 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 86 | install(CODE " 87 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 88 | " COMPONENT Runtime) 89 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 90 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 91 | 92 | # Install the AOT library on non-Debug builds only. 93 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 94 | CONFIGURATIONS Profile;Release 95 | COMPONENT Runtime) 96 | -------------------------------------------------------------------------------- /example/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "run_loop.cpp" 8 | "utils.cpp" 9 | "win32_window.cpp" 10 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 11 | "Runner.rc" 12 | "runner.exe.manifest" 13 | ) 14 | apply_standard_settings(${BINARY_NAME}) 15 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 16 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 17 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 18 | add_dependencies(${BINARY_NAME} flutter_assemble) 19 | -------------------------------------------------------------------------------- /example/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "A new Flutter project." "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "example" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "example.exe" "\0" 98 | VALUE "ProductName", "example" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(RunLoop* run_loop, 8 | const flutter::DartProject& project) 9 | : run_loop_(run_loop), project_(project) {} 10 | 11 | FlutterWindow::~FlutterWindow() {} 12 | 13 | bool FlutterWindow::OnCreate() { 14 | if (!Win32Window::OnCreate()) { 15 | return false; 16 | } 17 | 18 | RECT frame = GetClientArea(); 19 | 20 | // The size here must match the window dimensions to avoid unnecessary surface 21 | // creation / destruction in the startup path. 22 | flutter_controller_ = std::make_unique( 23 | frame.right - frame.left, frame.bottom - frame.top, project_); 24 | // Ensure that basic setup of the controller was successful. 25 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 26 | return false; 27 | } 28 | RegisterPlugins(flutter_controller_->engine()); 29 | run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); 30 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 31 | return true; 32 | } 33 | 34 | void FlutterWindow::OnDestroy() { 35 | if (flutter_controller_) { 36 | run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); 37 | flutter_controller_ = nullptr; 38 | } 39 | 40 | Win32Window::OnDestroy(); 41 | } 42 | 43 | LRESULT 44 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 45 | WPARAM const wparam, 46 | LPARAM const lparam) noexcept { 47 | // Give Flutter, including plugins, an opporutunity to handle window messages. 48 | if (flutter_controller_) { 49 | std::optional result = 50 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 51 | lparam); 52 | if (result) { 53 | return *result; 54 | } 55 | } 56 | 57 | switch (message) { 58 | case WM_FONTCHANGE: 59 | flutter_controller_->engine()->ReloadSystemFonts(); 60 | break; 61 | } 62 | 63 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 64 | } 65 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "run_loop.h" 10 | #include "win32_window.h" 11 | 12 | // A window that does nothing but host a Flutter view. 13 | class FlutterWindow : public Win32Window { 14 | public: 15 | // Creates a new FlutterWindow driven by the |run_loop|, hosting a 16 | // Flutter view running |project|. 17 | explicit FlutterWindow(RunLoop* run_loop, 18 | const flutter::DartProject& project); 19 | virtual ~FlutterWindow(); 20 | 21 | protected: 22 | // Win32Window: 23 | bool OnCreate() override; 24 | void OnDestroy() override; 25 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 26 | LPARAM const lparam) noexcept override; 27 | 28 | private: 29 | // The run loop driving events for this window. 30 | RunLoop* run_loop_; 31 | 32 | // The project to run. 33 | flutter::DartProject project_; 34 | 35 | // The Flutter instance hosted by this window. 36 | std::unique_ptr flutter_controller_; 37 | }; 38 | 39 | #endif // RUNNER_FLUTTER_WINDOW_H_ 40 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "run_loop.h" 7 | #include "utils.h" 8 | 9 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 10 | _In_ wchar_t *command_line, _In_ int show_command) { 11 | // Attach to console when present (e.g., 'flutter run') or create a 12 | // new console when running with a debugger. 13 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 14 | CreateAndAttachConsole(); 15 | } 16 | 17 | // Initialize COM, so that it is available for use in the library and/or 18 | // plugins. 19 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 20 | 21 | RunLoop run_loop; 22 | 23 | flutter::DartProject project(L"data"); 24 | 25 | std::vector command_line_arguments = 26 | GetCommandLineArguments(); 27 | 28 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 29 | 30 | FlutterWindow window(&run_loop, project); 31 | Win32Window::Point origin(10, 10); 32 | Win32Window::Size size(1280, 720); 33 | if (!window.CreateAndShow(L"example", origin, size)) { 34 | return EXIT_FAILURE; 35 | } 36 | window.SetQuitOnClose(true); 37 | 38 | run_loop.Run(); 39 | 40 | ::CoUninitialize(); 41 | return EXIT_SUCCESS; 42 | } 43 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwdata/implicitly_animated_reorderable_list/99bf2830efa9049f166340417505f593228eaff4/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /example/windows/runner/run_loop.cpp: -------------------------------------------------------------------------------- 1 | #include "run_loop.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | RunLoop::RunLoop() {} 8 | 9 | RunLoop::~RunLoop() {} 10 | 11 | void RunLoop::Run() { 12 | bool keep_running = true; 13 | TimePoint next_flutter_event_time = TimePoint::clock::now(); 14 | while (keep_running) { 15 | std::chrono::nanoseconds wait_duration = 16 | std::max(std::chrono::nanoseconds(0), 17 | next_flutter_event_time - TimePoint::clock::now()); 18 | ::MsgWaitForMultipleObjects( 19 | 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), 20 | QS_ALLINPUT); 21 | bool processed_events = false; 22 | MSG message; 23 | // All pending Windows messages must be processed; MsgWaitForMultipleObjects 24 | // won't return again for items left in the queue after PeekMessage. 25 | while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { 26 | processed_events = true; 27 | if (message.message == WM_QUIT) { 28 | keep_running = false; 29 | break; 30 | } 31 | ::TranslateMessage(&message); 32 | ::DispatchMessage(&message); 33 | // Allow Flutter to process messages each time a Windows message is 34 | // processed, to prevent starvation. 35 | next_flutter_event_time = 36 | std::min(next_flutter_event_time, ProcessFlutterMessages()); 37 | } 38 | // If the PeekMessage loop didn't run, process Flutter messages. 39 | if (!processed_events) { 40 | next_flutter_event_time = 41 | std::min(next_flutter_event_time, ProcessFlutterMessages()); 42 | } 43 | } 44 | } 45 | 46 | void RunLoop::RegisterFlutterInstance( 47 | flutter::FlutterEngine* flutter_instance) { 48 | flutter_instances_.insert(flutter_instance); 49 | } 50 | 51 | void RunLoop::UnregisterFlutterInstance( 52 | flutter::FlutterEngine* flutter_instance) { 53 | flutter_instances_.erase(flutter_instance); 54 | } 55 | 56 | RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { 57 | TimePoint next_event_time = TimePoint::max(); 58 | for (auto instance : flutter_instances_) { 59 | std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); 60 | if (wait_duration != std::chrono::nanoseconds::max()) { 61 | next_event_time = 62 | std::min(next_event_time, TimePoint::clock::now() + wait_duration); 63 | } 64 | } 65 | return next_event_time; 66 | } 67 | -------------------------------------------------------------------------------- /example/windows/runner/run_loop.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_RUN_LOOP_H_ 2 | #define RUNNER_RUN_LOOP_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | // A runloop that will service events for Flutter instances as well 10 | // as native messages. 11 | class RunLoop { 12 | public: 13 | RunLoop(); 14 | ~RunLoop(); 15 | 16 | // Prevent copying 17 | RunLoop(RunLoop const&) = delete; 18 | RunLoop& operator=(RunLoop const&) = delete; 19 | 20 | // Runs the run loop until the application quits. 21 | void Run(); 22 | 23 | // Registers the given Flutter instance for event servicing. 24 | void RegisterFlutterInstance( 25 | flutter::FlutterEngine* flutter_instance); 26 | 27 | // Unregisters the given Flutter instance from event servicing. 28 | void UnregisterFlutterInstance( 29 | flutter::FlutterEngine* flutter_instance); 30 | 31 | private: 32 | using TimePoint = std::chrono::steady_clock::time_point; 33 | 34 | // Processes all currently pending messages for registered Flutter instances. 35 | TimePoint ProcessFlutterMessages(); 36 | 37 | std::set flutter_instances_; 38 | }; 39 | 40 | #endif // RUNNER_RUN_LOOP_H_ 41 | -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | namespace { 8 | 9 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 10 | 11 | // The number of Win32Window objects that currently exist. 12 | static int g_active_window_count = 0; 13 | 14 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 15 | 16 | // Scale helper to convert logical scaler values to physical using passed in 17 | // scale factor 18 | int Scale(int source, double scale_factor) { 19 | return static_cast(source * scale_factor); 20 | } 21 | 22 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 23 | // This API is only needed for PerMonitor V1 awareness mode. 24 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 25 | HMODULE user32_module = LoadLibraryA("User32.dll"); 26 | if (!user32_module) { 27 | return; 28 | } 29 | auto enable_non_client_dpi_scaling = 30 | reinterpret_cast( 31 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 32 | if (enable_non_client_dpi_scaling != nullptr) { 33 | enable_non_client_dpi_scaling(hwnd); 34 | FreeLibrary(user32_module); 35 | } 36 | } 37 | 38 | } // namespace 39 | 40 | // Manages the Win32Window's window class registration. 41 | class WindowClassRegistrar { 42 | public: 43 | ~WindowClassRegistrar() = default; 44 | 45 | // Returns the singleton registar instance. 46 | static WindowClassRegistrar* GetInstance() { 47 | if (!instance_) { 48 | instance_ = new WindowClassRegistrar(); 49 | } 50 | return instance_; 51 | } 52 | 53 | // Returns the name of the window class, registering the class if it hasn't 54 | // previously been registered. 55 | const wchar_t* GetWindowClass(); 56 | 57 | // Unregisters the window class. Should only be called if there are no 58 | // instances of the window. 59 | void UnregisterWindowClass(); 60 | 61 | private: 62 | WindowClassRegistrar() = default; 63 | 64 | static WindowClassRegistrar* instance_; 65 | 66 | bool class_registered_ = false; 67 | }; 68 | 69 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 70 | 71 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 72 | if (!class_registered_) { 73 | WNDCLASS window_class{}; 74 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 75 | window_class.lpszClassName = kWindowClassName; 76 | window_class.style = CS_HREDRAW | CS_VREDRAW; 77 | window_class.cbClsExtra = 0; 78 | window_class.cbWndExtra = 0; 79 | window_class.hInstance = GetModuleHandle(nullptr); 80 | window_class.hIcon = 81 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 82 | window_class.hbrBackground = 0; 83 | window_class.lpszMenuName = nullptr; 84 | window_class.lpfnWndProc = Win32Window::WndProc; 85 | RegisterClass(&window_class); 86 | class_registered_ = true; 87 | } 88 | return kWindowClassName; 89 | } 90 | 91 | void WindowClassRegistrar::UnregisterWindowClass() { 92 | UnregisterClass(kWindowClassName, nullptr); 93 | class_registered_ = false; 94 | } 95 | 96 | Win32Window::Win32Window() { 97 | ++g_active_window_count; 98 | } 99 | 100 | Win32Window::~Win32Window() { 101 | --g_active_window_count; 102 | Destroy(); 103 | } 104 | 105 | bool Win32Window::CreateAndShow(const std::wstring& title, 106 | const Point& origin, 107 | const Size& size) { 108 | Destroy(); 109 | 110 | const wchar_t* window_class = 111 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 112 | 113 | const POINT target_point = {static_cast(origin.x), 114 | static_cast(origin.y)}; 115 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 116 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 117 | double scale_factor = dpi / 96.0; 118 | 119 | HWND window = CreateWindow( 120 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 121 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 122 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 123 | nullptr, nullptr, GetModuleHandle(nullptr), this); 124 | 125 | if (!window) { 126 | return false; 127 | } 128 | 129 | return OnCreate(); 130 | } 131 | 132 | // static 133 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 134 | UINT const message, 135 | WPARAM const wparam, 136 | LPARAM const lparam) noexcept { 137 | if (message == WM_NCCREATE) { 138 | auto window_struct = reinterpret_cast(lparam); 139 | SetWindowLongPtr(window, GWLP_USERDATA, 140 | reinterpret_cast(window_struct->lpCreateParams)); 141 | 142 | auto that = static_cast(window_struct->lpCreateParams); 143 | EnableFullDpiSupportIfAvailable(window); 144 | that->window_handle_ = window; 145 | } else if (Win32Window* that = GetThisFromHandle(window)) { 146 | return that->MessageHandler(window, message, wparam, lparam); 147 | } 148 | 149 | return DefWindowProc(window, message, wparam, lparam); 150 | } 151 | 152 | LRESULT 153 | Win32Window::MessageHandler(HWND hwnd, 154 | UINT const message, 155 | WPARAM const wparam, 156 | LPARAM const lparam) noexcept { 157 | switch (message) { 158 | case WM_DESTROY: 159 | window_handle_ = nullptr; 160 | Destroy(); 161 | if (quit_on_close_) { 162 | PostQuitMessage(0); 163 | } 164 | return 0; 165 | 166 | case WM_DPICHANGED: { 167 | auto newRectSize = reinterpret_cast(lparam); 168 | LONG newWidth = newRectSize->right - newRectSize->left; 169 | LONG newHeight = newRectSize->bottom - newRectSize->top; 170 | 171 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 172 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 173 | 174 | return 0; 175 | } 176 | case WM_SIZE: { 177 | RECT rect = GetClientArea(); 178 | if (child_content_ != nullptr) { 179 | // Size and position the child window. 180 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 181 | rect.bottom - rect.top, TRUE); 182 | } 183 | return 0; 184 | } 185 | 186 | case WM_ACTIVATE: 187 | if (child_content_ != nullptr) { 188 | SetFocus(child_content_); 189 | } 190 | return 0; 191 | } 192 | 193 | return DefWindowProc(window_handle_, message, wparam, lparam); 194 | } 195 | 196 | void Win32Window::Destroy() { 197 | OnDestroy(); 198 | 199 | if (window_handle_) { 200 | DestroyWindow(window_handle_); 201 | window_handle_ = nullptr; 202 | } 203 | if (g_active_window_count == 0) { 204 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 205 | } 206 | } 207 | 208 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 209 | return reinterpret_cast( 210 | GetWindowLongPtr(window, GWLP_USERDATA)); 211 | } 212 | 213 | void Win32Window::SetChildContent(HWND content) { 214 | child_content_ = content; 215 | SetParent(content, window_handle_); 216 | RECT frame = GetClientArea(); 217 | 218 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 219 | frame.bottom - frame.top, true); 220 | 221 | SetFocus(child_content_); 222 | } 223 | 224 | RECT Win32Window::GetClientArea() { 225 | RECT frame; 226 | GetClientRect(window_handle_, &frame); 227 | return frame; 228 | } 229 | 230 | HWND Win32Window::GetHandle() { 231 | return window_handle_; 232 | } 233 | 234 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 235 | quit_on_close_ = quit_on_close; 236 | } 237 | 238 | bool Win32Window::OnCreate() { 239 | // No-op; provided for subclasses. 240 | return true; 241 | } 242 | 243 | void Win32Window::OnDestroy() { 244 | // No-op; provided for subclasses. 245 | } 246 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /lib/animated_list_plus.dart: -------------------------------------------------------------------------------- 1 | library animated_list_plus; 2 | 3 | export 'src/diff/myers_diff.dart'; 4 | export 'src/handle.dart'; 5 | export 'src/implicitly_animated_list.dart'; 6 | export 'src/implicitly_animated_reorderable_list.dart'; 7 | export 'src/reorderable.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/custom_sliver_animated_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const Duration _kDuration = Duration(milliseconds: 300); 5 | 6 | typedef DelegateBuilder = SliverChildBuilderDelegate Function( 7 | NullableIndexedWidgetBuilder builder, 8 | int itemCount, 9 | ); 10 | 11 | class CustomSliverAnimatedList extends StatefulWidget { 12 | /// Creates a sliver that animates items when they are inserted or removed. 13 | const CustomSliverAnimatedList({ 14 | Key? key, 15 | required this.itemBuilder, 16 | this.initialItemCount = 0, 17 | this.delegateBuilder, 18 | }) : assert(initialItemCount >= 0), 19 | super(key: key); 20 | 21 | /// Called, as needed, to build list item widgets. 22 | /// 23 | /// List items are only built when they're scrolled into view. 24 | /// 25 | /// The [AnimatedItemBuilder] index parameter indicates the item's 26 | /// position in the list. The value of the index parameter will be between 0 27 | /// and [initialItemCount] plus the total number of items that have been 28 | /// inserted with [CustomSliverAnimatedListState.insertItem] and less the total 29 | /// number of items that have been removed with 30 | /// [CustomSliverAnimatedListState.removeItem]. 31 | /// 32 | /// Implementations of this callback should assume that 33 | /// [CustomSliverAnimatedListState.removeItem] removes an item immediately. 34 | final AnimatedItemBuilder itemBuilder; 35 | 36 | /// {@macro flutter.widgets.animatedList.initialItemCount} 37 | final int initialItemCount; 38 | 39 | /// Builds the delegate to use for the list view. 40 | final DelegateBuilder? delegateBuilder; 41 | 42 | @override 43 | CustomSliverAnimatedListState createState() => 44 | CustomSliverAnimatedListState(); 45 | 46 | /// The state from the closest instance of this class that encloses the given 47 | /// context. 48 | /// 49 | /// This method is typically used by [CustomSliverAnimatedList] item widgets that 50 | /// insert or remove items in response to user input. 51 | /// 52 | /// If no [CustomSliverAnimatedList] surrounds the context given, then this function 53 | /// will assert in debug mode and throw an exception in release mode. 54 | /// 55 | /// See also: 56 | /// 57 | /// * [maybeOf], a similar function that will return null if no 58 | /// [SliverAnimatedList] ancestor is found. 59 | static CustomSliverAnimatedListState of(BuildContext context) { 60 | final CustomSliverAnimatedListState? result = 61 | context.findAncestorStateOfType(); 62 | assert(() { 63 | if (result == null) { 64 | throw FlutterError( 65 | 'SliverAnimatedList.of() called with a context that does not contain a SliverAnimatedList.\n' 66 | 'No SliverAnimatedListState ancestor could be found starting from the ' 67 | 'context that was passed to SliverAnimatedListState.of(). This can ' 68 | 'happen when the context provided is from the same StatefulWidget that ' 69 | 'built the AnimatedList. Please see the SliverAnimatedList documentation ' 70 | 'for examples of how to refer to an AnimatedListState object: ' 71 | 'https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html\n' 72 | 'The context used was:\n' 73 | ' $context', 74 | ); 75 | } 76 | return true; 77 | }()); 78 | return result!; 79 | } 80 | 81 | /// The state from the closest instance of this class that encloses the given 82 | /// context. 83 | /// 84 | /// This method is typically used by [CustomSliverAnimatedList] item widgets that 85 | /// insert or remove items in response to user input. 86 | /// 87 | /// If no [CustomSliverAnimatedList] surrounds the context given, then this function 88 | /// will return null. 89 | /// 90 | /// See also: 91 | /// 92 | /// * [of], a similar function that will throw if no [CustomSliverAnimatedList] 93 | /// ancestor is found. 94 | static CustomSliverAnimatedListState? maybeOf(BuildContext context) { 95 | return context.findAncestorStateOfType(); 96 | } 97 | } 98 | 99 | class CustomSliverAnimatedListState extends State 100 | with TickerProviderStateMixin { 101 | final List<_ActiveItem> _incomingItems = <_ActiveItem>[]; 102 | final List<_ActiveItem> _outgoingItems = <_ActiveItem>[]; 103 | int _itemsCount = 0; 104 | 105 | @override 106 | void initState() { 107 | super.initState(); 108 | _itemsCount = widget.initialItemCount; 109 | } 110 | 111 | @override 112 | void dispose() { 113 | for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) { 114 | item.controller!.dispose(); 115 | } 116 | super.dispose(); 117 | } 118 | 119 | _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) { 120 | final int i = binarySearch(items, _ActiveItem.index(itemIndex)); 121 | return i == -1 ? null : items.removeAt(i); 122 | } 123 | 124 | _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) { 125 | final int i = binarySearch(items, _ActiveItem.index(itemIndex)); 126 | return i == -1 ? null : items[i]; 127 | } 128 | 129 | // The insertItem() and removeItem() index parameters are defined as if the 130 | // removeItem() operation removed the corresponding list entry immediately. 131 | // The entry is only actually removed from the ListView when the remove animation 132 | // finishes. The entry is added to _outgoingItems when removeItem is called 133 | // and removed from _outgoingItems when the remove animation finishes. 134 | 135 | int _indexToItemIndex(int index) { 136 | int itemIndex = index; 137 | for (final _ActiveItem item in _outgoingItems) { 138 | if (item.itemIndex <= itemIndex) { 139 | itemIndex += 1; 140 | } else { 141 | break; 142 | } 143 | } 144 | return itemIndex; 145 | } 146 | 147 | int _itemIndexToIndex(int itemIndex) { 148 | int index = itemIndex; 149 | for (final _ActiveItem item in _outgoingItems) { 150 | assert(item.itemIndex != itemIndex); 151 | if (item.itemIndex < itemIndex) { 152 | index -= 1; 153 | } else { 154 | break; 155 | } 156 | } 157 | return index; 158 | } 159 | 160 | SliverChildDelegate _createDelegate() { 161 | return widget.delegateBuilder?.call(_itemBuilder, _itemsCount) ?? 162 | SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount); 163 | } 164 | 165 | /// Insert an item at [index] and start an animation that will be passed to 166 | /// [CustomSliverAnimatedList.itemBuilder] when the item is visible. 167 | /// 168 | /// This method's semantics are the same as Dart's [List.insert] method: 169 | /// it increases the length of the list by one and shifts all items at or 170 | /// after [index] towards the end of the list. 171 | void insertItem(int index, {Duration duration = _kDuration}) { 172 | assert(index >= 0); 173 | 174 | final int itemIndex = _indexToItemIndex(index); 175 | if (itemIndex < 0 || itemIndex > _itemsCount) { 176 | return; 177 | } 178 | 179 | // Increment the incoming and outgoing item indices to account 180 | // for the insertion. 181 | for (final _ActiveItem item in _incomingItems) { 182 | if (item.itemIndex >= itemIndex) item.itemIndex += 1; 183 | } 184 | for (final _ActiveItem item in _outgoingItems) { 185 | if (item.itemIndex >= itemIndex) item.itemIndex += 1; 186 | } 187 | 188 | final AnimationController controller = AnimationController( 189 | duration: duration, 190 | vsync: this, 191 | ); 192 | final _ActiveItem incomingItem = _ActiveItem.incoming( 193 | controller, 194 | itemIndex, 195 | ); 196 | setState(() { 197 | _incomingItems 198 | ..add(incomingItem) 199 | ..sort(); 200 | _itemsCount += 1; 201 | }); 202 | 203 | controller.forward().then((_) { 204 | _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)! 205 | .controller! 206 | .dispose(); 207 | }); 208 | } 209 | 210 | /// Remove the item at [index] and start an animation that will be passed 211 | /// to [builder] when the item is visible. 212 | /// 213 | /// Items are removed immediately. After an item has been removed, its index 214 | /// will no longer be passed to the [CustomSliverAnimatedList.itemBuilder]. However 215 | /// the item will still appear in the list for [duration] and during that time 216 | /// [builder] must construct its widget as needed. 217 | /// 218 | /// This method's semantics are the same as Dart's [List.remove] method: 219 | /// it decreases the length of the list by one and shifts all items at or 220 | /// before [index] towards the beginning of the list. 221 | void removeItem(int index, AnimatedRemovedItemBuilder builder, 222 | {Duration duration = _kDuration}) { 223 | assert(index >= 0); 224 | 225 | final int itemIndex = _indexToItemIndex(index); 226 | if (itemIndex < 0 || itemIndex >= _itemsCount) { 227 | return; 228 | } 229 | 230 | assert(_activeItemAt(_outgoingItems, itemIndex) == null); 231 | 232 | final _ActiveItem? incomingItem = 233 | _removeActiveItemAt(_incomingItems, itemIndex); 234 | final AnimationController controller = incomingItem?.controller ?? 235 | AnimationController(duration: duration, value: 1.0, vsync: this); 236 | final _ActiveItem outgoingItem = 237 | _ActiveItem.outgoing(controller, itemIndex, builder); 238 | setState(() { 239 | _outgoingItems 240 | ..add(outgoingItem) 241 | ..sort(); 242 | }); 243 | 244 | controller.reverse().then((void value) { 245 | _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)! 246 | .controller! 247 | .dispose(); 248 | 249 | // Decrement the incoming and outgoing item indices to account 250 | // for the removal. 251 | for (final _ActiveItem item in _incomingItems) { 252 | if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1; 253 | } 254 | for (final _ActiveItem item in _outgoingItems) { 255 | if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1; 256 | } 257 | 258 | setState(() => _itemsCount -= 1); 259 | }); 260 | } 261 | 262 | Widget _itemBuilder(BuildContext context, int itemIndex) { 263 | final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, itemIndex); 264 | if (outgoingItem != null) { 265 | return outgoingItem.removedItemBuilder!( 266 | context, 267 | outgoingItem.controller!.view, 268 | ); 269 | } 270 | 271 | final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, itemIndex); 272 | final Animation animation = 273 | incomingItem?.controller?.view ?? kAlwaysCompleteAnimation; 274 | return widget.itemBuilder( 275 | context, 276 | _itemIndexToIndex(itemIndex), 277 | animation, 278 | ); 279 | } 280 | 281 | @override 282 | Widget build(BuildContext context) { 283 | return SliverList( 284 | delegate: _createDelegate(), 285 | ); 286 | } 287 | } 288 | 289 | class _ActiveItem implements Comparable<_ActiveItem> { 290 | _ActiveItem.incoming(this.controller, this.itemIndex) 291 | : removedItemBuilder = null; 292 | _ActiveItem.outgoing( 293 | this.controller, this.itemIndex, this.removedItemBuilder); 294 | _ActiveItem.index(this.itemIndex) 295 | : controller = null, 296 | removedItemBuilder = null; 297 | 298 | final AnimationController? controller; 299 | final AnimatedRemovedItemBuilder? removedItemBuilder; 300 | int itemIndex; 301 | 302 | @override 303 | int compareTo(_ActiveItem other) => itemIndex - other.itemIndex; 304 | } 305 | -------------------------------------------------------------------------------- /lib/src/diff/diff.dart: -------------------------------------------------------------------------------- 1 | export 'diff_callback.dart'; 2 | export 'diff_delegate.dart'; 3 | export 'diff_model.dart'; 4 | export 'myers_diff.dart'; 5 | export 'path_node.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/diff/diff_callback.dart: -------------------------------------------------------------------------------- 1 | typedef ItemDiffUtil = bool Function(E oldItem, E newItem); 2 | 3 | /// A Callback class used by DiffUtil while calculating the diff between two lists. 4 | mixin DiffCallback { 5 | /// The list containing the new data. 6 | List? get newList; 7 | 8 | /// The list containing the old data. 9 | List? get oldList; 10 | 11 | /// Returns the size of the old list. 12 | int get oldListSize => oldList!.length; 13 | 14 | /// Returns the size of the list. 15 | int get newListSize => newList!.length; 16 | 17 | /// Called by the DiffUtil to decide whether two object represent the same Item. 18 | /// For example, if your items have unique ids, this method should check their id equality. 19 | bool areItemsTheSame(E oldItem, E newItem); 20 | 21 | /// Called by the DiffUtil to decide whether two object represent the same Item. 22 | /// For example, if your items have unique ids, this method should check their id equality. 23 | bool areContentsTheSame(E oldItem, E newItem); 24 | 25 | dynamic getChangePayload(E oldItem, E newItem) => null; 26 | 27 | /// Called when an item was inserted into the list. 28 | void onInserted(int index, E item); 29 | 30 | /// Called when an item was removed from the list. 31 | void onRemoved(int index); 32 | 33 | /// Called when an item in the list changed but not its position in the list. 34 | void onChanged(int startIndex, List itemsChanged); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/diff/diff_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'diff.dart'; 2 | 3 | class DiffDelegate { 4 | final DiffCallback _callback; 5 | const DiffDelegate(this._callback); 6 | 7 | void applyDiffs(List diffs) { 8 | for (final diff in diffs) { 9 | if (diff is Insertion) { 10 | _applyInsertion(diff as Insertion); 11 | } else if (diff is Deletion) { 12 | _applyDeletion(diff); 13 | } else if (diff is Modification) { 14 | _applyModification(diff as Modification); 15 | } 16 | } 17 | } 18 | 19 | void _applyModification(Modification diff) { 20 | final diffLength = diff.items.length; 21 | 22 | if (diff.size != diffLength) { 23 | if (diff.size > diffLength) { 24 | var sizeDifference = diff.size - diffLength; 25 | while (sizeDifference > 0) { 26 | _callback.onRemoved(diff.index + sizeDifference); 27 | sizeDifference--; 28 | } 29 | } else { 30 | var insertIndex = diff.size; 31 | while (insertIndex < diffLength) { 32 | _callback.onInserted( 33 | insertIndex + diff.index, 34 | diff.items[insertIndex], 35 | ); 36 | insertIndex++; 37 | } 38 | } 39 | } 40 | 41 | final changedItems = diff.items.take(diff.size).toList(); 42 | _callback.onChanged(diff.index, changedItems); 43 | } 44 | 45 | void _applyDeletion(Deletion diff) { 46 | for (var i = diff.size - 1; i >= 0; i--) { 47 | _callback.onRemoved(diff.index + i); 48 | } 49 | } 50 | 51 | void _applyInsertion(Insertion diff) { 52 | for (var i = 0; i < diff.size; i++) { 53 | _callback.onInserted(diff.index + i, diff.items[i]); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/diff/diff_model.dart: -------------------------------------------------------------------------------- 1 | abstract class Diff implements Comparable { 2 | final int index; 3 | final int size; 4 | const Diff( 5 | this.index, 6 | this.size, 7 | ); 8 | 9 | @override 10 | String toString() => '${runtimeType.toString()}(index: $index, size: $size)'; 11 | 12 | @override 13 | int compareTo(Diff other) => index - other.index; 14 | } 15 | 16 | class Insertion extends Diff { 17 | final List items; 18 | const Insertion( 19 | int index, 20 | int size, 21 | this.items, 22 | ) : super(index, size); 23 | } 24 | 25 | class Deletion extends Diff { 26 | const Deletion( 27 | int index, 28 | int size, 29 | ) : super(index, size); 30 | } 31 | 32 | class Modification extends Diff { 33 | final List items; 34 | const Modification( 35 | int index, 36 | int size, 37 | this.items, 38 | ) : super(index, size); 39 | 40 | @override 41 | String toString() { 42 | return '${runtimeType.toString()}(index: $index, size: $size, items: $items)'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/diff/myers_diff.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../src.dart'; 4 | 5 | // This implementation of the MyersDiff algorithm was originally written by David Bota 6 | // over here https://gitlab.com/otsoaUnLoco/animated-stream-list. 7 | 8 | class _DiffArguments { 9 | final List oldList; 10 | final List newList; 11 | _DiffArguments(this.oldList, this.newList); 12 | } 13 | 14 | class MyersDiff { 15 | static ItemDiffUtil? eq; 16 | static ItemDiffUtil? cq; 17 | 18 | static int isolateThreshold = 1500; 19 | 20 | static Future> withCallback( 21 | DiffCallback cb, { 22 | bool? spawnIsolate, 23 | }) { 24 | return diff( 25 | cb.newList!, 26 | cb.oldList!, 27 | areItemsTheSame: cb.areItemsTheSame, 28 | spawnIsolate: spawnIsolate, 29 | ); 30 | } 31 | 32 | static Future> diff( 33 | List newList, 34 | List oldList, { 35 | ItemDiffUtil? areItemsTheSame, 36 | bool? spawnIsolate, 37 | }) { 38 | eq = (a, b) => areItemsTheSame?.call(a, b) ?? a == b; 39 | cq = (a, b) => false; 40 | 41 | final args = _DiffArguments(oldList, newList); 42 | 43 | // We can significantly improve the performance by not spawning a new 44 | // isolate for shorter lists. 45 | spawnIsolate ??= (newList.length * oldList.length) > isolateThreshold; 46 | if (spawnIsolate) { 47 | return compute(_myersDiff, args); 48 | } 49 | 50 | return Future.value(_myersDiff(args)); 51 | } 52 | } 53 | 54 | List _myersDiff(_DiffArguments args) { 55 | final List oldList = args.oldList; 56 | final List newList = args.newList; 57 | 58 | if (oldList == newList) return []; 59 | 60 | final oldSize = oldList.length; 61 | final newSize = newList.length; 62 | 63 | if (oldSize == 0) { 64 | return [Insertion(0, newSize, newList)]; 65 | } 66 | 67 | if (newSize == 0) { 68 | return [Deletion(0, oldSize)]; 69 | } 70 | 71 | final equals = MyersDiff.eq ?? (a, b) => a == b; 72 | final path = _buildPath(oldList, newList, equals)!; 73 | final diffs = _buildPatch(path, oldList, newList)..sort(); 74 | return diffs.reversed.toList(growable: true); 75 | } 76 | 77 | PathNode? _buildPath( 78 | List oldList, List newList, ItemDiffUtil equals) { 79 | final oldSize = oldList.length; 80 | final newSize = newList.length; 81 | 82 | final int max = oldSize + newSize + 1; 83 | final int size = (2 * max) + 1; 84 | final int middle = size ~/ 2; 85 | final List diagonal = List.filled(size, null); 86 | 87 | diagonal[middle + 1] = Snake(0, -1, null); 88 | 89 | for (int d = 0; d < max; d++) { 90 | for (int k = -d; k <= d; k += 2) { 91 | final int kmiddle = middle + k; 92 | final int kplus = kmiddle + 1; 93 | final int kminus = kmiddle - 1; 94 | PathNode? prev; 95 | 96 | int i; 97 | if ((k == -d) || 98 | (k != d && 99 | diagonal[kminus]!.originIndex < diagonal[kplus]!.originIndex)) { 100 | i = diagonal[kplus]!.originIndex; 101 | prev = diagonal[kplus]; 102 | } else { 103 | i = diagonal[kminus]!.originIndex + 1; 104 | prev = diagonal[kminus]; 105 | } 106 | 107 | diagonal[kminus] = null; 108 | 109 | int j = i - k; 110 | PathNode node = DiffNode(i, j, prev); 111 | while (i < oldSize && j < newSize && equals(oldList[i], newList[j])) { 112 | i++; 113 | j++; 114 | } 115 | 116 | if (i > node.originIndex) { 117 | node = Snake(i, j, node); 118 | } 119 | 120 | diagonal[kmiddle] = node; 121 | 122 | if (i >= oldSize && j >= newSize) { 123 | return diagonal[kmiddle]; 124 | } 125 | } 126 | diagonal[middle + d - 1] = null; 127 | } 128 | 129 | throw Exception(); 130 | } 131 | 132 | List _buildPatch(PathNode path, List oldList, List newList) { 133 | final List diffs = []; 134 | 135 | if (path.isSnake) { 136 | // ignore: parameter_assignments 137 | path = path.previousNode!; 138 | } 139 | 140 | while (path.previousNode != null && path.previousNode!.revisedIndex >= 0) { 141 | assert(!path.isSnake); 142 | 143 | final i = path.originIndex; 144 | final j = path.revisedIndex; 145 | 146 | // ignore: parameter_assignments 147 | path = path.previousNode!; 148 | final iAnchor = path.originIndex; 149 | final jAnchor = path.revisedIndex; 150 | 151 | final List original = oldList.sublist(iAnchor, i); 152 | final List revised = newList.sublist(jAnchor, j); 153 | 154 | if (original.isEmpty && revised.isNotEmpty) { 155 | diffs.add(Insertion(iAnchor, revised.length, revised)); 156 | } else if (original.isNotEmpty && revised.isEmpty) { 157 | diffs.add(Deletion(iAnchor, original.length)); 158 | } else { 159 | diffs.add(Modification(iAnchor, original.length, revised)); 160 | } 161 | 162 | if (path.isSnake) { 163 | // ignore: parameter_assignments 164 | path = path.previousNode!; 165 | } 166 | } 167 | 168 | return diffs; 169 | } 170 | -------------------------------------------------------------------------------- /lib/src/diff/path_node.dart: -------------------------------------------------------------------------------- 1 | abstract class PathNode { 2 | final int originIndex; 3 | final int revisedIndex; 4 | final PathNode? previousNode; 5 | PathNode( 6 | this.originIndex, 7 | this.revisedIndex, 8 | this.previousNode, 9 | ); 10 | 11 | bool get isSnake; 12 | 13 | bool get isBootStrap => originIndex < 0 || revisedIndex < 0; 14 | 15 | PathNode? previousSnake() { 16 | if (isBootStrap) return null; 17 | if (!isSnake && previousNode != null) return previousNode!.previousSnake(); 18 | return this; 19 | } 20 | 21 | @override 22 | String toString() { 23 | final buffer = StringBuffer()..write('['); 24 | PathNode? node = this; 25 | while (node != null) { 26 | buffer 27 | ..write('(') 28 | ..write('${node.originIndex.toString()}') 29 | ..write(',') 30 | ..write('${node.revisedIndex.toString()}') 31 | ..write(')'); 32 | 33 | node = node.previousNode; 34 | } 35 | buffer.write(']'); 36 | return buffer.toString(); 37 | } 38 | } 39 | 40 | class Snake extends PathNode { 41 | Snake( 42 | int originIndex, 43 | int revisedIndex, 44 | PathNode? previousNode, 45 | ) : super( 46 | originIndex, 47 | revisedIndex, 48 | previousNode, 49 | ); 50 | 51 | @override 52 | bool get isSnake => true; 53 | } 54 | 55 | class DiffNode extends PathNode { 56 | DiffNode( 57 | int originIndex, 58 | int revisedIndex, 59 | PathNode? previousNode, 60 | ) : super( 61 | originIndex, 62 | revisedIndex, 63 | previousNode?.previousSnake(), 64 | ); 65 | 66 | @override 67 | bool get isSnake => false; 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/handle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'src.dart'; 5 | 6 | /// A `Widget` that is used to initiate a drag/reorder of a [Reorderable] inside an 7 | /// [ImplicitlyAnimatedReorderableList]. 8 | /// 9 | /// A `Handle` must have a [Reorderable] and an [ImplicitlyAnimatedReorderableList] 10 | /// as its ancestor. 11 | class Handle extends StatefulWidget { 12 | /// The child of this Handle that can initiate a reorder. 13 | /// 14 | /// This might for instance be an [Icon] or a [ListTile]. 15 | final Widget child; 16 | 17 | /// The delay between when a pointer touched the [child] and 18 | /// when the drag is initiated. 19 | /// 20 | /// If the Handle wraps the whole item, the delay should be greater 21 | /// than the default `Duration.zero` as otherwise the list might become unscrollable. 22 | /// 23 | /// When the [ImplicitlyAnimatedReorderableList] was scrolled in the mean time, 24 | /// the reorder will be canceled. 25 | /// If the [ImplicitlyAnimatedReorderableList] uses a `NeverScrollableScrollPhysics` 26 | /// the Handle will instead use a parent `Scrollable` if there is one. 27 | final Duration delay; 28 | 29 | /// Whether to vibrate when a drag has been initiated. 30 | final bool vibrate; 31 | 32 | /// Whether the handle should capture the pointer event of the drag. 33 | /// 34 | /// When this is set to `true`, the `Hanlde` is not allowed to change 35 | /// the parent between normal and dragged state. 36 | final bool capturePointer; 37 | 38 | final bool enabled; 39 | 40 | /// Creates a widget that can initiate a drag/reorder of an item inside an 41 | /// [ImplicitlyAnimatedReorderableList]. 42 | /// 43 | /// A Handle must have a [Reorderable] and an [ImplicitlyAnimatedReorderableList] 44 | /// as its ancestor. 45 | const Handle({ 46 | Key? key, 47 | required this.child, 48 | this.delay = Duration.zero, 49 | this.capturePointer = true, 50 | this.vibrate = true, 51 | this.enabled = true, 52 | }) : super(key: key); 53 | 54 | @override 55 | _HandleState createState() => _HandleState(); 56 | } 57 | 58 | class _HandleState extends State { 59 | ScrollableState? _parent; 60 | // A custom handler used to cancel the pending onDragStart callbacks. 61 | Handler? _handler; 62 | // The parent Reorderable item. 63 | ReorderableState? _reorderable; 64 | // The parent list. 65 | ImplicitlyAnimatedReorderableListState? _list; 66 | // Whether the ImplicitlyAnimatedReorderableList has a 67 | // scrollDirection of Axis.vertical. 68 | bool get _isVertical => _list?.isVertical ?? true; 69 | 70 | Offset? _pointer; 71 | late double _downOffset; 72 | double? _startOffset; 73 | double? _currentOffset; 74 | double get _delta => (_currentOffset ?? 0) - (_startOffset ?? 0); 75 | 76 | // Use flags from the list as this State object is being 77 | // recreated between dragged and normal state. 78 | bool get _inDrag => _list!.inDrag; 79 | bool get _inReorder => _list!.inReorder; 80 | 81 | // The pixel offset of a possible parent Scrollable 82 | // used to capture it. 83 | double _parentPixels = 0.0; 84 | 85 | void _onDragStarted() { 86 | // If the list is already in drag we dont want to 87 | // initiate a new reorder. 88 | if (_inReorder) return; 89 | 90 | final moveDelta = (_downOffset - _currentOffset!).abs(); 91 | if (moveDelta > 10.0) { 92 | return; 93 | } 94 | 95 | _parentPixels = _parent?.position.pixels ?? 0.0; 96 | 97 | _captureParentList(); 98 | _startOffset = _currentOffset; 99 | 100 | _list?.onDragStarted(_reorderable?.key); 101 | _reorderable!.rebuild(); 102 | 103 | _vibrate(); 104 | } 105 | 106 | void _onDragUpdated(Offset pointer) { 107 | _list?.onDragUpdated(_delta); 108 | _captureParentList(); 109 | } 110 | 111 | void _onDragEnded() { 112 | _handler?.cancel(); 113 | _list?.onDragEnded(); 114 | _captureParentList(); 115 | } 116 | 117 | void _vibrate() { 118 | if (widget.vibrate) HapticFeedback.mediumImpact(); 119 | } 120 | 121 | void _captureParentList() { 122 | // Listener does not capture the drag of this Handle 123 | // however we also cannot use GestureDetector to capture 124 | // the drag on the Handle, as this might make the whole 125 | // list unscrollable (e.g. when the Handle wraps a whole ListTile). 126 | // 127 | // This seems to be the only working solution to this problem. 128 | _parent?.position.jumpTo(_parentPixels); 129 | } 130 | 131 | @override 132 | Widget build(BuildContext context) { 133 | if (!widget.enabled) return widget.child; 134 | 135 | _list = ImplicitlyAnimatedReorderableList.maybeOf(context); 136 | assert(_list != null, 137 | 'No ancestor ImplicitlyAnimatedReorderableList was found in the hierarchy!'); 138 | _reorderable = Reorderable.maybeOf(context); 139 | assert(_reorderable != null, 140 | 'No ancestor Reorderable was found in the hierarchy!'); 141 | _parent = Scrollable.maybeOf(_list!.context); 142 | 143 | // Sometimes the cancel callbacks of the GestureDetector 144 | // are erroneously invoked. Use a plain Listener instead 145 | // for now. 146 | return Listener( 147 | behavior: HitTestBehavior.translucent, 148 | onPointerDown: (event) => _onDown(event.localPosition), 149 | onPointerMove: (event) => _onUpdate(event.localPosition), 150 | onPointerUp: (_) => _onUp(), 151 | onPointerCancel: (_) => _onUp(), 152 | child: widget.child, 153 | ); 154 | } 155 | 156 | void _onDown(Offset pointer) { 157 | _pointer = pointer; 158 | _currentOffset = _offset(_pointer); 159 | _downOffset = _offset(_pointer); 160 | 161 | // Ensure the list is not already in a reordering 162 | // state when initiating a new reorder operation. 163 | if (!_inDrag) { 164 | _onUp(); 165 | 166 | _handler = postDuration( 167 | widget.delay, 168 | _onDragStarted, 169 | ); 170 | } 171 | } 172 | 173 | void _onUpdate(Offset pointer) { 174 | _pointer = pointer; 175 | _currentOffset = _offset(_pointer); 176 | 177 | if (_inDrag && _inReorder) { 178 | _onDragUpdated(pointer); 179 | } 180 | } 181 | 182 | void _onUp() { 183 | _handler?.cancel(); 184 | if (_inDrag) _onDragEnded(); 185 | } 186 | 187 | double _offset(Offset? offset) => _isVertical ? offset!.dy : offset!.dx; 188 | } 189 | -------------------------------------------------------------------------------- /lib/src/implicitly_animated_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:animated_list_plus/src/custom_sliver_animated_list.dart'; 2 | import 'package:animated_list_plus/src/util/sliver_child_separated_builder_delegate.dart'; 3 | import 'package:flutter/material.dart' hide AnimatedItemBuilder; 4 | 5 | import 'src.dart'; 6 | 7 | /// A Flutter ListView that implicitly animates between the changes of two lists. 8 | class ImplicitlyAnimatedList extends StatelessWidget { 9 | /// The current data that this [ImplicitlyAnimatedList] should represent. 10 | final List items; 11 | 12 | /// Called, as needed, to build list item widgets. 13 | /// 14 | /// List items are only built when they're scrolled into view. 15 | final AnimatedItemBuilder itemBuilder; 16 | 17 | /// Called to build widgets that get placed between 18 | /// itemBuilder(context, index) and itemBuilder(context, index + 1). 19 | final NullableIndexedWidgetBuilder? separatorBuilder; 20 | 21 | /// An optional builder when an item was removed from the list. 22 | /// 23 | /// If not specified, the [ImplicitlyAnimatedList] uses the [itemBuilder] with 24 | /// the animation reversed. 25 | final RemovedItemBuilder? removeItemBuilder; 26 | 27 | /// An optional builder when an item in the list was changed but not its position. 28 | /// 29 | /// The [UpdatedItemBuilder] animation will run from 1 to 0 and back to 1 again, while 30 | /// the item parameter will be the old item in the first half of the animation and the new item 31 | /// in the latter half of the animation. This allows you for example to fade between the old and 32 | /// the new item. 33 | /// 34 | /// If not specified, changes will appear instantaneously. 35 | final UpdatedItemBuilder? updateItemBuilder; 36 | 37 | /// Called by the DiffUtil to decide whether two object represent the same Item. 38 | /// For example, if your items have unique ids, this method should check their id equality. 39 | final ItemDiffUtil areItemsTheSame; 40 | 41 | /// The duration of the animation when an item was inserted into the list. 42 | final Duration insertDuration; 43 | 44 | /// The duration of the animation when an item was removed from the list. 45 | final Duration removeDuration; 46 | 47 | /// The duration of the animation when an item changed in the list. 48 | final Duration updateDuration; 49 | 50 | /// Whether to spawn a new isolate on which to calculate the diff on. 51 | /// 52 | /// Usually you wont have to specify this value as the MyersDiff implementation will 53 | /// use its own metrics to decide, whether a new isolate has to be spawned or not for 54 | /// optimal performance. 55 | final bool? spawnIsolate; 56 | 57 | /// The axis along which the scroll view scrolls. 58 | /// 59 | /// Defaults to [Axis.vertical]. 60 | final Axis scrollDirection; 61 | 62 | /// Whether the scroll view scrolls in the reading direction. 63 | /// 64 | /// For example, if the reading direction is left-to-right and 65 | /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from 66 | /// left to right when [reverse] is false and from right to left when 67 | /// [reverse] is true. 68 | /// 69 | /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view 70 | /// scrolls from top to bottom when [reverse] is false and from bottom to top 71 | /// when [reverse] is true. 72 | /// 73 | /// Defaults to false. 74 | final bool reverse; 75 | 76 | /// An object that can be used to control the position to which this scroll 77 | /// view is scrolled. 78 | /// 79 | /// Must be null if [primary] is true. 80 | /// 81 | /// A [ScrollController] serves several purposes. It can be used to control 82 | /// the initial scroll position (see [ScrollController.initialScrollOffset]). 83 | /// It can be used to control whether the scroll view should automatically 84 | /// save and restore its scroll position in the [PageStorage] (see 85 | /// [ScrollController.keepScrollOffset]). It can be used to read the current 86 | /// scroll position (see [ScrollController.offset]), or change it (see 87 | /// [ScrollController.animateTo]). 88 | final ScrollController? controller; 89 | 90 | /// Whether this is the primary scroll view associated with the parent 91 | /// [PrimaryScrollController]. 92 | /// 93 | /// On iOS, this identifies the scroll view that will scroll to top in 94 | /// response to a tap in the status bar. 95 | /// 96 | /// Defaults to true when [scrollDirection] is [Axis.vertical] and 97 | /// [controller] is null. 98 | final bool? primary; 99 | 100 | /// How the scroll view should respond to user input. 101 | /// 102 | /// For example, determines how the scroll view continues to animate after the 103 | /// user stops dragging the scroll view. 104 | /// 105 | /// Defaults to matching platform conventions. 106 | final ScrollPhysics? physics; 107 | 108 | /// Whether the extent of the scroll view in the [scrollDirection] should be 109 | /// determined by the contents being viewed. 110 | /// 111 | /// If the scroll view does not shrink wrap, then the scroll view will expand 112 | /// to the maximum allowed size in the [scrollDirection]. If the scroll view 113 | /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must 114 | /// be true. 115 | /// 116 | /// Shrink wrapping the content of the scroll view is significantly more 117 | /// expensive than expanding to the maximum allowed size because the content 118 | /// can expand and contract during scrolling, which means the size of the 119 | /// scroll view needs to be recomputed whenever the scroll position changes. 120 | /// 121 | /// Defaults to false. 122 | final bool shrinkWrap; 123 | 124 | /// The amount of space by which to inset the children. 125 | final EdgeInsetsGeometry? padding; 126 | 127 | /// The clip behavior to be used by the scroll view. 128 | /// 129 | /// Defaults to [Clip.hardEdge]. 130 | final Clip clipBehavior; 131 | 132 | /// Creates a Flutter ListView that implicitly animates between the changes 133 | /// of two lists. 134 | const ImplicitlyAnimatedList({ 135 | Key? key, 136 | required this.items, 137 | required this.itemBuilder, 138 | required this.areItemsTheSame, 139 | this.separatorBuilder, 140 | this.removeItemBuilder, 141 | this.updateItemBuilder, 142 | this.insertDuration = const Duration(milliseconds: 500), 143 | this.removeDuration = const Duration(milliseconds: 500), 144 | this.updateDuration = const Duration(milliseconds: 500), 145 | this.spawnIsolate, 146 | this.scrollDirection = Axis.vertical, 147 | this.reverse = false, 148 | this.controller, 149 | this.primary, 150 | this.physics, 151 | this.shrinkWrap = false, 152 | this.padding, 153 | this.clipBehavior = Clip.hardEdge, 154 | }) : super(key: key); 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | final separatorBuilder = this.separatorBuilder; 159 | 160 | return CustomScrollView( 161 | scrollDirection: scrollDirection, 162 | reverse: reverse, 163 | controller: controller, 164 | primary: primary, 165 | physics: physics, 166 | shrinkWrap: shrinkWrap, 167 | clipBehavior: clipBehavior, 168 | slivers: [ 169 | SliverPadding( 170 | padding: padding ?? const EdgeInsets.all(0), 171 | sliver: separatorBuilder == null 172 | ? SliverImplicitlyAnimatedList( 173 | items: items, 174 | itemBuilder: itemBuilder, 175 | areItemsTheSame: areItemsTheSame, 176 | updateItemBuilder: updateItemBuilder, 177 | removeItemBuilder: removeItemBuilder, 178 | insertDuration: insertDuration, 179 | removeDuration: removeDuration, 180 | updateDuration: updateDuration, 181 | spawnIsolate: spawnIsolate, 182 | ) 183 | : SliverImplicitlyAnimatedList.separated( 184 | items: items, 185 | itemBuilder: itemBuilder, 186 | separatorBuilder: separatorBuilder, 187 | areItemsTheSame: areItemsTheSame, 188 | updateItemBuilder: updateItemBuilder, 189 | removeItemBuilder: removeItemBuilder, 190 | insertDuration: insertDuration, 191 | removeDuration: removeDuration, 192 | updateDuration: updateDuration, 193 | spawnIsolate: spawnIsolate, 194 | ), 195 | ), 196 | ], 197 | ); 198 | } 199 | } 200 | 201 | /// A Flutter Sliver that implicitly animates between the changes of two lists. 202 | class SliverImplicitlyAnimatedList 203 | extends ImplicitlyAnimatedListBase { 204 | /// Creates a Flutter Sliver that implicitly animates between the changes of two lists. 205 | /// 206 | /// {@template implicitly_animated_reorderable_list.constructor} 207 | /// The [items] parameter represents the current items that should be displayed in 208 | /// the list. 209 | /// 210 | /// The [itemBuilder] callback is used to build each child as needed. The parent must 211 | /// be a [Reorderable] widget. 212 | /// 213 | /// The [areItemsTheSame] callback is called by the DiffUtil to decide whether two objects 214 | /// represent the same item. For example, if your items have unique ids, this method should 215 | /// check their id equality. 216 | /// 217 | /// The [onReorderFinished] callback is called in response to when the dragged item has 218 | /// been released and animated to its final destination. Here you should update 219 | /// the underlying data in your model/bloc/database etc. 220 | /// 221 | /// The [spawnIsolate] flag indicates whether to spawn a new isolate on which to 222 | /// calculate the diff between the lists. Usually you wont have to specify this 223 | /// value as the MyersDiff implementation will use its own metrics to decide, whether 224 | /// a new isolate has to be spawned or not for optimal performance. 225 | /// {@endtemplate} 226 | const SliverImplicitlyAnimatedList({ 227 | Key? key, 228 | required List items, 229 | required AnimatedItemBuilder itemBuilder, 230 | required ItemDiffUtil areItemsTheSame, 231 | RemovedItemBuilder? removeItemBuilder, 232 | UpdatedItemBuilder? updateItemBuilder, 233 | Duration insertDuration = const Duration(milliseconds: 500), 234 | Duration removeDuration = const Duration(milliseconds: 500), 235 | Duration updateDuration = const Duration(milliseconds: 500), 236 | bool? spawnIsolate, 237 | }) : super( 238 | key: key, 239 | items: items, 240 | itemBuilder: itemBuilder, 241 | delegateBuilder: null, 242 | areItemsTheSame: areItemsTheSame, 243 | removeItemBuilder: removeItemBuilder, 244 | updateItemBuilder: updateItemBuilder, 245 | insertDuration: insertDuration, 246 | removeDuration: removeDuration, 247 | updateDuration: updateDuration, 248 | spawnIsolate: spawnIsolate, 249 | ); 250 | 251 | /// Creates a Flutter Sliver that implicitly animates between the changes of two lists. 252 | /// 253 | /// {@template implicitly_animated_reorderable_list.constructor} 254 | /// The [items] parameter represents the current items that should be displayed in 255 | /// the list. 256 | /// 257 | /// The [itemBuilder] callback is used to build each child as needed. The parent must 258 | /// be a [Reorderable] widget. 259 | /// 260 | /// The [separatorBuilder] is the widget that gets placed between 261 | /// itemBuilder(context, index) and itemBuilder(context, index + 1). 262 | /// 263 | /// The [areItemsTheSame] callback is called by the DiffUtil to decide whether two objects 264 | /// represent the same item. For example, if your items have unique ids, this method should 265 | /// check their id equality. 266 | /// 267 | /// The [onReorderFinished] callback is called in response to when the dragged item has 268 | /// been released and animated to its final destination. Here you should update 269 | /// the underlying data in your model/bloc/database etc. 270 | /// 271 | /// The [spawnIsolate] flag indicates whether to spawn a new isolate on which to 272 | /// calculate the diff between the lists. Usually you wont have to specify this 273 | /// value as the MyersDiff implementation will use its own metrics to decide, whether 274 | /// a new isolate has to be spawned or not for optimal performance. 275 | /// {@endtemplate} 276 | SliverImplicitlyAnimatedList.separated({ 277 | Key? key, 278 | required List items, 279 | required AnimatedItemBuilder itemBuilder, 280 | required ItemDiffUtil areItemsTheSame, 281 | required NullableIndexedWidgetBuilder separatorBuilder, 282 | RemovedItemBuilder? removeItemBuilder, 283 | UpdatedItemBuilder? updateItemBuilder, 284 | Duration insertDuration = const Duration(milliseconds: 500), 285 | Duration removeDuration = const Duration(milliseconds: 500), 286 | Duration updateDuration = const Duration(milliseconds: 500), 287 | bool? spawnIsolate, 288 | }) : super( 289 | key: key, 290 | items: items, 291 | itemBuilder: itemBuilder, 292 | delegateBuilder: (builder, itemCount) => 293 | SliverChildSeparatedBuilderDelegate( 294 | itemBuilder: builder, 295 | separatorBuilder: separatorBuilder, 296 | itemCount: itemCount, 297 | ), 298 | areItemsTheSame: areItemsTheSame, 299 | removeItemBuilder: removeItemBuilder, 300 | updateItemBuilder: updateItemBuilder, 301 | insertDuration: insertDuration, 302 | removeDuration: removeDuration, 303 | updateDuration: updateDuration, 304 | spawnIsolate: spawnIsolate, 305 | ); 306 | 307 | @override 308 | _SliverImplicitlyAnimatedListState createState() => 309 | _SliverImplicitlyAnimatedListState(); 310 | } 311 | 312 | class _SliverImplicitlyAnimatedListState 313 | extends ImplicitlyAnimatedListBaseState, E> { 315 | @override 316 | Widget build(BuildContext context) { 317 | return CustomSliverAnimatedList( 318 | key: animatedListKey, 319 | initialItemCount: newList.length, 320 | itemBuilder: (context, index, animation) { 321 | final E? item = data.getOrNull(index) ?? 322 | newList.getOrNull(index) ?? 323 | oldList.getOrNull(index); 324 | final didChange = changes[item] != null; 325 | 326 | if (item == null) { 327 | return Container(); 328 | } else if (updateItemBuilder != null && didChange) { 329 | return buildUpdatedItemWidget(item); 330 | } else { 331 | return itemBuilder(context, animation, item, index); 332 | } 333 | }, 334 | delegateBuilder: widget.delegateBuilder, 335 | ); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /lib/src/implicitly_animated_list_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:animated_list_plus/src/custom_sliver_animated_list.dart'; 4 | import 'package:async/async.dart'; 5 | 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/material.dart' hide AnimatedItemBuilder; 8 | 9 | import 'src.dart'; 10 | 11 | typedef AnimatedItemBuilder = W Function( 12 | BuildContext context, Animation animation, E item, int i); 13 | 14 | typedef RemovedItemBuilder = W Function( 15 | BuildContext context, Animation animation, E item); 16 | 17 | typedef UpdatedItemBuilder = W Function( 18 | BuildContext context, Animation animation, E item); 19 | 20 | abstract class ImplicitlyAnimatedListBase 21 | extends StatefulWidget { 22 | /// Called, as needed, to build list item widgets. 23 | /// 24 | /// List items are only built when they're scrolled into view. 25 | final AnimatedItemBuilder itemBuilder; 26 | 27 | /// Called to build widgets that get placed between 28 | /// itemBuilder(context, index) and itemBuilder(context, index + 1). 29 | final DelegateBuilder? delegateBuilder; 30 | 31 | /// An optional builder when an item was removed from the list. 32 | /// 33 | /// If not specified, the [ImplicitlyAnimatedList] uses the [itemBuilder] with 34 | /// the animation reversed. 35 | final RemovedItemBuilder? removeItemBuilder; 36 | 37 | /// An optional builder when an item in the list was changed but not its position. 38 | /// 39 | /// The [UpdatedItemBuilder] animation will run from 1 to 0 and back to 1 again, while 40 | /// the item parameter will be the old item in the first half of the animation and the new item 41 | /// in the latter half of the animation. This allows you for example to fade between the old and 42 | /// the new item. 43 | /// 44 | /// If not specified, changes will appear instantaneously. 45 | final UpdatedItemBuilder? updateItemBuilder; 46 | 47 | /// The data that this [ImplicitlyAnimatedList] should represent. 48 | final List items; 49 | 50 | /// Called by the DiffUtil to decide whether two object represent the same Item. 51 | /// For example, if your items have unique ids, this method should check their id equality. 52 | final ItemDiffUtil areItemsTheSame; 53 | 54 | /// The duration of the animation when an item was inserted into the list. 55 | final Duration insertDuration; 56 | 57 | /// The duration of the animation when an item was removed from the list. 58 | final Duration removeDuration; 59 | 60 | /// The duration of the animation when an item changed in the list. 61 | final Duration updateDuration; 62 | 63 | /// Whether to spawn a new isolate on which to calculate the diff on. 64 | /// 65 | /// Usually you wont have to specify this value as the MyersDiff implementation will 66 | /// use its own metrics to decide, whether a new isolate has to be spawned or not for 67 | /// optimal performance. 68 | final bool? spawnIsolate; 69 | const ImplicitlyAnimatedListBase({ 70 | Key? key, 71 | required this.items, 72 | required this.areItemsTheSame, 73 | required this.itemBuilder, 74 | required this.delegateBuilder, 75 | required this.removeItemBuilder, 76 | required this.updateItemBuilder, 77 | required this.insertDuration, 78 | required this.removeDuration, 79 | required this.updateDuration, 80 | required this.spawnIsolate, 81 | }) : super(key: key); 82 | } 83 | 84 | abstract class ImplicitlyAnimatedListBaseState, E extends Object> 86 | extends State with DiffCallback, TickerProviderStateMixin { 87 | @protected 88 | GlobalKey animatedListKey = GlobalKey(); 89 | 90 | @nonVirtual 91 | @protected 92 | CustomSliverAnimatedListState get list => animatedListKey.currentState!; 93 | 94 | late final DiffDelegate _delegate = DiffDelegate(this); 95 | CancelableOperation? _diffOperation; 96 | 97 | // Animation controller for custom animation that are not supported 98 | // by the [AnimatedList], like updates. 99 | late final updateAnimController = AnimationController(vsync: this); 100 | late final Animation updateAnimation = TweenSequence([ 101 | TweenSequenceItem( 102 | tween: Tween(begin: 1.0, end: 0.0), 103 | weight: 0.5, 104 | ), 105 | TweenSequenceItem( 106 | tween: Tween(begin: 0.0, end: 1.0), 107 | weight: 0.5, 108 | ), 109 | ]).animate(updateAnimController); 110 | 111 | // The currently active items. 112 | late List _data = List.from(widget.items); 113 | List get data => _data; 114 | // The items that have newly come in that 115 | // will get diffed into the dataset. 116 | late List _newItems = List.from(widget.items); 117 | // The previous dataSet. 118 | late List _oldItems = List.from(data); 119 | // 120 | Completer? _mutex; 121 | 122 | @nonVirtual 123 | @override 124 | List get newList => _newItems; 125 | 126 | @nonVirtual 127 | @override 128 | List get oldList => _oldItems; 129 | 130 | final Map _changes = {}; 131 | 132 | @nonVirtual 133 | @protected 134 | Map get changes => _changes; 135 | 136 | @nonVirtual 137 | @protected 138 | AnimatedItemBuilder get itemBuilder => widget.itemBuilder; 139 | @nonVirtual 140 | @protected 141 | RemovedItemBuilder? get removeItemBuilder => widget.removeItemBuilder; 142 | @nonVirtual 143 | @protected 144 | UpdatedItemBuilder? get updateItemBuilder => widget.updateItemBuilder; 145 | 146 | @override 147 | void initState() { 148 | super.initState(); 149 | 150 | didUpdateWidget(widget); 151 | } 152 | 153 | @override 154 | void didUpdateWidget(ImplicitlyAnimatedListBase oldWidget) { 155 | super.didUpdateWidget(oldWidget as B); 156 | 157 | updateAnimController.duration = widget.updateDuration; 158 | 159 | _updateList(); 160 | } 161 | 162 | Future _updateList() async { 163 | await _mutex?.future; 164 | 165 | _newItems = List.from(widget.items); 166 | _oldItems = List.from(data); 167 | 168 | _calcDiffs(); 169 | } 170 | 171 | Future _calcDiffs() async { 172 | if (!mounted) return; 173 | 174 | // Don't check for too long lists the list equality as 175 | // this would begin to take longer than the diff 176 | // algorithm itself. 177 | final areListsShortEnoughForEqualityCheck = 178 | _oldItems.length < 100 && _newItems.length < 100; 179 | final areListsEqual = 180 | areListsShortEnoughForEqualityCheck && listEquals(_oldItems, _newItems); 181 | 182 | if (!areListsEqual) { 183 | _changes.clear(); 184 | 185 | await _diffOperation?.cancel(); 186 | _diffOperation = CancelableOperation.fromFuture( 187 | MyersDiff.withCallback(this, spawnIsolate: widget.spawnIsolate), 188 | ); 189 | 190 | _diffOperation?.then((diffs) { 191 | // diffs is null when the operation 192 | // gets canceled. 193 | if (diffs == null || !mounted) { 194 | return; 195 | } 196 | 197 | _delegate.applyDiffs(diffs); 198 | _data = List.from(_newItems); 199 | 200 | updateAnimController 201 | ..reset() 202 | ..forward(); 203 | 204 | setState(() {}); 205 | }); 206 | } else { 207 | // Always update the list with the newest data, 208 | // even if the lists have the same value equality. 209 | _data = List.from(_newItems); 210 | } 211 | } 212 | 213 | @nonVirtual 214 | @protected 215 | @override 216 | bool areContentsTheSame(E oldItem, E newItem) => true; 217 | 218 | @nonVirtual 219 | @protected 220 | @override 221 | bool areItemsTheSame(E oldItem, E newItem) => 222 | widget.areItemsTheSame(oldItem, newItem); 223 | 224 | @mustCallSuper 225 | @protected 226 | @override 227 | void onInserted(int index, E item) => 228 | list.insertItem(index, duration: widget.insertDuration); 229 | 230 | @mustCallSuper 231 | @protected 232 | @override 233 | void onRemoved(int index) { 234 | if (index >= oldList.length) return; 235 | 236 | final item = oldList[index]; 237 | 238 | list.removeItem( 239 | index, 240 | (context, animation) => 241 | removeItemBuilder?.call(context, animation, item) ?? 242 | itemBuilder(context, animation, item, index), 243 | duration: widget.removeDuration, 244 | ); 245 | } 246 | 247 | @mustCallSuper 248 | @protected 249 | @override 250 | void onChanged(int startIndex, List itemsChanged) { 251 | int i = 0; 252 | for (final item in itemsChanged) { 253 | final index = startIndex + i; 254 | if (index >= data.length) continue; 255 | 256 | _changes[item] = data[index]; 257 | i++; 258 | } 259 | } 260 | 261 | @nonVirtual 262 | @protected 263 | Widget buildItem( 264 | BuildContext context, Animation animation, E item, int index) { 265 | if (updateItemBuilder != null && changes[item] != null) { 266 | return buildUpdatedItemWidget(item); 267 | } 268 | 269 | return itemBuilder(context, animation, item, index); 270 | } 271 | 272 | @protected 273 | Widget buildUpdatedItemWidget(E newItem) { 274 | final oldItem = _changes[newItem]; 275 | 276 | return AnimatedBuilder( 277 | animation: updateAnimation, 278 | builder: (context, _) { 279 | final value = updateAnimController.value; 280 | final item = value < 0.5 ? oldItem : newItem; 281 | 282 | return updateItemBuilder!(context, updateAnimation, item!); 283 | }, 284 | ); 285 | } 286 | 287 | @override 288 | void dispose() { 289 | updateAnimController.dispose(); 290 | super.dispose(); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /lib/src/reorderable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'src.dart'; 4 | 5 | typedef ReorderableBuilder = Widget Function( 6 | BuildContext context, 7 | Animation animation, 8 | bool inDrag, 9 | ); 10 | 11 | /// The parent widget of every item in an [ImplicitlyAnimatedReorderableList]. 12 | class Reorderable extends StatefulWidget { 13 | /// Called, as needed, to build the child this Reorderable. 14 | /// 15 | /// The [ReorderableBuilder] `animation` parameter supplies you with an animation you can use to 16 | /// transition between the normal and the dragged state of the item. The `inDrag` parameter 17 | /// indicates whether this item is currently being dragged/reordered. 18 | final ReorderableBuilder? builder; 19 | 20 | /// Used, as needed, to build the child this Reorderable. 21 | /// 22 | /// This can be used to show a child that should not have 23 | /// any animation when being dragged or dropped. 24 | final Widget? child; 25 | 26 | /// Creates a reorderable widget that must be the parent of every 27 | /// item in an [ImplicitlyAnimatedReorderableList]. 28 | /// 29 | /// A [key] must be specified that uniquely identifies this Reorderable 30 | /// in the list. The value of the key should not change throughout 31 | /// the lifecycle of the item. For example if your item has a unique id, 32 | /// you could use that with a `ValueKey`. 33 | /// 34 | /// Either [builder] or [child] must be specified. The [builder] 35 | /// can be used for custom animations when the item should be animated 36 | /// between dragged and normal state. If you don't want to show any 37 | /// animation between dragged and normal state, you can also use the 38 | /// [child] as a shorthand instead. 39 | const Reorderable({ 40 | required Key key, 41 | this.builder, 42 | this.child, 43 | }) : assert(builder != null || child != null), 44 | super(key: key); 45 | 46 | @override 47 | ReorderableState createState() => ReorderableState(); 48 | 49 | static ReorderableState? maybeOf(BuildContext context) { 50 | return context.findAncestorStateOfType(); 51 | } 52 | } 53 | 54 | class ReorderableState extends State 55 | with SingleTickerProviderStateMixin { 56 | late Key key = widget.key ?? UniqueKey(); 57 | 58 | late final _dragController = AnimationController( 59 | duration: const Duration(milliseconds: 300), 60 | vsync: this, 61 | ); 62 | 63 | late final _dragAnimation = CurvedAnimation( 64 | parent: _dragController, 65 | curve: Curves.linear, 66 | ); 67 | 68 | Animation? _translation; 69 | 70 | bool _isVertical = true; 71 | 72 | bool _inDrag = false; 73 | bool get inDrag => _inDrag; 74 | set inDrag(bool value) { 75 | if (value != inDrag) { 76 | _inDrag = value; 77 | value ? _dragController.animateTo(1.0) : _dragController.animateBack(0.0); 78 | } 79 | } 80 | 81 | // ignore: avoid_setters_without_getters 82 | set duration(Duration value) => _dragController.duration = value; 83 | 84 | void setTranslation(Animation? animation) { 85 | if (mounted) { 86 | setState(() { 87 | _translation = animation; 88 | }); 89 | } 90 | } 91 | 92 | void rebuild() { 93 | if (mounted) { 94 | setState(() {}); 95 | } 96 | } 97 | 98 | void _registerItem() { 99 | final list = ImplicitlyAnimatedReorderableList.maybeOf(context)!; 100 | 101 | list.registerItem(this); 102 | _dragController.duration = list.widget.settleDuration; 103 | 104 | inDrag = list.dragItem?.key == key && list.inDrag; 105 | _isVertical = list.isVertical; 106 | } 107 | 108 | @override 109 | Widget build(BuildContext context) { 110 | _registerItem(); 111 | 112 | final child = () { 113 | if (widget.child != null) { 114 | return widget.child; 115 | } else { 116 | return widget.builder!(context, _dragAnimation, _inDrag); 117 | } 118 | }(); 119 | 120 | return AnimatedBuilder( 121 | child: child, 122 | animation: _translation ?? const AlwaysStoppedAnimation(0.0), 123 | builder: (context, child) { 124 | final offset = _translation?.value ?? 0.0; 125 | 126 | return Transform.translate( 127 | offset: Offset( 128 | _isVertical ? 0.0 : offset, 129 | _isVertical ? offset : 0.0, 130 | ), 131 | child: child, 132 | ); 133 | }, 134 | ); 135 | } 136 | 137 | @override 138 | void dispose() { 139 | _dragController.dispose(); 140 | super.dispose(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/src.dart: -------------------------------------------------------------------------------- 1 | export 'diff/diff.dart'; 2 | export 'handle.dart'; 3 | export 'implicitly_animated_list.dart'; 4 | export 'implicitly_animated_list_base.dart'; 5 | export 'implicitly_animated_reorderable_list.dart'; 6 | export 'reorderable.dart'; 7 | export 'util/util.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/transitions/size_fade_transition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A transition that fades the `child` in or out before shrinking or expanding 4 | /// to the `childs` size along the `axis`. 5 | /// 6 | /// This can be used as a item transition in an [ImplicitlyAnimatedReorderableList]. 7 | class SizeFadeTransition extends StatefulWidget { 8 | /// The animation to be used. 9 | final Animation animation; 10 | 11 | /// The curve of the animation. 12 | final Curve curve; 13 | 14 | /// How long the [Interval] for the [SizeTransition] should be. 15 | /// 16 | /// The value must be between 0 and 1. 17 | /// 18 | /// For example a `sizeFraction` of `0.66` would result in `Interval(0.0, 0.66)` 19 | /// for the size animation and `Interval(0.66, 1.0)` for the opacity animation. 20 | final double sizeFraction; 21 | 22 | /// [Axis.horizontal] modifies the width, 23 | /// [Axis.vertical] modifies the height. 24 | final Axis axis; 25 | 26 | /// Describes how to align the child along the axis the [animation] is 27 | /// modifying. 28 | /// 29 | /// A value of -1.0 indicates the top when [axis] is [Axis.vertical], and the 30 | /// start when [axis] is [Axis.horizontal]. The start is on the left when the 31 | /// text direction in effect is [TextDirection.ltr] and on the right when it 32 | /// is [TextDirection.rtl]. 33 | /// 34 | /// A value of 1.0 indicates the bottom or end, depending upon the [axis]. 35 | /// 36 | /// A value of 0.0 (the default) indicates the center for either [axis] value. 37 | final double axisAlignment; 38 | 39 | /// The child widget. 40 | final Widget? child; 41 | const SizeFadeTransition({ 42 | Key? key, 43 | required this.animation, 44 | this.sizeFraction = 2 / 3, 45 | this.curve = Curves.linear, 46 | this.axis = Axis.vertical, 47 | this.axisAlignment = 0.0, 48 | this.child, 49 | }) : assert(sizeFraction >= 0.0 && sizeFraction <= 1.0), 50 | super(key: key); 51 | 52 | @override 53 | _SizeFadeTransitionState createState() => _SizeFadeTransitionState(); 54 | } 55 | 56 | class _SizeFadeTransitionState extends State { 57 | late Animation size; 58 | late Animation opacity; 59 | 60 | @override 61 | void initState() { 62 | super.initState(); 63 | didUpdateWidget(widget); 64 | } 65 | 66 | @override 67 | void didUpdateWidget(SizeFadeTransition oldWidget) { 68 | super.didUpdateWidget(oldWidget); 69 | 70 | final curve = 71 | CurvedAnimation(parent: widget.animation, curve: widget.curve); 72 | size = CurvedAnimation( 73 | curve: Interval(0.0, widget.sizeFraction), parent: curve); 74 | opacity = CurvedAnimation( 75 | curve: Interval(widget.sizeFraction, 1.0), parent: curve); 76 | } 77 | 78 | @override 79 | Widget build(BuildContext context) { 80 | return SizeTransition( 81 | sizeFactor: size as Animation, 82 | axis: widget.axis, 83 | axisAlignment: widget.axisAlignment, 84 | child: FadeTransition( 85 | opacity: opacity as Animation, 86 | child: widget.child, 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/transitions/transitions.dart: -------------------------------------------------------------------------------- 1 | export 'size_fade_transition.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/util/handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | /// A class that can schedule a function to run at a later time using Future.delayed. 4 | /// Additionally it supports canceling the future by not invoking the callback function 5 | /// if it was canceled before. 6 | class Handler { 7 | VoidCallback? _callback; 8 | bool _canceled = false; 9 | bool _finished = false; 10 | bool _invoked = false; 11 | 12 | void post(Duration delay, VoidCallback callback) { 13 | if (!_invoked) { 14 | _callback = callback; 15 | _invoked = true; 16 | 17 | Future.delayed(delay, () { 18 | if (!isFinished) { 19 | callback(); 20 | } 21 | 22 | _finished = true; 23 | }); 24 | } 25 | } 26 | 27 | void runNow() { 28 | if (!isFinished) { 29 | _callback?.call(); 30 | _finished = true; 31 | } 32 | } 33 | 34 | bool get isFinished => _finished || _canceled; 35 | bool get isCanceled => _canceled; 36 | 37 | void cancel() => _canceled = true; 38 | } 39 | 40 | Handler post(int delay, VoidCallback callback) { 41 | return Handler()..post(Duration(milliseconds: delay), callback); 42 | } 43 | 44 | Handler postDuration(Duration delay, VoidCallback callback) { 45 | return Handler()..post(delay, callback); 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/util/invisible.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Invisible extends StatelessWidget { 4 | final bool invisible; 5 | final Widget? child; 6 | const Invisible({ 7 | Key? key, 8 | this.child, 9 | this.invisible = false, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Visibility( 15 | visible: !invisible, 16 | maintainSize: true, 17 | maintainAnimation: true, 18 | maintainState: true, 19 | child: child!, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/util/key_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension GlobalKeyExtension on GlobalKey { 4 | RenderBox? get renderBox => currentContext?.renderBox; 5 | 6 | Size? get size => renderBox?.size; 7 | 8 | double? get height => size?.height; 9 | 10 | double? get width => size?.width; 11 | 12 | Offset? get offset => renderBox?.offset; 13 | } 14 | 15 | extension BuildContextExtension on BuildContext { 16 | RenderBox? get renderBox => findRenderObject() as RenderBox?; 17 | 18 | Size? get size => renderBox?.size; 19 | 20 | double? get height => size?.height; 21 | 22 | double? get width => size?.width; 23 | 24 | Offset? get offset => renderBox?.offset; 25 | } 26 | 27 | extension RenderBoxExtension on RenderBox { 28 | Offset get offset => localToGlobal(Offset.zero); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/util/sliver_child_separated_builder_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | class SliverChildSeparatedBuilderDelegate extends SliverChildBuilderDelegate { 6 | SliverChildSeparatedBuilderDelegate({ 7 | required NullableIndexedWidgetBuilder itemBuilder, 8 | ChildIndexGetter? findChildIndexCallback, 9 | required NullableIndexedWidgetBuilder separatorBuilder, 10 | int? itemCount, 11 | bool addAutomaticKeepAlives = true, 12 | bool addRepaintBoundaries = true, 13 | bool addSemanticIndexes = true, 14 | }) : super( 15 | (BuildContext context, int index) { 16 | final int itemIndex = index ~/ 2; 17 | final Widget? widget; 18 | if (index.isEven) { 19 | widget = itemBuilder(context, itemIndex); 20 | } else { 21 | widget = separatorBuilder(context, itemIndex); 22 | // ignore: prefer_asserts_with_message , we use FlutterError 23 | assert(() { 24 | if (widget == null) { 25 | throw FlutterError('separatorBuilder cannot return null.'); 26 | } 27 | 28 | return true; 29 | }()); 30 | } 31 | 32 | return widget; 33 | }, 34 | findChildIndexCallback: findChildIndexCallback, 35 | childCount: itemCount == null ? null : math.max(0, itemCount * 2 - 1), 36 | addAutomaticKeepAlives: addAutomaticKeepAlives, 37 | addRepaintBoundaries: addRepaintBoundaries, 38 | addSemanticIndexes: addSemanticIndexes, 39 | semanticIndexCallback: (Widget _, int index) { 40 | return index.isEven ? index ~/ 2 : null; 41 | }, 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/util/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | export 'handler.dart'; 6 | export 'invisible.dart'; 7 | export 'key_extensions.dart'; 8 | 9 | void postFrame(VoidCallback callback) => 10 | WidgetsBinding.instance.addPostFrameCallback((_) => callback()); 11 | 12 | extension ListExtension on List { 13 | E? getOrNull(int index) { 14 | try { 15 | return this[index]; 16 | // ignore: avoid_catching_errors 17 | } on Error { 18 | return null; 19 | } on Exception { 20 | return null; 21 | } 22 | } 23 | 24 | E? get firstOrNull => isNotEmpty ? first : null; 25 | } 26 | 27 | extension NumExtension on T { 28 | bool isBetween(T min, T max) => this >= min && this <= max; 29 | 30 | T atLeast(T min) => math.max(this, min); 31 | T atMost(T max) => math.min(this, max); 32 | } 33 | -------------------------------------------------------------------------------- /lib/transitions.dart: -------------------------------------------------------------------------------- 1 | library transitions; 2 | 3 | export 'src/transitions/transitions.dart'; 4 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: animated_list_plus 2 | description: A Flutter ListView that implicitly animates between the changes of 3 | two lists with the support to reorder its items. 4 | version: 0.5.2 5 | homepage: https://github.com/wwwdata/implicitly_animated_reorderable_list 6 | 7 | environment: 8 | sdk: '>=2.12.0 <4.0.0' 9 | flutter: ">=3.7.0" 10 | 11 | dependencies: 12 | async: ^2.8.1 13 | flutter: 14 | sdk: flutter 15 | meta: ^1.7.0 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | -------------------------------------------------------------------------------- /test/diff_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:animated_list_plus/animated_list_plus.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | // ignore_for_file: avoid_print 5 | 6 | void main() { 7 | test('Should detect deletions correctly', () async { 8 | // arrange 9 | final oldItems = List.generate(10, (index) => _Model(index.toString())); 10 | // If you uncomment this, everything will work as expected. 11 | // final newItems = List.from(oldItems); 12 | final newItems = List.generate(10, (index) => _Model(index.toString())); 13 | newItems.removeAt(1); 14 | 15 | // act 16 | final diff = await MyersDiff.diff<_Model>( 17 | newItems, 18 | oldItems, 19 | areItemsTheSame: (oldItem, newItem) => oldItem.id == newItem.id, 20 | // If you set this to false, everything will work as expected. 21 | spawnIsolate: true, 22 | ); 23 | // assert 24 | 25 | print('old: ${oldItems.map((e) => e.id).toList()}'); 26 | print('new: ${newItems.map((e) => e.id).toList()}'); 27 | print('diff $diff'); 28 | }); 29 | } 30 | 31 | class _Model { 32 | final String id; 33 | _Model(this.id); 34 | 35 | @override 36 | String toString() => id; 37 | } 38 | --------------------------------------------------------------------------------