Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
f844fada
Unverified
Commit
f844fada
authored
Jul 30, 2018
by
David Shuckerow
Committed by
GitHub
Jul 30, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Reorderable list widget and Material demo (#18374)
parent
711ecf7f
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
1128 additions
and
0 deletions
+1128
-0
material.dart
examples/flutter_gallery/lib/demo/material/material.dart
+1
-0
reorderable_list_demo.dart
...tter_gallery/lib/demo/material/reorderable_list_demo.dart
+192
-0
demos.dart
examples/flutter_gallery/lib/gallery/demos.dart
+8
-0
material.dart
packages/flutter/lib/material.dart
+1
-0
reorderable_list.dart
packages/flutter/lib/src/material/reorderable_list.dart
+489
-0
reorderable_list_test.dart
packages/flutter/test/material/reorderable_list_test.dart
+437
-0
No files found.
examples/flutter_gallery/lib/demo/material/material.dart
View file @
f844fada
...
@@ -23,6 +23,7 @@ export 'overscroll_demo.dart';
...
@@ -23,6 +23,7 @@ export 'overscroll_demo.dart';
export
'page_selector_demo.dart'
;
export
'page_selector_demo.dart'
;
export
'persistent_bottom_sheet_demo.dart'
;
export
'persistent_bottom_sheet_demo.dart'
;
export
'progress_indicator_demo.dart'
;
export
'progress_indicator_demo.dart'
;
export
'reorderable_list_demo.dart'
;
export
'scrollable_tabs_demo.dart'
;
export
'scrollable_tabs_demo.dart'
;
export
'search_demo.dart'
;
export
'search_demo.dart'
;
export
'selection_controls_demo.dart'
;
export
'selection_controls_demo.dart'
;
...
...
examples/flutter_gallery/lib/demo/material/reorderable_list_demo.dart
0 → 100644
View file @
f844fada
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
enum
_ReorderableListType
{
/// A list tile that contains a [CircleAvatar].
horizontalAvatar
,
/// A list tile that contains a [CircleAvatar].
verticalAvatar
,
/// A list tile that contains three lines of text and a checkbox.
threeLine
,
}
class
ReorderableListDemo
extends
StatefulWidget
{
const
ReorderableListDemo
({
Key
key
})
:
super
(
key:
key
);
static
const
String
routeName
=
'/material/reorderable-list'
;
@override
_ListDemoState
createState
()
=>
new
_ListDemoState
();
}
class
_ListItem
{
_ListItem
(
this
.
value
,
this
.
checkState
);
final
String
value
;
bool
checkState
;
}
class
_ListDemoState
extends
State
<
ReorderableListDemo
>
{
static
final
GlobalKey
<
ScaffoldState
>
scaffoldKey
=
new
GlobalKey
<
ScaffoldState
>();
PersistentBottomSheetController
<
Null
>
_bottomSheet
;
_ReorderableListType
_itemType
=
_ReorderableListType
.
threeLine
;
bool
_reverseSort
=
false
;
final
List
<
_ListItem
>
_items
=
<
String
>[
'A'
,
'B'
,
'C'
,
'D'
,
'E'
,
'F'
,
'G'
,
'H'
,
'I'
,
'J'
,
'K'
,
'L'
,
'M'
,
'N'
,
].
map
((
String
item
)
=>
new
_ListItem
(
item
,
false
)).
toList
();
void
changeItemType
(
_ReorderableListType
type
)
{
setState
(()
{
_itemType
=
type
;
});
// Rebuild the bottom sheet to reflect the selected list view.
_bottomSheet
?.
setState
(()
{
});
// Close the bottom sheet to give the user a clear view of the list.
_bottomSheet
?.
close
();
}
void
_showConfigurationSheet
()
{
setState
(()
{
_bottomSheet
=
scaffoldKey
.
currentState
.
showBottomSheet
((
BuildContext
bottomSheetContext
)
{
return
new
DecoratedBox
(
decoration:
const
BoxDecoration
(
border:
const
Border
(
top:
const
BorderSide
(
color:
Colors
.
black26
)),
),
child:
new
ListView
(
shrinkWrap:
true
,
primary:
false
,
children:
<
Widget
>[
new
RadioListTile
<
_ReorderableListType
>(
dense:
true
,
title:
const
Text
(
'Horizontal Avatars'
),
value:
_ReorderableListType
.
horizontalAvatar
,
groupValue:
_itemType
,
onChanged:
changeItemType
,
),
new
RadioListTile
<
_ReorderableListType
>(
dense:
true
,
title:
const
Text
(
'Vertical Avatars'
),
value:
_ReorderableListType
.
verticalAvatar
,
groupValue:
_itemType
,
onChanged:
changeItemType
,
),
new
RadioListTile
<
_ReorderableListType
>(
dense:
true
,
title:
const
Text
(
'Three-line'
),
value:
_ReorderableListType
.
threeLine
,
groupValue:
_itemType
,
onChanged:
changeItemType
,
),
],
),
);
});
// Garbage collect the bottom sheet when it closes.
_bottomSheet
.
closed
.
whenComplete
(()
{
if
(
mounted
)
{
setState
(()
{
_bottomSheet
=
null
;
});
}
});
});
}
Widget
buildListTile
(
_ListItem
item
)
{
const
Widget
secondary
=
const
Text
(
'Even more additional list item information appears on line three.'
,
);
Widget
listTile
;
switch
(
_itemType
)
{
case
_ReorderableListType
.
threeLine
:
listTile
=
new
CheckboxListTile
(
key:
new
Key
(
item
.
value
),
isThreeLine:
true
,
value:
item
.
checkState
??
false
,
onChanged:
(
bool
newValue
)
{
setState
(()
{
item
.
checkState
=
newValue
;
});
},
title:
new
Text
(
'This item represents
${item.value}
.'
),
subtitle:
secondary
,
secondary:
const
Icon
(
Icons
.
drag_handle
),
);
break
;
case
_ReorderableListType
.
horizontalAvatar
:
case
_ReorderableListType
.
verticalAvatar
:
listTile
=
new
Container
(
key:
new
Key
(
item
.
value
),
height:
100.0
,
width:
100.0
,
child:
new
CircleAvatar
(
child:
new
Text
(
item
.
value
),
backgroundColor:
Colors
.
green
,
),
);
break
;
}
return
listTile
;
}
void
_onReorder
(
int
oldIndex
,
int
newIndex
)
{
setState
(()
{
if
(
newIndex
>
oldIndex
)
{
newIndex
-=
1
;
}
final
_ListItem
item
=
_items
.
removeAt
(
oldIndex
);
_items
.
insert
(
newIndex
,
item
);
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
new
Scaffold
(
key:
scaffoldKey
,
appBar:
new
AppBar
(
title:
const
Text
(
'Reorderable list'
),
actions:
<
Widget
>[
new
IconButton
(
icon:
const
Icon
(
Icons
.
sort_by_alpha
),
tooltip:
'Sort'
,
onPressed:
()
{
setState
(()
{
_reverseSort
=
!
_reverseSort
;
_items
.
sort
((
_ListItem
a
,
_ListItem
b
)
=>
_reverseSort
?
b
.
value
.
compareTo
(
a
.
value
)
:
a
.
value
.
compareTo
(
b
.
value
));
});
},
),
new
IconButton
(
icon:
const
Icon
(
Icons
.
more_vert
),
tooltip:
'Show menu'
,
onPressed:
_bottomSheet
==
null
?
_showConfigurationSheet
:
null
,
),
],
),
body:
new
Scrollbar
(
child:
new
ReorderableListView
(
header:
_itemType
!=
_ReorderableListType
.
threeLine
?
new
Padding
(
padding:
const
EdgeInsets
.
all
(
8.0
),
child:
new
Text
(
'Header of the list'
,
style:
Theme
.
of
(
context
).
textTheme
.
headline
))
:
null
,
onReorder:
_onReorder
,
scrollDirection:
_itemType
==
_ReorderableListType
.
horizontalAvatar
?
Axis
.
horizontal
:
Axis
.
vertical
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
8.0
),
children:
_items
.
map
(
buildListTile
).
toList
(),
),
),
);
}
}
examples/flutter_gallery/lib/gallery/demos.dart
View file @
f844fada
...
@@ -265,6 +265,14 @@ List<GalleryDemo> _buildGalleryDemos() {
...
@@ -265,6 +265,14 @@ List<GalleryDemo> _buildGalleryDemos() {
routeName:
LeaveBehindDemo
.
routeName
,
routeName:
LeaveBehindDemo
.
routeName
,
buildRoute:
(
BuildContext
context
)
=>
const
LeaveBehindDemo
(),
buildRoute:
(
BuildContext
context
)
=>
const
LeaveBehindDemo
(),
),
),
new
GalleryDemo
(
title:
'Lists: reorderable'
,
subtitle:
'Reorderable lists'
,
icon:
GalleryIcons
.
list_alt
,
category:
_kMaterialComponents
,
routeName:
ReorderableListDemo
.
routeName
,
buildRoute:
(
BuildContext
context
)
=>
const
ReorderableListDemo
(),
),
new
GalleryDemo
(
new
GalleryDemo
(
title:
'Menus'
,
title:
'Menus'
,
subtitle:
'Menu buttons and simple menus'
,
subtitle:
'Menu buttons and simple menus'
,
...
...
packages/flutter/lib/material.dart
View file @
f844fada
...
@@ -76,6 +76,7 @@ export 'src/material/radio.dart';
...
@@ -76,6 +76,7 @@ export 'src/material/radio.dart';
export
'src/material/radio_list_tile.dart'
;
export
'src/material/radio_list_tile.dart'
;
export
'src/material/raised_button.dart'
;
export
'src/material/raised_button.dart'
;
export
'src/material/refresh_indicator.dart'
;
export
'src/material/refresh_indicator.dart'
;
export
'src/material/reorderable_list.dart'
;
export
'src/material/scaffold.dart'
;
export
'src/material/scaffold.dart'
;
export
'src/material/scrollbar.dart'
;
export
'src/material/scrollbar.dart'
;
export
'src/material/search.dart'
;
export
'src/material/search.dart'
;
...
...
packages/flutter/lib/src/material/reorderable_list.dart
0 → 100644
View file @
f844fada
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/rendering.dart'
;
import
'material.dart'
;
/// The callback used by [ReorderableListView] to move an item to a new
/// position in a list.
///
/// Implementations should remove the corresponding list item at [oldIndex]
/// and reinsert it at [newIndex].
///
/// Note that if [oldIndex] is before [newIndex], removing the item at [oldIndex]
/// from the list will reduce the list's length by one. Implementations used
/// by [ReorderableListView] will need to account for this when inserting before
/// [newIndex].
///
/// Example implementation:
///
/// ```dart
/// final List<MyDataObject> backingList = <MyDataObject>[/* ... */];
///
/// void onReorder(int oldIndex, int newIndex) {
/// if (oldIndex < newIndex) {
/// // removing the item at oldIndex will shorten the list by 1.
/// newIndex -= 1;
/// }
/// final MyDataObject element = backingList.removeAt(oldIndex);
/// backingList.insert(newIndex, element);
/// }
/// ```
typedef
void
OnReorderCallback
(
int
oldIndex
,
int
newIndex
);
/// A list whose items the user can interactively reorder by dragging.
///
/// This class is appropriate for views with a small number of
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the list view instead of just
/// those children that are actually visible.
///
/// All [children] must have a key.
class
ReorderableListView
extends
StatefulWidget
{
/// Creates a reorderable list.
ReorderableListView
({
this
.
header
,
@required
this
.
children
,
@required
this
.
onReorder
,
this
.
scrollDirection
=
Axis
.
vertical
,
this
.
padding
,
}):
assert
(
scrollDirection
!=
null
),
assert
(
onReorder
!=
null
),
assert
(
children
!=
null
),
assert
(
children
.
every
((
Widget
w
)
=>
w
.
key
!=
null
),
'All children of this widget must have a key.'
,
);
/// A non-reorderable header widget to show before the list.
///
/// If null, no header will appear before the list.
final
Widget
header
;
/// The widgets to display.
final
List
<
Widget
>
children
;
/// The [Axis] along which the list scrolls.
///
/// List [children] can only drag along this [Axis].
final
Axis
scrollDirection
;
/// The amount of space by which to inset the [children].
final
EdgeInsets
padding
;
/// Called when a list child is dropped into a new position to shuffle the
/// underlying list.
///
/// This [ReorderableListView] calls [onReorder] after a list child is dropped
/// into a new position.
final
OnReorderCallback
onReorder
;
@override
_ReorderableListViewState
createState
()
=>
new
_ReorderableListViewState
();
}
// This top-level state manages an Overlay that contains the list and
// also any Draggables it creates.
//
// _ReorderableListContent manages the list itself and reorder operations.
//
// The Overlay doesn't properly keep state by building new overlay entries,
// and so we cache a single OverlayEntry for use as the list layer.
// That overlay entry then builds a _ReorderableListContent which may
// insert Draggables into the Overlay above itself.
class
_ReorderableListViewState
extends
State
<
ReorderableListView
>
{
// We use an inner overlay so that the dragging list item doesn't draw outside of the list itself.
final
GlobalKey
_overlayKey
=
new
GlobalKey
(
debugLabel:
'
$ReorderableListView
overlay key'
);
// This entry contains the scrolling list itself.
OverlayEntry
_listOverlayEntry
;
@override
void
initState
()
{
super
.
initState
();
_listOverlayEntry
=
new
OverlayEntry
(
opaque:
true
,
builder:
(
BuildContext
context
)
{
return
new
_ReorderableListContent
(
header:
widget
.
header
,
children:
widget
.
children
,
scrollDirection:
widget
.
scrollDirection
,
onReorder:
widget
.
onReorder
,
padding:
widget
.
padding
,
);
},
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
new
Overlay
(
key:
_overlayKey
,
initialEntries:
<
OverlayEntry
>[
_listOverlayEntry
,
]);
}
}
// This widget is responsible for the inside of the Overlay in the
// ReorderableListView.
class
_ReorderableListContent
extends
StatefulWidget
{
const
_ReorderableListContent
({
@required
this
.
header
,
@required
this
.
children
,
@required
this
.
scrollDirection
,
@required
this
.
padding
,
@required
this
.
onReorder
,
});
final
Widget
header
;
final
List
<
Widget
>
children
;
final
Axis
scrollDirection
;
final
EdgeInsets
padding
;
final
OnReorderCallback
onReorder
;
@override
_ReorderableListContentState
createState
()
=>
new
_ReorderableListContentState
();
}
class
_ReorderableListContentState
extends
State
<
_ReorderableListContent
>
with
TickerProviderStateMixin
{
// The extent along the [widget.scrollDirection] axis to allow a child to
// drop into when the user reorders list children.
//
// This value is used when the extents haven't yet been calculated from
// the currently dragging widget, such as when it first builds.
static
const
double
_defaultDropAreaExtent
=
100.0
;
// The additional margin to place around a computed drop area.
static
const
double
_dropAreaMargin
=
8.0
;
// How long an animation to reorder an element in the list takes.
static
const
Duration
_reorderAnimationDuration
=
const
Duration
(
milliseconds:
200
);
// How long an animation to scroll to an off-screen element in the
// list takes.
static
const
Duration
_scrollAnimationDuration
=
const
Duration
(
milliseconds:
200
);
// Controls scrolls and measures scroll progress.
final
ScrollController
_scrollController
=
new
ScrollController
();
// This controls the entrance of the dragging widget into a new place.
AnimationController
_entranceController
;
// This controls the 'ghost' of the dragging widget, which is left behind
// where the widget used to be.
AnimationController
_ghostController
;
// The member of widget.children currently being dragged.
//
// Null if no drag is underway.
Key
_dragging
;
// The last computed size of the feedback widget being dragged.
Size
_draggingFeedbackSize
;
// The location that the dragging widget occupied before it started to drag.
int
_dragStartIndex
=
0
;
// The index that the dragging widget most recently left.
// This is used to show an animation of the widget's position.
int
_ghostIndex
=
0
;
// The index that the dragging widget currently occupies.
int
_currentIndex
=
0
;
// The widget to move the dragging widget too after the current index.
int
_nextIndex
=
0
;
// Whether or not we are currently scrolling this view to show a widget.
bool
_scrolling
=
false
;
double
get
_dropAreaExtent
{
if
(
_draggingFeedbackSize
==
null
)
{
return
_defaultDropAreaExtent
;
}
double
dropAreaWithoutMargin
;
switch
(
widget
.
scrollDirection
)
{
case
Axis
.
horizontal
:
dropAreaWithoutMargin
=
_draggingFeedbackSize
.
width
;
break
;
case
Axis
.
vertical
:
default
:
dropAreaWithoutMargin
=
_draggingFeedbackSize
.
height
;
break
;
}
return
dropAreaWithoutMargin
+
_dropAreaMargin
;
}
@override
void
initState
()
{
super
.
initState
();
_entranceController
=
new
AnimationController
(
vsync:
this
,
duration:
_reorderAnimationDuration
);
_ghostController
=
new
AnimationController
(
vsync:
this
,
duration:
_reorderAnimationDuration
);
_entranceController
.
addStatusListener
(
_onEntranceStatusChanged
);
}
@override
void
dispose
()
{
_entranceController
.
dispose
();
_ghostController
.
dispose
();
super
.
dispose
();
}
// Animates the droppable space from _currentIndex to _nextIndex.
void
_requestAnimationToNextIndex
()
{
if
(
_entranceController
.
isCompleted
)
{
_ghostIndex
=
_currentIndex
;
if
(
_nextIndex
==
_currentIndex
)
{
return
;
}
_currentIndex
=
_nextIndex
;
_ghostController
.
reverse
(
from:
1.0
);
_entranceController
.
forward
(
from:
0.0
);
}
}
// Requests animation to the latest next index if it changes during an animation.
void
_onEntranceStatusChanged
(
AnimationStatus
status
)
{
if
(
status
==
AnimationStatus
.
completed
)
{
setState
(()
{
_requestAnimationToNextIndex
();
});
}
}
// Scrolls to a target context if that context is not on the screen.
void
_scrollTo
(
BuildContext
context
)
{
if
(
_scrolling
)
return
;
final
RenderObject
contextObject
=
context
.
findRenderObject
();
final
RenderAbstractViewport
viewport
=
RenderAbstractViewport
.
of
(
contextObject
);
assert
(
viewport
!=
null
);
// If and only if the current scroll offset falls in-between the offsets
// necessary to reveal the selected context at the top or bottom of the
// screen, then it is already on-screen.
final
double
margin
=
_dropAreaExtent
;
final
double
scrollOffset
=
_scrollController
.
offset
;
final
double
topOffset
=
max
(
_scrollController
.
position
.
minScrollExtent
,
viewport
.
getOffsetToReveal
(
contextObject
,
0.0
).
offset
-
margin
,
);
final
double
bottomOffset
=
min
(
_scrollController
.
position
.
maxScrollExtent
,
viewport
.
getOffsetToReveal
(
contextObject
,
1.0
).
offset
+
margin
,
);
final
bool
onScreen
=
scrollOffset
<=
topOffset
&&
scrollOffset
>=
bottomOffset
;
// If the context is off screen, then we request a scroll to make it visible.
if
(!
onScreen
)
{
_scrolling
=
true
;
_scrollController
.
position
.
animateTo
(
scrollOffset
<
bottomOffset
?
bottomOffset
:
topOffset
,
duration:
_scrollAnimationDuration
,
curve:
Curves
.
easeInOut
,
).
then
((
Null
none
)
{
setState
(()
{
_scrolling
=
false
;
});
});
}
}
// Wraps children in Row or Column, so that the children flow in
// the widget's scrollDirection.
Widget
_buildContainerForScrollDirection
({
List
<
Widget
>
children
})
{
switch
(
widget
.
scrollDirection
)
{
case
Axis
.
horizontal
:
return
new
Row
(
children:
children
);
case
Axis
.
vertical
:
default
:
return
new
Column
(
children:
children
);
}
}
// Wraps one of the widget's children in a DragTarget and Draggable.
// Handles up the logic for dragging and reordering items in the list.
Widget
_wrap
(
Widget
toWrap
,
int
index
,
BoxConstraints
constraints
)
{
assert
(
toWrap
.
key
!=
null
);
// We create a global key based on both the child key and index
// so that when we reorder the list, a key doesn't get created twice.
final
GlobalObjectKey
keyIndexGlobalKey
=
new
GlobalObjectKey
(
toWrap
.
key
);
// We pass the toWrapWithGlobalKey into the Draggable so that when a list
// item gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
final
Widget
toWrapWithGlobalKey
=
new
KeyedSubtree
(
key:
keyIndexGlobalKey
,
child:
toWrap
);
// Starts dragging toWrap.
void
onDragStarted
()
{
setState
(()
{
_dragging
=
toWrap
.
key
;
_dragStartIndex
=
index
;
_ghostIndex
=
index
;
_currentIndex
=
index
;
_entranceController
.
value
=
1.0
;
_draggingFeedbackSize
=
keyIndexGlobalKey
.
currentContext
.
size
;
});
}
// Drops toWrap into the last position it was hovering over.
void
onDragEnded
()
{
setState
(()
{
if
(
_dragStartIndex
!=
_currentIndex
)
widget
.
onReorder
(
_dragStartIndex
,
_currentIndex
);
// Animates leftover space in the drop area closed.
// TODO(djshuckerow): bring the animation in line with the Material
// specifications.
_ghostController
.
reverse
(
from:
0.1
);
_entranceController
.
reverse
(
from:
0.1
);
_dragging
=
null
;
});
}
Widget
buildDragTarget
(
BuildContext
context
,
List
<
Key
>
acceptedCandidates
,
List
<
dynamic
>
rejectedCandidates
)
{
// We build the draggable inside of a layout builder so that we can
// constrain the size of the feedback dragging widget.
Widget
child
=
new
LongPressDraggable
<
Key
>(
maxSimultaneousDrags:
1
,
axis:
widget
.
scrollDirection
,
data:
toWrap
.
key
,
ignoringFeedbackSemantics:
false
,
feedback:
new
Container
(
alignment:
Alignment
.
topLeft
,
// These constraints will limit the cross axis of the drawn widget.
constraints:
constraints
,
child:
new
Material
(
elevation:
6.0
,
child:
toWrapWithGlobalKey
,
),
),
child:
_dragging
==
toWrap
.
key
?
const
SizedBox
()
:
toWrapWithGlobalKey
,
childWhenDragging:
const
SizedBox
(),
dragAnchor:
DragAnchor
.
child
,
onDragStarted:
onDragStarted
,
// When the drag ends inside a DragTarget widget, the drag
// succeeds, and we reorder the widget into position appropriately.
onDragCompleted:
onDragEnded
,
// When the drag does not end inside a DragTarget widget, the
// drag fails, but we still reorder the widget to the last position it
// had been dragged to.
onDraggableCanceled:
(
Velocity
velocity
,
Offset
offset
)
{
onDragEnded
();
},
);
// The target for dropping at the end of the list doesn't need to be
// draggable.
if
(
index
>=
widget
.
children
.
length
)
{
child
=
toWrap
;
}
// Determine the size of the drop area to show under the dragging widget.
Widget
spacing
;
switch
(
widget
.
scrollDirection
)
{
case
Axis
.
horizontal
:
spacing
=
new
SizedBox
(
width:
_dropAreaExtent
);
break
;
case
Axis
.
vertical
:
default
:
spacing
=
new
SizedBox
(
height:
_dropAreaExtent
);
break
;
}
// We open up a space under where the dragging widget currently is to
// show it can be dropped.
if
(
_currentIndex
==
index
)
{
return
_buildContainerForScrollDirection
(
children:
<
Widget
>[
new
SizeTransition
(
sizeFactor:
_entranceController
,
axis:
widget
.
scrollDirection
,
child:
spacing
),
child
,
]);
}
// We close up the space under where the dragging widget previously was
// with the ghostController animation.
if
(
_ghostIndex
==
index
)
{
return
_buildContainerForScrollDirection
(
children:
<
Widget
>[
new
SizeTransition
(
sizeFactor:
_ghostController
,
axis:
widget
.
scrollDirection
,
child:
spacing
,
),
child
,
]);
}
return
child
;
}
// We wrap the drag target in a Builder so that we can scroll to its specific context.
return
new
KeyedSubtree
(
key:
new
Key
(
'#
$ReorderableListView
|KeyedSubtree|
${toWrap.key}
'
),
child:
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
DragTarget
<
Key
>(
builder:
buildDragTarget
,
onWillAccept:
(
Key
toAccept
)
{
setState
(()
{
_nextIndex
=
index
;
_requestAnimationToNextIndex
();
});
_scrollTo
(
context
);
// If the target is not the original starting point, then we will accept the drop.
return
_dragging
==
toAccept
&&
toAccept
!=
toWrap
.
key
;
},
onAccept:
(
Key
accepted
)
{},
onLeave:
(
Key
leaving
)
{},
);
}),
);
}
@override
Widget
build
(
BuildContext
context
)
{
// We use the layout builder to constrain the cross-axis size of dragging child widgets.
return
new
LayoutBuilder
(
builder:
(
BuildContext
context
,
BoxConstraints
constraints
)
{
final
List
<
Widget
>
wrappedChildren
=
<
Widget
>[];
if
(
widget
.
header
!=
null
)
{
wrappedChildren
.
add
(
widget
.
header
);
}
for
(
int
i
=
0
;
i
<
widget
.
children
.
length
;
i
+=
1
)
{
wrappedChildren
.
add
(
_wrap
(
widget
.
children
[
i
],
i
,
constraints
));
}
const
Key
endWidgetKey
=
const
Key
(
'DraggableList - End Widget'
);
Widget
finalDropArea
;
switch
(
widget
.
scrollDirection
)
{
case
Axis
.
horizontal
:
finalDropArea
=
new
SizedBox
(
key:
endWidgetKey
,
width:
_defaultDropAreaExtent
,
height:
constraints
.
maxHeight
,
);
break
;
case
Axis
.
vertical
:
default
:
finalDropArea
=
new
SizedBox
(
key:
endWidgetKey
,
height:
_defaultDropAreaExtent
,
width:
constraints
.
maxWidth
,
);
break
;
}
wrappedChildren
.
add
(
_wrap
(
finalDropArea
,
widget
.
children
.
length
,
constraints
),
);
return
new
SingleChildScrollView
(
scrollDirection:
widget
.
scrollDirection
,
child:
_buildContainerForScrollDirection
(
children:
wrappedChildren
),
padding:
widget
.
padding
,
controller:
_scrollController
,
);
});
}
}
packages/flutter/test/material/reorderable_list_test.dart
0 → 100644
View file @
f844fada
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/gestures.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/material.dart'
;
void
main
(
)
{
group
(
'
$ReorderableListView
'
,
()
{
const
double
itemHeight
=
48.0
;
const
List
<
String
>
originalListItems
=
const
<
String
>[
'Item 1'
,
'Item 2'
,
'Item 3'
,
'Item 4'
];
List
<
String
>
listItems
;
void
onReorder
(
int
oldIndex
,
int
newIndex
)
{
if
(
oldIndex
<
newIndex
)
{
newIndex
-=
1
;
}
final
String
element
=
listItems
.
removeAt
(
oldIndex
);
listItems
.
insert
(
newIndex
,
element
);
}
Widget
listItemToWidget
(
String
listItem
)
{
return
new
SizedBox
(
key:
new
Key
(
listItem
),
height:
itemHeight
,
width:
itemHeight
,
child:
new
Text
(
listItem
),
);
}
Widget
build
({
Widget
header
,
Axis
scrollDirection
=
Axis
.
vertical
})
{
return
new
MaterialApp
(
home:
new
SizedBox
(
height:
itemHeight
*
10
,
width:
itemHeight
*
10
,
child:
new
ReorderableListView
(
header:
header
,
children:
listItems
.
map
(
listItemToWidget
).
toList
(),
scrollDirection:
scrollDirection
,
onReorder:
onReorder
,
),
),
);
}
setUp
(()
{
// Copy the original list into listItems.
listItems
=
originalListItems
.
toList
();
});
group
(
'in vertical mode'
,
()
{
testWidgets
(
'reorders its contents only when a drag finishes'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
());
expect
(
listItems
,
orderedEquals
(
originalListItems
));
final
TestGesture
drag
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'Item 1'
)));
await
tester
.
pump
(
kLongPressTimeout
+
kPressTimeout
);
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
drag
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Item 4'
)));
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
drag
.
up
();
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 2'
,
'Item 3'
,
'Item 1'
,
'Item 4'
]));
});
testWidgets
(
'allows reordering from the very top to the very bottom'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
());
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 1'
)),
tester
.
getCenter
(
find
.
text
(
'Item 4'
))
+
const
Offset
(
0.0
,
itemHeight
*
2
),
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 2'
,
'Item 3'
,
'Item 4'
,
'Item 1'
]));
});
testWidgets
(
'allows reordering from the very bottom to the very top'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
());
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 4'
)),
tester
.
getCenter
(
find
.
text
(
'Item 1'
)),
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 4'
,
'Item 1'
,
'Item 2'
,
'Item 3'
]));
});
testWidgets
(
'allows reordering inside the middle of the widget'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
());
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 3'
)),
tester
.
getCenter
(
find
.
text
(
'Item 2'
)),
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 1'
,
'Item 3'
,
'Item 2'
,
'Item 4'
]));
});
testWidgets
(
'properly reorders with a header'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
(
header:
const
Text
(
'Header Text'
)));
expect
(
find
.
text
(
'Header Text'
),
findsOneWidget
);
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 1'
)),
tester
.
getCenter
(
find
.
text
(
'Item 4'
))
+
const
Offset
(
0.0
,
itemHeight
*
2
),
);
expect
(
find
.
text
(
'Header Text'
),
findsOneWidget
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 2'
,
'Item 3'
,
'Item 4'
,
'Item 1'
]));
});
testWidgets
(
'properly determines the vertical drop area extents'
,
(
WidgetTester
tester
)
async
{
final
Widget
reorderableListView
=
new
ReorderableListView
(
children:
const
<
Widget
>[
const
SizedBox
(
key:
const
Key
(
'Normal item'
),
height:
itemHeight
,
child:
const
Text
(
'Normal item'
),
),
const
SizedBox
(
key:
const
Key
(
'Tall item'
),
height:
itemHeight
*
2
,
child:
const
Text
(
'Tall item'
),
),
const
SizedBox
(
key:
const
Key
(
'Last item'
),
height:
itemHeight
,
child:
const
Text
(
'Last item'
),
)
],
scrollDirection:
Axis
.
vertical
,
onReorder:
(
int
oldIndex
,
int
newIndex
)
{},
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
SizedBox
(
height:
itemHeight
*
10
,
child:
reorderableListView
,
),
));
Element
getContentElement
()
{
final
SingleChildScrollView
listScrollView
=
find
.
byType
(
SingleChildScrollView
).
evaluate
().
first
.
widget
;
final
Widget
scrollContents
=
listScrollView
.
child
;
final
Element
contentElement
=
find
.
byElementPredicate
((
Element
element
)
=>
element
.
widget
==
scrollContents
).
evaluate
().
first
;
return
contentElement
;
}
const
double
kNonDraggingListHeight
=
292.0
;
// The list view pads the drop area by 8dp.
const
double
kDraggingListHeight
=
300.0
;
// Drag a normal text item
expect
(
getContentElement
().
size
.
height
,
kNonDraggingListHeight
);
TestGesture
drag
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'Normal item'
)));
await
tester
.
pump
(
kLongPressTimeout
+
kPressTimeout
);
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
height
,
kDraggingListHeight
);
// Move it
await
drag
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Last item'
)));
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
height
,
kDraggingListHeight
);
// Drop it
await
drag
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
height
,
kNonDraggingListHeight
);
// Drag a tall item
drag
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'Tall item'
)));
await
tester
.
pump
(
kLongPressTimeout
+
kPressTimeout
);
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
height
,
kDraggingListHeight
);
// Move it
await
drag
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Last item'
)));
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
height
,
kDraggingListHeight
);
// Drop it
await
drag
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
height
,
kNonDraggingListHeight
);
});
testWidgets
(
'Preserves children states when the list parent changes the order'
,
(
WidgetTester
tester
)
async
{
_StatefulState
findState
(
Key
key
)
{
return
find
.
byElementPredicate
((
Element
element
)
=>
element
.
ancestorWidgetOfExactType
(
_Stateful
)?.
key
==
key
)
.
evaluate
()
.
first
.
ancestorStateOfType
(
const
TypeMatcher
<
_StatefulState
>());
}
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
ReorderableListView
(
children:
<
Widget
>[
new
_Stateful
(
key:
const
Key
(
'A'
)),
new
_Stateful
(
key:
const
Key
(
'B'
)),
new
_Stateful
(
key:
const
Key
(
'C'
)),
],
onReorder:
(
int
oldIndex
,
int
newIndex
)
{},
),
));
await
tester
.
tap
(
find
.
byKey
(
const
Key
(
'A'
)));
await
tester
.
pumpAndSettle
();
// Only the 'A' widget should be checked.
expect
(
findState
(
const
Key
(
'A'
)).
checked
,
true
);
expect
(
findState
(
const
Key
(
'B'
)).
checked
,
false
);
expect
(
findState
(
const
Key
(
'C'
)).
checked
,
false
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
ReorderableListView
(
children:
<
Widget
>[
new
_Stateful
(
key:
const
Key
(
'B'
)),
new
_Stateful
(
key:
const
Key
(
'C'
)),
new
_Stateful
(
key:
const
Key
(
'A'
)),
],
onReorder:
(
int
oldIndex
,
int
newIndex
)
{},
),
));
// Only the 'A' widget should be checked.
expect
(
findState
(
const
Key
(
'B'
)).
checked
,
false
);
expect
(
findState
(
const
Key
(
'C'
)).
checked
,
false
);
expect
(
findState
(
const
Key
(
'A'
)).
checked
,
true
);
});
});
group
(
'in horizontal mode'
,
()
{
testWidgets
(
'allows reordering from the very top to the very bottom'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
(
scrollDirection:
Axis
.
horizontal
));
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 1'
)),
tester
.
getCenter
(
find
.
text
(
'Item 4'
))
+
const
Offset
(
itemHeight
*
2
,
0.0
),
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 2'
,
'Item 3'
,
'Item 4'
,
'Item 1'
]));
});
testWidgets
(
'allows reordering from the very bottom to the very top'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
(
scrollDirection:
Axis
.
horizontal
));
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 4'
)),
tester
.
getCenter
(
find
.
text
(
'Item 1'
)),
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 4'
,
'Item 1'
,
'Item 2'
,
'Item 3'
]));
});
testWidgets
(
'allows reordering inside the middle of the widget'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
(
scrollDirection:
Axis
.
horizontal
));
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 3'
)),
tester
.
getCenter
(
find
.
text
(
'Item 2'
)),
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 1'
,
'Item 3'
,
'Item 2'
,
'Item 4'
]));
});
testWidgets
(
'properly reorders with a header'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
build
(
header:
const
Text
(
'Header Text'
),
scrollDirection:
Axis
.
horizontal
));
expect
(
find
.
text
(
'Header Text'
),
findsOneWidget
);
expect
(
listItems
,
orderedEquals
(
originalListItems
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 1'
)),
tester
.
getCenter
(
find
.
text
(
'Item 4'
))
+
const
Offset
(
itemHeight
*
2
,
0.0
),
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Header Text'
),
findsOneWidget
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 2'
,
'Item 3'
,
'Item 4'
,
'Item 1'
]));
await
tester
.
pumpWidget
(
build
(
header:
const
Text
(
'Header Text'
),
scrollDirection:
Axis
.
horizontal
));
await
longPressDrag
(
tester
,
tester
.
getCenter
(
find
.
text
(
'Item 4'
)),
tester
.
getCenter
(
find
.
text
(
'Item 3'
)),
);
expect
(
find
.
text
(
'Header Text'
),
findsOneWidget
);
expect
(
listItems
,
orderedEquals
(<
String
>[
'Item 2'
,
'Item 4'
,
'Item 3'
,
'Item 1'
]));
});
testWidgets
(
'properly determines the horizontal drop area extents'
,
(
WidgetTester
tester
)
async
{
final
Widget
reorderableListView
=
new
ReorderableListView
(
children:
const
<
Widget
>[
const
SizedBox
(
key:
const
Key
(
'Normal item'
),
width:
itemHeight
,
child:
const
Text
(
'Normal item'
),
),
const
SizedBox
(
key:
const
Key
(
'Tall item'
),
width:
itemHeight
*
2
,
child:
const
Text
(
'Tall item'
),
),
const
SizedBox
(
key:
const
Key
(
'Last item'
),
width:
itemHeight
,
child:
const
Text
(
'Last item'
),
)
],
scrollDirection:
Axis
.
horizontal
,
onReorder:
(
int
oldIndex
,
int
newIndex
)
{},
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
SizedBox
(
width:
itemHeight
*
10
,
child:
reorderableListView
,
),
));
Element
getContentElement
()
{
final
SingleChildScrollView
listScrollView
=
find
.
byType
(
SingleChildScrollView
).
evaluate
().
first
.
widget
;
final
Widget
scrollContents
=
listScrollView
.
child
;
final
Element
contentElement
=
find
.
byElementPredicate
((
Element
element
)
=>
element
.
widget
==
scrollContents
).
evaluate
().
first
;
return
contentElement
;
}
const
double
kNonDraggingListWidth
=
292.0
;
// The list view pads the drop area by 8dp.
const
double
kDraggingListWidth
=
300.0
;
// Drag a normal text item
expect
(
getContentElement
().
size
.
width
,
kNonDraggingListWidth
);
TestGesture
drag
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'Normal item'
)));
await
tester
.
pump
(
kLongPressTimeout
+
kPressTimeout
);
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
width
,
kDraggingListWidth
);
// Move it
await
drag
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Last item'
)));
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
width
,
kDraggingListWidth
);
// Drop it
await
drag
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
width
,
kNonDraggingListWidth
);
// Drag a tall item
drag
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'Tall item'
)));
await
tester
.
pump
(
kLongPressTimeout
+
kPressTimeout
);
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
width
,
kDraggingListWidth
);
// Move it
await
drag
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Last item'
)));
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
width
,
kDraggingListWidth
);
// Drop it
await
drag
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
getContentElement
().
size
.
width
,
kNonDraggingListWidth
);
});
testWidgets
(
'Preserves children states when the list parent changes the order'
,
(
WidgetTester
tester
)
async
{
_StatefulState
findState
(
Key
key
)
{
return
find
.
byElementPredicate
((
Element
element
)
=>
element
.
ancestorWidgetOfExactType
(
_Stateful
)?.
key
==
key
)
.
evaluate
()
.
first
.
ancestorStateOfType
(
const
TypeMatcher
<
_StatefulState
>());
}
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
ReorderableListView
(
children:
<
Widget
>[
new
_Stateful
(
key:
const
Key
(
'A'
)),
new
_Stateful
(
key:
const
Key
(
'B'
)),
new
_Stateful
(
key:
const
Key
(
'C'
)),
],
onReorder:
(
int
oldIndex
,
int
newIndex
)
{},
scrollDirection:
Axis
.
horizontal
,
),
));
await
tester
.
tap
(
find
.
byKey
(
const
Key
(
'A'
)));
await
tester
.
pumpAndSettle
();
// Only the 'A' widget should be checked.
expect
(
findState
(
const
Key
(
'A'
)).
checked
,
true
);
expect
(
findState
(
const
Key
(
'B'
)).
checked
,
false
);
expect
(
findState
(
const
Key
(
'C'
)).
checked
,
false
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
ReorderableListView
(
children:
<
Widget
>[
new
_Stateful
(
key:
const
Key
(
'B'
)),
new
_Stateful
(
key:
const
Key
(
'C'
)),
new
_Stateful
(
key:
const
Key
(
'A'
)),
],
onReorder:
(
int
oldIndex
,
int
newIndex
)
{},
scrollDirection:
Axis
.
horizontal
,
),
));
// Only the 'A' widget should be checked.
expect
(
findState
(
const
Key
(
'B'
)).
checked
,
false
);
expect
(
findState
(
const
Key
(
'C'
)).
checked
,
false
);
expect
(
findState
(
const
Key
(
'A'
)).
checked
,
true
);
});
});
// TODO(djshuckerow): figure out how to write a test for scrolling the list.
});
}
Future
<
void
>
longPressDrag
(
WidgetTester
tester
,
Offset
start
,
Offset
end
)
async
{
final
TestGesture
drag
=
await
tester
.
startGesture
(
start
);
await
tester
.
pump
(
kLongPressTimeout
+
kPressTimeout
);
await
drag
.
moveTo
(
end
);
await
tester
.
pump
(
kPressTimeout
);
await
drag
.
up
();
}
class
_Stateful
extends
StatefulWidget
{
// Ignoring the preference for const constructors because we want to test with regular non-const instances.
// ignore:prefer_const_constructors
// ignore:prefer_const_constructors_in_immutables
_Stateful
({
Key
key
})
:
super
(
key:
key
);
@override
State
<
StatefulWidget
>
createState
()
=>
new
_StatefulState
();
}
class
_StatefulState
extends
State
<
_Stateful
>
{
bool
checked
=
false
;
@override
Widget
build
(
BuildContext
context
)
{
return
new
Container
(
width:
48.0
,
height:
48.0
,
child:
new
Material
(
child:
new
Checkbox
(
value:
checked
,
onChanged:
(
bool
newValue
)
=>
checked
=
newValue
,
),
),
);
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment