├── test └── dragablegridview_test.dart ├── .gitignore ├── lib ├── dragablegridviewbin.dart └── dragablegridview_flutter.dart ├── example ├── gridviewitembin.dart └── example.dart ├── CHANGELOG.md ├── pubspec.yaml ├── pubspec.lock ├── README.md └── LICENSE /test/dragablegridview_test.dart: -------------------------------------------------------------------------------- 1 | void main() { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | ios/.generated/ 9 | ios/Flutter/Generated.xcconfig 10 | ios/Runner/GeneratedPluginRegistrant.* 11 | *.iml 12 | .idea 13 | -------------------------------------------------------------------------------- /lib/dragablegridviewbin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DragAbleGridViewBin { 4 | double dragPointX=0.0; 5 | double dragPointY=0.0; 6 | double lastTimePositionX=0.0; 7 | double lastTimePositionY=0.0; 8 | GlobalKey containerKey=new GlobalKey(); 9 | GlobalKey containerKeyChild=new GlobalKey(); 10 | bool isLongPress=false; 11 | bool dragAble=false; 12 | ///是否隐藏,默认不隐藏 13 | bool offstage=false; 14 | } 15 | -------------------------------------------------------------------------------- /example/gridviewitembin.dart: -------------------------------------------------------------------------------- 1 | import 'package:dragablegridview_flutter/dragablegridviewbin.dart'; 2 | 3 | class ItemBin extends DragAbleGridViewBin{ 4 | 5 | ItemBin( this.data); 6 | 7 | String data; 8 | 9 | @override 10 | String toString() { 11 | return 'ItemBin{data: $data, dragPointX: $dragPointX, dragPointY: $dragPointY, lastTimePositionX: $lastTimePositionX, lastTimePositionY: $lastTimePositionY, containerKey: $containerKey, containerKeyChild: $containerKeyChild, isLongPress: $isLongPress, dragAble: $dragAble}'; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.2.5] - 2019.05.31 2 | 3 | * add deleteIcon click listener 4 | 5 | ## [0.2.3] - 2019.05.31 6 | 7 | * Fix a bug when deleteIcon is null 8 | 9 | ## [0.2.2] - 2019.05.25 10 | 11 | * Fix the stuck when dragging 12 | 13 | ## [0.2.1] - 2019.05.24 14 | 15 | * Fix some problems that Android 9.0 and above can't use 16 | 17 | ## [0.2.0] - 2019.04.25 18 | 19 | * Repair adsorption effect 20 | 21 | ## [0.1.9] - 2019.04.24 22 | 23 | * Update deleteIcon from Image to Widget 24 | 25 | ## [0.1.8] - 2019.04.23 26 | 27 | * Fix bug 28 | 29 | ## [0.1.7] - 2019.04.23 30 | 31 | * Performance optimization 32 | 33 | ## [0.1.6] - 2019.02.20 34 | 35 | * Reduce parameters 36 | 37 | ## [0.1.5] - 2019.02.18 38 | 39 | * Dragable function is controllable, animation duration is controllable 40 | 41 | ## [0.1.4] - 2019.01.27 42 | 43 | * Optimize code and fix bugs 44 | 45 | ## [0.1.3] - 2019.01.23 46 | 47 | * Add delete item function 48 | 49 | ## [0.1.1] - 2018.10.15 50 | 51 | * Add a detailed description 52 | * Upgrade version to above 0.1 53 | * Specification sample code 54 | 55 | ## [0.0.4] - 2018.10.13 56 | 57 | * Modify README description error and format code 58 | 59 | ## [0.0.1] - 2018.09.30 60 | 61 | * Describe initial release. 62 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dragablegridview_flutter 2 | description: A dragable gridview,Long-pressed triggers draggable state,GridView reordering after release your finger 3 | version: 0.2.5 4 | author: baoolong 5 | homepage: https://github.com/baoolong/DragableGridview 6 | 7 | environment: 8 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://www.dartlang.org/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | 24 | # To add assets to your package, add an assets section, like this: 25 | # assets: 26 | # - images/a_dot_burr.jpeg 27 | # - images/a_dot_ham.jpeg 28 | # 29 | # For details regarding assets in packages, see 30 | # https://flutter.io/assets-and-images/#from-packages 31 | # 32 | # An image asset can refer to one or more resolution-specific "variants", see 33 | # https://flutter.io/assets-and-images/#resolution-aware. 34 | 35 | # To add custom fonts to your package, add a fonts section here, 36 | # in this "flutter" section. Each entry in this list should have a 37 | # "family" key with the font family name, and a "fonts" key with a 38 | # list giving the asset and other descriptors for the font. For 39 | # example: 40 | # fonts: 41 | # - family: Schyler 42 | # fonts: 43 | # - asset: fonts/Schyler-Regular.ttf 44 | # - asset: fonts/Schyler-Italic.ttf 45 | # style: italic 46 | # - family: Trajan Pro 47 | # fonts: 48 | # - asset: fonts/TrajanPro.ttf 49 | # - asset: fonts/TrajanPro_Bold.ttf 50 | # weight: 700 51 | # 52 | # For details regarding fonts in packages, see 53 | # https://flutter.io/custom-fonts/#from-packages 54 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:dragablegridview_flutter/dragablegridview_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'gridviewitembin.dart'; 5 | 6 | 7 | class DragAbleGridViewDemo extends StatefulWidget{ 8 | @override 9 | State createState() { 10 | return new DragAbleGridViewDemoState(); 11 | } 12 | } 13 | 14 | class DragAbleGridViewDemoState extends State{ 15 | 16 | List itemBins=new List(); 17 | String actionTxtEdit="编辑"; 18 | String actionTxtComplete="完成"; 19 | String actionTxt; 20 | var editSwitchController=EditSwitchController(); 21 | final List heroes=["鲁班","虞姬","甄姬","黄盖","张飞","关羽","刘备","曹操","赵云","孙策","庄周","廉颇","后裔","妲己","荆轲",]; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | actionTxt=actionTxtEdit; 27 | heroes.forEach((heroName) { 28 | itemBins.add(new ItemBin(heroName)); 29 | } 30 | ); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return new Scaffold( 36 | appBar: new AppBar( 37 | title: new Text("可拖拽GridView"), 38 | actions: [ 39 | new Center( 40 | child: new GestureDetector( 41 | child: new Container( 42 | child: new Text(actionTxt,style: TextStyle(fontSize: 19.0),), 43 | margin: EdgeInsets.only(right: 12), 44 | ), 45 | onTap: (){ 46 | changeActionState(); 47 | editSwitchController.editStateChanged(); 48 | }, 49 | ) 50 | ) 51 | ], 52 | ), 53 | body: new DragAbleGridView( 54 | mainAxisSpacing:10.0, 55 | crossAxisSpacing:10.0, 56 | childAspectRatio:1.8, 57 | crossAxisCount: 4, 58 | itemBins:itemBins, 59 | editSwitchController:editSwitchController, 60 | /******************************new parameter*********************************/ 61 | isOpenDragAble: true, 62 | animationDuration: 300, //milliseconds 63 | longPressDuration: 800, //milliseconds 64 | /******************************new parameter*********************************/ 65 | deleteIcon: new Image.asset("images/close.png",width: 15.0 ,height: 15.0 ), 66 | child: (int position){ 67 | return new Container( 68 | padding: EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0), 69 | decoration: new BoxDecoration( 70 | borderRadius: BorderRadius.all(new Radius.circular(3.0)), 71 | border: new Border.all(color: Colors.blue), 72 | ), 73 | //因为本布局和删除图标同处于一个Stack内,设置marginTop和marginRight能让图标处于合适的位置 74 | //Because this layout and the delete_Icon are in the same Stack, setting marginTop and marginRight will make the icon in the proper position. 75 | margin: EdgeInsets.only(top: 6.0,right: 6.0), 76 | child: new Text( 77 | itemBins[position].data, 78 | style: new TextStyle(fontSize: 16.0,color: Colors.blue),), 79 | ); 80 | }, 81 | editChangeListener: (){ 82 | changeActionState(); 83 | }, 84 | ), 85 | ); 86 | } 87 | 88 | void changeActionState(){ 89 | if(actionTxt==actionTxtEdit){ 90 | setState(() { 91 | actionTxt=actionTxtComplete; 92 | }); 93 | }else{ 94 | setState(() { 95 | actionTxt=actionTxtEdit; 96 | }); 97 | } 98 | } 99 | } 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.1.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.0.4" 18 | charcode: 19 | dependency: transitive 20 | description: 21 | name: charcode 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.1.2" 25 | collection: 26 | dependency: transitive 27 | description: 28 | name: collection 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.14.11" 32 | flutter: 33 | dependency: "direct main" 34 | description: flutter 35 | source: sdk 36 | version: "0.0.0" 37 | flutter_test: 38 | dependency: "direct dev" 39 | description: flutter 40 | source: sdk 41 | version: "0.0.0" 42 | matcher: 43 | dependency: transitive 44 | description: 45 | name: matcher 46 | url: "https://pub.flutter-io.cn" 47 | source: hosted 48 | version: "0.12.5" 49 | meta: 50 | dependency: transitive 51 | description: 52 | name: meta 53 | url: "https://pub.flutter-io.cn" 54 | source: hosted 55 | version: "1.1.6" 56 | path: 57 | dependency: transitive 58 | description: 59 | name: path 60 | url: "https://pub.flutter-io.cn" 61 | source: hosted 62 | version: "1.6.2" 63 | pedantic: 64 | dependency: transitive 65 | description: 66 | name: pedantic 67 | url: "https://pub.flutter-io.cn" 68 | source: hosted 69 | version: "1.5.0" 70 | quiver: 71 | dependency: transitive 72 | description: 73 | name: quiver 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "2.0.2" 77 | sky_engine: 78 | dependency: transitive 79 | description: flutter 80 | source: sdk 81 | version: "0.0.99" 82 | source_span: 83 | dependency: transitive 84 | description: 85 | name: source_span 86 | url: "https://pub.flutter-io.cn" 87 | source: hosted 88 | version: "1.5.5" 89 | stack_trace: 90 | dependency: transitive 91 | description: 92 | name: stack_trace 93 | url: "https://pub.flutter-io.cn" 94 | source: hosted 95 | version: "1.9.3" 96 | stream_channel: 97 | dependency: transitive 98 | description: 99 | name: stream_channel 100 | url: "https://pub.flutter-io.cn" 101 | source: hosted 102 | version: "2.0.0" 103 | string_scanner: 104 | dependency: transitive 105 | description: 106 | name: string_scanner 107 | url: "https://pub.flutter-io.cn" 108 | source: hosted 109 | version: "1.0.4" 110 | term_glyph: 111 | dependency: transitive 112 | description: 113 | name: term_glyph 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "1.1.0" 117 | test_api: 118 | dependency: transitive 119 | description: 120 | name: test_api 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "0.2.4" 124 | typed_data: 125 | dependency: transitive 126 | description: 127 | name: typed_data 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "1.1.6" 131 | vector_math: 132 | dependency: transitive 133 | description: 134 | name: vector_math 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "2.0.8" 138 | sdks: 139 | dart: ">=2.2.0 <3.0.0" 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DragableGridview 2 | [![pub package](https://img.shields.io/pub/v/dragablegridview_flutter.svg)](https://pub.dartlang.org/packages/dragablegridview_flutter) 3 | 4 | 用GridView编写的可拖动排序View,可任意拖动位置.长按触发拖动,松开手指重新排序,为满足大家的需求,现在增加了“删除动画” ;可能有的人只需要删除动画,而有的人却需要拖动动画,还有的人2个都需要, 5 | 我们可以在代码里通过属性来控制使用哪个功能 6 | 7 | 请不要在拖动的时候更新增加或者删除的数据 8 | 9 | A dragable gridview,Long-pressed triggers draggable state,When dragging until covere other items, other Items trigger the animation to make room for the draggable 10 | Item,GridView reordering after release your finger,Now I have added "Delete Animation"; some people may only need the delete animation, some people only need the drag 11 | animation, and some people need it all. We can control the function by properties. 12 | 13 | Please do not update the added data or deleted data while dragging 14 | 15 | HomePage:[https://github.com/baoolong/DragableGridview](https://github.com/baoolong/DragableGridview) 16 | 17 | MoreWidght:[https://github.com/OpenFlutter/PullToRefresh](https://github.com/OpenFlutter/PullToRefresh) 18 | 19 | 20 | 21 | ## Usage 22 | 23 | Add this to your package's pubspec.yaml file: 24 | 25 | dependencies: 26 | dragablegridview_flutter: ^0.2.5 27 | 28 | Add it to your dart file: 29 | 30 | import 'package:dragablegridview_flutter/dragablegridview_flutter.dart'; 31 | 32 | And GridView dataBin must extends DragAbleGridViewBin ,Add it to your dataBin file 33 | 34 | import 'package:dragablegridview_flutter/dragablegridviewbin.dart'; 35 | 36 | ## Example 37 | 38 | #### DataBin example (must extends DragAbleGridViewBin) 39 | 40 | import 'package:dragablegridview_flutter/dragablegridviewbin.dart'; 41 | 42 | class ItemBin extends DragAbleGridViewBin{ 43 | 44 | ItemBin( this.data); 45 | 46 | String data; 47 | 48 | @override 49 | String toString() { 50 | return 'ItemBin{data: $data, dragPointX: $dragPointX, dragPointY: $dragPointY, lastTimePositionX: $lastTimePositionX, lastTimePositionY: $lastTimePositionY, containerKey: $containerKey, containerKeyChild: $containerKeyChild, isLongPress: $isLongPress, dragAble: $dragAble}'; 51 | } 52 | 53 | } 54 | 55 | #### Widget Usage Example 56 | 57 | import 'package:dragablegridview_flutter/dragablegridview_flutter.dart'; 58 | import 'package:flutter/material.dart'; 59 | 60 | import 'gridviewitembin.dart'; 61 | 62 | 63 | class DragAbleGridViewDemo extends StatefulWidget{ 64 | @override 65 | State createState() { 66 | return new DragAbleGridViewDemoState(); 67 | } 68 | } 69 | 70 | class DragAbleGridViewDemoState extends State{ 71 | 72 | List itemBins=new List(); 73 | String actionTxtEdit="编辑"; 74 | String actionTxtComplete="完成"; 75 | String actionTxt; 76 | var editSwitchController=EditSwitchController(); 77 | final List heroes=["鲁班","虞姬","甄姬","黄盖","张飞","关羽","刘备","曹操","赵云","孙策","庄周","廉颇","后裔","妲己","荆轲",]; 78 | 79 | @override 80 | void initState() { 81 | super.initState(); 82 | actionTxt=actionTxtEdit; 83 | heroes.forEach((heroName) { 84 | itemBins.add(new ItemBin(heroName)); 85 | } 86 | ); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return new Scaffold( 92 | appBar: new AppBar( 93 | title: new Text("可拖拽GridView"), 94 | actions: [ 95 | new Center( 96 | child: new GestureDetector( 97 | child: new Container( 98 | child: new Text(actionTxt,style: TextStyle(fontSize: 19.0),), 99 | margin: EdgeInsets.only(right: 12), 100 | ), 101 | onTap: (){ 102 | changeActionState(); 103 | editSwitchController.editStateChanged(); 104 | }, 105 | ) 106 | ) 107 | ], 108 | ), 109 | body: new DragAbleGridView( 110 | mainAxisSpacing:10.0, 111 | crossAxisSpacing:10.0, 112 | childAspectRatio:1.8, 113 | crossAxisCount: 4, 114 | itemBins:itemBins, 115 | editSwitchController:editSwitchController, 116 | /******************************new parameter*********************************/ 117 | isOpenDragAble: true, 118 | animationDuration: 300, //milliseconds 119 | longPressDuration: 800, //milliseconds 120 | /******************************new parameter*********************************/ 121 | deleteIcon: new Image.asset("images/close.png",width: 15.0 ,height: 15.0 ), 122 | child: (int position){ 123 | return new Container( 124 | padding: EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0), 125 | decoration: new BoxDecoration( 126 | borderRadius: BorderRadius.all(new Radius.circular(3.0)), 127 | border: new Border.all(color: Colors.blue), 128 | ), 129 | //因为本布局和删除图标同处于一个Stack内,设置marginTop和marginRight能让图标处于合适的位置 130 | //Because this layout and the delete_Icon are in the same Stack, setting marginTop and marginRight will make the icon in the proper position. 131 | margin: EdgeInsets.only(top: 6.0,right: 6.0), 132 | child: new Text( 133 | itemBins[position].data, 134 | style: new TextStyle(fontSize: 16.0,color: Colors.blue),), 135 | ); 136 | }, 137 | editChangeListener: (){ 138 | changeActionState(); 139 | }, 140 | ), 141 | ); 142 | } 143 | 144 | void changeActionState(){ 145 | if(actionTxt==actionTxtEdit){ 146 | setState(() { 147 | actionTxt=actionTxtComplete; 148 | }); 149 | }else{ 150 | setState(() { 151 | actionTxt=actionTxtEdit; 152 | }); 153 | } 154 | } 155 | } 156 | 157 | ## Properties 158 | 159 | | properties | type | defaults | description | 160 | | ---- | ---- | ---- | ---- | 161 | | child | typedef | @required | gridview's child at each position 162 | | itemBins | List | @required | the data to be show by gridview's children 163 | | crossAxisCount | int | 4 | how many children to be show in a row ; 一行显示几个child 164 | | crossAxisSpacing | double | 1.0 | cross axis spacing ; 和滑动方向垂直的那个方向上 child之间的空隙 165 | | mainAxisSpacing | double | 0.0 | main axis spacing ; 滑动方向child之间的空隙 166 | | childAspectRatio | double | 0.0 | child aspect ratio ; child的纵横比 167 | | editSwitchController | class | null | the switch controller that to trigger editing by clicking the button ; 编辑开关控制器,可通过点击按钮触发编辑 168 | | editChangeListener | typedef | null | when you long press to trigger the edit state, you can listener this state to change the state of the edit button ; 长按触发编辑状态,可监听状态来改变编辑按钮(编辑开关 ,通过按钮触发编辑)的状态 169 | | isOpenDragAble | bool | false | whether to enable the dragable function;是否启用拖动功能 170 | | animationDuration | int | 300 | animation duration;动画持续的时长 171 | | longPressDuration | int | 800 | long press duration;长按触发拖动的时长 172 | | deleteIcon | Image | null | Delete button icon,Do not set this property if you do not use the delete function;删除按钮的图标,如果不使用删除功能,不要设置此属性 173 | 174 | ## LICENSE 175 | MIT License 176 | 177 | Copyright (c) 2018 178 | 179 | Permission is hereby granted, free of charge, to any person obtaining a copy 180 | of this software and associated documentation files (the "Software"), to deal 181 | in the Software without restriction, including without limitation the rights 182 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 183 | copies of the Software, and to permit persons to whom the Software is 184 | furnished to do so, subject to the following conditions: 185 | 186 | The above copyright notice and this permission notice shall be included in all 187 | copies or substantial portions of the Software. 188 | 189 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 190 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 191 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 192 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 193 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 194 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 195 | SOFTWARE. 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/dragablegridview_flutter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dragablegridview_flutter/dragablegridviewbin.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | typedef CreateChild = Widget Function(int position); 7 | typedef EditChangeListener(); 8 | typedef DeleteIconClickListener =void Function(int index); 9 | 10 | ///准备修改的大纲:3.要适配2-3个文字 11 | class DragAbleGridView extends StatefulWidget{ 12 | 13 | final CreateChild child; 14 | final List itemBins; 15 | ///GridView一行显示几个child 16 | final int crossAxisCount; 17 | ///为了便于计算 Item之间的空隙都用crossAxisSpacing 18 | final double crossAxisSpacing; 19 | final double mainAxisSpacing; 20 | //cross-axis to the main-axis 21 | final double childAspectRatio; 22 | ///编辑开关控制器,可通过点击按钮触发编辑 23 | final EditSwitchController editSwitchController; 24 | ///长按触发编辑状态,可监听状态来改变编辑按钮(编辑开关 ,通过按钮触发编辑)的状态 25 | final EditChangeListener editChangeListener; 26 | final bool isOpenDragAble; 27 | final int animationDuration; 28 | final int longPressDuration; 29 | ///删除按钮 30 | final Widget deleteIcon; 31 | final DeleteIconClickListener deleteIconClickListener; 32 | 33 | 34 | DragAbleGridView({ 35 | @required this.child, 36 | @required this.itemBins, 37 | this.crossAxisCount:4, 38 | this.childAspectRatio:1.0, 39 | this.mainAxisSpacing:0.0, 40 | this.crossAxisSpacing:0.0, 41 | this.editSwitchController, 42 | this.editChangeListener, 43 | this.isOpenDragAble:false, 44 | this.animationDuration:300, 45 | this.longPressDuration:800, 46 | this.deleteIcon, 47 | this.deleteIconClickListener, 48 | }) :assert( 49 | child!=null, 50 | itemBins!=null, 51 | ); 52 | 53 | @override 54 | State createState() { 55 | return new DragAbleGridViewState(); 56 | } 57 | } 58 | 59 | class DragAbleGridViewState extends State with SingleTickerProviderStateMixin implements DragAbleViewListener{ 60 | 61 | var physics=new ScrollPhysics(); 62 | double screenWidth; 63 | double screenHeight; 64 | ///在拖动过程中Item position 的位置记录 65 | List itemPositions; 66 | ///下面4个变量具体看onTapDown()方法里面的代码,有具体的备注 67 | double itemWidth = 0.0; 68 | double itemHeight = 0.0; 69 | double itemWidthChild = 0.0; 70 | double itemHeightChild = 0.0; 71 | ///下面2个变量具体看onTapDown()方法里面的代码,有具体的备注 72 | double blankSpaceHorizontal = 0.0; 73 | double blankSpaceVertical = 0.0; 74 | double xBlankPlace = 0.0; 75 | double yBlankPlace = 0.0; 76 | 77 | Animation animation; 78 | AnimationController controller; 79 | int startPosition; 80 | int endPosition; 81 | bool isRest=false; 82 | ///覆盖超过1/5则触发动画,宽和高只要有一个满足就可以触发 83 | //double areaCoverageRatio=1/5; 84 | Timer timer; 85 | bool isRemoveItem=false; 86 | bool isHideDeleteIcon=true; 87 | Future _future; 88 | double xyDistance = 0.0; 89 | double yDistance = 0.0; 90 | double xDistance = 0.0; 91 | 92 | 93 | 94 | @override 95 | void initState() { 96 | super.initState(); 97 | widget.editSwitchController.dragAbleGridViewState=this; 98 | controller = new AnimationController(duration: Duration(milliseconds : widget.animationDuration), vsync: this); 99 | animation = new Tween(begin:0.0,end: 1.0).animate(controller) 100 | ..addListener(() { 101 | T offsetBin; 102 | int childWidgetPosition; 103 | 104 | if(isRest){ 105 | if(startPosition>endPosition){ 106 | for(int i=endPosition; iendPosition){ 137 | for(int i=endPosition; i oldWidget) { 209 | if(itemPositions.length!= widget.itemBins.length){ 210 | _initItemPositions(); 211 | } 212 | super.didUpdateWidget(oldWidget); 213 | } 214 | 215 | 216 | @override 217 | void didChangeDependencies() { 218 | super.didChangeDependencies(); 219 | Size screenSize=MediaQuery.of(context).size; 220 | screenWidth=screenSize.width; 221 | screenHeight=screenSize.height; 222 | } 223 | 224 | 225 | 226 | @override 227 | Widget build(BuildContext context) { 228 | return new GridView.builder( 229 | physics: physics, 230 | scrollDirection: Axis.vertical, 231 | itemCount: widget.itemBins.length, 232 | gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount( 233 | crossAxisCount: widget.crossAxisCount, 234 | childAspectRatio: widget.childAspectRatio, 235 | crossAxisSpacing: widget.crossAxisSpacing, 236 | mainAxisSpacing: widget.mainAxisSpacing 237 | ), 238 | itemBuilder: (BuildContext contexts,int index){ 239 | return DragAbleContentView( 240 | isOpenDragAble: widget.isOpenDragAble, 241 | screenHeight: screenHeight, 242 | screenWidth: screenWidth, 243 | isHideDeleteIcon: isHideDeleteIcon, 244 | controller: controller, 245 | longPressDuration: widget.longPressDuration, 246 | index: index, 247 | dragAbleGridViewBin: widget.itemBins[index], 248 | dragAbleViewListener: this, 249 | child: new Stack( 250 | alignment: Alignment.topRight, 251 | children: [ 252 | widget.child(index), 253 | new Offstage( 254 | offstage: isHideDeleteIcon, 255 | child: new GestureDetector( 256 | child: widget.deleteIcon ?? Container(height: 0, width: 0), 257 | onTap: () { 258 | widget.deleteIconClickListener(index); 259 | setState(() { 260 | widget.itemBins[index].offstage=true; 261 | }); 262 | startPosition=index; 263 | endPosition=widget.itemBins.length-1; 264 | getWidgetsSize(widget.itemBins[index]); 265 | isRemoveItem=true; 266 | _future=controller.forward(); 267 | }, 268 | ), 269 | ), 270 | ], 271 | ), 272 | ); 273 | }); 274 | 275 | } 276 | 277 | 278 | 279 | 280 | ///如果item的大小都不一样大,那每次拖动前都必须计算item的相关尺寸 281 | @override 282 | void getWidgetsSize(DragAbleGridViewBin pressItemBin){ 283 | if(itemWidth == 0) { 284 | //获取 不 带边框的Container的宽度 285 | itemWidth = pressItemBin.containerKey.currentContext 286 | .findRenderObject() 287 | .paintBounds 288 | .size 289 | .width; 290 | } 291 | if(itemHeight ==0) { 292 | itemHeight = pressItemBin.containerKey.currentContext 293 | .findRenderObject() 294 | .paintBounds 295 | .size 296 | .height; 297 | } 298 | 299 | if(itemWidthChild==0) { 300 | //获取 带边框 的Container的宽度,就是可见的Item视图的宽度 301 | itemWidthChild = pressItemBin.containerKeyChild.currentContext 302 | .findRenderObject() 303 | .paintBounds 304 | .size 305 | .width; 306 | } 307 | if(itemHeightChild==0){ 308 | itemHeightChild = pressItemBin.containerKeyChild.currentContext 309 | .findRenderObject() 310 | .paintBounds 311 | .size 312 | .height; 313 | } 314 | 315 | if(blankSpaceHorizontal==0){ 316 | //获取 不带边框 和它的子View (带边框 的Container)左右两边的空白部分的宽度 317 | blankSpaceHorizontal = (itemWidth - itemWidthChild) / 2; 318 | } 319 | 320 | if(blankSpaceVertical==0) { 321 | blankSpaceVertical = (itemHeight - itemHeightChild) / 2; 322 | } 323 | 324 | if(xBlankPlace==0){ 325 | //边框和父布局之间的空白部分 + gridView Item之间的space + 相邻Item边框和父布局之间的空白部分 326 | //所以 一个View和相邻View空白的部分计算如下 ,也就是说大于这个值 两个Item则能相遇叠加 327 | xBlankPlace = blankSpaceHorizontal * 2 + widget.crossAxisSpacing; 328 | } 329 | 330 | if(yBlankPlace==0) { 331 | yBlankPlace = blankSpaceVertical * 2 + widget.mainAxisSpacing; 332 | } 333 | 334 | if(xyDistance == 0){ 335 | xyDistance = screenWidth-itemWidth; 336 | } 337 | 338 | if(yDistance == 0){ 339 | yDistance = itemHeight + widget.mainAxisSpacing; 340 | } 341 | 342 | if(xDistance == 0){ 343 | xDistance = itemWidth + widget.crossAxisSpacing; 344 | } 345 | } 346 | 347 | 348 | int geyXTransferItemCount(int index,double xBlankPlace,double dragPointX){ 349 | 350 | //最大边界 和 最小边界 351 | //double maxBoundWidth = itemWidthChild * (1-areaCoverageRatio); 352 | //double minBoundWidth = itemWidthChild * areaCoverageRatio; 353 | 354 | //是否越过空白间隙,未越过则表示在原地,或覆盖自己原位置的一部分,未拖动到其他Item上,或者已经拖动过多次现在又拖回来了;越过则有多种情况 355 | if(dragPointX.abs()>xBlankPlace){ 356 | if(dragPointX>0){ 357 | //↑↑↑表示移动到自己原位置的右手边 358 | return checkXAxleRight(index,xBlankPlace,dragPointX); 359 | }else{ 360 | //↑↑↑表示移动到自己原位置的左手边 361 | return checkXAxleLeft(index,xBlankPlace,dragPointX); 362 | } 363 | }else{ 364 | //↑↑↑连一个空白的区域都未越过 肯定是呆在自己的原位置,返回index 365 | return 0; 366 | } 367 | } 368 | 369 | ///当被拖动到自己位置右侧时 370 | int checkXAxleRight(int index,double xBlankPlace,double dragPointX){ 371 | double aSection=xBlankPlace+itemWidthChild; 372 | 373 | double rightTransferDistance=dragPointX.abs()+itemWidthChild; 374 | //计算左右边框的余数 375 | double rightBorder=rightTransferDistance%aSection; 376 | double leftBorder=dragPointX.abs()%aSection; 377 | 378 | //与2个item有粘连时,计算占比多的就是要目标位置 379 | if(rightBorderrightBorder){ 381 | //left占比多,那左侧未将要动画的目标位置 382 | return (dragPointX.abs()/aSection).floor(); 383 | }else{ 384 | //right占比多 385 | return (rightTransferDistance/aSection).floor(); 386 | } 387 | 388 | }else if (rightBorder>itemWidthChild&&leftBorderitemWidthChild) { 392 | //right粘连,左侧的边框在空白区域 393 | return (rightTransferDistance/aSection).floor(); 394 | }else { 395 | //左右两边均没有粘连时,说明左右两边处于空白区域,返回0即可 396 | return 0; 397 | } 398 | } 399 | 400 | ///X轴方向上,当被拖动到自己位置左侧时 401 | int checkXAxleLeft(int index,double xBlankPlace,double dragPointX){ 402 | double aSection=xBlankPlace+itemWidthChild; 403 | 404 | double leftTransferDistance=dragPointX.abs()+itemWidthChild; 405 | 406 | //计算左右边框的余数 407 | double leftBorder=leftTransferDistance%aSection; 408 | double rightBorder=dragPointX.abs()%aSection; 409 | 410 | //与2个item有粘连时,计算占比多的就是要目标位置 411 | if(rightBorderleftBorder){ 413 | //right占比多,那右侧为将要动画的目标位置 414 | return -(dragPointX.abs()/aSection).floor(); 415 | }else{ 416 | //left占比多 417 | return -(leftTransferDistance/aSection).floor(); 418 | } 419 | 420 | }else if (rightBorder>itemWidthChild&&leftBorderitemWidthChild) { 425 | //right粘连,左侧的边框在空白区域 426 | return -(dragPointX.abs()/aSection).floor(); 427 | 428 | }else { 429 | //左右两边均没有粘连时,说明左右两边处于空白区域,返回0即可 430 | return 0; 431 | } 432 | } 433 | 434 | 435 | 436 | ///计算Y轴方向需要移动几个Item 437 | /// 1. 目标拖动距离拖动不满足, 2. 拖动到其他Item的,3. 和任何Item都没有粘连,5.和多个item有重叠 等4种情况 438 | /// 还要考虑一点就是 虽然Y轴不满足1/5--4/5覆盖率,但是X轴满足 439 | int geyYTransferItemCount(int index,double yBlankPlace,double dragPointY){ 440 | 441 | //最大边界 和 最小边界 442 | //double maxBoundHeight = itemHeightChild * (1-areaCoverageRatio); 443 | //double minBoundHeight = itemHeightChild * areaCoverageRatio; 444 | 445 | //上下边框是否都满足 覆盖1/5--4/5高度的要求 446 | //bool isTopBoundLegitimate = topBorder > minBoundHeight && topBorder < maxBoundHeight; 447 | //bool isBottomBoundLegitimate = bottomBorder > minBoundHeight && bottomBorder < maxBoundHeight; 448 | 449 | //是否越过空白间隙,未越过则表示在原地,或覆盖自己原位置的一部分,未拖动到其他Item上,或者已经拖动过多次现在又拖回来了;越过则有多种情况 450 | if(dragPointY.abs()>yBlankPlace){ 451 | //↑↑↑越过则有多种情况↓↓↓ 452 | if(dragPointY>0){ 453 | //↑↑↑表示拖动的Item现在处于原位置之下 454 | return checkYAxleBelow(index,yBlankPlace,dragPointY); 455 | }else{ 456 | //↑↑↑表示拖动的Item现在处于原位置之上 457 | return checkYAxleAbove(index,yBlankPlace,dragPointY); 458 | } 459 | }else{ 460 | //↑↑↑未越过 返回index 461 | return index; 462 | } 463 | } 464 | 465 | 466 | ///Y轴上当被拖动到原位置之上时,计算拖动了几行 467 | int checkYAxleAbove(int index,double yBlankPlace,double dragPointY){ 468 | double aSection=yBlankPlace+itemHeightChild; 469 | 470 | double topTransferDistance=dragPointY.abs()+itemHeightChild; 471 | 472 | //求下边框的余数,余数小于itemHeightChild,表示和下面的item覆盖,余数大于itemHeightChild,表示下边框处于空白的区域 473 | double topBorder = (topTransferDistance)%aSection; 474 | //求上边框的余数 ,余数小于itemHeightChild,表示和上面的Item覆盖 ,余数大于itemHeightChild,表示上边框处于空白区域 475 | double bottomBorder = dragPointY.abs()%aSection; 476 | 477 | if(topBordertopBorder){ 480 | //↑↑↑粘连2个 要计算哪个占比多,topBorder越小 覆盖面积越大 ,bottomBorder越大 覆盖面积越大; 481 | //下边框占比叫较大 482 | return index-(dragPointY.abs()/aSection).floor()*widget.crossAxisCount; 483 | }else{ 484 | //↑↑↑上边框占比大 485 | return index-(topTransferDistance/aSection).floor()*widget.crossAxisCount; 486 | } 487 | }else if(topBorder>itemHeightChild&&bottomBorderitemHeightChild){ 492 | //↑↑↑上边框在覆盖区域,下边框在空白区域 493 | return index-(topTransferDistance/aSection).floor()*widget.crossAxisCount; 494 | 495 | }else{ 496 | //和哪个Item都没有覆盖,上下边框都在空白的区域。返回Index即可 497 | return index; 498 | } 499 | } 500 | 501 | /// 还要考虑一点就是 虽然Y轴不满足1/5--4/5覆盖率,但是X轴满足,所以返回的时候同时返回目标index 和 是否满足Y的覆盖条件 502 | int checkYAxleBelow(int index,double yBlankPlace,double dragPointY){ 503 | double aSection=yBlankPlace+itemHeightChild; 504 | 505 | double bottomTransferDistance=dragPointY.abs()+itemHeightChild; 506 | 507 | //求下边框的余数,余数小于itemHeightChild,表示和下面的item覆盖,余数大于itemHeightChild,表示下边框处于空白的区域 508 | double bottomBorder = bottomTransferDistance%aSection; 509 | //求上边框的余数 ,余数小于itemHeightChild,表示和上面的Item覆盖 ,余数大于itemHeightChild,表示上边框处于空白区域 510 | double topBorder = dragPointY.abs()%aSection; 511 | 512 | if(bottomBorderbottomBorder){ 515 | //↑↑↑粘连2个 要计算哪个占比多,topBorder越小 覆盖面积越大 ,bottomBorder越大 覆盖面积越大; 516 | //↑↑↑上面占比大 517 | return index + (dragPointY.abs()/aSection).floor()*widget.crossAxisCount; 518 | 519 | }else{ 520 | //↑↑↑下面占比大 521 | return index + (bottomTransferDistance/aSection).floor()*widget.crossAxisCount; 522 | } 523 | }else if(topBorder>itemHeightChild&&bottomBorderitemHeightChild){ //topBorder=0 549 | &&widget.itemBins[index].dragAble){ 550 | endPosition=x+y; 551 | _future=controller.forward(); 552 | } 553 | } 554 | 555 | ///拖动结束后,根据 itemPositions 里面的排序,将itemBins重新排序 556 | ///并重新初始化 itemPositions 557 | @override 558 | void onPanEndEvent(index) async{ 559 | widget.itemBins[index].dragAble=false; 560 | if(controller.isAnimating){ 561 | await _future; 562 | } 563 | setState(() { 564 | List itemBi = new List(); 565 | T bin; 566 | for (int i = 0; i < itemPositions.length; i++) { 567 | bin=widget.itemBins[itemPositions[i]]; 568 | bin.dragPointX = 0.0; 569 | bin.dragPointY = 0.0; 570 | bin.lastTimePositionX = 0.0; 571 | bin.lastTimePositionY = 0.0; 572 | itemBi.add(bin); 573 | } 574 | widget.itemBins.clear(); 575 | widget.itemBins.addAll(itemBi); 576 | _initItemPositions(); 577 | }); 578 | } 579 | 580 | ///外部使用EditSwitchController控制编辑状态 581 | ///当调用该方法时 将GridView Item上的删除图标的状态取非 来改变状态 582 | void changeDeleteIconState(){ 583 | setState(() { 584 | isHideDeleteIcon=!isHideDeleteIcon; 585 | }); 586 | } 587 | 588 | @override 589 | void onTapDown(int index) { 590 | endPosition=index; 591 | } 592 | 593 | 594 | @override 595 | double getItemHeight() { 596 | return itemHeight; 597 | } 598 | 599 | @override 600 | double getItemWidth() { 601 | return itemWidth; 602 | } 603 | 604 | @override 605 | void onPressSuccess(int index) { 606 | setState(() { 607 | startPosition=index; 608 | if(widget.editChangeListener!=null&&isHideDeleteIcon==true){ 609 | widget.editChangeListener(); 610 | } 611 | isHideDeleteIcon=false; 612 | }); 613 | } 614 | } 615 | 616 | 617 | class EditSwitchController{ 618 | DragAbleGridViewState dragAbleGridViewState; 619 | 620 | void editStateChanged(){ 621 | dragAbleGridViewState.changeDeleteIconState(); 622 | } 623 | } 624 | 625 | 626 | class DragAbleContentView extends StatefulWidget{ 627 | 628 | final Widget child; 629 | final bool isOpenDragAble; 630 | final double screenWidth,screenHeight; 631 | final bool isHideDeleteIcon; 632 | final AnimationController controller; 633 | final int longPressDuration; 634 | final int index; 635 | final T dragAbleGridViewBin; 636 | final DragAbleViewListener dragAbleViewListener; 637 | 638 | DragAbleContentView({ 639 | @required this.child, 640 | @required this.isOpenDragAble, 641 | @required this.screenHeight, 642 | @required this.screenWidth, 643 | @required this.isHideDeleteIcon, 644 | @required this.controller, 645 | @required this.longPressDuration, 646 | @required this.index, 647 | @required this.dragAbleGridViewBin, 648 | @required this.dragAbleViewListener, 649 | }); 650 | 651 | @override 652 | State createState() { 653 | return DragAbleContentViewState(); 654 | } 655 | } 656 | 657 | 658 | class DragAbleContentViewState extends State>{ 659 | 660 | Timer timer; 661 | 662 | @override 663 | Widget build(BuildContext context) { 664 | return new GestureDetector( 665 | onTapDown: widget.isOpenDragAble ? (detail){ 666 | handleOnTapDownEvent(detail); 667 | } : null, 668 | onPanUpdate: widget.isOpenDragAble ? (updateDetail){ 669 | handleOnPanUpdateEvent(updateDetail); 670 | } : null, 671 | onPanEnd: widget.isOpenDragAble ? (upDetail){ 672 | handleOnPanEndEvent(widget.index); 673 | } : null, 674 | onTapUp: widget.isOpenDragAble ? (tapUpDetails){ 675 | handleOnTapUp(); 676 | } : null, 677 | child:new Offstage( 678 | offstage: widget.dragAbleGridViewBin.offstage, 679 | child: new Container( 680 | alignment: Alignment.center, 681 | //color: Colors.grey, 682 | key: widget.dragAbleGridViewBin.containerKey, 683 | child: new OverflowBox( 684 | maxWidth: widget.screenWidth, 685 | maxHeight: widget.screenHeight, 686 | alignment: Alignment.center, 687 | child: new Center( 688 | child: new Container( 689 | key: widget.dragAbleGridViewBin.containerKeyChild, 690 | transform: new Matrix4.translationValues(widget.dragAbleGridViewBin.dragPointX, widget.dragAbleGridViewBin.dragPointY, 0.0), 691 | child: widget.child, 692 | ), 693 | ) 694 | ), 695 | ), 696 | ), 697 | ); 698 | } 699 | 700 | 701 | 702 | void handleOnPanEndEvent(int index){ 703 | T pressItemBin = widget.dragAbleGridViewBin; 704 | pressItemBin.isLongPress=false; 705 | if(!pressItemBin.dragAble) { 706 | pressItemBin.dragPointY = 0.0; 707 | pressItemBin.dragPointX = 0.0; 708 | }else { 709 | widget.dragAbleGridViewBin.dragAble=false; 710 | widget.dragAbleViewListener.onPanEndEvent(index); 711 | } 712 | } 713 | 714 | 715 | void handleOnTapUp(){ 716 | T pressItemBin = widget.dragAbleGridViewBin; 717 | pressItemBin.isLongPress=false; 718 | if(!widget.isHideDeleteIcon) { 719 | setState(() { 720 | pressItemBin.dragPointY = 0.0; 721 | pressItemBin.dragPointX = 0.0; 722 | }); 723 | } 724 | } 725 | 726 | 727 | void handleOnPanUpdateEvent(DragUpdateDetails updateDetail){ 728 | T pressItemBin = widget.dragAbleGridViewBin; 729 | pressItemBin.isLongPress=false; 730 | if(pressItemBin.dragAble) { 731 | 732 | double deltaDy = updateDetail.delta.dy; 733 | double deltaDx = updateDetail.delta.dx; 734 | 735 | double dragPointY = pressItemBin.dragPointY += deltaDy; 736 | double dragPointX = pressItemBin.dragPointX += deltaDx; 737 | 738 | if(widget.controller.isAnimating){ 739 | return; 740 | } 741 | bool isMove = deltaDy.abs()>0.0 || deltaDx.abs()>0.0; 742 | 743 | if(isMove){ 744 | if(timer!=null&&timer.isActive) { 745 | timer.cancel(); 746 | } 747 | setState(() {}); 748 | timer=new Timer(new Duration(milliseconds: 100), (){ 749 | widget.dragAbleViewListener.onFingerPause(widget.index,dragPointX,dragPointY,updateDetail); 750 | }); 751 | } 752 | } 753 | } 754 | 755 | 756 | void handleOnTapDownEvent(TapDownDetails detail){ 757 | T pressItemBin = widget.dragAbleGridViewBin; 758 | widget.dragAbleViewListener.getWidgetsSize(pressItemBin); 759 | 760 | if(!widget.isHideDeleteIcon) { 761 | //获取控件在屏幕中的y坐标 762 | double ss = pressItemBin.containerKey.currentContext.findRenderObject().getTransformTo(null).getTranslation().y; 763 | double aa = pressItemBin.containerKey.currentContext.findRenderObject().getTransformTo(null).getTranslation().x; 764 | 765 | //计算手指点下去后,控件应该偏移多少像素 766 | double itemHeight = widget.dragAbleViewListener.getItemHeight(); 767 | double itemWidth = widget.dragAbleViewListener.getItemWidth(); 768 | pressItemBin.dragPointY = detail.globalPosition.dy - ss - itemHeight / 2; 769 | pressItemBin.dragPointX = detail.globalPosition.dx - aa - itemWidth / 2; 770 | } 771 | 772 | //标识长按事件开始 773 | pressItemBin.isLongPress=true; 774 | //将可拖动标识置为false;(dragAble 为 true时 控件可拖动 ,暂时置为false 等达到长按时间才视为需要拖动) 775 | pressItemBin.dragAble=false; 776 | widget.dragAbleViewListener.onTapDown(widget.index); 777 | _handLongPress(); 778 | } 779 | 780 | 781 | 782 | ///自定义长按事件,只有长按800毫秒 才能触发拖动 783 | void _handLongPress() async{ 784 | await Future.delayed(new Duration(milliseconds: widget.longPressDuration)); 785 | if( widget.dragAbleGridViewBin.isLongPress){ 786 | setState(() { 787 | widget.dragAbleGridViewBin.dragAble=true; 788 | });//吸附效果 SetState不能删除 789 | widget.dragAbleViewListener.onPressSuccess(widget.index); 790 | } 791 | } 792 | } 793 | 794 | abstract class DragAbleViewListener{ 795 | void getWidgetsSize(T pressItemBin); 796 | void onTapDown(int index); 797 | void onFingerPause(int index ,double dragPointX,double dragPointY,DragUpdateDetails updateDetail); 798 | void onPanEndEvent(int index); 799 | double getItemHeight(); 800 | double getItemWidth(); 801 | void onPressSuccess(int index); 802 | } 803 | --------------------------------------------------------------------------------