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
7523ab5c
Unverified
Commit
7523ab5c
authored
Oct 07, 2022
by
Greg Spencer
Committed by
GitHub
Oct 07, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add `SliverAnimatedGrid` and `AnimatedGrid` (#112982)
parent
78c23305
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1825 additions
and
7 deletions
+1825
-7
animated_grid.0.dart
examples/api/lib/widgets/animated_grid/animated_grid.0.dart
+231
-0
sliver_animated_grid.0.dart
...api/lib/widgets/animated_grid/sliver_animated_grid.0.dart
+229
-0
animated_grid.0_test.dart
.../api/test/widgets/animated_grid/animated_grid.0_test.dart
+43
-0
sliver_animated_grid.0_test.dart
...st/widgets/animated_grid/sliver_animated_grid.0_test.dart
+43
-0
theme_data.dart
packages/flutter/lib/src/material/theme_data.dart
+2
-3
sliver_grid.dart
packages/flutter/lib/src/rendering/sliver_grid.dart
+6
-3
animated_grid.dart
packages/flutter/lib/src/widgets/animated_grid.dart
+721
-0
animated_list.dart
packages/flutter/lib/src/widgets/animated_list.dart
+8
-0
sliver.dart
packages/flutter/lib/src/widgets/sliver.dart
+5
-1
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
animated_grid_test.dart
packages/flutter/test/widgets/animated_grid_test.dart
+536
-0
No files found.
examples/api/lib/widgets/animated_grid/animated_grid.0.dart
0 → 100644
View file @
7523ab5c
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Flutter code sample for [AnimatedGrid].
import
'package:flutter/material.dart'
;
void
main
(
)
{
runApp
(
const
AnimatedGridSample
());
}
class
AnimatedGridSample
extends
StatefulWidget
{
const
AnimatedGridSample
({
super
.
key
});
@override
State
<
AnimatedGridSample
>
createState
()
=>
_AnimatedGridSampleState
();
}
class
_AnimatedGridSampleState
extends
State
<
AnimatedGridSample
>
{
final
GlobalKey
<
AnimatedGridState
>
_gridKey
=
GlobalKey
<
AnimatedGridState
>();
late
ListModel
<
int
>
_list
;
int
?
_selectedItem
;
late
int
_nextItem
;
// The next item inserted when the user presses the '+' button.
@override
void
initState
()
{
super
.
initState
();
_list
=
ListModel
<
int
>(
listKey:
_gridKey
,
initialItems:
<
int
>[
0
,
1
,
2
,
3
,
4
,
5
],
removedItemBuilder:
_buildRemovedItem
,
);
_nextItem
=
6
;
}
// Used to build list items that haven't been removed.
Widget
_buildItem
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
CardItem
(
animation:
animation
,
item:
_list
[
index
],
selected:
_selectedItem
==
_list
[
index
],
onTap:
()
{
setState
(()
{
_selectedItem
=
_selectedItem
==
_list
[
index
]
?
null
:
_list
[
index
];
});
},
);
}
// Used to build an item after it has been removed from the list. This method
// is needed because a removed item remains visible until its animation has
// completed (even though it's gone as far as this ListModel is concerned).
// The widget will be used by the [AnimatedGridState.removeItem] method's
// [AnimatedGridRemovedItemBuilder] parameter.
Widget
_buildRemovedItem
(
int
item
,
BuildContext
context
,
Animation
<
double
>
animation
)
{
return
CardItem
(
animation:
animation
,
item:
item
,
removing:
true
,
// No gesture detector here: we don't want removed items to be interactive.
);
}
// Insert the "next item" into the list model.
void
_insert
()
{
final
int
index
=
_selectedItem
==
null
?
_list
.
length
:
_list
.
indexOf
(
_selectedItem
!);
setState
(()
{
_list
.
insert
(
index
,
_nextItem
++);
});
}
// Remove the selected item from the list model.
void
_remove
()
{
if
(
_selectedItem
!=
null
)
{
setState
(()
{
_list
.
removeAt
(
_list
.
indexOf
(
_selectedItem
!));
_selectedItem
=
null
;
});
}
else
if
(
_list
.
length
>
0
)
{
setState
(()
{
_list
.
removeAt
(
_list
.
length
-
1
);
});
}
}
@override
Widget
build
(
BuildContext
context
)
{
return
MaterialApp
(
debugShowCheckedModeBanner:
false
,
home:
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'AnimatedGrid'
,
style:
TextStyle
(
fontSize:
30
),
),
centerTitle:
true
,
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
remove_circle
),
iconSize:
32
,
onPressed:
(
_list
.
length
>
0
)
?
_remove
:
null
,
tooltip:
'remove the selected item'
,
),
actions:
<
Widget
>[
IconButton
(
icon:
const
Icon
(
Icons
.
add_circle
),
iconSize:
32
,
onPressed:
_insert
,
tooltip:
'insert a new item'
,
),
],
),
body:
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
AnimatedGrid
(
key:
_gridKey
,
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
mainAxisSpacing:
10.0
,
crossAxisSpacing:
10.0
,
),
initialItemCount:
_list
.
length
,
itemBuilder:
_buildItem
,
),
),
),
);
}
}
typedef
RemovedItemBuilder
<
T
>
=
Widget
Function
(
T
item
,
BuildContext
context
,
Animation
<
double
>
animation
);
/// Keeps a Dart [List] in sync with an [AnimatedGrid].
///
/// The [insert] and [removeAt] methods apply to both the internal list and
/// the animated list that belongs to [listKey].
///
/// This class only exposes as much of the Dart List API as is needed by the
/// sample app. More list methods are easily added, however methods that
/// mutate the list must make the same changes to the animated list in terms
/// of [AnimatedGridState.insertItem] and [AnimatedGrid.removeItem].
class
ListModel
<
E
>
{
ListModel
({
required
this
.
listKey
,
required
this
.
removedItemBuilder
,
Iterable
<
E
>?
initialItems
,
})
:
_items
=
List
<
E
>.
from
(
initialItems
??
<
E
>[]);
final
GlobalKey
<
AnimatedGridState
>
listKey
;
final
RemovedItemBuilder
<
E
>
removedItemBuilder
;
final
List
<
E
>
_items
;
AnimatedGridState
?
get
_animatedGrid
=>
listKey
.
currentState
;
void
insert
(
int
index
,
E
item
)
{
_items
.
insert
(
index
,
item
);
_animatedGrid
!.
insertItem
(
index
,
duration:
const
Duration
(
milliseconds:
500
),
);
}
E
removeAt
(
int
index
)
{
final
E
removedItem
=
_items
.
removeAt
(
index
);
if
(
removedItem
!=
null
)
{
_animatedGrid
!.
removeItem
(
index
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
{
return
removedItemBuilder
(
removedItem
,
context
,
animation
);
},
);
}
return
removedItem
;
}
int
get
length
=>
_items
.
length
;
E
operator
[](
int
index
)
=>
_items
[
index
];
int
indexOf
(
E
item
)
=>
_items
.
indexOf
(
item
);
}
/// Displays its integer item as 'item N' on a Card whose color is based on
/// the item's value.
///
/// The text is displayed in bright green if [selected] is
/// true. This widget's height is based on the [animation] parameter, it
/// varies from 0 to 128 as the animation varies from 0.0 to 1.0.
class
CardItem
extends
StatelessWidget
{
const
CardItem
({
super
.
key
,
this
.
onTap
,
this
.
selected
=
false
,
this
.
removing
=
false
,
required
this
.
animation
,
required
this
.
item
,
})
:
assert
(
item
>=
0
);
final
Animation
<
double
>
animation
;
final
VoidCallback
?
onTap
;
final
int
item
;
final
bool
selected
;
final
bool
removing
;
@override
Widget
build
(
BuildContext
context
)
{
TextStyle
textStyle
=
Theme
.
of
(
context
).
textTheme
.
headlineMedium
!;
if
(
selected
)
{
textStyle
=
textStyle
.
copyWith
(
color:
Colors
.
lightGreenAccent
[
400
]);
}
return
Padding
(
padding:
const
EdgeInsets
.
all
(
2.0
),
child:
ScaleTransition
(
scale:
CurvedAnimation
(
parent:
animation
,
curve:
removing
?
Curves
.
easeInOut
:
Curves
.
bounceOut
),
child:
GestureDetector
(
behavior:
HitTestBehavior
.
opaque
,
onTap:
onTap
,
child:
SizedBox
(
height:
80.0
,
child:
Card
(
color:
Colors
.
primaries
[
item
%
Colors
.
primaries
.
length
],
child:
Center
(
child:
Text
(
'
${item + 1}
'
,
style:
textStyle
),
),
),
),
),
),
);
}
}
examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart
0 → 100644
View file @
7523ab5c
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Flutter code sample for [SliverAnimatedGrid].
import
'package:flutter/material.dart'
;
void
main
(
)
=>
runApp
(
const
SliverAnimatedGridSample
());
class
SliverAnimatedGridSample
extends
StatefulWidget
{
const
SliverAnimatedGridSample
({
super
.
key
});
@override
State
<
SliverAnimatedGridSample
>
createState
()
=>
_SliverAnimatedGridSampleState
();
}
class
_SliverAnimatedGridSampleState
extends
State
<
SliverAnimatedGridSample
>
{
final
GlobalKey
<
SliverAnimatedGridState
>
_listKey
=
GlobalKey
<
SliverAnimatedGridState
>();
final
GlobalKey
<
ScaffoldState
>
_scaffoldKey
=
GlobalKey
<
ScaffoldState
>();
final
GlobalKey
<
ScaffoldMessengerState
>
_scaffoldMessengerKey
=
GlobalKey
<
ScaffoldMessengerState
>();
late
ListModel
<
int
>
_list
;
int
?
_selectedItem
;
late
int
_nextItem
;
// The next item inserted when the user presses the '+' button.
@override
void
initState
()
{
super
.
initState
();
_list
=
ListModel
<
int
>(
listKey:
_listKey
,
initialItems:
<
int
>[
0
,
1
,
2
,
3
,
4
,
5
],
removedItemBuilder:
_buildRemovedItem
,
);
_nextItem
=
6
;
}
// Used to build list items that haven't been removed.
Widget
_buildItem
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
CardItem
(
animation:
animation
,
item:
_list
[
index
],
selected:
_selectedItem
==
_list
[
index
],
onTap:
()
{
setState
(()
{
_selectedItem
=
_selectedItem
==
_list
[
index
]
?
null
:
_list
[
index
];
});
},
);
}
// Used to build an item after it has been removed from the list. This
// method is needed because a removed item remains visible until its
// animation has completed (even though it's gone as far this ListModel is
// concerned). The widget will be used by the
// [AnimatedGridState.removeItem] method's
// [AnimatedGridRemovedItemBuilder] parameter.
Widget
_buildRemovedItem
(
int
item
,
BuildContext
context
,
Animation
<
double
>
animation
)
{
return
CardItem
(
animation:
animation
,
removing:
true
,
item:
item
,
);
}
// Insert the "next item" into the list model.
void
_insert
()
{
final
int
index
=
_selectedItem
==
null
?
_list
.
length
:
_list
.
indexOf
(
_selectedItem
!);
_list
.
insert
(
index
,
_nextItem
++);
}
// Remove the selected item from the list model.
void
_remove
()
{
if
(
_selectedItem
!=
null
)
{
_list
.
removeAt
(
_list
.
indexOf
(
_selectedItem
!));
}
else
{
_list
.
removeAt
(
_list
.
length
-
1
);
}
setState
(()
{
_selectedItem
=
null
;
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
MaterialApp
(
scaffoldMessengerKey:
_scaffoldMessengerKey
,
debugShowCheckedModeBanner:
false
,
home:
Scaffold
(
key:
_scaffoldKey
,
body:
CustomScrollView
(
slivers:
<
Widget
>[
SliverAppBar
(
title:
const
Text
(
'SliverAnimatedGrid'
,
style:
TextStyle
(
fontSize:
30
),
),
expandedHeight:
60
,
centerTitle:
true
,
backgroundColor:
Colors
.
amber
[
900
],
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
remove_circle
),
onPressed:
_remove
,
tooltip:
'Remove the selected item, or the last item if none selected.'
,
iconSize:
32
,
),
actions:
<
Widget
>[
IconButton
(
icon:
const
Icon
(
Icons
.
add_circle
),
onPressed:
_insert
,
tooltip:
'Insert a new item.'
,
iconSize:
32
,
),
],
),
SliverAnimatedGrid
(
key:
_listKey
,
initialItemCount:
_list
.
length
,
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
mainAxisSpacing:
10.0
,
crossAxisSpacing:
10.0
,
),
itemBuilder:
_buildItem
,
),
],
),
),
);
}
}
typedef
RemovedItemBuilder
=
Widget
Function
(
int
item
,
BuildContext
context
,
Animation
<
double
>
animation
);
// Keeps a Dart [List] in sync with an [AnimatedGrid].
//
// The [insert] and [removeAt] methods apply to both the internal list and
// the animated list that belongs to [listKey].
//
// This class only exposes as much of the Dart List API as is needed by the
// sample app. More list methods are easily added, however methods that
// mutate the list must make the same changes to the animated list in terms
// of [AnimatedGridState.insertItem] and [AnimatedGrid.removeItem].
class
ListModel
<
E
>
{
ListModel
({
required
this
.
listKey
,
required
this
.
removedItemBuilder
,
Iterable
<
E
>?
initialItems
,
})
:
_items
=
List
<
E
>.
from
(
initialItems
??
<
E
>[]);
final
GlobalKey
<
SliverAnimatedGridState
>
listKey
;
final
RemovedItemBuilder
removedItemBuilder
;
final
List
<
E
>
_items
;
SliverAnimatedGridState
get
_animatedGrid
=>
listKey
.
currentState
!;
void
insert
(
int
index
,
E
item
)
{
_items
.
insert
(
index
,
item
);
_animatedGrid
.
insertItem
(
index
);
}
E
removeAt
(
int
index
)
{
final
E
removedItem
=
_items
.
removeAt
(
index
);
if
(
removedItem
!=
null
)
{
_animatedGrid
.
removeItem
(
index
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
=>
removedItemBuilder
(
index
,
context
,
animation
),
);
}
return
removedItem
;
}
int
get
length
=>
_items
.
length
;
E
operator
[](
int
index
)
=>
_items
[
index
];
int
indexOf
(
E
item
)
=>
_items
.
indexOf
(
item
);
}
// Displays its integer item as 'Item N' on a Card whose color is based on
// the item's value.
//
// The card turns gray when [selected] is true. This widget's height
// is based on the [animation] parameter. It varies as the animation value
// transitions from 0.0 to 1.0.
class
CardItem
extends
StatelessWidget
{
const
CardItem
({
super
.
key
,
this
.
onTap
,
this
.
selected
=
false
,
this
.
removing
=
false
,
required
this
.
animation
,
required
this
.
item
,
})
:
assert
(
item
>=
0
);
final
Animation
<
double
>
animation
;
final
VoidCallback
?
onTap
;
final
int
item
;
final
bool
selected
;
final
bool
removing
;
@override
Widget
build
(
BuildContext
context
)
{
return
Padding
(
padding:
const
EdgeInsets
.
only
(
left:
2.0
,
right:
2.0
,
top:
2.0
,
),
child:
ScaleTransition
(
scale:
CurvedAnimation
(
parent:
animation
,
curve:
removing
?
Curves
.
easeInOut
:
Curves
.
bounceOut
),
child:
GestureDetector
(
onTap:
onTap
,
child:
SizedBox
(
height:
80.0
,
child:
Card
(
color:
selected
?
Colors
.
black12
:
Colors
.
primaries
[
item
%
Colors
.
primaries
.
length
],
child:
Center
(
child:
Text
(
(
item
+
1
).
toString
(),
style:
Theme
.
of
(
context
).
textTheme
.
headlineMedium
,
),
),
),
),
),
),
);
}
}
examples/api/test/widgets/animated_grid/animated_grid.0_test.dart
0 → 100644
View file @
7523ab5c
// Copyright 2014 The Flutter 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/material.dart'
;
import
'package:flutter_api_samples/widgets/animated_grid/animated_grid.0.dart'
as
example
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'AnimatedGrid example'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
example
.
AnimatedGridSample
(),
);
expect
(
find
.
text
(
'1'
),
findsOneWidget
);
expect
(
find
.
text
(
'2'
),
findsOneWidget
);
expect
(
find
.
text
(
'3'
),
findsOneWidget
);
expect
(
find
.
text
(
'4'
),
findsOneWidget
);
expect
(
find
.
text
(
'5'
),
findsOneWidget
);
expect
(
find
.
text
(
'6'
),
findsOneWidget
);
expect
(
find
.
text
(
'7'
),
findsNothing
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
add_circle
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'7'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove_circle
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'7'
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'2'
));
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove_circle
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'2'
),
findsNothing
);
expect
(
find
.
text
(
'6'
),
findsOneWidget
);
});
}
examples/api/test/widgets/animated_grid/sliver_animated_grid.0_test.dart
0 → 100644
View file @
7523ab5c
// Copyright 2014 The Flutter 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/material.dart'
;
import
'package:flutter_api_samples/widgets/animated_grid/sliver_animated_grid.0.dart'
as
example
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'SliverAnimatedGrid example'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
example
.
SliverAnimatedGridSample
(),
);
expect
(
find
.
text
(
'1'
),
findsOneWidget
);
expect
(
find
.
text
(
'2'
),
findsOneWidget
);
expect
(
find
.
text
(
'3'
),
findsOneWidget
);
expect
(
find
.
text
(
'4'
),
findsOneWidget
);
expect
(
find
.
text
(
'5'
),
findsOneWidget
);
expect
(
find
.
text
(
'6'
),
findsOneWidget
);
expect
(
find
.
text
(
'7'
),
findsNothing
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
add_circle
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'7'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove_circle
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'7'
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'2'
));
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
remove_circle
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'2'
),
findsNothing
);
expect
(
find
.
text
(
'6'
),
findsOneWidget
);
});
}
packages/flutter/lib/src/material/theme_data.dart
View file @
7523ab5c
...
...
@@ -1609,9 +1609,8 @@ class ThemeData with Diagnosticable {
/// Obsolete property that was originally used as the foreground
/// color for widgets (knobs, text, overscroll edge effect, etc).
///
/// The material library no longer uses this property. In most cases
/// the theme's [colorScheme] [ColorScheme.secondary] property is now
/// used instead.
/// The material library no longer uses this property. In most cases the
/// [colorScheme]'s [ColorScheme.secondary] property is now used instead.
///
/// Apps should migrate uses of this property to the theme's [colorScheme]
/// [ColorScheme.secondary] color. In cases where a color is needed that
...
...
packages/flutter/lib/src/rendering/sliver_grid.dart
View file @
7523ab5c
...
...
@@ -461,7 +461,10 @@ class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate {
@override
SliverGridLayout
getLayout
(
SliverConstraints
constraints
)
{
assert
(
_debugAssertIsValid
(
constraints
.
crossAxisExtent
));
final
int
crossAxisCount
=
(
constraints
.
crossAxisExtent
/
(
maxCrossAxisExtent
+
crossAxisSpacing
)).
ceil
();
int
crossAxisCount
=
(
constraints
.
crossAxisExtent
/
(
maxCrossAxisExtent
+
crossAxisSpacing
)).
ceil
();
// TODO(gspencergoog): Figure out why we need this in release mode (and only
// in release mode). https://github.com/flutter/flutter/issues/113109
crossAxisCount
=
crossAxisCount
<
1
?
1
:
crossAxisCount
;
final
double
usableCrossAxisExtent
=
math
.
max
(
0.0
,
constraints
.
crossAxisExtent
-
crossAxisSpacing
*
(
crossAxisCount
-
1
),
...
...
@@ -584,8 +587,6 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
}
final
SliverGridGeometry
firstChildGridGeometry
=
layout
.
getGeometryForChildIndex
(
firstIndex
);
final
double
leadingScrollOffset
=
firstChildGridGeometry
.
scrollOffset
;
double
trailingScrollOffset
=
firstChildGridGeometry
.
trailingScrollOffset
;
if
(
firstChild
==
null
)
{
if
(!
addInitialChild
(
index:
firstIndex
,
layoutOffset:
firstChildGridGeometry
.
scrollOffset
))
{
...
...
@@ -600,6 +601,8 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
}
}
final
double
leadingScrollOffset
=
firstChildGridGeometry
.
scrollOffset
;
double
trailingScrollOffset
=
firstChildGridGeometry
.
trailingScrollOffset
;
RenderBox
?
trailingChildWithLayout
;
for
(
int
index
=
indexOf
(
firstChild
!)
-
1
;
index
>=
firstIndex
;
--
index
)
{
...
...
packages/flutter/lib/src/widgets/animated_grid.dart
0 → 100644
View file @
7523ab5c
// Copyright 2014 The Flutter 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
'basic.dart'
;
import
'framework.dart'
;
import
'scroll_controller.dart'
;
import
'scroll_physics.dart'
;
import
'scroll_view.dart'
;
import
'sliver.dart'
;
import
'ticker_provider.dart'
;
/// Signature for the builder callback used by widgets like [AnimatedGrid] to
/// build their animated children.
///
/// The `context` argument is the build context where the widget will be
/// created, the `index` is the index of the item to be built, and the
/// `animation` is an [Animation] that should be used to animate an entry
/// transition for the widget that is built.
///
/// - [AnimatedRemovedItemBuilder], a builder that is for removing items with
/// animations instead of adding them.
typedef
AnimatedItemBuilder
=
Widget
Function
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
);
/// Signature for the builder callback used by widgets like [AnimatedGrid] (in
/// [AnimatedGridState.removeItem]) to animated their children after they have
/// been removed.
///
/// The `context` argument is the build context where the widget will be
/// created, and the `animation` is an [Animation] that should be used to
/// animate an exit transition for the widget that is built.
///
/// See also:
///
/// - [AnimatedItemBuilder], a builder that is for adding items with animations
/// instead of removing them.
typedef
AnimatedRemovedItemBuilder
=
Widget
Function
(
BuildContext
context
,
Animation
<
double
>
animation
);
// The default insert/remove animation duration.
const
Duration
_kDuration
=
Duration
(
milliseconds:
300
);
// Incoming and outgoing AnimatedGrid items.
class
_ActiveItem
implements
Comparable
<
_ActiveItem
>
{
_ActiveItem
.
incoming
(
this
.
controller
,
this
.
itemIndex
)
:
removedItemBuilder
=
null
;
_ActiveItem
.
outgoing
(
this
.
controller
,
this
.
itemIndex
,
this
.
removedItemBuilder
);
_ActiveItem
.
index
(
this
.
itemIndex
)
:
controller
=
null
,
removedItemBuilder
=
null
;
final
AnimationController
?
controller
;
final
AnimatedRemovedItemBuilder
?
removedItemBuilder
;
int
itemIndex
;
@override
int
compareTo
(
_ActiveItem
other
)
=>
itemIndex
-
other
.
itemIndex
;
}
/// A scrolling container that animates items when they are inserted or removed
/// in a grid.
///
/// This widget's [AnimatedGridState] can be used to dynamically insert or
/// remove items. To refer to the [AnimatedGridState] either provide a
/// [GlobalKey] or use the static [of] method from an item's input callback.
///
/// This widget is similar to one created by [GridView.builder].
///
/// {@tool dartpad}
/// This sample application uses an [AnimatedGrid] to create an effect when
/// items are removed or added to the grid.
///
/// ** See code in examples/api/lib/widgets/animated_grid/animated_grid.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [SliverAnimatedGrid], a sliver which animates items when they are inserted
/// into or removed from a grid.
/// * [SliverAnimatedList], a sliver which animates items added and removed from
/// a list instead of a grid.
/// * [AnimatedList], which animates items added and removed from a list instead
/// of a grid.
class
AnimatedGrid
extends
StatefulWidget
{
/// Creates a scrolling container that animates items when they are inserted
/// or removed.
const
AnimatedGrid
({
super
.
key
,
required
this
.
itemBuilder
,
required
this
.
gridDelegate
,
this
.
initialItemCount
=
0
,
this
.
scrollDirection
=
Axis
.
vertical
,
this
.
reverse
=
false
,
this
.
controller
,
this
.
primary
,
this
.
physics
,
this
.
padding
,
this
.
clipBehavior
=
Clip
.
hardEdge
,
})
:
assert
(
itemBuilder
!=
null
),
assert
(
initialItemCount
!=
null
&&
initialItemCount
>=
0
);
/// Called, as needed, to build grid item widgets.
///
/// Grid items are only built when they're scrolled into view.
///
/// The [AnimatedItemBuilder] index parameter indicates the item's position in
/// the grid. The value of the index parameter will be between 0 and
/// [initialItemCount] plus the total number of items that have been inserted
/// with [AnimatedGridState.insertItem] and less the total number of items
/// that have been removed with [AnimatedGridState.removeItem].
///
/// Implementations of this callback should assume that
/// [AnimatedGridState.removeItem] removes an item immediately.
final
AnimatedItemBuilder
itemBuilder
;
/// A delegate that controls the layout of the children within the
/// [AnimatedGrid].
///
/// See also:
///
/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
/// a fixed number of tiles in the cross axis.
/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
/// tiles that have a maximum cross-axis extent.
final
SliverGridDelegate
gridDelegate
;
/// {@template flutter.widgets.AnimatedGrid.initialItemCount}
/// The number of items the grid will start with.
///
/// The appearance of the initial items is not animated. They
/// are created, as needed, by [itemBuilder] with an animation parameter
/// of [kAlwaysCompleteAnimation].
/// {@endtemplate}
final
int
initialItemCount
;
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
final
Axis
scrollDirection
;
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final
bool
reverse
;
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
final
ScrollController
?
controller
;
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// On iOS, this identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final
bool
?
primary
;
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions.
final
ScrollPhysics
?
physics
;
/// The amount of space by which to inset the children.
final
EdgeInsetsGeometry
?
padding
;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final
Clip
clipBehavior
;
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [AnimatedGrid] item widgets that insert
/// or remove items in response to user input.
///
/// If no [AnimatedGrid] surrounds the context given, then this function will
/// assert in debug mode and throw an exception in release mode.
///
/// This method can be expensive (it walks the element tree).
///
/// See also:
///
/// * [maybeOf], a similar function that will return null if no
/// [AnimatedGrid] ancestor is found.
static
AnimatedGridState
of
(
BuildContext
context
)
{
assert
(
context
!=
null
);
final
AnimatedGridState
?
result
=
context
.
findAncestorStateOfType
<
AnimatedGridState
>();
assert
(()
{
if
(
result
==
null
)
{
throw
FlutterError
.
fromParts
(<
DiagnosticsNode
>[
ErrorSummary
(
'AnimatedGrid.of() called with a context that does not contain an AnimatedGrid.'
),
ErrorDescription
(
'No AnimatedGrid ancestor could be found starting from the context that was passed to AnimatedGrid.of().'
,
),
ErrorHint
(
'This can happen when the context provided is from the same StatefulWidget that '
'built the AnimatedGrid. Please see the AnimatedGrid documentation for examples '
'of how to refer to an AnimatedGridState object:
\n
'
' https://api.flutter.dev/flutter/widgets/AnimatedGridState-class.html'
,
),
context
.
describeElement
(
'The context used was'
),
]);
}
return
true
;
}());
return
result
!;
}
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [AnimatedGrid] item widgets that insert
/// or remove items in response to user input.
///
/// If no [AnimatedGrid] surrounds the context given, then this function will
/// return null.
///
/// This method can be expensive (it walks the element tree).
///
/// See also:
///
/// * [of], a similar function that will throw if no [AnimatedGrid] ancestor
/// is found.
static
AnimatedGridState
?
maybeOf
(
BuildContext
context
)
{
assert
(
context
!=
null
);
return
context
.
findAncestorStateOfType
<
AnimatedGridState
>();
}
@override
AnimatedGridState
createState
()
=>
AnimatedGridState
();
}
/// The state for a scrolling container that animates items when they are
/// inserted or removed.
///
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [AnimatedGrid.itemBuilder] whenever the item's widget
/// is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder
/// parameter.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [AnimatedGrid]'s state with a global key:
///
/// ```dart
/// // (e.g. in a stateful widget)
/// GlobalKey<AnimatedGridState> gridKey = GlobalKey<AnimatedGridState>();
///
/// // ...
///
/// @override
/// Widget build(BuildContext context) {
/// return AnimatedGrid(
/// key: gridKey,
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
/// return const Placeholder();
/// },
/// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 100.0),
/// );
/// }
///
/// // ...
///
/// void _updateGrid() {
/// // adds "123" to the AnimatedGrid
/// gridKey.currentState!.insertItem(123);
/// }
/// ```
///
/// [AnimatedGrid] item input handlers can also refer to their [AnimatedGridState]
/// with the static [AnimatedGrid.of] method.
class
AnimatedGridState
extends
State
<
AnimatedGrid
>
with
TickerProviderStateMixin
<
AnimatedGrid
>
{
final
GlobalKey
<
SliverAnimatedGridState
>
_sliverAnimatedGridKey
=
GlobalKey
();
/// Insert an item at [index] and start an animation that will be passed
/// to [AnimatedGrid.itemBuilder] when the item is visible.
///
/// This method's semantics are the same as Dart's [List.insert] method: it
/// increases the length of the list of items in the grid by one and shifts
/// all items at or after [index] towards the end of the list of items in the
/// grid.
void
insertItem
(
int
index
,
{
Duration
duration
=
_kDuration
})
{
_sliverAnimatedGridKey
.
currentState
!.
insertItem
(
index
,
duration:
duration
);
}
/// Remove the item at `index` and start an animation that will be passed to
/// `builder` when the item is visible.
///
/// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [AnimatedGrid.itemBuilder]. However, the
/// item will still appear in the grid for `duration` and during that time
/// `builder` must construct its widget as needed.
///
/// This method's semantics are the same as Dart's [List.remove] method: it
/// decreases the length of the list of items in the grid by one and shifts
/// all items at or before `index` towards the beginning of the list of items
/// in the grid.
///
/// See also:
///
/// - [AnimatedRemovedItemBuilder], which describes the arguments to the
/// `builder` argument.
void
removeItem
(
int
index
,
AnimatedRemovedItemBuilder
builder
,
{
Duration
duration
=
_kDuration
})
{
_sliverAnimatedGridKey
.
currentState
!.
removeItem
(
index
,
builder
,
duration:
duration
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
CustomScrollView
(
scrollDirection:
widget
.
scrollDirection
,
reverse:
widget
.
reverse
,
controller:
widget
.
controller
,
primary:
widget
.
primary
,
physics:
widget
.
physics
,
clipBehavior:
widget
.
clipBehavior
,
slivers:
<
Widget
>[
SliverPadding
(
padding:
widget
.
padding
??
EdgeInsets
.
zero
,
sliver:
SliverAnimatedGrid
(
key:
_sliverAnimatedGridKey
,
gridDelegate:
widget
.
gridDelegate
,
itemBuilder:
widget
.
itemBuilder
,
initialItemCount:
widget
.
initialItemCount
,
),
),
],
);
}
}
/// A sliver that animates items when they are inserted or removed in a grid.
///
/// This widget's [SliverAnimatedGridState] can be used to dynamically insert or
/// remove items. To refer to the [SliverAnimatedGridState] either provide a
/// [GlobalKey] or use the static [SliverAnimatedGrid.of] method from an item's
/// input callback.
///
/// {@tool dartpad}
/// This sample application uses a [SliverAnimatedGrid] to create an animated
/// effect when items are removed or added to the grid.
///
/// ** See code in examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
/// they are inserted into or removed from a grid.
/// * [SliverGrid], which does not animate items when they are inserted or
/// removed from a grid.
/// * [SliverList], which displays a non-animated list of items.
/// * [SliverAnimatedList], which animates items added and removed from a list
/// instead of a grid.
class
SliverAnimatedGrid
extends
StatefulWidget
{
/// Creates a sliver that animates items when they are inserted or removed.
const
SliverAnimatedGrid
({
super
.
key
,
required
this
.
itemBuilder
,
required
this
.
gridDelegate
,
this
.
findChildIndexCallback
,
this
.
initialItemCount
=
0
,
})
:
assert
(
itemBuilder
!=
null
),
assert
(
initialItemCount
!=
null
&&
initialItemCount
>=
0
);
/// Called, as needed, to build grid item widgets.
///
/// Grid items are only built when they're scrolled into view.
///
/// The [AnimatedItemBuilder] index parameter indicates the item's position in
/// the grid. The value of the index parameter will be between 0 and
/// [initialItemCount] plus the total number of items that have been inserted
/// with [SliverAnimatedGridState.insertItem] and less the total number of
/// items that have been removed with [SliverAnimatedGridState.removeItem].
///
/// Implementations of this callback should assume that
/// [SliverAnimatedGridState.removeItem] removes an item immediately.
final
AnimatedItemBuilder
itemBuilder
;
/// A delegate that controls the layout of the children within the
/// [SliverAnimatedGrid].
///
/// See also:
///
/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
/// a fixed number of tiles in the cross axis.
/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
/// tiles that have a maximum cross-axis extent.
final
SliverGridDelegate
gridDelegate
;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
final
ChildIndexGetter
?
findChildIndexCallback
;
/// {@macro flutter.widgets.AnimatedGrid.initialItemCount}
final
int
initialItemCount
;
@override
SliverAnimatedGridState
createState
()
=>
SliverAnimatedGridState
();
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [SliverAnimatedGrid] item widgets that
/// insert or remove items in response to user input.
///
/// If no [SliverAnimatedGrid] surrounds the context given, then this function
/// will assert in debug mode and throw an exception in release mode.
///
/// This method can be expensive (it walks the element tree).
///
/// See also:
///
/// * [maybeOf], a similar function that will return null if no
/// [SliverAnimatedGrid] ancestor is found.
static
SliverAnimatedGridState
of
(
BuildContext
context
)
{
assert
(
context
!=
null
);
final
SliverAnimatedGridState
?
result
=
context
.
findAncestorStateOfType
<
SliverAnimatedGridState
>();
assert
(()
{
if
(
result
==
null
)
{
throw
FlutterError
(
'SliverAnimatedGrid.of() called with a context that does not contain a SliverAnimatedGrid.
\n
'
'No SliverAnimatedGridState ancestor could be found starting from the '
'context that was passed to SliverAnimatedGridState.of(). This can '
'happen when the context provided is from the same StatefulWidget that '
'built the AnimatedGrid. Please see the SliverAnimatedGrid documentation '
'for examples of how to refer to an AnimatedGridState object: '
'https://api.flutter.dev/flutter/widgets/SliverAnimatedGridState-class.html
\n
'
'The context used was:
\n
'
'
$context
'
,
);
}
return
true
;
}());
return
result
!;
}
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [SliverAnimatedGrid] item widgets that
/// insert or remove items in response to user input.
///
/// If no [SliverAnimatedGrid] surrounds the context given, then this function
/// will return null.
///
/// This method can be expensive (it walks the element tree).
///
/// See also:
///
/// * [of], a similar function that will throw if no [SliverAnimatedGrid]
/// ancestor is found.
static
SliverAnimatedGridState
?
maybeOf
(
BuildContext
context
)
{
assert
(
context
!=
null
);
return
context
.
findAncestorStateOfType
<
SliverAnimatedGridState
>();
}
}
/// The state for a sliver that animates items when they are
/// inserted or removed.
///
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [SliverAnimatedGrid.itemBuilder] whenever the item's
/// widget is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder
/// parameter.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [SliverAnimatedGrid]'s state with a global key:
///
/// ```dart
/// // (e.g. in a stateful widget)
/// GlobalKey<AnimatedGridState> gridKey = GlobalKey<AnimatedGridState>();
///
/// // ...
///
/// @override
/// Widget build(BuildContext context) {
/// return AnimatedGrid(
/// key: gridKey,
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
/// return const Placeholder();
/// },
/// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 100.0),
/// );
/// }
///
/// // ...
///
/// void _updateGrid() {
/// // adds "123" to the AnimatedGrid
/// gridKey.currentState!.insertItem(123);
/// }
/// ```
///
/// [SliverAnimatedGrid] item input handlers can also refer to their
/// [SliverAnimatedGridState] with the static [SliverAnimatedGrid.of] method.
class
SliverAnimatedGridState
extends
State
<
SliverAnimatedGrid
>
with
TickerProviderStateMixin
{
final
List
<
_ActiveItem
>
_incomingItems
=
<
_ActiveItem
>[];
final
List
<
_ActiveItem
>
_outgoingItems
=
<
_ActiveItem
>[];
int
_itemsCount
=
0
;
@override
void
initState
()
{
super
.
initState
();
_itemsCount
=
widget
.
initialItemCount
;
}
@override
void
dispose
()
{
for
(
final
_ActiveItem
item
in
_incomingItems
.
followedBy
(
_outgoingItems
))
{
item
.
controller
!.
dispose
();
}
super
.
dispose
();
}
_ActiveItem
?
_removeActiveItemAt
(
List
<
_ActiveItem
>
items
,
int
itemIndex
)
{
final
int
i
=
binarySearch
(
items
,
_ActiveItem
.
index
(
itemIndex
));
return
i
==
-
1
?
null
:
items
.
removeAt
(
i
);
}
_ActiveItem
?
_activeItemAt
(
List
<
_ActiveItem
>
items
,
int
itemIndex
)
{
final
int
i
=
binarySearch
(
items
,
_ActiveItem
.
index
(
itemIndex
));
return
i
==
-
1
?
null
:
items
[
i
];
}
// The insertItem() and removeItem() index parameters are defined as if the
// removeItem() operation removed the corresponding grid entry immediately.
// The entry is only actually removed from the grid when the remove animation
// finishes. The entry is added to _outgoingItems when removeItem is called
// and removed from _outgoingItems when the remove animation finishes.
int
_indexToItemIndex
(
int
index
)
{
int
itemIndex
=
index
;
for
(
final
_ActiveItem
item
in
_outgoingItems
)
{
if
(
item
.
itemIndex
<=
itemIndex
)
{
itemIndex
+=
1
;
}
else
{
break
;
}
}
return
itemIndex
;
}
int
_itemIndexToIndex
(
int
itemIndex
)
{
int
index
=
itemIndex
;
for
(
final
_ActiveItem
item
in
_outgoingItems
)
{
assert
(
item
.
itemIndex
!=
itemIndex
);
if
(
item
.
itemIndex
<
itemIndex
)
{
index
-=
1
;
}
else
{
break
;
}
}
return
index
;
}
SliverChildDelegate
_createDelegate
()
{
return
SliverChildBuilderDelegate
(
_itemBuilder
,
childCount:
_itemsCount
,
findChildIndexCallback:
widget
.
findChildIndexCallback
==
null
?
null
:
(
Key
key
)
{
final
int
?
index
=
widget
.
findChildIndexCallback
!(
key
);
return
index
!=
null
?
_indexToItemIndex
(
index
)
:
null
;
},
);
}
/// Insert an item at [index] and start an animation that will be passed to
/// [SliverAnimatedGrid.itemBuilder] when the item is visible.
///
/// This method's semantics are the same as Dart's [List.insert] method: it
/// increases the length of the list of items in the grid by one and shifts
/// all items at or after [index] towards the end of the list of items in the
/// grid.
void
insertItem
(
int
index
,
{
Duration
duration
=
_kDuration
})
{
assert
(
index
!=
null
&&
index
>=
0
);
assert
(
duration
!=
null
);
final
int
itemIndex
=
_indexToItemIndex
(
index
);
assert
(
itemIndex
>=
0
&&
itemIndex
<=
_itemsCount
);
// Increment the incoming and outgoing item indices to account
// for the insertion.
for
(
final
_ActiveItem
item
in
_incomingItems
)
{
if
(
item
.
itemIndex
>=
itemIndex
)
{
item
.
itemIndex
+=
1
;
}
}
for
(
final
_ActiveItem
item
in
_outgoingItems
)
{
if
(
item
.
itemIndex
>=
itemIndex
)
{
item
.
itemIndex
+=
1
;
}
}
final
AnimationController
controller
=
AnimationController
(
duration:
duration
,
vsync:
this
,
);
final
_ActiveItem
incomingItem
=
_ActiveItem
.
incoming
(
controller
,
itemIndex
,
);
setState
(()
{
_incomingItems
..
add
(
incomingItem
)
..
sort
();
_itemsCount
+=
1
;
});
controller
.
forward
().
then
<
void
>((
_
)
{
_removeActiveItemAt
(
_incomingItems
,
incomingItem
.
itemIndex
)!.
controller
!.
dispose
();
});
}
/// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible.
///
/// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [SliverAnimatedGrid.itemBuilder]. However
/// the item will still appear in the grid for [duration] and during that time
/// [builder] must construct its widget as needed.
///
/// This method's semantics are the same as Dart's [List.remove] method: it
/// decreases the length of the list of items in the grid by one and shifts
/// all items at or before [index] towards the beginning of the list of items
/// in the grid.
void
removeItem
(
int
index
,
AnimatedRemovedItemBuilder
builder
,
{
Duration
duration
=
_kDuration
})
{
assert
(
index
!=
null
&&
index
>=
0
);
assert
(
builder
!=
null
);
assert
(
duration
!=
null
);
final
int
itemIndex
=
_indexToItemIndex
(
index
);
assert
(
itemIndex
>=
0
&&
itemIndex
<
_itemsCount
);
assert
(
_activeItemAt
(
_outgoingItems
,
itemIndex
)
==
null
);
final
_ActiveItem
?
incomingItem
=
_removeActiveItemAt
(
_incomingItems
,
itemIndex
);
final
AnimationController
controller
=
incomingItem
?.
controller
??
AnimationController
(
duration:
duration
,
value:
1.0
,
vsync:
this
);
final
_ActiveItem
outgoingItem
=
_ActiveItem
.
outgoing
(
controller
,
itemIndex
,
builder
);
setState
(()
{
_outgoingItems
..
add
(
outgoingItem
)
..
sort
();
});
controller
.
reverse
().
then
<
void
>((
void
value
)
{
_removeActiveItemAt
(
_outgoingItems
,
outgoingItem
.
itemIndex
)!.
controller
!.
dispose
();
// Decrement the incoming and outgoing item indices to account
// for the removal.
for
(
final
_ActiveItem
item
in
_incomingItems
)
{
if
(
item
.
itemIndex
>
outgoingItem
.
itemIndex
)
{
item
.
itemIndex
-=
1
;
}
}
for
(
final
_ActiveItem
item
in
_outgoingItems
)
{
if
(
item
.
itemIndex
>
outgoingItem
.
itemIndex
)
{
item
.
itemIndex
-=
1
;
}
}
setState
(()
=>
_itemsCount
-=
1
);
});
}
Widget
_itemBuilder
(
BuildContext
context
,
int
itemIndex
)
{
final
_ActiveItem
?
outgoingItem
=
_activeItemAt
(
_outgoingItems
,
itemIndex
);
if
(
outgoingItem
!=
null
)
{
return
outgoingItem
.
removedItemBuilder
!(
context
,
outgoingItem
.
controller
!.
view
,
);
}
final
_ActiveItem
?
incomingItem
=
_activeItemAt
(
_incomingItems
,
itemIndex
);
final
Animation
<
double
>
animation
=
incomingItem
?.
controller
?.
view
??
kAlwaysCompleteAnimation
;
return
widget
.
itemBuilder
(
context
,
_itemIndexToIndex
(
itemIndex
),
animation
,
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
SliverGrid
(
gridDelegate:
widget
.
gridDelegate
,
delegate:
_createDelegate
(),
);
}
}
packages/flutter/lib/src/widgets/animated_list.dart
View file @
7523ab5c
...
...
@@ -58,6 +58,10 @@ class _ActiveItem implements Comparable<_ActiveItem> {
///
/// * [SliverAnimatedList], a sliver that animates items when they are inserted
/// or removed from a list.
/// * [SliverAnimatedGrid], a sliver which animates items when they are
/// inserted or removed from a grid.
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
/// they are inserted or removed in a grid.
class
AnimatedList
extends
StatefulWidget
{
/// Creates a scrolling container that animates items when they are inserted
/// or removed.
...
...
@@ -349,6 +353,10 @@ class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixi
/// removed.
/// * [AnimatedList], a non-sliver scrolling container that animates items when
/// they are inserted or removed.
/// * [SliverAnimatedGrid], a sliver which animates items when they are
/// inserted into or removed from a grid.
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
/// they are inserted into or removed from a grid.
class
SliverAnimatedList
extends
StatefulWidget
{
/// Creates a sliver that animates items when they are inserted or removed.
const
SliverAnimatedList
({
...
...
packages/flutter/lib/src/widgets/sliver.dart
View file @
7523ab5c
...
...
@@ -1023,7 +1023,11 @@ abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget {
/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList]
/// except that it uses a prototype list item instead of a pixel value to define
/// the main axis extent of each item.
/// * [SliverGrid], which places its children in arbitrary positions.
/// * [SliverAnimatedList], which animates items added to or removed from a
/// list.
/// * [SliverGrid], which places multiple children in a two dimensional grid.
/// * [SliverAnimatedGrid], a sliver which animates items when they are
/// inserted into or removed from a grid.
class
SliverList
extends
SliverMultiBoxAdaptorWidget
{
/// Creates a sliver that places box children in a linear array.
const
SliverList
({
...
...
packages/flutter/lib/widgets.dart
View file @
7523ab5c
...
...
@@ -19,6 +19,7 @@ export 'foundation.dart' show UniqueKey;
export
'rendering.dart'
show
TextSelectionHandleType
;
export
'src/widgets/actions.dart'
;
export
'src/widgets/animated_cross_fade.dart'
;
export
'src/widgets/animated_grid.dart'
;
export
'src/widgets/animated_list.dart'
;
export
'src/widgets/animated_size.dart'
;
export
'src/widgets/animated_switcher.dart'
;
...
...
packages/flutter/test/widgets/animated_grid_test.dart
0 → 100644
View file @
7523ab5c
// Copyright 2014 The Flutter 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/src/foundation/diagnostics.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
// Regression test for https://github.com/flutter/flutter/issues/100451
testWidgets
(
'SliverAnimatedGrid.builder respects findChildIndexCallback'
,
(
WidgetTester
tester
)
async
{
bool
finderCalled
=
false
;
int
itemCount
=
7
;
late
StateSetter
stateSetter
;
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
stateSetter
=
setState
;
return
CustomScrollView
(
slivers:
<
Widget
>[
SliverAnimatedGrid
(
initialItemCount:
itemCount
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
=>
Container
(
key:
Key
(
'
$index
'
),
height:
2000.0
,
),
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
mainAxisSpacing:
10.0
,
crossAxisSpacing:
10.0
,
),
findChildIndexCallback:
(
Key
key
)
{
finderCalled
=
true
;
return
null
;
},
),
],
);
},
),
));
expect
(
finderCalled
,
false
);
// Trigger update.
stateSetter
(()
=>
itemCount
=
77
);
await
tester
.
pump
();
expect
(
finderCalled
,
true
);
});
testWidgets
(
'AnimatedGrid'
,
(
WidgetTester
tester
)
async
{
Widget
builder
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'item
$index
'
),
),
);
}
final
GlobalKey
<
AnimatedGridState
>
listKey
=
GlobalKey
<
AnimatedGridState
>();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
AnimatedGrid
(
key:
listKey
,
initialItemCount:
2
,
itemBuilder:
builder
,
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
mainAxisSpacing:
10.0
,
crossAxisSpacing:
10.0
,
),
),
),
);
expect
(
find
.
byWidgetPredicate
((
Widget
widget
)
{
return
widget
is
SliverAnimatedGrid
&&
widget
.
initialItemCount
==
2
&&
widget
.
itemBuilder
==
builder
;
}),
findsOneWidget
);
listKey
.
currentState
!.
insertItem
(
0
);
await
tester
.
pump
();
expect
(
find
.
text
(
'item 2'
),
findsOneWidget
);
listKey
.
currentState
!.
removeItem
(
2
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
{
return
const
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'removing item'
)),
);
},
duration:
const
Duration
(
milliseconds:
100
),
);
await
tester
.
pump
();
expect
(
find
.
text
(
'removing item'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 2'
),
findsNothing
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'removing item'
),
findsNothing
);
});
group
(
'SliverAnimatedGrid'
,
()
{
testWidgets
(
'initialItemCount'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Animation
<
double
>>
animations
=
<
int
,
Animation
<
double
>>{};
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
CustomScrollView
(
slivers:
<
Widget
>[
SliverAnimatedGrid
(
initialItemCount:
2
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
animations
[
index
]
=
animation
;
return
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'item
$index
'
),
),
);
},
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
mainAxisSpacing:
10.0
,
crossAxisSpacing:
10.0
,
),
),
],
),
),
);
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
expect
(
animations
.
containsKey
(
0
),
true
);
expect
(
animations
.
containsKey
(
1
),
true
);
expect
(
animations
[
0
]!.
value
,
1.0
);
expect
(
animations
[
1
]!.
value
,
1.0
);
});
testWidgets
(
'insert'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
SliverAnimatedGridState
>
listKey
=
GlobalKey
<
SliverAnimatedGridState
>();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
CustomScrollView
(
slivers:
<
Widget
>[
SliverAnimatedGrid
(
key:
listKey
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
ScaleTransition
(
key:
ValueKey
<
int
>(
index
),
scale:
animation
,
child:
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'item
$index
'
)),
),
);
},
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
),
),
],
),
),
);
double
itemScale
(
int
index
)
=>
tester
.
widget
<
ScaleTransition
>(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
scale
.
value
;
double
itemLeft
(
int
index
)
=>
tester
.
getTopLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dx
;
double
itemRight
(
int
index
)
=>
tester
.
getTopRight
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dx
;
listKey
.
currentState
!.
insertItem
(
0
,
duration:
const
Duration
(
milliseconds:
100
));
await
tester
.
pump
();
// Newly inserted item 0's scale should animate from 0 to 1
expect
(
itemScale
(
0
),
0.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemScale
(
0
),
0.5
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemScale
(
0
),
1.0
);
// The list now contains one fully expanded item at the top:
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
expect
(
itemLeft
(
0
),
0.0
);
expect
(
itemRight
(
0
),
100.0
);
listKey
.
currentState
!.
insertItem
(
0
,
duration:
const
Duration
(
milliseconds:
100
));
listKey
.
currentState
!.
insertItem
(
0
,
duration:
const
Duration
(
milliseconds:
100
));
await
tester
.
pump
();
// The scale of the newly inserted items at index 0 and 1 should animate
// from 0 to 1.
// The scale of the original item, now at index 2, should remain 1.
expect
(
itemScale
(
0
),
0.0
);
expect
(
itemScale
(
1
),
0.0
);
expect
(
itemScale
(
2
),
1.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemScale
(
0
),
0.5
);
expect
(
itemScale
(
1
),
0.5
);
expect
(
itemScale
(
2
),
1.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemScale
(
0
),
1.0
);
expect
(
itemScale
(
1
),
1.0
);
expect
(
itemScale
(
2
),
1.0
);
// The newly inserted "item 1" and "item 2" appear above "item 0"
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 2'
),
findsOneWidget
);
expect
(
itemLeft
(
0
),
0.0
);
expect
(
itemRight
(
0
),
100.0
);
expect
(
itemLeft
(
1
),
100.0
);
expect
(
itemRight
(
1
),
200.0
);
expect
(
itemLeft
(
2
),
200.0
);
expect
(
itemRight
(
2
),
300.0
);
});
testWidgets
(
'remove'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
SliverAnimatedGridState
>
listKey
=
GlobalKey
<
SliverAnimatedGridState
>();
final
List
<
int
>
items
=
<
int
>[
0
,
1
,
2
];
Widget
buildItem
(
BuildContext
context
,
int
item
,
Animation
<
double
>
animation
)
{
return
ScaleTransition
(
key:
ValueKey
<
int
>(
item
),
scale:
animation
,
child:
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'item
$item
'
,
textDirection:
TextDirection
.
ltr
),
),
),
);
}
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
CustomScrollView
(
slivers:
<
Widget
>[
SliverAnimatedGrid
(
key:
listKey
,
initialItemCount:
3
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
buildItem
(
context
,
items
[
index
],
animation
);
},
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
),
),
],
),
),
);
double
itemScale
(
int
index
)
=>
tester
.
widget
<
ScaleTransition
>(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
scale
.
value
;
double
itemLeft
(
int
index
)
=>
tester
.
getTopLeft
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dx
;
double
itemRight
(
int
index
)
=>
tester
.
getTopRight
(
find
.
byKey
(
ValueKey
<
int
>(
index
),
skipOffstage:
false
)).
dx
;
expect
(
find
.
text
(
'item 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'item 2'
),
findsOneWidget
);
items
.
removeAt
(
0
);
listKey
.
currentState
!.
removeItem
(
0
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
=>
buildItem
(
context
,
0
,
animation
),
duration:
const
Duration
(
milliseconds:
100
),
);
// Items 0, 1, 2 at 0, 100, 200. All heights 100.
expect
(
itemLeft
(
0
),
0.0
);
expect
(
itemRight
(
0
),
100.0
);
expect
(
itemLeft
(
1
),
100.0
);
expect
(
itemRight
(
1
),
200.0
);
expect
(
itemLeft
(
2
),
200.0
);
expect
(
itemRight
(
2
),
300.0
);
// Newly removed item 0's height should animate from 100 to 0 over 100ms
// Items 0, 1, 2 at 0, 50, 150. Item 0's height is 50.
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
itemScale
(
0
),
0.5
);
expect
(
itemScale
(
1
),
1.0
);
expect
(
itemScale
(
2
),
1.0
);
// Items 1, 2 at 0, 100.
await
tester
.
pumpAndSettle
();
expect
(
itemLeft
(
1
),
0.0
);
expect
(
itemRight
(
1
),
100.0
);
expect
(
itemLeft
(
2
),
100.0
);
expect
(
itemRight
(
2
),
200.0
);
});
testWidgets
(
'works in combination with other slivers'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
SliverAnimatedGridState
>
listKey
=
GlobalKey
<
SliverAnimatedGridState
>();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
CustomScrollView
(
slivers:
<
Widget
>[
SliverList
(
delegate:
SliverChildListDelegate
(<
Widget
>[
const
SizedBox
(
height:
100
),
const
SizedBox
(
height:
100
),
]),
),
SliverAnimatedGrid
(
key:
listKey
,
initialItemCount:
3
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
SizedBox
(
height:
100
,
child:
Text
(
'item
$index
'
),
);
},
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
),
),
],
),
),
);
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 0'
)).
dx
,
0
);
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 1'
)).
dx
,
100
);
listKey
.
currentState
!.
insertItem
(
3
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 3'
)).
dx
,
300
);
listKey
.
currentState
!.
removeItem
(
0
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
{
return
ScaleTransition
(
scale:
animation
,
key:
const
ObjectKey
(
'removing'
),
child:
const
SizedBox
(
height:
100
,
child:
Text
(
'removing'
),
),
);
},
duration:
const
Duration
(
seconds:
1
),
);
await
tester
.
pump
();
expect
(
find
.
text
(
'item 3'
),
findsNothing
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
tester
.
widget
<
ScaleTransition
>(
find
.
byKey
(
const
ObjectKey
(
'removing'
),
skipOffstage:
false
)).
scale
.
value
,
0.5
,
);
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 0'
)).
dx
,
100
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'removing'
),
findsNothing
);
expect
(
tester
.
getTopLeft
(
find
.
text
(
'item 0'
)).
dx
,
0
);
});
testWidgets
(
'passes correctly derived index of findChildIndexCallback to the inner SliverChildBuilderDelegate'
,
(
WidgetTester
tester
)
async
{
final
List
<
int
>
items
=
<
int
>[
0
,
1
,
2
,
3
];
final
GlobalKey
<
SliverAnimatedGridState
>
listKey
=
GlobalKey
<
SliverAnimatedGridState
>();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
CustomScrollView
(
slivers:
<
Widget
>[
SliverAnimatedGrid
(
key:
listKey
,
initialItemCount:
items
.
length
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
animation
)
{
return
_StatefulListItem
(
key:
ValueKey
<
int
>(
items
[
index
]),
index:
index
,
);
},
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
mainAxisSpacing:
10.0
,
crossAxisSpacing:
10.0
,
),
findChildIndexCallback:
(
Key
key
)
{
final
int
index
=
items
.
indexOf
((
key
as
ValueKey
<
int
>).
value
);
return
index
==
-
1
?
null
:
index
;
},
),
],
),
),
);
// get all list entries in order
final
List
<
Text
>
listEntries
=
find
.
byType
(
Text
).
evaluate
().
map
((
Element
e
)
=>
e
.
widget
as
Text
).
toList
();
// check that the list is rendered in the correct order
expect
(
listEntries
[
0
].
data
,
equals
(
'item 0'
));
expect
(
listEntries
[
1
].
data
,
equals
(
'item 1'
));
expect
(
listEntries
[
2
].
data
,
equals
(
'item 2'
));
expect
(
listEntries
[
3
].
data
,
equals
(
'item 3'
));
// delete one item
listKey
.
currentState
?.
removeItem
(
0
,
(
BuildContext
context
,
Animation
<
double
>
animation
)
{
return
Container
();
});
// delete from list
items
.
removeAt
(
0
);
// reorder list
items
.
insert
(
0
,
items
.
removeLast
());
// render with new list order
await
tester
.
pumpAndSettle
();
// get all list entries in order
final
List
<
Text
>
reorderedListEntries
=
find
.
byType
(
Text
).
evaluate
().
map
((
Element
e
)
=>
e
.
widget
as
Text
).
toList
();
// check that the stateful items of the list are rendered in the order provided by findChildIndexCallback
expect
(
reorderedListEntries
[
0
].
data
,
equals
(
'item 3'
));
expect
(
reorderedListEntries
[
1
].
data
,
equals
(
'item 1'
));
expect
(
reorderedListEntries
[
2
].
data
,
equals
(
'item 2'
));
});
});
testWidgets
(
'AnimatedGrid.of() and maybeOf called with a context that does not contain AnimatedGrid'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
key
=
GlobalKey
();
await
tester
.
pumpWidget
(
Container
(
key:
key
));
late
FlutterError
error
;
expect
(
AnimatedGrid
.
maybeOf
(
key
.
currentContext
!),
isNull
);
try
{
AnimatedGrid
.
of
(
key
.
currentContext
!);
}
on
FlutterError
catch
(
e
)
{
error
=
e
;
}
expect
(
error
.
diagnostics
.
length
,
4
);
expect
(
error
.
diagnostics
[
2
].
level
,
DiagnosticLevel
.
hint
);
expect
(
error
.
diagnostics
[
2
].
toStringDeep
(),
equalsIgnoringHashCodes
(
'This can happen when the context provided is from the same
\n
'
'StatefulWidget that built the AnimatedGrid. Please see the
\n
'
'AnimatedGrid documentation for examples of how to refer to an
\n
'
'AnimatedGridState object:
\n
'
' https://api.flutter.dev/flutter/widgets/AnimatedGridState-class.html
\n
'
,
),
);
expect
(
error
.
diagnostics
[
3
],
isA
<
DiagnosticsProperty
<
Element
>>());
expect
(
error
.
toStringDeep
(),
equalsIgnoringHashCodes
(
'FlutterError
\n
'
' AnimatedGrid.of() called with a context that does not contain an
\n
'
' AnimatedGrid.
\n
'
' No AnimatedGrid ancestor could be found starting from the context
\n
'
' that was passed to AnimatedGrid.of().
\n
'
' This can happen when the context provided is from the same
\n
'
' StatefulWidget that built the AnimatedGrid. Please see the
\n
'
' AnimatedGrid documentation for examples of how to refer to an
\n
'
' AnimatedGridState object:
\n
'
' https://api.flutter.dev/flutter/widgets/AnimatedGridState-class.html
\n
'
' The context used was:
\n
'
' Container-[GlobalKey#32cc6]
\n
'
,
),
);
},
);
testWidgets
(
'AnimatedGrid.clipBehavior is forwarded to its inner CustomScrollView'
,
(
WidgetTester
tester
)
async
{
const
Clip
clipBehavior
=
Clip
.
none
;
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
AnimatedGrid
(
initialItemCount:
2
,
clipBehavior:
clipBehavior
,
itemBuilder:
(
BuildContext
context
,
int
index
,
Animation
<
double
>
_
)
{
return
SizedBox
(
height:
100.0
,
child:
Center
(
child:
Text
(
'item
$index
'
),
),
);
},
gridDelegate:
const
SliverGridDelegateWithMaxCrossAxisExtent
(
maxCrossAxisExtent:
100.0
,
mainAxisSpacing:
10.0
,
crossAxisSpacing:
10.0
,
),
),
),
);
expect
(
tester
.
widget
<
CustomScrollView
>(
find
.
byType
(
CustomScrollView
)).
clipBehavior
,
clipBehavior
);
});
}
class
_StatefulListItem
extends
StatefulWidget
{
const
_StatefulListItem
({
super
.
key
,
required
this
.
index
,
});
final
int
index
;
@override
_StatefulListItemState
createState
()
=>
_StatefulListItemState
();
}
class
_StatefulListItemState
extends
State
<
_StatefulListItem
>
{
late
final
int
number
=
widget
.
index
;
@override
Widget
build
(
BuildContext
context
)
{
return
Text
(
'item
$number
'
);
}
}
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