[
86 | Expanded(
87 | child: Container(
88 | decoration: const BoxDecoration(
89 | borderRadius:
90 | BorderRadius.vertical(bottom: Radius.circular(7.0)),
91 | color: Colors.pink,
92 | ),
93 | padding: const EdgeInsets.all(10),
94 | child: Text(
95 | 'Footer ${innerList.name}',
96 | style: Theme.of(context).primaryTextTheme.headlineSmall,
97 | ),
98 | ),
99 | ),
100 | ],
101 | ),
102 | leftSide: const VerticalDivider(
103 | color: Colors.pink,
104 | width: 1.5,
105 | thickness: 1.5,
106 | ),
107 | rightSide: const VerticalDivider(
108 | color: Colors.pink,
109 | width: 1.5,
110 | thickness: 1.5,
111 | ),
112 | children: List.generate(innerList.children.length,
113 | (index) => _buildItem(innerList.children[index])),
114 | );
115 | }
116 |
117 | _buildItem(String item) {
118 | return DragAndDropItem(
119 | child: ListTile(
120 | title: Text(item),
121 | ),
122 | );
123 | }
124 |
125 | _onItemReorder(
126 | int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) {
127 | setState(() {
128 | var movedItem = _lists[oldListIndex].children.removeAt(oldItemIndex);
129 | _lists[newListIndex].children.insert(newItemIndex, movedItem);
130 | });
131 | }
132 |
133 | _onListReorder(int oldListIndex, int newListIndex) {
134 | setState(() {
135 | var movedList = _lists.removeAt(oldListIndex);
136 | _lists.insert(newListIndex, movedList);
137 | });
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # drag\_and\_drop\_lists
2 | Two-level drag and drop reorderable lists.
3 |
4 | ## Features
5 | - Reorder elements between multiple lists
6 | - Reorder lists
7 | - Drag and drop new elements from outside of the lists
8 | - Vertical or horizontal layout
9 | - Use with drag handles, long presses, or short presses
10 | - Expandable lists
11 | - Can be used in slivers
12 | - Prevent individual lists/elements from being able to be dragged
13 | - Easy to extend with custom layouts
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ## Known Issues
26 | There is currently (as of flutter v. 1.24.0-1.0.pre) an issue only on web where dragging an item with some descendant that includes an InkWell widget with an onTap method will throw an exception. This includes having a ListTile with an onTap method defined.
27 |
28 | This seems to be resolved by using a GestureDetector and its onTap method instead of the InkWell.
29 |
30 | See the following issues:
31 | * [#14](https://github.com/philip-brink/DragAndDropLists/issues/14)
32 | * [Flutter #69774](https://github.com/flutter/flutter/issues/69774)
33 | * [Flutter #67044](https://github.com/flutter/flutter/issues/67044)
34 | * [Flutter #66887](https://github.com/flutter/flutter/issues/66887)
35 |
36 | ## Usage
37 | To use this plugin, add `drag_and_drop_lists` as a [dependency in your pubspec.yaml file.](https://flutter.dev/docs/development/packages-and-plugins/using-packages)
38 | For example:
39 |
40 | ```
41 | dependencies:
42 | drag_and_drop_lists: ^0.2.1
43 | ```
44 |
45 | Now in your Dart code, you can use: `import 'package:drag_and_drop_lists/drag_and_drop_lists.dart';`
46 |
47 | To add the lists, add a `DragAndDropLists` widget. Set its children to a list of `DragAndDropList`. Likewise, set the children of `DragAndDropList` to a list of `DragAndDropItem`.
48 | For example:
49 |
50 | ```
51 | // Outer list
52 | List _contents;
53 |
54 | @override
55 | void initState() {
56 | super.initState();
57 |
58 | // Generate a list
59 | _contents = List.generate(10, (index) {
60 | return DragAndDropList(
61 | header: Text('Header $index'),
62 | children: [
63 | DragAndDropItem(
64 | child: Text('$index.1'),
65 | ),
66 | DragAndDropItem(
67 | child: Text('$index.2'),
68 | ),
69 | DragAndDropItem(
70 | child: Text('$index.3'),
71 | ),
72 | ],
73 | );
74 | });
75 | }
76 |
77 | @override
78 | Widget build(BuildContext context) {
79 | // Add a DragAndDropLists. The only required parameters are children,
80 | // onItemReorder, and onListReorder. All other parameters are used for
81 | // styling the lists and changing its behaviour. See the samples in the
82 | // example app for many more ways to configure this.
83 | return DragAndDropLists(
84 | children: _contents,
85 | onItemReorder: _onItemReorder,
86 | onListReorder: _onListReorder,
87 | );
88 | }
89 |
90 | _onItemReorder(int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) {
91 | setState(() {
92 | var movedItem = _contents[oldListIndex].children.removeAt(oldItemIndex);
93 | _contents[newListIndex].children.insert(newItemIndex, movedItem);
94 | });
95 | }
96 |
97 | _onListReorder(int oldListIndex, int newListIndex) {
98 | setState(() {
99 | var movedList = _contents.removeAt(oldListIndex);
100 | _contents.insert(newListIndex, movedList);
101 | });
102 | }
103 |
104 | ```
105 |
106 | For further examples, see the example app or [view the example code](https://github.com/philip-brink/DragAndDropLists/tree/master/example/lib) directly.
107 |
--------------------------------------------------------------------------------
/example/lib/list_tile_example.dart:
--------------------------------------------------------------------------------
1 | import 'package:drag_and_drop_lists/drag_and_drop_lists.dart';
2 | import 'package:example/navigation_drawer.dart' as navigation_drawer;
3 | import 'package:flutter/material.dart';
4 |
5 | class ListTileExample extends StatefulWidget {
6 | const ListTileExample({Key? key}) : super(key: key);
7 |
8 | @override
9 | State createState() => _ListTileExample();
10 | }
11 |
12 | class _ListTileExample extends State {
13 | late List _contents;
14 |
15 | @override
16 | void initState() {
17 | super.initState();
18 |
19 | _contents = List.generate(4, (index) {
20 | return DragAndDropList(
21 | header: Column(
22 | children: [
23 | ListTile(
24 | title: Text(
25 | 'Header $index',
26 | ),
27 | subtitle: Text('Header $index subtitle'),
28 | ),
29 | const Divider(),
30 | ],
31 | ),
32 | footer: Column(
33 | children: [
34 | const Divider(),
35 | ListTile(
36 | title: Text(
37 | 'Footer $index',
38 | ),
39 | subtitle: Text('Footer $index subtitle'),
40 | ),
41 | ],
42 | ),
43 | children: [
44 | DragAndDropItem(
45 | child: ListTile(
46 | title: Text(
47 | 'Sub $index.1',
48 | ),
49 | trailing: const Icon(Icons.access_alarm),
50 | ),
51 | ),
52 | DragAndDropItem(
53 | child: ListTile(
54 | title: Text(
55 | 'Sub $index.2',
56 | ),
57 | trailing: const Icon(Icons.alarm_off),
58 | ),
59 | ),
60 | DragAndDropItem(
61 | child: ListTile(
62 | title: Text(
63 | 'Sub $index.3',
64 | ),
65 | trailing: const Icon(Icons.alarm_on),
66 | ),
67 | ),
68 | ],
69 | );
70 | });
71 | }
72 |
73 | @override
74 | Widget build(BuildContext context) {
75 | return Scaffold(
76 | appBar: AppBar(
77 | title: const Text('List Tiles'),
78 | ),
79 | drawer: const navigation_drawer.NavigationDrawer(),
80 | body: DragAndDropLists(
81 | children: _contents,
82 | onItemReorder: _onItemReorder,
83 | onListReorder: _onListReorder,
84 | listGhost: Padding(
85 | padding: const EdgeInsets.symmetric(vertical: 30.0),
86 | child: Center(
87 | child: Container(
88 | padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 100.0),
89 | decoration: BoxDecoration(
90 | border: Border.all(),
91 | borderRadius: BorderRadius.circular(7.0),
92 | ),
93 | child: const Icon(Icons.add_box),
94 | ),
95 | ),
96 | ),
97 | listPadding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10),
98 | contentsWhenEmpty: Row(
99 | children: [
100 | const Expanded(
101 | child: Padding(
102 | padding: EdgeInsets.only(left: 40, right: 10),
103 | child: Divider(),
104 | ),
105 | ),
106 | Text(
107 | 'Empty List',
108 | style: TextStyle(
109 | color: Theme.of(context).textTheme.titleSmall!.color,
110 | fontStyle: FontStyle.italic),
111 | ),
112 | const Expanded(
113 | child: Padding(
114 | padding: EdgeInsets.only(left: 20, right: 40),
115 | child: Divider(),
116 | ),
117 | ),
118 | ],
119 | ),
120 | listDecoration: BoxDecoration(
121 | color: Theme.of(context).canvasColor,
122 | borderRadius: const BorderRadius.all(Radius.circular(6.0)),
123 | boxShadow: [
124 | BoxShadow(
125 | color: Colors.grey.withOpacity(0.5),
126 | spreadRadius: 2,
127 | blurRadius: 3,
128 | offset: const Offset(0, 3), // changes position of shadow
129 | ),
130 | ],
131 | ),
132 | ),
133 | );
134 | }
135 |
136 | _onItemReorder(
137 | int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) {
138 | setState(() {
139 | var movedItem = _contents[oldListIndex].children.removeAt(oldItemIndex);
140 | _contents[newListIndex].children.insert(newItemIndex, movedItem);
141 | });
142 | }
143 |
144 | _onListReorder(int oldListIndex, int newListIndex) {
145 | setState(() {
146 | var movedList = _contents.removeAt(oldListIndex);
147 | _contents.insert(newListIndex, movedList);
148 | });
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/example/lib/drag_handle_example.dart:
--------------------------------------------------------------------------------
1 | import 'package:drag_and_drop_lists/drag_and_drop_lists.dart';
2 | import 'package:example/navigation_drawer.dart' as navigation_drawer;
3 | import 'package:flutter/material.dart';
4 |
5 | class DragHandleExample extends StatefulWidget {
6 | const DragHandleExample({Key? key}) : super(key: key);
7 |
8 | @override
9 | State createState() => _DragHandleExample();
10 | }
11 |
12 | class _DragHandleExample extends State {
13 | late List _contents;
14 |
15 | @override
16 | void initState() {
17 | super.initState();
18 |
19 | _contents = List.generate(4, (index) {
20 | return DragAndDropList(
21 | header: Column(
22 | children: [
23 | Row(
24 | children: [
25 | Padding(
26 | padding: const EdgeInsets.only(left: 8, bottom: 4),
27 | child: Text(
28 | 'Header $index',
29 | style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
30 | ),
31 | ),
32 | ],
33 | ),
34 | ],
35 | ),
36 | children: [
37 | DragAndDropItem(
38 | child: Row(
39 | children: [
40 | Padding(
41 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
42 | child: Text(
43 | 'Sub $index.1',
44 | ),
45 | ),
46 | ],
47 | ),
48 | ),
49 | DragAndDropItem(
50 | child: Row(
51 | children: [
52 | Padding(
53 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
54 | child: Text(
55 | 'Sub $index.2',
56 | ),
57 | ),
58 | ],
59 | ),
60 | ),
61 | DragAndDropItem(
62 | child: Row(
63 | children: [
64 | Padding(
65 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
66 | child: Text(
67 | 'Sub $index.3',
68 | ),
69 | ),
70 | ],
71 | ),
72 | ),
73 | ],
74 | );
75 | });
76 | }
77 |
78 | @override
79 | Widget build(BuildContext context) {
80 | var backgroundColor = const Color.fromARGB(255, 243, 242, 248);
81 |
82 | return Scaffold(
83 | backgroundColor: backgroundColor,
84 | appBar: AppBar(
85 | title: const Text('Drag Handle'),
86 | ),
87 | drawer: const navigation_drawer.NavigationDrawer(),
88 | body: DragAndDropLists(
89 | children: _contents,
90 | onItemReorder: _onItemReorder,
91 | onListReorder: _onListReorder,
92 | listPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
93 | itemDivider: Divider(
94 | thickness: 2,
95 | height: 2,
96 | color: backgroundColor,
97 | ),
98 | itemDecorationWhileDragging: BoxDecoration(
99 | color: Colors.white,
100 | boxShadow: [
101 | BoxShadow(
102 | color: Colors.grey.withOpacity(0.5),
103 | spreadRadius: 2,
104 | blurRadius: 3,
105 | offset: const Offset(0, 0), // changes position of shadow
106 | ),
107 | ],
108 | ),
109 | listInnerDecoration: BoxDecoration(
110 | color: Theme.of(context).canvasColor,
111 | borderRadius: const BorderRadius.all(Radius.circular(8.0)),
112 | ),
113 | lastItemTargetHeight: 8,
114 | addLastItemTargetHeightToTop: true,
115 | lastListTargetSize: 40,
116 | listDragHandle: const DragHandle(
117 | verticalAlignment: DragHandleVerticalAlignment.top,
118 | child: Padding(
119 | padding: EdgeInsets.only(right: 10),
120 | child: Icon(
121 | Icons.menu,
122 | color: Colors.black26,
123 | ),
124 | ),
125 | ),
126 | itemDragHandle: const DragHandle(
127 | child: Padding(
128 | padding: EdgeInsets.only(right: 10),
129 | child: Icon(
130 | Icons.menu,
131 | color: Colors.blueGrey,
132 | ),
133 | ),
134 | ),
135 | ),
136 | );
137 | }
138 |
139 | _onItemReorder(
140 | int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex) {
141 | setState(() {
142 | var movedItem = _contents[oldListIndex].children.removeAt(oldItemIndex);
143 | _contents[newListIndex].children.insert(newItemIndex, movedItem);
144 | });
145 | }
146 |
147 | _onListReorder(int oldListIndex, int newListIndex) {
148 | setState(() {
149 | var movedList = _contents.removeAt(oldListIndex);
150 | _contents.insert(newListIndex, movedList);
151 | });
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/example/linux/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # Project-level configuration.
2 | cmake_minimum_required(VERSION 3.10)
3 | project(runner LANGUAGES CXX)
4 |
5 | # The name of the executable created for the application. Change this to change
6 | # the on-disk name of your application.
7 | set(BINARY_NAME "example")
8 | # The unique GTK application identifier for this application. See:
9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID
10 | set(APPLICATION_ID "com.example.example")
11 |
12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
13 | # versions of CMake.
14 | cmake_policy(SET CMP0063 NEW)
15 |
16 | # Load bundled libraries from the lib/ directory relative to the binary.
17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
18 |
19 | # Root filesystem for cross-building.
20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT)
21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
27 | endif()
28 |
29 | # Define build configuration options.
30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
31 | set(CMAKE_BUILD_TYPE "Debug" CACHE
32 | STRING "Flutter build mode" FORCE)
33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
34 | "Debug" "Profile" "Release")
35 | endif()
36 |
37 | # Compilation settings that should be applied to most targets.
38 | #
39 | # Be cautious about adding new options here, as plugins use this function by
40 | # default. In most cases, you should add new options to specific targets instead
41 | # of modifying this function.
42 | function(APPLY_STANDARD_SETTINGS TARGET)
43 | target_compile_features(${TARGET} PUBLIC cxx_std_14)
44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror)
45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
47 | endfunction()
48 |
49 | # Flutter library and tool build rules.
50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
51 | add_subdirectory(${FLUTTER_MANAGED_DIR})
52 |
53 | # System-level dependencies.
54 | find_package(PkgConfig REQUIRED)
55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
56 |
57 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
58 |
59 | # Define the application target. To change its name, change BINARY_NAME above,
60 | # not the value here, or `flutter run` will no longer work.
61 | #
62 | # Any new source files that you add to the application should be added here.
63 | add_executable(${BINARY_NAME}
64 | "main.cc"
65 | "my_application.cc"
66 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
67 | )
68 |
69 | # Apply the standard set of build settings. This can be removed for applications
70 | # that need different build settings.
71 | apply_standard_settings(${BINARY_NAME})
72 |
73 | # Add dependency libraries. Add any application-specific dependencies here.
74 | target_link_libraries(${BINARY_NAME} PRIVATE flutter)
75 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
76 |
77 | # Run the Flutter tool portions of the build. This must not be removed.
78 | add_dependencies(${BINARY_NAME} flutter_assemble)
79 |
80 | # Only the install-generated bundle's copy of the executable will launch
81 | # correctly, since the resources must in the right relative locations. To avoid
82 | # people trying to run the unbundled copy, put it in a subdirectory instead of
83 | # the default top-level location.
84 | set_target_properties(${BINARY_NAME}
85 | PROPERTIES
86 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
87 | )
88 |
89 | # Generated plugin build rules, which manage building the plugins and adding
90 | # them to the application.
91 | include(flutter/generated_plugins.cmake)
92 |
93 |
94 | # === Installation ===
95 | # By default, "installing" just makes a relocatable bundle in the build
96 | # directory.
97 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
98 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
99 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
100 | endif()
101 |
102 | # Start with a clean build bundle directory every time.
103 | install(CODE "
104 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
105 | " COMPONENT Runtime)
106 |
107 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
108 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
109 |
110 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
111 | COMPONENT Runtime)
112 |
113 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
114 | COMPONENT Runtime)
115 |
116 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
117 | COMPONENT Runtime)
118 |
119 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
120 | install(FILES "${bundled_library}"
121 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
122 | COMPONENT Runtime)
123 | endforeach(bundled_library)
124 |
125 | # Fully re-copy the assets directory on each build to avoid having stale files
126 | # from a previous install.
127 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
128 | install(CODE "
129 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
130 | " COMPONENT Runtime)
131 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
132 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
133 |
134 | # Install the AOT library on non-Debug builds only.
135 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
136 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
137 | COMPONENT Runtime)
138 | endif()
139 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | [0.3.3] - 4 August 2022
4 |
5 | * Update to flutter 3 (thanks [@mauriceraguseinit](https://github.com/mauriceraguseinit))
6 |
7 | [0.3.2+2] - 21 October 2021
8 |
9 | * Replace flutter deprecated elements
10 |
11 | [0.3.2+1] - 21 October 2021
12 |
13 | * Fix last list target for horizontal lists (thanks [@nvloc120](https://github.com/nvloc120)).
14 | * Add ability to remove top padding when there is a widget before the DragAndDropLists (See [flutter/flutter#14842](https://github.com/flutter/flutter/issues/14842), thanks [@aliasgarlabs](https://github.com/aliasgarlabs))
15 |
16 | ## [0.3.2] - 20 April 2021
17 |
18 | * Add optional feedback widget to items (thanks [@svoza10](https://github.com/svoza10)).
19 |
20 | ## [0.3.1] - 15 April 2021
21 |
22 | * Fix scrolling in wrong direction when text direction is right-to-left.
23 | * Fix drag-and-drop feedback widget alignment when text direction is right-to-left.
24 |
25 | ## [0.3.0+1] - 2 April 2021
26 |
27 | * Fix null crash and wrong drag handle used (thanks [@vbuberen](https://github.com/vbuberen)).
28 |
29 | ## [0.3.0] - 30 March 2021
30 |
31 | * DragHandle moved to own widget. To create any drag handle, use the new properties `listDragHandle` and `itemDragHandle` in `DragAndDropLists`.
32 | * Support null safety, see [details on migration](https://dart.dev/null-safety/migration-guide).
33 |
34 | ## [0.2.10] - 14 December 2020
35 |
36 | * Bug fix where `listDecorationWhileDragging` wasn't always being applied.
37 | * Allow DragAndDropLists to be contained in an external ListView when `disableScrolling` is set to `true`.
38 |
39 | ## [0.2.9+2] - 17 November 2020
40 |
41 | * Prevent individual lists inside of a horizontal DragAndDropLists from scrolling when `disableScrolling` is set to true.
42 |
43 | ## [0.2.9+1] - 13 November 2020
44 |
45 | * Bug fix to also not allow scrolling when `disableScrolling` is set to true when dragging and dropping items.
46 |
47 | ## [0.2.9] - 9 November 2020
48 |
49 | * Added `disableScrolling` parameter to `DragAndDropLists`.
50 |
51 | ## [0.2.8] - 6 November 2020
52 |
53 | * Added `listDividerOnLastChild` parameter to `DragAndDropLists`. This allows not showing a list divider after the last list (thanks [@Zexuz](https://github.com/Zexuz)).
54 |
55 | ## [0.2.7] - 21 October 2020
56 |
57 | * Added `onItemDraggingChanged` and `onListDraggingChanged` parameters to `DragAndDropLists`. This allows certain use cases where it is useful to be notified when dragging starts and ends
58 | * Refactored `DragAndDropItemWrapper` to accept a `DragAndDropBuilderParameters` instead of all the other parameters independently to allow for simpler and more consistent changes
59 |
60 | ## [0.2.6] - 20 October 2020
61 |
62 | * Always check mounted status when setting state
63 |
64 | ## [0.2.5] - 15 October 2020
65 |
66 | * Added `constrainDraggingAxis` parameter in `DragAndDropLists`. This is useful when setting custom drag targets outside of the DragAndDropLists.
67 |
68 | ## [0.2.4] - 15 October 2020
69 |
70 | * Added drag handle vertical alignment customization. See `listDragHandleVerticalAlignment` and `itemDragHandleVerticalAlignment` parameters in `DragAndDropLists`
71 | * Added mouse cursor change on web when hovering on drag handle
72 | * Fixed [itemDecorationWhileDragging only applied when dragHandle is provided?](https://github.com/philip-brink/DragAndDropLists/issues/11) (thanks [kjmj](https://github.com/kjmj))
73 | * Fixed bug where setState() was called after dispose when dragging items in a long list (See issue [Error in debug console when dragging item in long list](https://github.com/philip-brink/DragAndDropLists/issues/9), thanks [mivoligo](https://github.com/mivoligo))
74 | * Apply the itemDivider property to items in the DragAndDropListExpansion widget and use the lastItemTargetHeight instead of the constant value of 20 (thanks [kjmj](https://github.com/kjmj))
75 |
76 | ## [0.2.3] - 8 October 2020
77 |
78 | * Added `disableTopAndBottomBorders` parameter to `DragAndDropListExpansion` and `ProgrammaticExpansionTile` to allow for more styling options.
79 |
80 | ## [0.2.2] - 7 October 2020
81 |
82 | * Added function parameters for customizing the criteria for where an item or list can be dropped. See the parameters `listOnWillAccept`, `listTargetOnWillAccept`, `itemOnWillAccept` and `itemTargetOnWillAccept` in `DragAndDropLists`
83 | * Added function parameters for directly accessing items or lists that have been dropped. See the parameters `listOnAccept`, `listTargetOnAccept`, `itemOnAccept` and `itemTargetOnAccept` in `DragAndDropLists`
84 |
85 | ## [0.2.1] - 6 October 2020
86 |
87 | * Fixed bug where auto scrolling could occur even when not dragging an item (thanks [@ElenaKova](https://github.com/ElenaKova))
88 |
89 | ## [0.2.0] - 5 October 2020
90 |
91 | * Added option for drag handles. See `dragHandle` and `dragHandleOnLeft` parameters in `DragAndDropLists`
92 | * Added new example for drag handles
93 | * Added option for item dividers. See the `itemDivider` parameter in `DragAndDropLists`
94 | * Added option for inner list box decoration. See the `listInnerDecoration` parameter in `DragAndDropLists`
95 | * Added option for decoration while dragging lists and items. See the `itemDecorationWhileDragging` and `itemDecorationWhileDragging` parameters in `DragAndDropLists`
96 | * Removed unused `itemDecoration` parameter in `DragAndDropLists`
97 | * Fixed unused `itemDraggingWidth` parameter in `DragAndDropLists`
98 | * Configurable bottom padding for list and items. See the `lastItemTargetHeight`, `addLastItemTargetHeightToTop` and `lastListTargetSize` parameters in `DragAndDropLists`
99 | * Remove `pubspec.lock` (thanks [@freitzzz](https://github.com/freitzzz))
100 |
101 | ## [0.1.0] - 21 September 2020
102 |
103 | * Added canDrag option for lists
104 | * **Interface Change:** Any classes implementing `DragAndDropListInterface` need to add `canDrag`
105 |
106 | ## [0.0.6] - 24 July 2020
107 |
108 | * Fixed wrong parameter order for onItemAdd (thanks [@khauns](https://github.com/khauns))
109 | * ProgrammaticExpansionTile: include option for isThreeLine of ListTile
110 | * ProgrammaticExpansionTile: Remove required annotation for leading
111 |
112 | ## [0.0.5] - 24 July 2020
113 |
114 | * ProgrammaticExpansionTile: ensure that parent widget will always know its expanded/collapsed state
115 |
116 | ## [0.0.4] - 21 July 2020
117 |
118 | * Updated example project for web compatibility
119 |
120 | ## [0.0.3] - 21 July 2020
121 |
122 | * Formatted all with dartfmt
123 |
124 | ## [0.0.2] - 21 July 2020
125 |
126 | * Added API documentation
127 |
128 | ## [0.0.1] - 21 July 2020
129 |
130 | * Initial release
131 |
--------------------------------------------------------------------------------
/lib/drag_and_drop_list.dart:
--------------------------------------------------------------------------------
1 | import 'package:drag_and_drop_lists/drag_and_drop_builder_parameters.dart';
2 | import 'package:drag_and_drop_lists/drag_and_drop_item.dart';
3 | import 'package:drag_and_drop_lists/drag_and_drop_item_target.dart';
4 | import 'package:drag_and_drop_lists/drag_and_drop_item_wrapper.dart';
5 | import 'package:drag_and_drop_lists/drag_and_drop_list_interface.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:flutter/widgets.dart';
8 |
9 | class DragAndDropList implements DragAndDropListInterface {
10 | /// The widget that is displayed at the top of the list.
11 | final Widget? header;
12 |
13 | /// The widget that is displayed at the bottom of the list.
14 | final Widget? footer;
15 |
16 | /// The widget that is displayed to the left of the list.
17 | final Widget? leftSide;
18 |
19 | /// The widget that is displayed to the right of the list.
20 | final Widget? rightSide;
21 |
22 | /// The widget to be displayed when a list is empty.
23 | /// If this is not null, it will override that set in [DragAndDropLists.contentsWhenEmpty].
24 | final Widget? contentsWhenEmpty;
25 |
26 | /// The widget to be displayed as the last element in the list that will accept
27 | /// a dragged item.
28 | final Widget? lastTarget;
29 |
30 | /// The decoration displayed around a list.
31 | /// If this is not null, it will override that set in [DragAndDropLists.listDecoration].
32 | final Decoration? decoration;
33 |
34 | /// The vertical alignment of the contents in this list.
35 | /// If this is not null, it will override that set in [DragAndDropLists.verticalAlignment].
36 | final CrossAxisAlignment verticalAlignment;
37 |
38 | /// The horizontal alignment of the contents in this list.
39 | /// If this is not null, it will override that set in [DragAndDropLists.horizontalAlignment].
40 | final MainAxisAlignment horizontalAlignment;
41 |
42 | /// The child elements that will be contained in this list.
43 | /// It is possible to not provide any children when an empty list is desired.
44 | final List children;
45 |
46 | /// Whether or not this item can be dragged.
47 | /// Set to true if it can be reordered.
48 | /// Set to false if it must remain fixed.
49 | final bool canDrag;
50 |
51 | DragAndDropList({
52 | required this.children,
53 | this.header,
54 | this.footer,
55 | this.leftSide,
56 | this.rightSide,
57 | this.contentsWhenEmpty,
58 | this.lastTarget,
59 | this.decoration,
60 | this.horizontalAlignment = MainAxisAlignment.start,
61 | this.verticalAlignment = CrossAxisAlignment.start,
62 | this.canDrag = true,
63 | });
64 |
65 | @override
66 | Widget generateWidget(DragAndDropBuilderParameters params) {
67 | var contents = [];
68 | if (header != null) {
69 | contents.add(Flexible(child: header!));
70 | }
71 | Widget intrinsicHeight = IntrinsicHeight(
72 | child: Row(
73 | mainAxisAlignment: horizontalAlignment,
74 | mainAxisSize: MainAxisSize.max,
75 | crossAxisAlignment: CrossAxisAlignment.stretch,
76 | children: _generateDragAndDropListInnerContents(params),
77 | ),
78 | );
79 | if (params.axis == Axis.horizontal) {
80 | intrinsicHeight = Container(
81 | width: params.listWidth,
82 | child: intrinsicHeight,
83 | );
84 | }
85 | if (params.listInnerDecoration != null) {
86 | intrinsicHeight = Container(
87 | decoration: params.listInnerDecoration,
88 | child: intrinsicHeight,
89 | );
90 | }
91 | contents.add(intrinsicHeight);
92 |
93 | if (footer != null) {
94 | contents.add(Flexible(child: footer!));
95 | }
96 |
97 | return Container(
98 | width: params.axis == Axis.vertical
99 | ? double.infinity
100 | : params.listWidth - params.listPadding!.horizontal,
101 | decoration: decoration ?? params.listDecoration,
102 | child: Column(
103 | mainAxisSize: MainAxisSize.min,
104 | crossAxisAlignment: verticalAlignment,
105 | children: contents,
106 | ),
107 | );
108 | }
109 |
110 | List _generateDragAndDropListInnerContents(
111 | DragAndDropBuilderParameters parameters) {
112 | var contents = [];
113 | if (leftSide != null) {
114 | contents.add(leftSide!);
115 | }
116 | if (children.isNotEmpty) {
117 | List allChildren = [];
118 | if (parameters.addLastItemTargetHeightToTop) {
119 | allChildren.add(Padding(
120 | padding: EdgeInsets.only(top: parameters.lastItemTargetHeight),
121 | ));
122 | }
123 | for (int i = 0; i < children.length; i++) {
124 | allChildren.add(DragAndDropItemWrapper(
125 | child: children[i],
126 | parameters: parameters,
127 | ));
128 | if (parameters.itemDivider != null && i < children.length - 1) {
129 | allChildren.add(parameters.itemDivider!);
130 | }
131 | }
132 | allChildren.add(DragAndDropItemTarget(
133 | parent: this,
134 | parameters: parameters,
135 | onReorderOrAdd: parameters.onItemDropOnLastTarget!,
136 | child: lastTarget ??
137 | Container(
138 | height: parameters.lastItemTargetHeight,
139 | ),
140 | ));
141 | contents.add(
142 | Expanded(
143 | child: SingleChildScrollView(
144 | physics: NeverScrollableScrollPhysics(),
145 | child: Column(
146 | crossAxisAlignment: verticalAlignment,
147 | mainAxisSize: MainAxisSize.max,
148 | children: allChildren,
149 | ),
150 | ),
151 | ),
152 | );
153 | } else {
154 | contents.add(
155 | Expanded(
156 | child: SingleChildScrollView(
157 | physics: NeverScrollableScrollPhysics(),
158 | child: Column(
159 | mainAxisSize: MainAxisSize.max,
160 | children: [
161 | contentsWhenEmpty ??
162 | Text(
163 | 'Empty list',
164 | style: TextStyle(
165 | fontStyle: FontStyle.italic,
166 | ),
167 | ),
168 | DragAndDropItemTarget(
169 | parent: this,
170 | parameters: parameters,
171 | onReorderOrAdd: parameters.onItemDropOnLastTarget!,
172 | child: lastTarget ??
173 | Container(
174 | height: parameters.lastItemTargetHeight,
175 | ),
176 | ),
177 | ],
178 | ),
179 | ),
180 | ),
181 | );
182 | }
183 | if (rightSide != null) {
184 | contents.add(rightSide!);
185 | }
186 | return contents;
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/lib/drag_and_drop_list_expansion.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:drag_and_drop_lists/drag_and_drop_builder_parameters.dart';
4 | import 'package:drag_and_drop_lists/drag_and_drop_item.dart';
5 | import 'package:drag_and_drop_lists/drag_and_drop_item_target.dart';
6 | import 'package:drag_and_drop_lists/drag_and_drop_item_wrapper.dart';
7 | import 'package:drag_and_drop_lists/drag_and_drop_list_interface.dart';
8 | import 'package:drag_and_drop_lists/programmatic_expansion_tile.dart';
9 | import 'package:flutter/material.dart';
10 |
11 | typedef void OnExpansionChanged(bool expanded);
12 |
13 | /// This class mirrors flutter's [ExpansionTile], with similar options.
14 | class DragAndDropListExpansion implements DragAndDropListExpansionInterface {
15 | final Widget? title;
16 | final Widget? subtitle;
17 | final Widget? trailing;
18 | final Widget? leading;
19 | final bool initiallyExpanded;
20 |
21 | /// Set this to a unique key that will remain unchanged over the lifetime of the list.
22 | /// Used to maintain the expanded/collapsed states
23 | final Key listKey;
24 |
25 | /// This function will be called when the expansion of a tile is changed.
26 | final OnExpansionChanged? onExpansionChanged;
27 | final Color? backgroundColor;
28 | final List? children;
29 | final Widget? contentsWhenEmpty;
30 | final Widget? lastTarget;
31 |
32 | /// Whether or not this item can be dragged.
33 | /// Set to true if it can be reordered.
34 | /// Set to false if it must remain fixed.
35 | final bool canDrag;
36 |
37 | /// Disable to borders displayed at the top and bottom when expanded
38 | final bool disableTopAndBottomBorders;
39 |
40 | ValueNotifier _expanded = ValueNotifier(true);
41 | GlobalKey _expansionKey =
42 | GlobalKey();
43 |
44 | DragAndDropListExpansion({
45 | this.children,
46 | this.title,
47 | this.subtitle,
48 | this.trailing,
49 | this.leading,
50 | this.initiallyExpanded = false,
51 | this.backgroundColor,
52 | this.onExpansionChanged,
53 | this.contentsWhenEmpty,
54 | this.lastTarget,
55 | required this.listKey,
56 | this.canDrag = true,
57 | this.disableTopAndBottomBorders = false,
58 | }) {
59 | _expanded.value = initiallyExpanded;
60 | }
61 |
62 | @override
63 | Widget generateWidget(DragAndDropBuilderParameters params) {
64 | var contents = _generateDragAndDropListInnerContents(params);
65 |
66 | Widget expandable = ProgrammaticExpansionTile(
67 | title: title,
68 | listKey: listKey,
69 | subtitle: subtitle,
70 | trailing: trailing,
71 | leading: leading,
72 | disableTopAndBottomBorders: disableTopAndBottomBorders,
73 | backgroundColor: backgroundColor,
74 | initiallyExpanded: initiallyExpanded,
75 | onExpansionChanged: _onSetExpansion,
76 | key: _expansionKey,
77 | children: contents,
78 | );
79 |
80 | if (params.listDecoration != null) {
81 | expandable = Container(
82 | decoration: params.listDecoration,
83 | child: expandable,
84 | );
85 | }
86 |
87 | if (params.listPadding != null) {
88 | expandable = Padding(
89 | padding: params.listPadding!,
90 | child: expandable,
91 | );
92 | }
93 |
94 | Widget toReturn = ValueListenableBuilder(
95 | valueListenable: _expanded,
96 | child: expandable,
97 | builder: (context, dynamic error, child) {
98 | if (!_expanded.value) {
99 | return Stack(children: [
100 | child!,
101 | Positioned.fill(
102 | child: DragTarget(
103 | builder: (context, candidateData, rejectedData) {
104 | if (candidateData.isNotEmpty) {}
105 | return Container();
106 | },
107 | onWillAccept: (incoming) {
108 | _startExpansionTimer();
109 | return false;
110 | },
111 | onLeave: (incoming) {
112 | _stopExpansionTimer();
113 | },
114 | onAccept: (incoming) {},
115 | ),
116 | )
117 | ]);
118 | } else {
119 | return child!;
120 | }
121 | },
122 | );
123 |
124 | return toReturn;
125 | }
126 |
127 | List _generateDragAndDropListInnerContents(
128 | DragAndDropBuilderParameters parameters) {
129 | var contents = [];
130 | if (children != null && children!.isNotEmpty) {
131 | for (int i = 0; i < children!.length; i++) {
132 | contents.add(DragAndDropItemWrapper(
133 | child: children![i],
134 | parameters: parameters,
135 | ));
136 | if (parameters.itemDivider != null && i < children!.length - 1) {
137 | contents.add(parameters.itemDivider!);
138 | }
139 | }
140 | contents.add(DragAndDropItemTarget(
141 | parent: this,
142 | parameters: parameters,
143 | onReorderOrAdd: parameters.onItemDropOnLastTarget!,
144 | child: lastTarget ??
145 | Container(
146 | height: parameters.lastItemTargetHeight,
147 | ),
148 | ));
149 | } else {
150 | contents.add(
151 | contentsWhenEmpty ??
152 | Text(
153 | 'Empty list',
154 | style: TextStyle(
155 | fontStyle: FontStyle.italic,
156 | ),
157 | ),
158 | );
159 | contents.add(
160 | DragAndDropItemTarget(
161 | parent: this,
162 | parameters: parameters,
163 | onReorderOrAdd: parameters.onItemDropOnLastTarget!,
164 | child: lastTarget ??
165 | Container(
166 | height: parameters.lastItemTargetHeight,
167 | ),
168 | ),
169 | );
170 | }
171 | return contents;
172 | }
173 |
174 | @override
175 | toggleExpanded() {
176 | if (isExpanded)
177 | collapse();
178 | else
179 | expand();
180 | }
181 |
182 | @override
183 | collapse() {
184 | if (!isExpanded) {
185 | _expanded.value = false;
186 | _expansionKey.currentState!.collapse();
187 | }
188 | }
189 |
190 | @override
191 | expand() {
192 | if (!isExpanded) {
193 | _expanded.value = true;
194 | _expansionKey.currentState!.expand();
195 | }
196 | }
197 |
198 | _onSetExpansion(bool expanded) {
199 | _expanded.value = expanded;
200 |
201 | if (onExpansionChanged != null) onExpansionChanged!(expanded);
202 | }
203 |
204 | @override
205 | get isExpanded => _expanded.value;
206 |
207 | late Timer _expansionTimer;
208 |
209 | _startExpansionTimer() async {
210 | _expansionTimer = Timer(Duration(milliseconds: 400), _expansionCallback);
211 | }
212 |
213 | _stopExpansionTimer() async {
214 | if (_expansionTimer.isActive) {
215 | _expansionTimer.cancel();
216 | }
217 | }
218 |
219 | _expansionCallback() {
220 | expand();
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/example/windows/runner/win32_window.cpp:
--------------------------------------------------------------------------------
1 | #include "win32_window.h"
2 |
3 | #include
4 |
5 | #include "resource.h"
6 |
7 | namespace {
8 |
9 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
10 |
11 | // The number of Win32Window objects that currently exist.
12 | static int g_active_window_count = 0;
13 |
14 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
15 |
16 | // Scale helper to convert logical scaler values to physical using passed in
17 | // scale factor
18 | int Scale(int source, double scale_factor) {
19 | return static_cast(source * scale_factor);
20 | }
21 |
22 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
23 | // This API is only needed for PerMonitor V1 awareness mode.
24 | void EnableFullDpiSupportIfAvailable(HWND hwnd) {
25 | HMODULE user32_module = LoadLibraryA("User32.dll");
26 | if (!user32_module) {
27 | return;
28 | }
29 | auto enable_non_client_dpi_scaling =
30 | reinterpret_cast(
31 | GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
32 | if (enable_non_client_dpi_scaling != nullptr) {
33 | enable_non_client_dpi_scaling(hwnd);
34 | FreeLibrary(user32_module);
35 | }
36 | }
37 |
38 | } // namespace
39 |
40 | // Manages the Win32Window's window class registration.
41 | class WindowClassRegistrar {
42 | public:
43 | ~WindowClassRegistrar() = default;
44 |
45 | // Returns the singleton registar instance.
46 | static WindowClassRegistrar* GetInstance() {
47 | if (!instance_) {
48 | instance_ = new WindowClassRegistrar();
49 | }
50 | return instance_;
51 | }
52 |
53 | // Returns the name of the window class, registering the class if it hasn't
54 | // previously been registered.
55 | const wchar_t* GetWindowClass();
56 |
57 | // Unregisters the window class. Should only be called if there are no
58 | // instances of the window.
59 | void UnregisterWindowClass();
60 |
61 | private:
62 | WindowClassRegistrar() = default;
63 |
64 | static WindowClassRegistrar* instance_;
65 |
66 | bool class_registered_ = false;
67 | };
68 |
69 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
70 |
71 | const wchar_t* WindowClassRegistrar::GetWindowClass() {
72 | if (!class_registered_) {
73 | WNDCLASS window_class{};
74 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
75 | window_class.lpszClassName = kWindowClassName;
76 | window_class.style = CS_HREDRAW | CS_VREDRAW;
77 | window_class.cbClsExtra = 0;
78 | window_class.cbWndExtra = 0;
79 | window_class.hInstance = GetModuleHandle(nullptr);
80 | window_class.hIcon =
81 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
82 | window_class.hbrBackground = 0;
83 | window_class.lpszMenuName = nullptr;
84 | window_class.lpfnWndProc = Win32Window::WndProc;
85 | RegisterClass(&window_class);
86 | class_registered_ = true;
87 | }
88 | return kWindowClassName;
89 | }
90 |
91 | void WindowClassRegistrar::UnregisterWindowClass() {
92 | UnregisterClass(kWindowClassName, nullptr);
93 | class_registered_ = false;
94 | }
95 |
96 | Win32Window::Win32Window() {
97 | ++g_active_window_count;
98 | }
99 |
100 | Win32Window::~Win32Window() {
101 | --g_active_window_count;
102 | Destroy();
103 | }
104 |
105 | bool Win32Window::CreateAndShow(const std::wstring& title,
106 | const Point& origin,
107 | const Size& size) {
108 | Destroy();
109 |
110 | const wchar_t* window_class =
111 | WindowClassRegistrar::GetInstance()->GetWindowClass();
112 |
113 | const POINT target_point = {static_cast(origin.x),
114 | static_cast(origin.y)};
115 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
116 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
117 | double scale_factor = dpi / 96.0;
118 |
119 | HWND window = CreateWindow(
120 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
121 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
122 | Scale(size.width, scale_factor), Scale(size.height, scale_factor),
123 | nullptr, nullptr, GetModuleHandle(nullptr), this);
124 |
125 | if (!window) {
126 | return false;
127 | }
128 |
129 | return OnCreate();
130 | }
131 |
132 | // static
133 | LRESULT CALLBACK Win32Window::WndProc(HWND const window,
134 | UINT const message,
135 | WPARAM const wparam,
136 | LPARAM const lparam) noexcept {
137 | if (message == WM_NCCREATE) {
138 | auto window_struct = reinterpret_cast(lparam);
139 | SetWindowLongPtr(window, GWLP_USERDATA,
140 | reinterpret_cast(window_struct->lpCreateParams));
141 |
142 | auto that = static_cast(window_struct->lpCreateParams);
143 | EnableFullDpiSupportIfAvailable(window);
144 | that->window_handle_ = window;
145 | } else if (Win32Window* that = GetThisFromHandle(window)) {
146 | return that->MessageHandler(window, message, wparam, lparam);
147 | }
148 |
149 | return DefWindowProc(window, message, wparam, lparam);
150 | }
151 |
152 | LRESULT
153 | Win32Window::MessageHandler(HWND hwnd,
154 | UINT const message,
155 | WPARAM const wparam,
156 | LPARAM const lparam) noexcept {
157 | switch (message) {
158 | case WM_DESTROY:
159 | window_handle_ = nullptr;
160 | Destroy();
161 | if (quit_on_close_) {
162 | PostQuitMessage(0);
163 | }
164 | return 0;
165 |
166 | case WM_DPICHANGED: {
167 | auto newRectSize = reinterpret_cast(lparam);
168 | LONG newWidth = newRectSize->right - newRectSize->left;
169 | LONG newHeight = newRectSize->bottom - newRectSize->top;
170 |
171 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
172 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
173 |
174 | return 0;
175 | }
176 | case WM_SIZE: {
177 | RECT rect = GetClientArea();
178 | if (child_content_ != nullptr) {
179 | // Size and position the child window.
180 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
181 | rect.bottom - rect.top, TRUE);
182 | }
183 | return 0;
184 | }
185 |
186 | case WM_ACTIVATE:
187 | if (child_content_ != nullptr) {
188 | SetFocus(child_content_);
189 | }
190 | return 0;
191 | }
192 |
193 | return DefWindowProc(window_handle_, message, wparam, lparam);
194 | }
195 |
196 | void Win32Window::Destroy() {
197 | OnDestroy();
198 |
199 | if (window_handle_) {
200 | DestroyWindow(window_handle_);
201 | window_handle_ = nullptr;
202 | }
203 | if (g_active_window_count == 0) {
204 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
205 | }
206 | }
207 |
208 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
209 | return reinterpret_cast(
210 | GetWindowLongPtr(window, GWLP_USERDATA));
211 | }
212 |
213 | void Win32Window::SetChildContent(HWND content) {
214 | child_content_ = content;
215 | SetParent(content, window_handle_);
216 | RECT frame = GetClientArea();
217 |
218 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
219 | frame.bottom - frame.top, true);
220 |
221 | SetFocus(child_content_);
222 | }
223 |
224 | RECT Win32Window::GetClientArea() {
225 | RECT frame;
226 | GetClientRect(window_handle_, &frame);
227 | return frame;
228 | }
229 |
230 | HWND Win32Window::GetHandle() {
231 | return window_handle_;
232 | }
233 |
234 | void Win32Window::SetQuitOnClose(bool quit_on_close) {
235 | quit_on_close_ = quit_on_close;
236 | }
237 |
238 | bool Win32Window::OnCreate() {
239 | // No-op; provided for subclasses.
240 | return true;
241 | }
242 |
243 | void Win32Window::OnDestroy() {
244 | // No-op; provided for subclasses.
245 | }
246 |
--------------------------------------------------------------------------------
/lib/programmatic_expansion_tile.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Flutter Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter/scheduler.dart';
7 |
8 | const Duration _kExpand = Duration(milliseconds: 200);
9 |
10 | /// A single-line [ListTile] with a trailing button that expands or collapses
11 | /// the tile to reveal or hide the [children].
12 | ///
13 | /// This widget is typically used with [ListView] to create an
14 | /// "expand / collapse" list entry. When used with scrolling widgets like
15 | /// [ListView], a unique [PageStorageKey] must be specified to enable the
16 | /// [ProgrammaticExpansionTile] to save and restore its expanded state when it is scrolled
17 | /// in and out of view.
18 | ///
19 | /// See also:
20 | ///
21 | /// * [ListTile], useful for creating expansion tile [children] when the
22 | /// expansion tile represents a sublist.
23 | /// * The "Expand/collapse" section of
24 | /// .
25 | class ProgrammaticExpansionTile extends StatefulWidget {
26 | /// Creates a single-line [ListTile] with a trailing button that expands or collapses
27 | /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
28 | /// be non-null.
29 | const ProgrammaticExpansionTile({
30 | required Key key,
31 | required this.listKey,
32 | this.leading,
33 | required this.title,
34 | this.subtitle,
35 | this.isThreeLine = false,
36 | this.backgroundColor,
37 | this.onExpansionChanged,
38 | this.children = const [],
39 | this.trailing,
40 | this.initiallyExpanded = false,
41 | this.disableTopAndBottomBorders = false,
42 | }) : super(key: key);
43 |
44 | final Key listKey;
45 |
46 | /// A widget to display before the title.
47 | ///
48 | /// Typically a [CircleAvatar] widget.
49 | final Widget? leading;
50 |
51 | /// The primary content of the list item.
52 | ///
53 | /// Typically a [Text] widget.
54 | final Widget? title;
55 |
56 | /// Additional content displayed below the title.
57 | ///
58 | /// Typically a [Text] widget.
59 | final Widget? subtitle;
60 |
61 | /// Additional content displayed below the title.
62 | ///
63 | /// Typically a [Text] widget.
64 | final bool isThreeLine;
65 |
66 | /// Called when the tile expands or collapses.
67 | ///
68 | /// When the tile starts expanding, this function is called with the value
69 | /// true. When the tile starts collapsing, this function is called with
70 | /// the value false.
71 | final ValueChanged? onExpansionChanged;
72 |
73 | /// The widgets that are displayed when the tile expands.
74 | ///
75 | /// Typically [ListTile] widgets.
76 | final List children;
77 |
78 | /// The color to display behind the sublist when expanded.
79 | final Color? backgroundColor;
80 |
81 | /// A widget to display instead of a rotating arrow icon.
82 | final Widget? trailing;
83 |
84 | /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
85 | final bool initiallyExpanded;
86 |
87 | /// Disable to borders displayed at the top and bottom when expanded
88 | final bool disableTopAndBottomBorders;
89 |
90 | @override
91 | ProgrammaticExpansionTileState createState() =>
92 | ProgrammaticExpansionTileState();
93 | }
94 |
95 | class ProgrammaticExpansionTileState extends State
96 | with SingleTickerProviderStateMixin {
97 | static final Animatable _easeOutTween =
98 | CurveTween(curve: Curves.easeOut);
99 | static final Animatable _easeInTween =
100 | CurveTween(curve: Curves.easeIn);
101 | static final Animatable _halfTween =
102 | Tween(begin: 0.0, end: 0.5);
103 |
104 | final ColorTween _borderColorTween = ColorTween();
105 | final ColorTween _headerColorTween = ColorTween();
106 | final ColorTween _iconColorTween = ColorTween();
107 | final ColorTween _backgroundColorTween = ColorTween();
108 |
109 | late AnimationController _controller;
110 | late Animation _iconTurns;
111 | late Animation _heightFactor;
112 | late Animation _borderColor;
113 | late Animation _headerColor;
114 | late Animation _iconColor;
115 | late Animation _backgroundColor;
116 |
117 | bool _isExpanded = false;
118 |
119 | @override
120 | void initState() {
121 | super.initState();
122 | _controller = AnimationController(duration: _kExpand, vsync: this);
123 | _heightFactor = _controller.drive(_easeInTween);
124 | _iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
125 | _borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
126 | _headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
127 | _iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
128 | _backgroundColor =
129 | _controller.drive(_backgroundColorTween.chain(_easeOutTween));
130 |
131 | _isExpanded = PageStorage.of(context)
132 | ?.readState(context, identifier: widget.listKey) as bool? ??
133 | widget.initiallyExpanded;
134 | if (_isExpanded) _controller.value = 1.0;
135 |
136 | // Schedule the notification that widget has changed for after init
137 | // to ensure that the parent widget maintains the correct state
138 | SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
139 | if (widget.onExpansionChanged != null &&
140 | _isExpanded != widget.initiallyExpanded) {
141 | widget.onExpansionChanged!(_isExpanded);
142 | }
143 | });
144 | }
145 |
146 | @override
147 | void dispose() {
148 | _controller.dispose();
149 | super.dispose();
150 | }
151 |
152 | void expand() {
153 | _setExpanded(true);
154 | }
155 |
156 | void collapse() {
157 | _setExpanded(false);
158 | }
159 |
160 | void toggle() {
161 | _setExpanded(!_isExpanded);
162 | }
163 |
164 | void _setExpanded(bool expanded) {
165 | if (_isExpanded != expanded) {
166 | setState(() {
167 | _isExpanded = expanded;
168 | if (_isExpanded) {
169 | _controller.forward();
170 | } else {
171 | _controller.reverse().then((void value) {
172 | if (!mounted) return;
173 | setState(() {
174 | // Rebuild without widget.children.
175 | });
176 | });
177 | }
178 | PageStorage.of(context)
179 | ?.writeState(context, _isExpanded, identifier: widget.listKey);
180 | });
181 | if (widget.onExpansionChanged != null) {
182 | widget.onExpansionChanged!(_isExpanded);
183 | }
184 | }
185 | }
186 |
187 | Widget _buildChildren(BuildContext context, Widget? child) {
188 | final Color borderSideColor = _borderColor.value ?? Colors.transparent;
189 | bool setBorder = !widget.disableTopAndBottomBorders;
190 |
191 | return Container(
192 | decoration: BoxDecoration(
193 | color: _backgroundColor.value ?? Colors.transparent,
194 | border: setBorder
195 | ? Border(
196 | top: BorderSide(color: borderSideColor),
197 | bottom: BorderSide(color: borderSideColor),
198 | )
199 | : null,
200 | ),
201 | child: Column(
202 | mainAxisSize: MainAxisSize.min,
203 | children: [
204 | ListTileTheme.merge(
205 | iconColor: _iconColor.value,
206 | textColor: _headerColor.value,
207 | child: ListTile(
208 | onTap: toggle,
209 | leading: widget.leading,
210 | title: widget.title,
211 | subtitle: widget.subtitle,
212 | isThreeLine: widget.isThreeLine,
213 | trailing: widget.trailing ??
214 | RotationTransition(
215 | turns: _iconTurns,
216 | child: const Icon(Icons.expand_more),
217 | ),
218 | ),
219 | ),
220 | ClipRect(
221 | child: Align(
222 | heightFactor: _heightFactor.value,
223 | child: child,
224 | ),
225 | ),
226 | ],
227 | ),
228 | );
229 | }
230 |
231 | @override
232 | void didChangeDependencies() {
233 | final ThemeData theme = Theme.of(context);
234 | _borderColorTween.end = theme.dividerColor;
235 | _headerColorTween
236 | ..begin = theme.textTheme.titleSmall!.color
237 | ..end = theme.colorScheme.secondary;
238 | _iconColorTween
239 | ..begin = theme.unselectedWidgetColor
240 | ..end = theme.colorScheme.secondary;
241 | _backgroundColorTween.end = widget.backgroundColor;
242 | super.didChangeDependencies();
243 | }
244 |
245 | @override
246 | Widget build(BuildContext context) {
247 | final bool closed = !_isExpanded && _controller.isDismissed;
248 | return AnimatedBuilder(
249 | animation: _controller.view,
250 | builder: _buildChildren,
251 | child: closed ? null : Column(children: widget.children as List),
252 | );
253 | }
254 | }
255 |
--------------------------------------------------------------------------------