├── .gitignore ├── LICENSE ├── README.md └── snap ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── test │ └── example_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── Export.dart ├── misc.dart ├── snap.dart └── snap_controller.dart ├── pubspec.lock ├── pubspec.yaml └── test └── snap_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | /gitignore 2 | /android 3 | /build 4 | /ios 5 | .notes.md 6 | .notes.txt 7 | 8 | # Miscellaneous 9 | *.class 10 | *.log 11 | *.pyc 12 | *.swp 13 | .DS_Store 14 | .atom/ 15 | .buildlog/ 16 | .history 17 | .svn/ 18 | 19 | # IntelliJ related 20 | *.iml 21 | *.ipr 22 | *.iws 23 | .idea/ 24 | 25 | # The .vscode folder contains launch configuration and tasks you configure in 26 | # VS Code which you may wish to be included in version control, so this line 27 | # is commented out by default. 28 | #.vscode/ 29 | 30 | # Flutter/Dart/Pub related 31 | **/doc/api/ 32 | .dart_tool/ 33 | .flutter-plugins 34 | .packages 35 | .pub-cache/ 36 | .pub/ 37 | build/ 38 | 39 | # Android related 40 | **/android/**/gradle-wrapper.jar 41 | **/android/.gradle 42 | **/android/captures/ 43 | **/android/gradlew 44 | **/android/gradlew.bat 45 | **/android/local.properties 46 | **/android/**/GeneratedPluginRegistrant.java 47 | 48 | # iOS/XCode related 49 | **/ios/**/*.mode1v3 50 | **/ios/**/*.mode2v3 51 | **/ios/**/*.moved-aside 52 | **/ios/**/*.pbxuser 53 | **/ios/**/*.perspectivev3 54 | **/ios/**/*sync/ 55 | **/ios/**/.sconsign.dblite 56 | **/ios/**/.tags* 57 | **/ios/**/.vagrant/ 58 | **/ios/**/DerivedData/ 59 | **/ios/**/Icon? 60 | **/ios/**/Pods/ 61 | **/ios/**/.symlinks/ 62 | **/ios/**/profile 63 | **/ios/**/xcuserdata 64 | **/ios/.generated/ 65 | **/ios/Flutter/App.framework 66 | **/ios/Flutter/Flutter.framework 67 | **/ios/Flutter/Generated.xcconfig 68 | **/ios/Flutter/app.flx 69 | **/ios/Flutter/app.zip 70 | **/ios/Flutter/flutter_assets/ 71 | **/ios/ServiceDefinitions.json 72 | **/ios/Runner/GeneratedPluginRegistrant.* 73 | 74 | # Exceptions to above rules. 75 | !**/ios/**/default.mode1v3 76 | !**/ios/**/default.mode2v3 77 | !**/ios/**/default.pbxuser 78 | !**/ios/**/default.perspectivev3 79 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ali Yigit Bireroglu 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snap 2 | 3 | [comment]: <> (Badges) 4 | 5 | Cosmos Software 6 | 7 | 8 | Awesome Flutter 9 | 10 | 11 | [![Pub](https://img.shields.io/pub/v/snap?color=g)](https://pub.dev/packages/snap) 12 | [![License](https://img.shields.io/github/license/aliyigitbireroglu/flutter-snap?color=blue)](https://github.com/aliyigitbireroglu/flutter-snap/blob/master/LICENSE) 13 | 14 | [comment]: <> (Introduction) 15 | An extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets. 16 | 17 | Inspired by WhatsApp's in-app Youtube player. 18 | 19 | **It is highly recommended to read the documentation and run the example project on a real device to fully understand and inspect the full range 20 | of capabilities.** 21 | 22 | [comment]: <> (ToC) 23 | [Media](#media) | [Description](#description) | [How-to-Use](#howtouse) 24 | 25 | [comment]: <> (Notice) 26 | ## Notice 27 | * **[flick](https://pub.dev/packages/flick) works as intended on actual devices even if it might appear to fail rarely on simulators. Don't be 28 | discouraged!** 29 | * * * 30 | [comment]: <> (Recent) 31 | ## Recent 32 | * **[flick](https://pub.dev/packages/flick) is now added. It is amazing! See [Media](#media) for examples.** 33 | * * * 34 | 35 | 36 | [comment]: <> (Media) 37 | 38 | ## Media 39 | 40 | Watch on **Youtube**: 41 | 42 | [v1.0.0 with Flick](https://youtu.be/vNTBsMg1NXg) 43 |

44 | [v0.1.0](https://youtu.be/anHHG3JJPrI) 45 |

46 | 47 |

48 | 49 | 50 | [comment]: <> (Description) 51 | 52 | ## Description 53 | This is an extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets. 54 | 55 | Just wrap your *snapper* widget with the SnapController widget, fill the parameters, define your *snappable* widget and this package will take care 56 | of everything else. 57 | 58 | 59 | [comment]: <> (How-to-Use) 60 | 61 | ## How-to-Use 62 | *"The view is what is being moved. It is the widget that snaps to the bound. The bound is what the view is being snapped to."* 63 | 64 | First, define two GlobalKeys- one for your view and one for your bound: 65 | 66 | ``` 67 | GlobalKey bound = GlobalKey(); 68 | GlobalKey view = GlobalKey(); 69 | ``` 70 | 71 | Then, create a SnapController such as: 72 | 73 | ``` 74 | SnapController( 75 | uiChild(), //uiChild 76 | false, //useCache 77 | view, //viewKey 78 | bound, //boundKey 79 | Offset.zero, //constraintsMin 80 | const Offset(1.0, 1.0), //constraintsMax 81 | const Offset(0.75, 0.75), //flexibilityMin 82 | const Offset(0.75, 0.75), //flexibilityMax 83 | {Key key, 84 | customBoundWidth : 0, 85 | customBoundHeight : 0, 86 | snapTargets : [ 87 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 88 | const SnapTarget(Pivot.topRight, Pivot.topRight), 89 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 90 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 91 | const SnapTarget(Pivot.center, Pivot.center) 92 | ], 93 | animateSnap : true, 94 | useFlick : true, 95 | flickSensitivity : 0.075, 96 | onMove : _onMove, 97 | onDragStart : _onDragStart, 98 | onDragUpdate : _onDragUpdate, 99 | onDragEnd : _onDragEnd, 100 | onSnap : _onSnap}) 101 | 102 | Widget uiChild() { 103 | return Container( 104 | key: view, 105 | ... 106 | ); 107 | } 108 | 109 | void _onMove(Offset offset); 110 | 111 | void _onDragStart(dynamic dragDetails); 112 | void _onDragUpdate(dynamic dragDetails); 113 | void _onDragEnd(dynamic dragDetails); 114 | 115 | void _onSnap(Offset offset); 116 | ``` 117 | 118 | **Further Explanations:** 119 | 120 | *For a complete set of descriptions for all parameters and methods, see the [documentation](https://pub.dev/documentation/snap/latest/).* 121 | 122 | * Set [useCache] to true if your [uiChild] doesn't change during the Peek & Pop process. 123 | * Consider the following example: 124 | 125 | ``` 126 | Column( 127 | crossAxisAlignment: CrossAxisAlignment.start, 128 | children: [ 129 | Expanded( 130 | child: Align( 131 | key: bound, 132 | alignment: const Alignment(-1.0, -1.0), 133 | child: SnapController( 134 | uiChild(), 135 | true, 136 | view, 137 | bound, 138 | Offset.zero, 139 | const Offset(1.0, 1.0), 140 | const Offset(0.75, 0.75), 141 | const Offset(0.75, 0.75), 142 | snapTargets: [ 143 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 144 | const SnapTarget(Pivot.topRight, Pivot.topRight), 145 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 146 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 147 | const SnapTarget(Pivot.center, Pivot.center) 148 | ] 149 | ) 150 | ) 151 | ) 152 | ] 153 | ) 154 | ``` 155 | 156 | In this excerpt, the bound is an Align widget which expands through a Column widget. 157 | 158 | The SnapController is confined between Offset.zero and Offset(1.0, 1.0). This means the view will not exceed the limits of the bound. 159 | 160 | The flexibility is confined between Offset(0.75, 0.75) and Offset(0.75, 0.75). This means that the view can be moved beyond the horizontal/vertical 161 | min/max constraints with a flexibility of 0.75 before it snaps. 162 | 163 | The snapTargets determine from where and to where the view should snap once the movement is over. In this example: 164 | 165 | 1. The top left corner of the view can snap to the top left corner of the bound. 166 | 2. The top right corner of the view can snap to the top right corner of the bound. 167 | 3. The bottom left corner of the view can snap to the bottom left corner of the bound. 168 | 4. The bottom right corner of the view can snap to the bottom right corner of the bound. 169 | 5. The center of the view can snap to the center of the bound. 170 | 171 | Keep in mind that these constant values are provided only for the ease of use. snapTargets can consist of any values you wish. 172 | 173 | * Use [SnapControllerState]'s [bool isMoved(double treshold)] method to determine if the view is moved or not where [treshold] is the distance at 174 | which the view should be considered to be moved. 175 | 176 | 177 | [comment]: <> (Notes) 178 | ## Notes 179 | I started using and learning Flutter only some weeks ago so this package might have some parts that don't make sense, that should be completely 180 | different, that could be much better, etc. Please let me know! Nicely! 181 | 182 | Any help, suggestion or criticism is appreciated! 183 | 184 | Cheers. 185 | 186 | [comment]: <> (CosmosSoftware) 187 |

188 | 189 |

-------------------------------------------------------------------------------- /snap/.gitignore: -------------------------------------------------------------------------------- 1 | /gitignore 2 | /android 3 | /build 4 | /ios 5 | .notes.md 6 | .notes.txt 7 | 8 | # Miscellaneous 9 | *.class 10 | *.log 11 | *.pyc 12 | *.swp 13 | .DS_Store 14 | .atom/ 15 | .buildlog/ 16 | .history 17 | .svn/ 18 | 19 | # IntelliJ related 20 | *.iml 21 | *.ipr 22 | *.iws 23 | .idea/ 24 | 25 | # The .vscode folder contains launch configuration and tasks you configure in 26 | # VS Code which you may wish to be included in version control, so this line 27 | # is commented out by default. 28 | #.vscode/ 29 | 30 | # Flutter/Dart/Pub related 31 | **/doc/api/ 32 | .dart_tool/ 33 | .flutter-plugins 34 | .packages 35 | .pub-cache/ 36 | .pub/ 37 | build/ 38 | 39 | # Android related 40 | **/android/**/gradle-wrapper.jar 41 | **/android/.gradle 42 | **/android/captures/ 43 | **/android/gradlew 44 | **/android/gradlew.bat 45 | **/android/local.properties 46 | **/android/**/GeneratedPluginRegistrant.java 47 | 48 | # iOS/XCode related 49 | **/ios/**/*.mode1v3 50 | **/ios/**/*.mode2v3 51 | **/ios/**/*.moved-aside 52 | **/ios/**/*.pbxuser 53 | **/ios/**/*.perspectivev3 54 | **/ios/**/*sync/ 55 | **/ios/**/.sconsign.dblite 56 | **/ios/**/.tags* 57 | **/ios/**/.vagrant/ 58 | **/ios/**/DerivedData/ 59 | **/ios/**/Icon? 60 | **/ios/**/Pods/ 61 | **/ios/**/.symlinks/ 62 | **/ios/**/profile 63 | **/ios/**/xcuserdata 64 | **/ios/.generated/ 65 | **/ios/Flutter/App.framework 66 | **/ios/Flutter/Flutter.framework 67 | **/ios/Flutter/Generated.xcconfig 68 | **/ios/Flutter/app.flx 69 | **/ios/Flutter/app.zip 70 | **/ios/Flutter/flutter_assets/ 71 | **/ios/ServiceDefinitions.json 72 | **/ios/Runner/GeneratedPluginRegistrant.* 73 | 74 | # Exceptions to above rules. 75 | !**/ios/**/default.mode1v3 76 | !**/ios/**/default.mode2v3 77 | !**/ios/**/default.pbxuser 78 | !**/ios/**/default.perspectivev3 79 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 80 | -------------------------------------------------------------------------------- /snap/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /snap/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.0] - 04.05.2021 2 | 3 | * Migrated to Null Safety. 4 | 5 | ## [1.0.6] - 11.10.2020 6 | 7 | * Regular maintenance. 8 | 9 | ## [1.0.5] - 21.11.2019 10 | 11 | * [minSnapDistance] is added. If set, the snapping will not occur when no [SnapTarget] is found that is closer to the [uiChild] than this value. 12 | 13 | * Updated README. 14 | 15 | ## [1.0.4] - 07.09.2019 16 | 17 | * Minor changes. 18 | 19 | ## [1.0.3] - 30.08.2019 20 | 21 | * Minor changes. 22 | 23 | * Improved code style. 24 | 25 | * [1.0.3+1] Updated README. 26 | 27 | * [1.0.3+2] Updated README. 28 | 29 | ## [1.0.2] - 23.08.2019 30 | 31 | * Minor changes. 32 | 33 | * Improved code style with trailing commas. 34 | 35 | * [1.0.2+1] Minor changes. 36 | 37 | ## [1.0.1] - 20.08.2019 38 | 39 | * Improved code style. 40 | 41 | * Code excerpt added to the README. 42 | 43 | * [1.0.1+1] Updated README. 44 | 45 | ## [1.0.0] - 18.08.2019 46 | 47 | * [flick](https://pub.dev/packages/flick) is now implemented directly to the package. 48 | 49 | * Fine tuning. 50 | 51 | * [1.0.0+1] Updated README. 52 | 53 | ## [0.1.3] - 08.08.2019 54 | 55 | * Improved code style. 56 | 57 | ## [0.1.2] - 08.08.2019 58 | 59 | * Minor changes. 60 | 61 | ## [0.1.1] - 08.08.2019 62 | 63 | * Minor changes. 64 | 65 | * Documentation added. 66 | 67 | ## [0.1.0] - 07.08.2019 68 | 69 | * Initial release. Documentation pending. 70 | -------------------------------------------------------------------------------- /snap/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ali Yigit Bireroglu 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. 22 | -------------------------------------------------------------------------------- /snap/README.md: -------------------------------------------------------------------------------- 1 | # snap 2 | 3 | [comment]: <> (Badges) 4 | 5 | Cosmos Software 6 | 7 | 8 | Awesome Flutter 9 | 10 | 11 | [![Pub](https://img.shields.io/pub/v/snap?color=g)](https://pub.dev/packages/snap) 12 | [![License](https://img.shields.io/github/license/aliyigitbireroglu/flutter-snap?color=blue)](https://github.com/aliyigitbireroglu/flutter-snap/blob/master/LICENSE) 13 | 14 | [comment]: <> (Introduction) 15 | An extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets. 16 | 17 | Inspired by WhatsApp's in-app Youtube player. 18 | 19 | **It is highly recommended to read the documentation and run the example project on a real device to fully understand and inspect the full range 20 | of capabilities.** 21 | 22 | [comment]: <> (ToC) 23 | [Media](#media) | [Description](#description) | [How-to-Use](#howtouse) 24 | 25 | [comment]: <> (Notice) 26 | ## Notice 27 | * **[flick](https://pub.dev/packages/flick) works as intended on actual devices even if it might appear to fail rarely on simulators. Don't be 28 | discouraged!** 29 | * * * 30 | [comment]: <> (Recent) 31 | ## Recent 32 | * **[flick](https://pub.dev/packages/flick) is now added. It is amazing! See [Media](#media) for examples.** 33 | * * * 34 | 35 | 36 | [comment]: <> (Media) 37 | 38 | ## Media 39 | 40 | Watch on **Youtube**: 41 | 42 | [v1.0.0 with Flick](https://youtu.be/vNTBsMg1NXg) 43 |

44 | [v0.1.0](https://youtu.be/anHHG3JJPrI) 45 |

46 | 47 |

48 | 49 | 50 | [comment]: <> (Description) 51 | 52 | ## Description 53 | This is an extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets. 54 | 55 | Just wrap your *snapper* widget with the SnapController widget, fill the parameters, define your *snappable* widget and this package will take care 56 | of everything else. 57 | 58 | 59 | [comment]: <> (How-to-Use) 60 | 61 | ## How-to-Use 62 | *"The view is what is being moved. It is the widget that snaps to the bound. The bound is what the view is being snapped to."* 63 | 64 | First, define two GlobalKeys- one for your view and one for your bound: 65 | 66 | ``` 67 | GlobalKey bound = GlobalKey(); 68 | GlobalKey view = GlobalKey(); 69 | ``` 70 | 71 | Then, create a SnapController such as: 72 | 73 | ``` 74 | SnapController( 75 | uiChild(), //uiChild 76 | false, //useCache 77 | view, //viewKey 78 | bound, //boundKey 79 | Offset.zero, //constraintsMin 80 | const Offset(1.0, 1.0), //constraintsMax 81 | const Offset(0.75, 0.75), //flexibilityMin 82 | const Offset(0.75, 0.75), //flexibilityMax 83 | {Key key, 84 | customBoundWidth : 0, 85 | customBoundHeight : 0, 86 | snapTargets : [ 87 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 88 | const SnapTarget(Pivot.topRight, Pivot.topRight), 89 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 90 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 91 | const SnapTarget(Pivot.center, Pivot.center) 92 | ], 93 | minSnapDistance : 100.0, 94 | animateSnap : true, 95 | useFlick : true, 96 | flickSensitivity : 0.075, 97 | onMove : _onMove, 98 | onDragStart : _onDragStart, 99 | onDragUpdate : _onDragUpdate, 100 | onDragEnd : _onDragEnd, 101 | onSnap : _onSnap}) 102 | 103 | Widget uiChild() { 104 | return Container( 105 | key: view, 106 | ... 107 | ); 108 | } 109 | 110 | void _onMove(Offset offset); 111 | 112 | void _onDragStart(dynamic dragDetails); 113 | void _onDragUpdate(dynamic dragDetails); 114 | void _onDragEnd(dynamic dragDetails); 115 | 116 | void _onSnap(Offset offset); 117 | ``` 118 | 119 | **Further Explanations:** 120 | 121 | *For a complete set of descriptions for all parameters and methods, see the [documentation](https://pub.dev/documentation/snap/latest/).* 122 | 123 | * Set [useCache] to true if your [uiChild] doesn't change at runtime. 124 | * Consider the following example: 125 | 126 | ``` 127 | Column( 128 | crossAxisAlignment: CrossAxisAlignment.start, 129 | children: [ 130 | Expanded( 131 | child: Align( 132 | key: bound, 133 | alignment: const Alignment(-1.0, -1.0), 134 | child: SnapController( 135 | uiChild(), 136 | true, 137 | view, 138 | bound, 139 | Offset.zero, 140 | const Offset(1.0, 1.0), 141 | const Offset(0.75, 0.75), 142 | const Offset(0.75, 0.75), 143 | snapTargets: [ 144 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 145 | const SnapTarget(Pivot.topRight, Pivot.topRight), 146 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 147 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 148 | const SnapTarget(Pivot.center, Pivot.center) 149 | ] 150 | ) 151 | ) 152 | ) 153 | ] 154 | ) 155 | ``` 156 | 157 | In this excerpt, the bound is an Align widget which expands through a Column widget. 158 | 159 | The SnapController is confined between Offset.zero and Offset(1.0, 1.0). This means the view will not exceed the limits of the bound. 160 | 161 | The flexibility is confined between Offset(0.75, 0.75) and Offset(0.75, 0.75). This means that the view can be moved beyond the horizontal/vertical 162 | min/max constraints with a flexibility of 0.75 before it snaps. 163 | 164 | The snapTargets determine from where and to where the view should snap once the movement is over. In this example: 165 | 166 | 1. The top left corner of the view can snap to the top left corner of the bound. 167 | 2. The top right corner of the view can snap to the top right corner of the bound. 168 | 3. The bottom left corner of the view can snap to the bottom left corner of the bound. 169 | 4. The bottom right corner of the view can snap to the bottom right corner of the bound. 170 | 5. The center of the view can snap to the center of the bound. 171 | 172 | Keep in mind that these constant values are provided only for the ease of use. snapTargets can consist of any values you wish. 173 | 174 | * Use [SnapControllerState]'s [bool isMoved(double treshold)] method to determine if the view is moved or not where [treshold] is the distance at 175 | which the view should be considered to be moved. 176 | 177 | 178 | [comment]: <> (Notes) 179 | ## Notes 180 | I started using and learning Flutter only some weeks ago so this package might have some parts that don't make sense, that should be completely 181 | different, that could be much better, etc. Please let me know! Nicely! 182 | 183 | Any help, suggestion or criticism is appreciated! 184 | 185 | Cheers. 186 | 187 | [comment]: <> (CosmosSoftware) 188 |

189 | 190 |

191 | -------------------------------------------------------------------------------- /snap/example/.gitignore: -------------------------------------------------------------------------------- 1 | /gitignore 2 | /android 3 | /build 4 | /ios 5 | .notes.md 6 | .notes.txt 7 | 8 | # Miscellaneous 9 | *.class 10 | *.log 11 | *.pyc 12 | *.swp 13 | .DS_Store 14 | .atom/ 15 | .buildlog/ 16 | .history 17 | .svn/ 18 | 19 | # IntelliJ related 20 | *.iml 21 | *.ipr 22 | *.iws 23 | .idea/ 24 | 25 | # The .vscode folder contains launch configuration and tasks you configure in 26 | # VS Code which you may wish to be included in version control, so this line 27 | # is commented out by default. 28 | #.vscode/ 29 | 30 | # Flutter/Dart/Pub related 31 | **/doc/api/ 32 | .dart_tool/ 33 | .flutter-plugins 34 | .packages 35 | .pub-cache/ 36 | .pub/ 37 | build/ 38 | 39 | # Android related 40 | **/android/**/gradle-wrapper.jar 41 | **/android/.gradle 42 | **/android/captures/ 43 | **/android/gradlew 44 | **/android/gradlew.bat 45 | **/android/local.properties 46 | **/android/**/GeneratedPluginRegistrant.java 47 | 48 | # iOS/XCode related 49 | **/ios/**/*.mode1v3 50 | **/ios/**/*.mode2v3 51 | **/ios/**/*.moved-aside 52 | **/ios/**/*.pbxuser 53 | **/ios/**/*.perspectivev3 54 | **/ios/**/*sync/ 55 | **/ios/**/.sconsign.dblite 56 | **/ios/**/.tags* 57 | **/ios/**/.vagrant/ 58 | **/ios/**/DerivedData/ 59 | **/ios/**/Icon? 60 | **/ios/**/Pods/ 61 | **/ios/**/.symlinks/ 62 | **/ios/**/profile 63 | **/ios/**/xcuserdata 64 | **/ios/.generated/ 65 | **/ios/Flutter/App.framework 66 | **/ios/Flutter/Flutter.framework 67 | **/ios/Flutter/Generated.xcconfig 68 | **/ios/Flutter/app.flx 69 | **/ios/Flutter/app.zip 70 | **/ios/Flutter/flutter_assets/ 71 | **/ios/ServiceDefinitions.json 72 | **/ios/Runner/GeneratedPluginRegistrant.* 73 | 74 | # Exceptions to above rules. 75 | !**/ios/**/default.mode1v3 76 | !**/ios/**/default.mode2v3 77 | !**/ios/**/default.pbxuser 78 | !**/ios/**/default.perspectivev3 79 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 80 | -------------------------------------------------------------------------------- /snap/example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /snap/example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.0] - 04.05.2021 2 | 3 | * Migrated to Null Safety. 4 | 5 | ## [1.0.6] - 11.10.2020 6 | 7 | * Regular maintenance. 8 | 9 | ## [1.0.5] - 21.11.2019 10 | 11 | * [minSnapDistance] is added. If set, the snapping will not occur when no [SnapTarget] is found that is closer to the [uiChild] than this value. 12 | 13 | * Updated README. 14 | 15 | ## [1.0.4] - 07.09.2019 16 | 17 | * Minor changes. 18 | 19 | ## [1.0.3] - 30.08.2019 20 | 21 | * Minor changes. 22 | 23 | * Improved code style. 24 | 25 | * [1.0.3+1] Updated README. 26 | 27 | * [1.0.3+2] Updated README. 28 | 29 | ## [1.0.2] - 23.08.2019 30 | 31 | * Minor changes. 32 | 33 | * Improved code style with trailing commas. 34 | 35 | * [1.0.2+1] Minor changes. 36 | 37 | ## [1.0.1] - 20.08.2019 38 | 39 | * Improved code style. 40 | 41 | * Code excerpt added to the README. 42 | 43 | * [1.0.1+1] Updated README. 44 | 45 | ## [1.0.0] - 18.08.2019 46 | 47 | * [flick](https://pub.dev/packages/flick) is now implemented directly to the package. 48 | 49 | * Fine tuning. 50 | 51 | * [1.0.0+1] Updated README. 52 | 53 | ## [0.1.3] - 08.08.2019 54 | 55 | * Improved code style. 56 | 57 | ## [0.1.2] - 08.08.2019 58 | 59 | * Minor changes. 60 | 61 | ## [0.1.1] - 08.08.2019 62 | 63 | * Minor changes. 64 | 65 | * Documentation added. 66 | 67 | ## [0.1.0] - 07.08.2019 68 | 69 | * Initial release. Documentation pending. 70 | -------------------------------------------------------------------------------- /snap/example/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ali Yigit Bireroglu 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. 22 | -------------------------------------------------------------------------------- /snap/example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | Example Project for snap. 4 | 5 | 6 | # snap 7 | 8 | [comment]: <> (Badges) 9 | 10 | Cosmos Software 11 | 12 | 13 | Awesome Flutter 14 | 15 | 16 | [![Pub](https://img.shields.io/pub/v/snap?color=g)](https://pub.dev/packages/snap) 17 | [![License](https://img.shields.io/github/license/aliyigitbireroglu/flutter-snap?color=blue)](https://github.com/aliyigitbireroglu/flutter-snap/blob/master/LICENSE) 18 | 19 | [comment]: <> (Introduction) 20 | An extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets. 21 | 22 | Inspired by WhatsApp's in-app Youtube player. 23 | 24 | **It is highly recommended to read the documentation and run the example project on a real device to fully understand and inspect the full range 25 | of capabilities.** 26 | 27 | [comment]: <> (ToC) 28 | [Media](#media) | [Description](#description) | [How-to-Use](#howtouse) 29 | 30 | [comment]: <> (Notice) 31 | ## Notice 32 | * **[flick](https://pub.dev/packages/flick) works as intended on actual devices even if it might appear to fail rarely on simulators. Don't be 33 | discouraged!** 34 | * * * 35 | [comment]: <> (Recent) 36 | ## Recent 37 | * **[flick](https://pub.dev/packages/flick) is now added. It is amazing! See [Media](#media) for examples.** 38 | * * * 39 | 40 | 41 | [comment]: <> (Media) 42 | 43 | ## Media 44 | 45 | Watch on **Youtube**: 46 | 47 | [v1.0.0 with Flick](https://youtu.be/vNTBsMg1NXg) 48 |

49 | [v0.1.0](https://youtu.be/anHHG3JJPrI) 50 |

51 | 52 |

53 | 54 | 55 | [comment]: <> (Description) 56 | 57 | ## Description 58 | This is an extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets. 59 | 60 | Just wrap your *snapper* widget with the SnapController widget, fill the parameters, define your *snappable* widget and this package will take care 61 | of everything else. 62 | 63 | 64 | [comment]: <> (How-to-Use) 65 | 66 | ## How-to-Use 67 | *"The view is what is being moved. It is the widget that snaps to the bound. The bound is what the view is being snapped to."* 68 | 69 | First, define two GlobalKeys- one for your view and one for your bound: 70 | 71 | ``` 72 | GlobalKey bound = GlobalKey(); 73 | GlobalKey view = GlobalKey(); 74 | ``` 75 | 76 | Then, create a SnapController such as: 77 | 78 | ``` 79 | SnapController( 80 | uiChild(), //uiChild 81 | false, //useCache 82 | view, //viewKey 83 | bound, //boundKey 84 | Offset.zero, //constraintsMin 85 | const Offset(1.0, 1.0), //constraintsMax 86 | const Offset(0.75, 0.75), //flexibilityMin 87 | const Offset(0.75, 0.75), //flexibilityMax 88 | {Key key, 89 | customBoundWidth : 0, 90 | customBoundHeight : 0, 91 | snapTargets : [ 92 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 93 | const SnapTarget(Pivot.topRight, Pivot.topRight), 94 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 95 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 96 | const SnapTarget(Pivot.center, Pivot.center) 97 | ], 98 | minSnapDistance : 100.0, 99 | animateSnap : true, 100 | useFlick : true, 101 | flickSensitivity : 0.075, 102 | onMove : _onMove, 103 | onDragStart : _onDragStart, 104 | onDragUpdate : _onDragUpdate, 105 | onDragEnd : _onDragEnd, 106 | onSnap : _onSnap}) 107 | 108 | Widget uiChild() { 109 | return Container( 110 | key: view, 111 | ... 112 | ); 113 | } 114 | 115 | void _onMove(Offset offset); 116 | 117 | void _onDragStart(dynamic dragDetails); 118 | void _onDragUpdate(dynamic dragDetails); 119 | void _onDragEnd(dynamic dragDetails); 120 | 121 | void _onSnap(Offset offset); 122 | ``` 123 | 124 | **Further Explanations:** 125 | 126 | *For a complete set of descriptions for all parameters and methods, see the [documentation](https://pub.dev/documentation/snap/latest/).* 127 | 128 | * Set [useCache] to true if your [uiChild] doesn't change at runtime. 129 | * Consider the following example: 130 | 131 | ``` 132 | Column( 133 | crossAxisAlignment: CrossAxisAlignment.start, 134 | children: [ 135 | Expanded( 136 | child: Align( 137 | key: bound, 138 | alignment: const Alignment(-1.0, -1.0), 139 | child: SnapController( 140 | uiChild(), 141 | true, 142 | view, 143 | bound, 144 | Offset.zero, 145 | const Offset(1.0, 1.0), 146 | const Offset(0.75, 0.75), 147 | const Offset(0.75, 0.75), 148 | snapTargets: [ 149 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 150 | const SnapTarget(Pivot.topRight, Pivot.topRight), 151 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 152 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 153 | const SnapTarget(Pivot.center, Pivot.center) 154 | ] 155 | ) 156 | ) 157 | ) 158 | ] 159 | ) 160 | ``` 161 | 162 | In this excerpt, the bound is an Align widget which expands through a Column widget. 163 | 164 | The SnapController is confined between Offset.zero and Offset(1.0, 1.0). This means the view will not exceed the limits of the bound. 165 | 166 | The flexibility is confined between Offset(0.75, 0.75) and Offset(0.75, 0.75). This means that the view can be moved beyond the horizontal/vertical 167 | min/max constraints with a flexibility of 0.75 before it snaps. 168 | 169 | The snapTargets determine from where and to where the view should snap once the movement is over. In this example: 170 | 171 | 1. The top left corner of the view can snap to the top left corner of the bound. 172 | 2. The top right corner of the view can snap to the top right corner of the bound. 173 | 3. The bottom left corner of the view can snap to the bottom left corner of the bound. 174 | 4. The bottom right corner of the view can snap to the bottom right corner of the bound. 175 | 5. The center of the view can snap to the center of the bound. 176 | 177 | Keep in mind that these constant values are provided only for the ease of use. snapTargets can consist of any values you wish. 178 | 179 | * Use [SnapControllerState]'s [bool isMoved(double treshold)] method to determine if the view is moved or not where [treshold] is the distance at 180 | which the view should be considered to be moved. 181 | 182 | 183 | [comment]: <> (Notes) 184 | ## Notes 185 | I started using and learning Flutter only some weeks ago so this package might have some parts that don't make sense, that should be completely 186 | different, that could be much better, etc. Please let me know! Nicely! 187 | 188 | Any help, suggestion or criticism is appreciated! 189 | 190 | Cheers. 191 | 192 | [comment]: <> (CosmosSoftware) 193 |

194 | 195 |

196 | -------------------------------------------------------------------------------- /snap/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // © Cosmos Software | Ali Yigit Bireroglu / 3 | // All material used in the making of this code, project, program, application, software et cetera (the "Intellectual Property") / 4 | // belongs completely and solely to Ali Yigit Bireroglu. This includes but is not limited to the source code, the multimedia and / 5 | // other asset files. If you were granted this Intellectual Property for personal use, you are obligated to include this copyright / 6 | // text at all times. / 7 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 8 | //@formatter:off 9 | 10 | import 'package:flutter/material.dart'; 11 | 12 | import 'package:snap/snap.dart'; 13 | 14 | List bounds = []; 15 | List views = []; 16 | 17 | void main() => runApp(MyApp()); 18 | 19 | class MyApp extends StatelessWidget { 20 | @override 21 | Widget build(BuildContext context) { 22 | return MaterialApp( 23 | title: 'Snap Demo', 24 | theme: ThemeData(primarySwatch: Colors.blue), 25 | home: MyHomePage(title: 'Snap Demo'), 26 | ); 27 | } 28 | } 29 | 30 | class MyHomePage extends StatefulWidget { 31 | MyHomePage({Key? key, this.title}) : super(key: key); 32 | 33 | final String? title; 34 | 35 | @override 36 | _MyHomePageState createState() => _MyHomePageState(); 37 | } 38 | 39 | class _MyHomePageState extends State { 40 | double bottom = -200.0; 41 | 42 | @override 43 | void initState() { 44 | super.initState(); 45 | 46 | for (int i = 0; i < 6; i++) { 47 | bounds.add(GlobalKey()); 48 | views.add(GlobalKey()); 49 | } 50 | } 51 | 52 | Widget description(String text) { 53 | return Container( 54 | constraints: const BoxConstraints.expand(height: 75), 55 | color: Colors.green, 56 | child: Center( 57 | child: Padding( 58 | padding: const EdgeInsets.all(10), 59 | child: Text( 60 | text, 61 | style: const TextStyle( 62 | color: Colors.white, 63 | fontWeight: FontWeight.bold, 64 | fontSize: 15, 65 | ), 66 | textAlign: TextAlign.center, 67 | ), 68 | ), 69 | ), 70 | ); 71 | } 72 | 73 | Widget gap() { 74 | return Container( 75 | constraints: const BoxConstraints.expand(height: 25), 76 | color: Colors.transparent, 77 | child: Center( 78 | child: const Text( 79 | "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ", 80 | style: const TextStyle( 81 | color: Colors.black, 82 | fontWeight: FontWeight.bold, 83 | fontSize: 10, 84 | ), 85 | textAlign: TextAlign.center, 86 | ), 87 | ), 88 | ); 89 | } 90 | 91 | Widget normalBox(Key key, String text, Color color) { 92 | return Container( 93 | key: key, 94 | width: 200, 95 | height: 200, 96 | color: Colors.transparent, 97 | child: Padding( 98 | padding: const EdgeInsets.all(5), 99 | child: Container( 100 | constraints: BoxConstraints.expand(), 101 | decoration: BoxDecoration( 102 | color: color, 103 | borderRadius: const BorderRadius.all(const Radius.circular(10.0)), 104 | ), 105 | child: Center( 106 | child: Text( 107 | text, 108 | style: const TextStyle( 109 | color: Colors.white, 110 | fontWeight: FontWeight.bold, 111 | fontSize: 25, 112 | ), 113 | textAlign: TextAlign.center, 114 | ), 115 | ), 116 | ), 117 | ), 118 | ); 119 | } 120 | 121 | Widget translatedBox(Key key, String text, Color color) { 122 | return Transform.translate( 123 | offset: const Offset(50, 50), 124 | child: Container( 125 | key: key, 126 | width: 200, 127 | height: 200, 128 | color: Colors.transparent, 129 | child: Padding( 130 | padding: const EdgeInsets.all(5), 131 | child: Container( 132 | constraints: BoxConstraints.expand(), 133 | decoration: BoxDecoration( 134 | color: color, 135 | borderRadius: const BorderRadius.all(const Radius.circular(10.0)), 136 | ), 137 | child: Center( 138 | child: Text( 139 | text, 140 | style: const TextStyle( 141 | color: Colors.white, 142 | fontWeight: FontWeight.bold, 143 | fontSize: 25, 144 | ), 145 | textAlign: TextAlign.center, 146 | ), 147 | ), 148 | ), 149 | ), 150 | ), 151 | ); 152 | } 153 | 154 | Widget smallBox(Key key, String text, Color color) { 155 | return Container( 156 | key: key, 157 | width: 50, 158 | height: 50, 159 | color: Colors.transparent, 160 | child: Padding( 161 | padding: const EdgeInsets.all(5), 162 | child: Container( 163 | constraints: BoxConstraints.expand(), 164 | decoration: BoxDecoration( 165 | color: color, 166 | borderRadius: const BorderRadius.all(const Radius.circular(0.0)), 167 | ), 168 | child: Center( 169 | child: Text( 170 | text, 171 | style: const TextStyle( 172 | color: Colors.white, 173 | fontWeight: FontWeight.bold, 174 | fontSize: 25, 175 | ), 176 | textAlign: TextAlign.center, 177 | ), 178 | ), 179 | ), 180 | ), 181 | ); 182 | } 183 | 184 | Widget dottedBox(Key key, String text, Color color) { 185 | return Transform.translate( 186 | offset: const Offset(75, 100), 187 | child: Container( 188 | key: key, 189 | width: 200, 190 | height: 200, 191 | color: Colors.transparent, 192 | child: Stack( 193 | children: [ 194 | Padding( 195 | padding: const EdgeInsets.all(5), 196 | child: Container( 197 | constraints: BoxConstraints.expand(), 198 | decoration: BoxDecoration( 199 | color: color, 200 | borderRadius: const BorderRadius.all(const Radius.circular(10.0)), 201 | ), 202 | child: Center( 203 | child: Text( 204 | text, 205 | style: const TextStyle( 206 | color: Colors.white, 207 | fontWeight: FontWeight.bold, 208 | fontSize: 25, 209 | ), 210 | textAlign: TextAlign.center, 211 | ), 212 | ), 213 | ), 214 | ), 215 | Align( 216 | alignment: const Alignment(-0.9, -0.9), 217 | child: Container( 218 | width: 10, 219 | height: 10, 220 | color: Colors.orangeAccent, 221 | ), 222 | ), 223 | Align( 224 | alignment: const Alignment(0.9, -0.9), 225 | child: Container( 226 | width: 10, 227 | height: 10, 228 | color: Colors.black, 229 | ), 230 | ), 231 | Align( 232 | alignment: const Alignment(-0.9, 0.9), 233 | child: Container( 234 | width: 10, 235 | height: 10, 236 | color: Colors.deepPurpleAccent, 237 | ), 238 | ), 239 | ], 240 | ), 241 | ), 242 | ); 243 | } 244 | 245 | Widget firstNormalBoxView() { 246 | return normalBox( 247 | views[0], 248 | "Move & Snap", 249 | Colors.redAccent, 250 | ); 251 | } 252 | 253 | Widget animatedBoxView() { 254 | return normalBox( 255 | views[1], 256 | "Move & Snap", 257 | Colors.redAccent, 258 | ); 259 | } 260 | 261 | Widget dottedBoxView() { 262 | return dottedBox( 263 | views[2], 264 | "Move & Snap", 265 | Colors.redAccent, 266 | ); 267 | } 268 | 269 | Widget translatedBoxView() { 270 | return translatedBox( 271 | views[3], 272 | "Move & Snap", 273 | Colors.redAccent, 274 | ); 275 | } 276 | 277 | Widget smallBoxView() { 278 | return smallBox( 279 | views[4], 280 | "*", 281 | Colors.redAccent, 282 | ); 283 | } 284 | 285 | Widget secondNormalBoxView() { 286 | return normalBox( 287 | views[5], 288 | "Move & Snap", 289 | Colors.redAccent, 290 | ); 291 | } 292 | 293 | Widget thirdNormalBoxView() { 294 | return normalBox( 295 | views[6], 296 | "Move & Snap", 297 | Colors.redAccent, 298 | ); 299 | } 300 | 301 | @override 302 | Widget build(BuildContext context) { 303 | return Scaffold( 304 | appBar: AppBar(title: Text(widget.title!)), 305 | body: PageView( 306 | onPageChanged: (int index) { 307 | if (index == 1) 308 | setState(() { 309 | bottom = 0.0; 310 | }); 311 | else 312 | setState(() { 313 | bottom = -200.0; 314 | }); 315 | }, 316 | children: [ 317 | Scaffold( 318 | body: Column( 319 | crossAxisAlignment: CrossAxisAlignment.start, 320 | children: [ 321 | description("The box will snap to the corners or the center."), 322 | gap(), 323 | Expanded( 324 | child: Align( 325 | key: bounds[0], 326 | alignment: const Alignment(-1.0, -1.0), 327 | child: SnapController( 328 | firstNormalBoxView(), 329 | true, 330 | views[0], 331 | bounds[0], 332 | Offset.zero, 333 | const Offset(1.0, 1.0), 334 | const Offset(0.75, 0.75), 335 | const Offset(0.75, 0.75), 336 | snapTargets: [ 337 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 338 | const SnapTarget(Pivot.topRight, Pivot.topRight), 339 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 340 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 341 | const SnapTarget(Pivot.center, Pivot.center), 342 | ], 343 | minSnapDistance: 100, 344 | ), 345 | ), 346 | ), 347 | ], 348 | ), 349 | ), 350 | Scaffold( 351 | body: Column( 352 | crossAxisAlignment: CrossAxisAlignment.start, 353 | children: [ 354 | description("The box will snap to the closest side regardless of animation."), 355 | gap(), 356 | Expanded( 357 | child: Align( 358 | key: bounds[1], 359 | alignment: const Alignment(-1.0, -1.0), 360 | child: Stack( 361 | children: [ 362 | AnimatedPositioned( 363 | left: 0, 364 | bottom: bottom, 365 | duration: Duration(milliseconds: 300), 366 | child: SnapController( 367 | animatedBoxView(), 368 | true, 369 | views[1], 370 | bounds[1], 371 | Offset.zero, 372 | const Offset(1.0, 1.0), 373 | const Offset(0.75, 0.75), 374 | const Offset(0.75, 0.75), 375 | snapTargets: [ 376 | const SnapTarget(Pivot.closestAny, Pivot.closestAny), 377 | ], 378 | ), 379 | ), 380 | ], 381 | ), 382 | ), 383 | ), 384 | ], 385 | ), 386 | ), 387 | Scaffold( 388 | body: Column( 389 | crossAxisAlignment: CrossAxisAlignment.start, 390 | children: [ 391 | description("The box will snap to the closest matching color."), 392 | gap(), 393 | Expanded( 394 | child: Stack( 395 | children: [ 396 | Align( 397 | key: bounds[2], 398 | alignment: const Alignment(-1.0, -1.0), 399 | child: SnapController( 400 | dottedBoxView(), 401 | true, 402 | views[2], 403 | bounds[2], 404 | Offset.zero, 405 | const Offset(1.0, 1.0), 406 | const Offset(0.75, 0.75), 407 | const Offset(0.75, 0.75), 408 | snapTargets: [ 409 | const SnapTarget(const Offset(0.1, 0.1), const Offset(0.1, 0.1)), 410 | const SnapTarget(const Offset(0.9, 0.1), const Offset(0.85, 0.465)), 411 | const SnapTarget(const Offset(0.1, 0.9), const Offset(0.1, 0.9)), 412 | ], 413 | ), 414 | ), 415 | Align( 416 | alignment: const Alignment(-0.85, -0.85), 417 | child: Container( 418 | width: 10, 419 | height: 10, 420 | color: Colors.orangeAccent, 421 | ), 422 | ), 423 | Align( 424 | alignment: const Alignment(0.75, -0.1), 425 | child: Container( 426 | width: 10, 427 | height: 10, 428 | color: Colors.black, 429 | ), 430 | ), 431 | Align( 432 | alignment: const Alignment(-0.85, 0.85), 433 | child: Container( 434 | width: 10, 435 | height: 10, 436 | color: Colors.deepPurpleAccent, 437 | ), 438 | ), 439 | ], 440 | ), 441 | ), 442 | ], 443 | ), 444 | ), 445 | Scaffold( 446 | body: Column( 447 | crossAxisAlignment: CrossAxisAlignment.start, 448 | children: [ 449 | description("The box will snap to the corners or the center while maintaining the initial offset"), 450 | gap(), 451 | Expanded( 452 | child: Align( 453 | key: bounds[3], 454 | alignment: const Alignment(-1.0, -1.0), 455 | child: SnapController( 456 | translatedBoxView(), 457 | true, 458 | views[3], 459 | bounds[3], 460 | Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height), 461 | Offset(1.0, 1.0) - Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height), 462 | const Offset(0.75, 0.75), 463 | const Offset(0.75, 0.75), 464 | snapTargets: [ 465 | SnapTarget(Pivot.topLeft, Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height)), 466 | SnapTarget(Pivot.topRight, Offset(1.0 - 50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height)), 467 | SnapTarget(Pivot.bottomLeft, Offset(50.0 / MediaQuery.of(context).size.width, 1.0 - 50.0 / MediaQuery.of(context).size.height)), 468 | SnapTarget(Pivot.bottomRight, Offset(1.0, 1.0) - Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height)), 469 | const SnapTarget(Pivot.center, Pivot.center), 470 | ], 471 | ), 472 | ), 473 | ), 474 | ], 475 | ), 476 | ), 477 | Scaffold( 478 | body: Column( 479 | children: [ 480 | description("The box will snap to the closest side or the center within its container."), 481 | gap(), 482 | Container( 483 | width: 300, 484 | height: 300, 485 | color: Colors.orangeAccent, 486 | child: Align( 487 | key: bounds[4], 488 | alignment: const Alignment(-1.0, -1.0), 489 | child: SnapController( 490 | smallBoxView(), 491 | true, 492 | views[4], 493 | bounds[4], 494 | Offset.zero, 495 | const Offset(1.0, 1.0), 496 | const Offset(0.15, 0.15), 497 | const Offset(0.15, 0.15), 498 | snapTargets: [ 499 | const SnapTarget(Pivot.closestAny, Pivot.closestAny), 500 | const SnapTarget(Pivot.center, Pivot.center), 501 | ], 502 | ), 503 | ), 504 | ), 505 | ], 506 | ), 507 | ), 508 | Scaffold( 509 | body: Column( 510 | crossAxisAlignment: CrossAxisAlignment.start, 511 | children: [ 512 | description("The box will snap to the corners or the center without animation."), 513 | gap(), 514 | Expanded( 515 | child: Align( 516 | key: bounds[5], 517 | alignment: const Alignment(-1.0, -1.0), 518 | child: SnapController( 519 | secondNormalBoxView(), 520 | false, 521 | views[5], 522 | bounds[5], 523 | Offset.zero, 524 | const Offset(1.0, 1.0), 525 | const Offset(0.75, 0.75), 526 | const Offset(0.75, 0.75), 527 | snapTargets: [ 528 | const SnapTarget(Pivot.topLeft, Pivot.topLeft), 529 | const SnapTarget(Pivot.topRight, Pivot.topRight), 530 | const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft), 531 | const SnapTarget(Pivot.bottomRight, Pivot.bottomRight), 532 | const SnapTarget(Pivot.center, Pivot.center), 533 | ], 534 | animateSnap: false, 535 | ), 536 | ), 537 | ), 538 | ], 539 | ), 540 | ), 541 | ], 542 | ), 543 | ); 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /snap/example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.6.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0" 53 | flick: 54 | dependency: transitive 55 | description: 56 | name: flick 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.0.0" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.10" 77 | meta: 78 | dependency: transitive 79 | description: 80 | name: meta 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.3.0" 84 | path: 85 | dependency: transitive 86 | description: 87 | name: path 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.8.0" 91 | sky_engine: 92 | dependency: transitive 93 | description: flutter 94 | source: sdk 95 | version: "0.0.99" 96 | snap: 97 | dependency: "direct main" 98 | description: 99 | path: ".." 100 | relative: true 101 | source: path 102 | version: "2.0.0" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.8.1" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.0" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.2.0" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.3.0" 145 | typed_data: 146 | dependency: transitive 147 | description: 148 | name: typed_data 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.3.0" 152 | vector_math: 153 | dependency: transitive 154 | description: 155 | name: vector_math 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "2.1.0" 159 | sdks: 160 | dart: ">=2.12.0 <3.0.0" 161 | -------------------------------------------------------------------------------- /snap/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name : example 2 | description : Example Project for snap. 3 | version : 2.0.0 4 | author : Ali Yigit Bireroglu 5 | repository : https://github.com/aliyigitbireroglu/flutter-snap 6 | homepage : https://www.cosmossoftware.coffee 7 | 8 | environment : 9 | sdk: '>=2.12.0 <3.0.0' 10 | 11 | dependencies : 12 | flutter: 13 | sdk: flutter 14 | snap : 15 | path: ../ 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | 21 | flutter : 22 | uses-material-design: true 23 | -------------------------------------------------------------------------------- /snap/example/test/example_test.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyigitbireroglu/flutter-snap/2ad3714dbf73c10ac1391968fe6581c666e4c668/snap/example/test/example_test.dart -------------------------------------------------------------------------------- /snap/example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyigitbireroglu/flutter-snap/2ad3714dbf73c10ac1391968fe6581c666e4c668/snap/example/web/favicon.png -------------------------------------------------------------------------------- /snap/example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyigitbireroglu/flutter-snap/2ad3714dbf73c10ac1391968fe6581c666e4c668/snap/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /snap/example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyigitbireroglu/flutter-snap/2ad3714dbf73c10ac1391968fe6581c666e4c668/snap/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /snap/example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | example 27 | 28 | 29 | 30 | 33 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /snap/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 | -------------------------------------------------------------------------------- /snap/lib/Export.dart: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // © Cosmos Software | Ali Yigit Bireroglu / 3 | // All material used in the making of this code, project, program, application, software et cetera (the "Intellectual Property") / 4 | // belongs completely and solely to Ali Yigit Bireroglu. This includes but is not limited to the source code, the multimedia and / 5 | // other asset files. If you were granted this Intellectual Property for personal use, you are obligated to include this copyright / 6 | // text at all times. / 7 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 8 | //@formatter:off 9 | 10 | export 'snap_controller.dart'; 11 | export 'misc.dart'; 12 | -------------------------------------------------------------------------------- /snap/lib/misc.dart: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // © Cosmos Software | Ali Yigit Bireroglu / 3 | // All material used in the making of this code, project, program, application, software et cetera (the "Intellectual Property") / 4 | // belongs completely and solely to Ali Yigit Bireroglu. This includes but is not limited to the source code, the multimedia and / 5 | // other asset files. If you were granted this Intellectual Property for personal use, you are obligated to include this copyright / 6 | // text at all times. / 7 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 8 | //@formatter:off 9 | 10 | import 'package:flutter/widgets.dart'; 11 | 12 | typedef MoveCallback = void Function(Offset offset); 13 | typedef DragCallback = void Function(dynamic dragDetails); 14 | typedef SnapCallback = void Function(Offset offset); 15 | 16 | class Pivot { 17 | ///Use this value if you want your view to snap to the closest horizontal side (Left/Right). 18 | static const Offset closestHorizontal = const Offset(12345, 0); 19 | 20 | ///Use this value if you want your view to snap to the closest vertical side (Top/Bottom). 21 | static const Offset closestVertical = const Offset(0, 12345); 22 | 23 | ///Use this value if you want your view to snap to the closest side (Left/Right/Left/Right). 24 | static const Offset closestAny = const Offset(12345, 12345); 25 | 26 | static bool isClosestHorizontal(Offset offset) => offset == closestHorizontal; 27 | static bool isClosestVertical(Offset offset) => offset == closestVertical; 28 | static bool isClosestAny(Offset offset) => offset == closestAny; 29 | 30 | ///Use this value to easily assign a top left corner pivot to your view or your bound. 31 | static const Offset topLeft = const Offset(0, 0); 32 | 33 | ///Use this value to easily assign a top right corner pivot to your view or your bound. 34 | static const Offset topRight = const Offset(1, 0); 35 | 36 | ///Use this value to easily assign a bottom left corner pivot to your view or your bound. 37 | static const Offset bottomLeft = const Offset(0, 1); 38 | 39 | ///Use this value to easily assign a bottom right corner pivot to your view or your bound. 40 | static const Offset bottomRight = const Offset(1, 1); 41 | 42 | ///Use this value to easily assign a center pivot to your view or your bound. 43 | static const Offset center = const Offset(0.5, 0.5); 44 | } 45 | 46 | ///A simple class for organising pivot values. Your view will snap to your bound through the closest [SnapTarget.viewPivot] and [SnapTarget.boundPivot] 47 | ///match. For example, consider the following: 48 | ///I) [SnapTarget.viewPivot] = (0.1, 0.1) 49 | ///II) [SnapTarget.boundPivot] = (0.75, 0.75) 50 | ///These values determine that your view will snap to your bound at: 51 | ///I) The coordinate of the view at (10% [SnapControllerState.viewWidth], 10% [SnapControllerState.viewHeight]. 52 | ///II) The coordinate of the bound at (75% [SnapControllerState.boundWidth], 75% [SnapControllerState.boundHeight]. 53 | ///(All values consider the coordinate plane to start at (0,0) from the top left corner of the view or the bound.) 54 | ///See the provided example for further clarification. 55 | class SnapTarget { 56 | final Offset viewPivot; 57 | final Offset boundPivot; 58 | 59 | const SnapTarget( 60 | this.viewPivot, 61 | this.boundPivot, 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /snap/lib/snap.dart: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // © Cosmos Software | Ali Yigit Bireroglu / 3 | // All material used in the making of this code, project, program, application, software et cetera (the "Intellectual Property") / 4 | // belongs completely and solely to Ali Yigit Bireroglu. This includes but is not limited to the source code, the multimedia and / 5 | // other asset files. If you were granted this Intellectual Property for personal use, you are obligated to include this copyright / 6 | // text at all times. / 7 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 8 | //@formatter:off 9 | 10 | export 'Export.dart'; 11 | -------------------------------------------------------------------------------- /snap/lib/snap_controller.dart: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // © Cosmos Software | Ali Yigit Bireroglu / 3 | // All material used in the making of this code, project, program, application, software et cetera (the "Intellectual Property") / 4 | // belongs completely and solely to Ali Yigit Bireroglu. This includes but is not limited to the source code, the multimedia and / 5 | // other asset files. If you were granted this Intellectual Property for personal use, you are obligated to include this copyright / 6 | // text at all times. / 7 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 8 | //@formatter:off 9 | 10 | import 'dart:math'; 11 | 12 | import 'package:flutter/material.dart'; 13 | 14 | import 'package:flick/flick.dart'; 15 | import 'misc.dart' as Misc; 16 | import 'Export.dart'; 17 | 18 | ///The widget that is responsible of ALL Snap related logic and UI. It is important to define two essential concepts used for this package: 19 | ///I) The view is what is being moved. It is the widget that snaps to the bound. 20 | ///II) The bound is what the view is being snapped to. 21 | class SnapController extends StatefulWidget { 22 | ///The widget that is to be displayed on your UI. 23 | final Widget uiChild; 24 | 25 | ///Set this to true if your [uiChild] doesn't change at runtime. 26 | final bool useCache; 27 | 28 | ///The [GlobalKey] of the view. 29 | final GlobalKey viewKey; 30 | 31 | ///The [GlobalKey] of the bound. 32 | final GlobalKey boundKey; 33 | 34 | ///Use this value to set the lower left boundary of the movement. 35 | final Offset constraintsMin; 36 | 37 | ///Use this value to set the upper right boundary of the movement. 38 | final Offset constraintsMax; 39 | 40 | ///Use this value to set the lower left elasticity of the movement. 41 | final Offset flexibilityMin; 42 | 43 | ///Use this value to set the upper right elasticity of the movement. 44 | final Offset flexibilityMax; 45 | 46 | ///Use this value to set a custom bound width. If not set, [SnapController] will automatically calculate it via [boundKey]. 47 | final double customBoundWidth; 48 | 49 | ///Use this value to set a custom bound height. If not set, [SnapController] will automatically calculate it via [boundKey]. 50 | final double customBoundHeight; 51 | 52 | ///The list of [SnapTarget]s your view can snap to. 53 | final List? snapTargets; 54 | 55 | ///Use this value to set the minimum distance in pixels required for the snapping to occur. If no [SnapTarget] is found that is closer to the [uiChild] than this value, the snapping will not occur. 56 | final double minSnapDistance; 57 | 58 | ///Use this value to set whether the snapping should occur directly or via an animation. 59 | final bool animateSnap; 60 | 61 | ///Use this value to set whether flick should be used or not. 62 | final bool useFlick; 63 | 64 | ///Use this value to set the sensitivity of flick. 65 | final double flickSensitivity; 66 | 67 | ///The callback for when the view moves. 68 | final Misc.MoveCallback? onMove; 69 | 70 | ///The callback for when the drag starts. 71 | final Misc.DragCallback? onDragStart; 72 | 73 | ///The callback for when the drag updates. 74 | final Misc.DragCallback? onDragUpdate; 75 | 76 | ///The callback for when the drag ends. 77 | final Misc.DragCallback? onDragEnd; 78 | 79 | ///The callback for when the view snaps. 80 | final SnapCallback? onSnap; 81 | 82 | const SnapController( 83 | this.uiChild, 84 | this.useCache, 85 | this.viewKey, 86 | this.boundKey, 87 | this.constraintsMin, 88 | this.constraintsMax, 89 | this.flexibilityMin, 90 | this.flexibilityMax, { 91 | Key? key, 92 | this.customBoundWidth: 0, 93 | this.customBoundHeight: 0, 94 | this.snapTargets, 95 | this.minSnapDistance = 0, 96 | this.animateSnap: true, 97 | this.useFlick: true, 98 | this.flickSensitivity: 0.075, 99 | this.onMove, 100 | this.onDragStart, 101 | this.onDragUpdate, 102 | this.onDragEnd, 103 | this.onSnap, 104 | }) : super(key: key); 105 | 106 | @override 107 | SnapControllerState createState() { 108 | return SnapControllerState( 109 | useCache, 110 | viewKey, 111 | boundKey, 112 | constraintsMin, 113 | constraintsMax, 114 | flexibilityMin, 115 | flexibilityMax, 116 | snapTargets, 117 | minSnapDistance, 118 | animateSnap, 119 | useFlick, 120 | flickSensitivity, 121 | onMove, 122 | onDragStart, 123 | onDragUpdate, 124 | onDragEnd, 125 | onSnap, 126 | ); 127 | } 128 | } 129 | 130 | class SnapControllerState extends State with TickerProviderStateMixin { 131 | Widget? uiChild; 132 | final bool useCache; 133 | final GlobalKey viewKey; 134 | final GlobalKey boundKey; 135 | 136 | Offset constraintsMin; 137 | Offset constraintsMax; 138 | late Offset normalisedConstraintsMin; 139 | late Offset normalisedConstraintsMax; 140 | final Offset flexibilityMin; 141 | final Offset flexibilityMax; 142 | final List? snapTargets; 143 | 144 | bool canMove = true; 145 | final double minSnapDistance; 146 | final bool animateSnap; 147 | bool useFlick; 148 | final double flickSensitivity; 149 | final GlobalKey flickController = GlobalKey(); 150 | 151 | final Misc.MoveCallback? onMove; 152 | final Misc.DragCallback? onDragStart; 153 | final Misc.DragCallback? onDragUpdate; 154 | final Misc.DragCallback? onDragEnd; 155 | final SnapCallback? onSnap; 156 | 157 | RenderBox? viewRenderBox; 158 | double viewWidth = -1; 159 | double viewHeight = -1; 160 | Offset? viewOrigin; 161 | RenderBox? boundRenderBox; 162 | double boundWidth = -1; 163 | double boundHeight = -1; 164 | Offset? boundOrigin; 165 | 166 | Offset? beginDragPosition; 167 | Offset? updateDragPosition; 168 | Offset delta = Offset.zero; 169 | Offset overrideDelta = Offset.zero; 170 | 171 | ///The [AnimationController] used to move the view during snapping if [SnapController.animateSnap] is set to true. 172 | late AnimationController animationController; 173 | late Animation animation; 174 | 175 | final ValueNotifier deltaNotifier = ValueNotifier(Offset.zero); 176 | 177 | ///Use this value to determine the depth of debug logging that is actually only here for myself and the Swiss scientists. 178 | int _debugLevel = 0; 179 | 180 | SnapControllerState( 181 | this.useCache, 182 | this.viewKey, 183 | this.boundKey, 184 | this.constraintsMin, 185 | this.constraintsMax, 186 | this.flexibilityMin, 187 | this.flexibilityMax, 188 | this.snapTargets, 189 | this.minSnapDistance, 190 | this.animateSnap, 191 | this.useFlick, 192 | this.flickSensitivity, 193 | this.onMove, 194 | this.onDragStart, 195 | this.onDragUpdate, 196 | this.onDragEnd, 197 | this.onSnap, 198 | ); 199 | 200 | void animationListener() { 201 | deltaNotifier.value = animation.value; 202 | if (onMove != null) onMove!(deltaNotifier.value); 203 | } 204 | 205 | @override 206 | void initState() { 207 | super.initState(); 208 | 209 | if (!animateSnap) useFlick = false; 210 | 211 | if (useCache) uiChild = wrapper(); 212 | 213 | animationController = AnimationController( 214 | vsync: this, 215 | duration: const Duration(milliseconds: 333), 216 | lowerBound: 0, 217 | upperBound: 1, 218 | )..addListener(animationListener); 219 | animation = Tween( 220 | begin: Offset.zero, 221 | end: Offset.zero, 222 | ).animate( 223 | CurvedAnimation( 224 | parent: animationController, 225 | curve: Curves.fastOutSlowIn, 226 | ), 227 | ); 228 | 229 | checkViewAndBound(); 230 | } 231 | 232 | @override 233 | void dispose() { 234 | reset(); 235 | 236 | animationController.removeListener(animationListener); 237 | animationController.dispose(); 238 | 239 | super.dispose(); 240 | } 241 | 242 | void checkViewAndBound() { 243 | if (!viewIsSet) 244 | setView(); 245 | else 246 | checkViewOrigin(); 247 | if (!boundIsSet) 248 | setBound(); 249 | else 250 | checkBoundOrigin(); 251 | } 252 | 253 | void setView() { 254 | try { 255 | if (viewKey.currentContext == null) return; 256 | if (viewRenderBox == null) viewRenderBox = viewKey.currentContext!.findRenderObject() as RenderBox?; 257 | 258 | if (viewRenderBox != null) { 259 | if (viewRenderBox!.hasSize) { 260 | if (viewWidth == -1) viewWidth = viewRenderBox!.size.width; 261 | if (viewHeight == -1) viewHeight = viewRenderBox!.size.height; 262 | } 263 | 264 | if (viewOrigin == null) viewOrigin = viewRenderBox!.localToGlobal(Offset.zero); 265 | } 266 | } catch (_) {} 267 | } 268 | 269 | bool get viewIsSet => !(viewWidth == -1 || viewHeight == -1 || viewOrigin == null); 270 | 271 | void setBound() { 272 | try { 273 | if (boundKey.currentContext == null) return; 274 | if (boundRenderBox == null) boundRenderBox = boundKey.currentContext!.findRenderObject() as RenderBox?; 275 | 276 | if (boundRenderBox != null) { 277 | if (boundRenderBox!.hasSize) { 278 | if (boundWidth == -1) boundWidth = boundRenderBox!.size.width + widget.customBoundWidth; 279 | if (boundHeight == -1) boundHeight = boundRenderBox!.size.height + widget.customBoundHeight; 280 | 281 | if (boundWidth != -1 && boundHeight != -1) normaliseConstraints(); 282 | } 283 | } 284 | if (boundOrigin == null) boundOrigin = boundRenderBox!.localToGlobal(Offset.zero); 285 | } catch (_) {} 286 | } 287 | 288 | bool get boundIsSet => !(boundWidth == -1 || boundHeight == -1 || boundOrigin == null); 289 | 290 | void checkViewOrigin() { 291 | if (viewOrigin != viewRenderBox!.localToGlobal(Offset.zero) - deltaNotifier.value) viewOrigin = viewRenderBox!.localToGlobal(Offset.zero) - deltaNotifier.value; 292 | } 293 | 294 | void checkBoundOrigin() { 295 | if (boundOrigin != boundRenderBox!.localToGlobal(Offset.zero)) boundOrigin = boundRenderBox!.localToGlobal(Offset.zero); 296 | } 297 | 298 | void normaliseConstraints() { 299 | double constraintsMinX = constraintsMin.dx == double.negativeInfinity ? double.negativeInfinity : boundWidth * constraintsMin.dx; 300 | double constraintsMinY = constraintsMin.dy == double.negativeInfinity ? double.negativeInfinity : boundHeight * constraintsMin.dy; 301 | double constraintsMaxX = constraintsMax.dx == double.infinity ? double.infinity : boundWidth * constraintsMax.dx; 302 | double constraintsMaxY = constraintsMax.dy == double.infinity ? double.infinity : boundHeight * constraintsMax.dy; 303 | constraintsMin = Offset(constraintsMinX, constraintsMinY); 304 | constraintsMax = Offset(constraintsMaxX, constraintsMaxY); 305 | } 306 | 307 | void beginDrag(dynamic dragStartDetails) { 308 | if (!canMove) return; 309 | if (animationController.isAnimating) return; 310 | 311 | if (_debugLevel > 0) print("BeginDrag"); 312 | 313 | checkViewAndBound(); 314 | 315 | delta = deltaNotifier.value; 316 | beginDragPosition = dragStartDetails.localPosition; 317 | 318 | if (onDragStart != null) onDragStart!(dragStartDetails); 319 | } 320 | 321 | void updateDrag(dynamic dragUpdateDetails) { 322 | if (!canMove) return; 323 | if (animationController.isAnimating) return; 324 | 325 | if (_debugLevel > 0) print("UpdateDrag"); 326 | 327 | checkViewAndBound(); 328 | 329 | if (beginDragPosition == null) beginDrag(dragUpdateDetails); 330 | updateDragPosition = dragUpdateDetails.localPosition; 331 | setDelta(); 332 | 333 | if (onDragUpdate != null) onDragUpdate!(dragUpdateDetails); 334 | } 335 | 336 | void endDrag(dynamic dragEndDetails) { 337 | if (!canMove) return; 338 | if (animationController.isAnimating) return; 339 | 340 | if (_debugLevel > 0) print("EndDrag"); 341 | 342 | if (onDragEnd != null) onDragEnd!(dragEndDetails); 343 | 344 | if (!useFlick) snap(); 345 | } 346 | 347 | void onFlick(Offset offset) { 348 | snap(); 349 | } 350 | 351 | void setDelta() { 352 | if (beginDragPosition == null || updateDragPosition == null) return; 353 | if (!viewIsSet || !boundIsSet) return; 354 | 355 | Offset _delta = delta + Offset(updateDragPosition!.dx - beginDragPosition!.dx, updateDragPosition!.dy - beginDragPosition!.dy); 356 | normalisedConstraintsMin = constraintsMin - viewOrigin! + boundOrigin!; 357 | normalisedConstraintsMax = constraintsMax - viewOrigin! + boundOrigin! - Offset(viewWidth, viewHeight); 358 | if (_delta.dx < normalisedConstraintsMin.dx) _delta = Offset(normalisedConstraintsMin.dx - pow((_delta.dx - normalisedConstraintsMin.dx).abs(), flexibilityMin.dx) + 1.0, _delta.dy); 359 | if (_delta.dx > normalisedConstraintsMax.dx) _delta = Offset(normalisedConstraintsMax.dx + pow((_delta.dx - normalisedConstraintsMax.dx).abs(), flexibilityMax.dx) - 1.0, _delta.dy); 360 | if (_delta.dy < normalisedConstraintsMin.dy) _delta = Offset(_delta.dx, normalisedConstraintsMin.dy - pow((_delta.dy - normalisedConstraintsMin.dy).abs(), flexibilityMin.dy) + 1.0); 361 | if (_delta.dy > normalisedConstraintsMax.dy) _delta = Offset(_delta.dx, normalisedConstraintsMax.dy + pow((_delta.dy - normalisedConstraintsMax.dy).abs(), flexibilityMax.dy) - 1.0); 362 | 363 | deltaNotifier.value = _delta; 364 | 365 | if (onMove != null) onMove!(deltaNotifier.value); 366 | } 367 | 368 | double get maxLeft => boundOrigin!.dx - viewOrigin!.dx; 369 | 370 | double get maxRight => boundWidth + boundOrigin!.dx - viewWidth - viewOrigin!.dx; 371 | 372 | double get maxTop => boundOrigin!.dy - viewOrigin!.dy; 373 | 374 | double get maxBottom => boundHeight + boundOrigin!.dy - viewHeight - viewOrigin!.dy; 375 | 376 | Future snap() async { 377 | checkViewAndBound(); 378 | Offset snapTarget = getSnapTarget(); 379 | if (animateSnap) { 380 | await move(snapTarget); 381 | deltaNotifier.value = snapTarget; 382 | } else 383 | deltaNotifier.value = snapTarget; 384 | 385 | delta = Offset.zero; 386 | beginDragPosition = null; 387 | updateDragPosition = null; 388 | overrideDelta = Offset.zero; 389 | 390 | if (onSnap != null) onSnap!(deltaNotifier.value); 391 | } 392 | 393 | Offset getSnapTarget() { 394 | if (snapTargets == null) 395 | return deltaNotifier.value; 396 | else { 397 | Map map = Map(); 398 | snapTargets!.forEach((SnapTarget snapTarget) { 399 | if (Pivot.isClosestHorizontal(snapTarget.viewPivot) || Pivot.isClosestAny(snapTarget.viewPivot)) { 400 | Offset left = Offset(0 - viewOrigin!.dx + boundOrigin!.dx, deltaNotifier.value.dy.clamp(maxTop, maxBottom)); 401 | map[left] = Point(deltaNotifier.value.dx, deltaNotifier.value.dy).distanceTo(Point(left.dx, left.dy)); 402 | if (_debugLevel > 1) { 403 | print("--------------------"); 404 | print("Left"); 405 | print(snapTarget.boundPivot); 406 | print(snapTarget.viewPivot); 407 | print(boundWidth); 408 | print(boundHeight); 409 | print(boundOrigin); 410 | print(viewWidth); 411 | print(viewHeight); 412 | print(viewOrigin); 413 | print(left); 414 | print("--------------------"); 415 | } 416 | Offset right = Offset(boundWidth + -viewOrigin!.dx + boundOrigin!.dx - viewWidth, deltaNotifier.value.dy.clamp(maxTop, maxBottom)); 417 | map[right] = Point(deltaNotifier.value.dx, deltaNotifier.value.dy).distanceTo(Point(right.dx, right.dy)); 418 | if (_debugLevel > 1) { 419 | print("--------------------"); 420 | print("Right"); 421 | print(snapTarget.boundPivot); 422 | print(snapTarget.viewPivot); 423 | print(boundWidth); 424 | print(boundHeight); 425 | print(boundOrigin); 426 | print(viewWidth); 427 | print(viewHeight); 428 | print(viewOrigin); 429 | print(right); 430 | print("--------------------"); 431 | } 432 | } 433 | if (Pivot.isClosestVertical(snapTarget.viewPivot) || Pivot.isClosestAny(snapTarget.viewPivot)) { 434 | Offset top = Offset(deltaNotifier.value.dx.clamp(maxLeft, maxRight), 0 - viewOrigin!.dy + boundOrigin!.dy); 435 | map[top] = Point(deltaNotifier.value.dx, deltaNotifier.value.dy).distanceTo(Point(top.dx, top.dy)); 436 | if (_debugLevel > 1) { 437 | print("--------------------"); 438 | print("Top"); 439 | print(snapTarget.boundPivot); 440 | print(snapTarget.viewPivot); 441 | print(boundWidth); 442 | print(boundHeight); 443 | print(boundOrigin); 444 | print(viewWidth); 445 | print(viewHeight); 446 | print(viewOrigin); 447 | print(top); 448 | print("--------------------"); 449 | } 450 | Offset bottom = Offset(deltaNotifier.value.dx.clamp(maxLeft, maxRight), boundHeight + -viewOrigin!.dy + boundOrigin!.dy - viewHeight); 451 | map[bottom] = Point(deltaNotifier.value.dx, deltaNotifier.value.dy).distanceTo(Point(bottom.dx, bottom.dy)); 452 | if (_debugLevel > 1) { 453 | print("--------------------"); 454 | print("Bottom"); 455 | print(snapTarget.boundPivot); 456 | print(snapTarget.viewPivot); 457 | print(boundWidth); 458 | print(boundHeight); 459 | print(boundOrigin); 460 | print(viewWidth); 461 | print(viewHeight); 462 | print(viewOrigin); 463 | print(bottom); 464 | print("--------------------"); 465 | } 466 | } 467 | if (!Pivot.isClosestHorizontal(snapTarget.viewPivot) && !Pivot.isClosestVertical(snapTarget.viewPivot) && !Pivot.isClosestAny(snapTarget.viewPivot)) { 468 | Offset offset = Offset(boundWidth * snapTarget.boundPivot.dx + boundOrigin!.dx - viewWidth * snapTarget.viewPivot.dx - viewOrigin!.dx, boundHeight * snapTarget.boundPivot.dy + boundOrigin!.dy - viewHeight * snapTarget.viewPivot.dy - viewOrigin!.dy); 469 | if (_debugLevel > 1) { 470 | print("--------------------"); 471 | print(snapTarget.boundPivot); 472 | print(snapTarget.viewPivot); 473 | print(boundWidth); 474 | print(boundHeight); 475 | print(boundOrigin); 476 | print(viewWidth); 477 | print(viewHeight); 478 | print(viewOrigin); 479 | print(offset); 480 | print("--------------------"); 481 | } 482 | map[offset] = Point(deltaNotifier.value.dx, deltaNotifier.value.dy).distanceTo(Point(offset.dx, offset.dy)); 483 | } 484 | }); 485 | 486 | print(map.values.reduce(min)); 487 | if (map.values.reduce(min) > minSnapDistance) { 488 | return deltaNotifier.value; 489 | } 490 | return map.keys.firstWhere((Offset offset) { 491 | return map[offset] == map.values.reduce(min); 492 | }); 493 | } 494 | } 495 | 496 | Future move(Offset snapTarget) async { 497 | animation = Tween( 498 | begin: deltaNotifier.value, 499 | end: snapTarget, 500 | ).animate( 501 | CurvedAnimation( 502 | parent: animationController, 503 | curve: Curves.fastOutSlowIn, 504 | ), 505 | ); 506 | if (animationController.isAnimating) animationController.stop(); 507 | animationController.forward(from: 0); 508 | await Future.delayed(const Duration(milliseconds: 333)); 509 | return; 510 | } 511 | 512 | ///Use this function to determine if the view is moved or not. 513 | bool isMoved(double treshold) { 514 | return deltaNotifier.value.dx.abs() > treshold || deltaNotifier.value.dy.abs() > treshold; 515 | } 516 | 517 | void reset() { 518 | delta = Offset.zero; 519 | beginDragPosition = null; 520 | updateDragPosition = null; 521 | overrideDelta = Offset.zero; 522 | deltaNotifier.value = Offset.zero; 523 | } 524 | 525 | void softReset(Offset _constraintsMin, Offset _constraintsMax) { 526 | constraintsMin = _constraintsMin; 527 | constraintsMax = _constraintsMax; 528 | viewHeight = -1; 529 | viewWidth = -1; 530 | viewOrigin = null; 531 | boundHeight = -1; 532 | boundWidth = -1; 533 | boundOrigin = null; 534 | if (useFlick && flickController.currentState != null) flickController.currentState!.softReset(_constraintsMin, _constraintsMax); 535 | } 536 | 537 | Widget wrapper() { 538 | if (useFlick) 539 | return FlickController( 540 | widget.uiChild, 541 | useCache, 542 | viewKey, 543 | boundKey: boundKey, 544 | constraintsMin: constraintsMin, 545 | constraintsMax: constraintsMax, 546 | flexibilityMin: flexibilityMin, 547 | flexibilityMax: flexibilityMax, 548 | sensitivity: flickSensitivity, 549 | onDragStart: beginDrag, 550 | onDragUpdate: updateDrag, 551 | onDragEnd: endDrag, 552 | onFlick: onFlick, 553 | key: flickController, 554 | ); 555 | else 556 | return GestureDetector( 557 | behavior: HitTestBehavior.opaque, 558 | onVerticalDragStart: beginDrag, 559 | onVerticalDragUpdate: updateDrag, 560 | onVerticalDragEnd: endDrag, 561 | onHorizontalDragStart: beginDrag, 562 | onHorizontalDragUpdate: updateDrag, 563 | onHorizontalDragEnd: endDrag, 564 | child: widget.uiChild, 565 | ); 566 | } 567 | 568 | @override 569 | Widget build(BuildContext context) { 570 | checkViewAndBound(); 571 | 572 | return ValueListenableBuilder( 573 | child: useCache ? uiChild : null, 574 | builder: (BuildContext context, Offset delta, Widget? cachedChild) { 575 | return Transform.translate( 576 | offset: delta, 577 | child: useCache ? cachedChild : wrapper(), 578 | ); 579 | }, 580 | valueListenable: deltaNotifier, 581 | ); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /snap/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.6.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0" 53 | flick: 54 | dependency: "direct main" 55 | description: 56 | name: flick 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.0.0" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.10" 77 | meta: 78 | dependency: transitive 79 | description: 80 | name: meta 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.3.0" 84 | path: 85 | dependency: transitive 86 | description: 87 | name: path 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.8.0" 91 | sky_engine: 92 | dependency: transitive 93 | description: flutter 94 | source: sdk 95 | version: "0.0.99" 96 | source_span: 97 | dependency: transitive 98 | description: 99 | name: source_span 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "1.8.1" 103 | stack_trace: 104 | dependency: transitive 105 | description: 106 | name: stack_trace 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.10.0" 110 | stream_channel: 111 | dependency: transitive 112 | description: 113 | name: stream_channel 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "2.1.0" 117 | string_scanner: 118 | dependency: transitive 119 | description: 120 | name: string_scanner 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.1.0" 124 | term_glyph: 125 | dependency: transitive 126 | description: 127 | name: term_glyph 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.2.0" 131 | test_api: 132 | dependency: transitive 133 | description: 134 | name: test_api 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "0.3.0" 138 | typed_data: 139 | dependency: transitive 140 | description: 141 | name: typed_data 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.3.0" 145 | vector_math: 146 | dependency: transitive 147 | description: 148 | name: vector_math 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "2.1.0" 152 | sdks: 153 | dart: ">=2.12.0 <3.0.0" 154 | -------------------------------------------------------------------------------- /snap/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name : snap 2 | description : An extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets. 3 | version : 2.0.0 4 | author : Ali Yigit Bireroglu 5 | repository : https://github.com/aliyigitbireroglu/flutter-snap 6 | homepage : https://www.cosmossoftware.coffee 7 | 8 | environment : 9 | sdk: '>=2.12.0 <3.0.0' 10 | 11 | dependencies : 12 | flutter: 13 | sdk: flutter 14 | flick : ^2.0.0 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | 20 | flutter : 21 | -------------------------------------------------------------------------------- /snap/test/snap_test.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyigitbireroglu/flutter-snap/2ad3714dbf73c10ac1391968fe6581c666e4c668/snap/test/snap_test.dart --------------------------------------------------------------------------------