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
c8ad2014
Commit
c8ad2014
authored
May 03, 2017
by
Ian Hickson
Committed by
GitHub
May 03, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Save scroll position when scrolling. (#9740)
parent
89044a0e
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
200 additions
and
94 deletions
+200
-94
page_view.dart
packages/flutter/lib/src/widgets/page_view.dart
+15
-0
scroll_context.dart
packages/flutter/lib/src/widgets/scroll_context.dart
+8
-0
scroll_controller.dart
packages/flutter/lib/src/widgets/scroll_controller.dart
+18
-1
scroll_position.dart
packages/flutter/lib/src/widgets/scroll_position.dart
+48
-4
scrollable.dart
packages/flutter/lib/src/widgets/scrollable.dart
+3
-0
remember_scroll_position_test.dart
...s/flutter/test/widgets/remember_scroll_position_test.dart
+108
-89
No files found.
packages/flutter/lib/src/widgets/page_view.dart
View file @
c8ad2014
...
...
@@ -12,6 +12,7 @@ import 'package:flutter/rendering.dart';
import
'basic.dart'
;
import
'framework.dart'
;
import
'notification_listener.dart'
;
import
'page_storage.dart'
;
import
'scroll_context.dart'
;
import
'scroll_controller.dart'
;
import
'scroll_metrics.dart'
;
...
...
@@ -187,6 +188,20 @@ class _PagePosition extends ScrollPositionWithSingleContext {
double
get
page
=>
pixels
==
null
?
null
:
getPageFromPixels
(
pixels
.
clamp
(
minScrollExtent
,
maxScrollExtent
),
viewportDimension
);
@override
void
saveScrollOffset
()
{
PageStorage
.
of
(
context
.
storageContext
)?.
writeState
(
context
.
storageContext
,
getPageFromPixels
(
pixels
,
viewportDimension
));
}
@override
void
restoreScrollOffset
()
{
if
(
pixels
==
null
)
{
final
double
value
=
PageStorage
.
of
(
context
.
storageContext
)?.
readState
(
context
.
storageContext
);
if
(
value
!=
null
)
correctPixels
(
getPixelsFromPage
(
value
));
}
}
@override
bool
applyViewportDimension
(
double
viewportDimension
)
{
final
double
oldViewportDimensions
=
this
.
viewportDimension
;
...
...
packages/flutter/lib/src/widgets/scroll_context.dart
View file @
c8ad2014
...
...
@@ -27,6 +27,14 @@ abstract class ScrollContext {
/// [ScrollBehavior.buildViewportChrome].
BuildContext
get
notificationContext
;
/// The [BuildContext] that should be used when searching for a [PageStorage].
///
/// This context is typically the context of the scrollable widget itself. In
/// particular, it should involve any [GlobalKey]s that are dynamically
/// created as part of creating the scrolling widget, since those would be
/// different each time the widget is created.
BuildContext
get
storageContext
;
/// A [TickerProvider] to use when animating the scroll position.
TickerProvider
get
vsync
;
...
...
packages/flutter/lib/src/widgets/scroll_controller.dart
View file @
c8ad2014
...
...
@@ -180,7 +180,24 @@ class ScrollController extends ChangeNotifier {
/// resizes.
///
/// By default, returns a [ScrollPositionWithSingleContext].
ScrollPosition
createScrollPosition
(
///
/// The arguments are generally passed to the [ScrollPosition] being created:
///
/// * `physics`: An instance of [ScrollPhysics] that determines how the
/// [ScrollPosition] should react to user interactions, how it should
/// simulate scrolling when released or flung, etc. The value will not be
/// null. It typically comes from the [ScrollView] or other widget that
/// creates the [Scrollable], or, if none was provided, from the ambient
/// [ScrollConfiguration].
/// * `context`: A [ScrollContext] used for communicating with the object
/// that is to own the [ScrollPosition] (typically, this is the
/// [Scrollable] itself).
/// * `oldPosition`: If this is not the first time a [ScrollPosition] has
/// been created for this [Scrollable], this will be the previous instance.
/// This is used when the environment has changed and the [Scrollable]
/// needs to recreate the [ScrollPosition] object. It is null the first
/// time the [ScrollPosition] is created.
ScrollPosition
createScrollPosition
(
ScrollPhysics
physics
,
ScrollContext
context
,
ScrollPosition
oldPosition
,
...
...
packages/flutter/lib/src/widgets/scroll_position.dart
View file @
c8ad2014
...
...
@@ -12,6 +12,7 @@ import 'package:flutter/scheduler.dart';
import
'basic.dart'
;
import
'framework.dart'
;
import
'gesture_detector.dart'
;
import
'page_storage.dart'
;
import
'scroll_activity.dart'
;
import
'scroll_context.dart'
;
import
'scroll_metrics.dart'
;
...
...
@@ -71,6 +72,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
assert
(
context
.
vsync
!=
null
);
if
(
oldPosition
!=
null
)
absorb
(
oldPosition
);
restoreScrollOffset
();
}
/// How the scroll position should respond to user input.
...
...
@@ -259,15 +261,54 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// To cause the position to jump or animate to a new value, consider [jumpTo]
/// or [animateTo].
///
/// This should not be called during layout. Consider [correctPixels] if you
/// find you need to adjust the position during layout.
/// This should not be called during layout (e.g. when setting the initial
/// scroll offset). Consider [correctPixels] if you find you need to adjust
/// the position during layout.
@protected
void
forcePixels
(
double
value
)
{
assert
(
_
pixels
!=
null
);
assert
(
pixels
!=
null
);
_pixels
=
value
;
notifyListeners
();
}
/// Called whenever scrolling ends, to store the current scroll offset in a
/// storage mechanism with a lifetime that matches the app's lifetime.
///
/// The stored value will be used by [restoreScrollOffset] when the
/// [ScrollPosition] is recreated, in the case of the [Scrollable] being
/// disposed then recreated in the same session. This might happen, for
/// instance, if a [ListView] is on one of the pages inside a [TabBarView],
/// and that page is displayed, then hidden, then displayed again.
///
/// The default implementation writes the [pixels] using the nearest
/// [PageStorage] found from the [context]'s [ScrollContext.storageContext]
/// property.
@protected
void
saveScrollOffset
()
{
PageStorage
.
of
(
context
.
storageContext
)?.
writeState
(
context
.
storageContext
,
pixels
);
}
/// Called whenever the [ScrollPosition] is created, to restore the scroll
/// offset if possible.
///
/// The value is stored by [saveScrollOffset] when the scroll position
/// changes, so that it can be restored in the case of the [Scrollable] being
/// disposed then recreated in the same session. This might happen, for
/// instance, if a [ListView] is on one of the pages inside a [TabBarView],
/// and that page is displayed, then hidden, then displayed again.
///
/// The default implementation reads the value from the nearest [PageStorage]
/// found from the [context]'s [ScrollContext.storageContext] property, and
/// sets it using [correctPixels], if [pixels] is still null.
@protected
void
restoreScrollOffset
()
{
if
(
pixels
==
null
)
{
final
double
value
=
PageStorage
.
of
(
context
.
storageContext
)?.
readState
(
context
.
storageContext
);
if
(
value
!=
null
)
correctPixels
(
value
);
}
}
/// Returns the overscroll by applying the boundary conditions.
///
/// If the given value is in bounds, returns 0.0. Otherwise, returns the
...
...
@@ -467,7 +508,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
oldIgnorePointer
=
_activity
.
shouldIgnorePointer
;
wasScrolling
=
_activity
.
isScrolling
;
if
(
wasScrolling
&&
!
newActivity
.
isScrolling
)
didEndScroll
();
didEndScroll
();
// notifies and then saves the scroll offset
_activity
.
dispose
();
}
else
{
oldIgnorePointer
=
false
;
...
...
@@ -495,8 +536,11 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
}
/// Called by [beginActivity] to report when an activity has ended.
///
/// This also saves the scroll offset using [saveScrollOffset].
void
didEndScroll
()
{
activity
.
dispatchScrollEndNotification
(
cloneMetrics
(),
context
.
notificationContext
);
saveScrollOffset
();
}
/// Called by [setPixels] to report overscroll when an attempt is made to
...
...
packages/flutter/lib/src/widgets/scrollable.dart
View file @
c8ad2014
...
...
@@ -358,6 +358,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
@override
BuildContext
get
notificationContext
=>
_gestureDetectorKey
.
currentContext
;
@override
BuildContext
get
storageContext
=>
context
;
// TOUCH HANDLERS
Drag
_drag
;
...
...
packages/flutter/test/widgets/remember_scroll_position_test.dart
View file @
c8ad2014
...
...
@@ -8,6 +8,10 @@ import 'package:meta/meta.dart';
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/material.dart'
;
ScrollController
_controller
=
new
ScrollController
(
initialScrollOffset:
110.0
,
);
class
ThePositiveNumbers
extends
StatelessWidget
{
const
ThePositiveNumbers
({
@required
this
.
from
});
final
int
from
;
...
...
@@ -15,6 +19,7 @@ class ThePositiveNumbers extends StatelessWidget {
Widget
build
(
BuildContext
context
)
{
return
new
ListView
.
builder
(
itemExtent:
100.0
,
controller:
_controller
,
itemBuilder:
(
BuildContext
context
,
int
index
)
{
return
new
Text
(
'
${index + from}
'
,
key:
new
ValueKey
<
int
>(
index
));
}
...
...
@@ -22,98 +27,112 @@ class ThePositiveNumbers extends StatelessWidget {
}
}
Future
<
Null
>
performTest
(
WidgetTester
tester
)
async
{
Future
<
Null
>
performTest
(
WidgetTester
tester
,
bool
maintainState
)
async
{
final
GlobalKey
<
NavigatorState
>
navigatorKey
=
new
GlobalKey
<
NavigatorState
>();
await
tester
.
pumpWidget
(
new
Navigator
(
key:
navigatorKey
,
onGenerateRoute:
(
RouteSettings
settings
)
{
if
(
settings
.
name
==
'/'
)
{
return
new
MaterialPageRoute
<
Null
>(
settings:
settings
,
builder:
(
_
)
=>
new
Container
(
child:
const
ThePositiveNumbers
(
from:
0
)),
maintainState:
maintainState
,
);
}
else
if
(
settings
.
name
==
'/second'
)
{
return
new
MaterialPageRoute
<
Null
>(
settings:
settings
,
builder:
(
_
)
=>
new
Container
(
child:
const
ThePositiveNumbers
(
from:
10000
)),
maintainState:
maintainState
,
);
}
return
null
;
}
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 110.0, so we should have 7 items, 1..7.
expect
(
find
.
text
(
'0'
),
findsNothing
);
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'
),
findsOneWidget
);
expect
(
find
.
text
(
'8'
),
findsNothing
);
expect
(
find
.
text
(
'10'
),
findsNothing
);
expect
(
find
.
text
(
'100'
),
findsNothing
);
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
jumpTo
(
1000.0
);
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect
(
find
.
text
(
'0'
),
findsNothing
);
expect
(
find
.
text
(
'1'
),
findsNothing
);
expect
(
find
.
text
(
'8'
),
findsNothing
);
expect
(
find
.
text
(
'9'
),
findsNothing
);
expect
(
find
.
text
(
'10'
),
findsOneWidget
);
expect
(
find
.
text
(
'11'
),
findsOneWidget
);
expect
(
find
.
text
(
'12'
),
findsOneWidget
);
expect
(
find
.
text
(
'13'
),
findsOneWidget
);
expect
(
find
.
text
(
'14'
),
findsOneWidget
);
expect
(
find
.
text
(
'15'
),
findsOneWidget
);
expect
(
find
.
text
(
'16'
),
findsNothing
);
expect
(
find
.
text
(
'100'
),
findsNothing
);
navigatorKey
.
currentState
.
pushNamed
(
'/second'
);
await
tester
.
pump
();
// navigating always takes two frames, one to start...
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// ...and one to end the transition
// the second list is now visible, starting at 10001
expect
(
find
.
text
(
'0'
),
findsNothing
);
expect
(
find
.
text
(
'1'
),
findsNothing
);
expect
(
find
.
text
(
'10'
),
findsNothing
);
expect
(
find
.
text
(
'11'
),
findsNothing
);
expect
(
find
.
text
(
'10000'
),
findsNothing
);
expect
(
find
.
text
(
'10001'
),
findsOneWidget
);
expect
(
find
.
text
(
'10002'
),
findsOneWidget
);
expect
(
find
.
text
(
'10003'
),
findsOneWidget
);
expect
(
find
.
text
(
'10004'
),
findsOneWidget
);
expect
(
find
.
text
(
'10005'
),
findsOneWidget
);
expect
(
find
.
text
(
'10006'
),
findsOneWidget
);
expect
(
find
.
text
(
'10007'
),
findsOneWidget
);
expect
(
find
.
text
(
'10008'
),
findsNothing
);
expect
(
find
.
text
(
'10010'
),
findsNothing
);
expect
(
find
.
text
(
'10100'
),
findsNothing
);
navigatorKey
.
currentState
.
pop
();
await
tester
.
pump
();
// again, navigating always takes two frames
// Ensure we don't clamp the scroll offset even during the navigation.
// https://github.com/flutter/flutter/issues/4883
final
ScrollableState
state
=
tester
.
state
(
find
.
byType
(
Scrollable
).
first
);
expect
(
state
.
position
.
pixels
,
equals
(
1000.0
));
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect
(
find
.
text
(
'0'
),
findsNothing
);
expect
(
find
.
text
(
'1'
),
findsNothing
);
expect
(
find
.
text
(
'8'
),
findsNothing
);
expect
(
find
.
text
(
'9'
),
findsNothing
);
expect
(
find
.
text
(
'10'
),
findsOneWidget
);
expect
(
find
.
text
(
'11'
),
findsOneWidget
);
expect
(
find
.
text
(
'12'
),
findsOneWidget
);
expect
(
find
.
text
(
'13'
),
findsOneWidget
);
expect
(
find
.
text
(
'14'
),
findsOneWidget
);
expect
(
find
.
text
(
'15'
),
findsOneWidget
);
expect
(
find
.
text
(
'16'
),
findsNothing
);
expect
(
find
.
text
(
'100'
),
findsNothing
);
}
void
main
(
)
{
testWidgets
(
'whether we remember our scroll position'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
NavigatorState
>
navigatorKey
=
new
GlobalKey
<
NavigatorState
>();
await
tester
.
pumpWidget
(
new
Navigator
(
key:
navigatorKey
,
onGenerateRoute:
(
RouteSettings
settings
)
{
if
(
settings
.
name
==
'/'
)
{
return
new
MaterialPageRoute
<
Null
>(
settings:
settings
,
builder:
(
_
)
=>
new
Container
(
child:
const
ThePositiveNumbers
(
from:
0
)),
);
}
else
if
(
settings
.
name
==
'/second'
)
{
return
new
MaterialPageRoute
<
Null
>(
settings:
settings
,
builder:
(
_
)
=>
new
Container
(
child:
const
ThePositiveNumbers
(
from:
10000
)),
);
}
return
null
;
}
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// zero, so we should have exactly 6 items, 0..5.
expect
(
find
.
text
(
'0'
),
findsOneWidget
);
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'
),
findsNothing
);
expect
(
find
.
text
(
'10'
),
findsNothing
);
expect
(
find
.
text
(
'100'
),
findsNothing
);
tester
.
state
<
ScrollableState
>(
find
.
byType
(
Scrollable
)).
position
.
jumpTo
(
1000.0
);
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect
(
find
.
text
(
'0'
),
findsNothing
);
expect
(
find
.
text
(
'8'
),
findsNothing
);
expect
(
find
.
text
(
'9'
),
findsNothing
);
expect
(
find
.
text
(
'10'
),
findsOneWidget
);
expect
(
find
.
text
(
'11'
),
findsOneWidget
);
expect
(
find
.
text
(
'12'
),
findsOneWidget
);
expect
(
find
.
text
(
'13'
),
findsOneWidget
);
expect
(
find
.
text
(
'14'
),
findsOneWidget
);
expect
(
find
.
text
(
'15'
),
findsOneWidget
);
expect
(
find
.
text
(
'16'
),
findsNothing
);
expect
(
find
.
text
(
'100'
),
findsNothing
);
navigatorKey
.
currentState
.
pushNamed
(
'/second'
);
await
tester
.
pump
();
// navigating always takes two frames, one to start...
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// ...and one to end the transition
// the second list is now visible, starting at 10000
expect
(
find
.
text
(
'10000'
),
findsOneWidget
);
expect
(
find
.
text
(
'10001'
),
findsOneWidget
);
expect
(
find
.
text
(
'10002'
),
findsOneWidget
);
expect
(
find
.
text
(
'10003'
),
findsOneWidget
);
expect
(
find
.
text
(
'10004'
),
findsOneWidget
);
expect
(
find
.
text
(
'10005'
),
findsOneWidget
);
expect
(
find
.
text
(
'10006'
),
findsNothing
);
expect
(
find
.
text
(
'10010'
),
findsNothing
);
expect
(
find
.
text
(
'10100'
),
findsNothing
);
navigatorKey
.
currentState
.
pop
();
await
tester
.
pump
();
// again, navigating always takes two frames
// Ensure we don't clamp the scroll offset even during the navigation.
// https://github.com/flutter/flutter/issues/4883
final
ScrollableState
state
=
tester
.
state
(
find
.
byType
(
Scrollable
).
first
);
expect
(
state
.
position
.
pixels
,
equals
(
1000.0
));
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect
(
find
.
text
(
'0'
),
findsNothing
);
expect
(
find
.
text
(
'8'
),
findsNothing
);
expect
(
find
.
text
(
'9'
),
findsNothing
);
expect
(
find
.
text
(
'10'
),
findsOneWidget
);
expect
(
find
.
text
(
'11'
),
findsOneWidget
);
expect
(
find
.
text
(
'12'
),
findsOneWidget
);
expect
(
find
.
text
(
'13'
),
findsOneWidget
);
expect
(
find
.
text
(
'14'
),
findsOneWidget
);
expect
(
find
.
text
(
'15'
),
findsOneWidget
);
expect
(
find
.
text
(
'16'
),
findsNothing
);
expect
(
find
.
text
(
'100'
),
findsNothing
);
await
performTest
(
tester
,
true
);
await
performTest
(
tester
,
false
);
});
}
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