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
bcfc293c
Commit
bcfc293c
authored
Jan 25, 2020
by
Dan Field
Committed by
Flutter GitHub Bot
Jan 25, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
recommendDeferredLoading (#49319)
parent
6b8f013a
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
303 additions
and
0 deletions
+303
-0
scroll_physics.dart
packages/flutter/lib/src/widgets/scroll_physics.dart
+53
-0
scroll_position.dart
packages/flutter/lib/src/widgets/scroll_position.dart
+17
-0
scrollable.dart
packages/flutter/lib/src/widgets/scrollable.dart
+24
-0
scrollable_test.dart
packages/flutter/test/widgets/scrollable_test.dart
+209
-0
No files found.
packages/flutter/lib/src/widgets/scroll_physics.dart
View file @
bcfc293c
...
...
@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import
'package:flutter/physics.dart'
;
import
'binding.dart'
show
WidgetsBinding
;
import
'framework.dart'
;
import
'overscroll_indicator.dart'
;
import
'scroll_metrics.dart'
;
import
'scroll_simulation.dart'
;
...
...
@@ -135,6 +136,58 @@ class ScrollPhysics {
return
parent
.
shouldAcceptUserOffset
(
position
);
}
/// Provides a heuristic to determine if expensive frame-bound tasks should be
/// deferred.
///
/// The velocity parameter must not be null, but may be positive, negative, or
/// zero.
///
/// The metrics parameter must not be null.
///
/// The context parameter must not be null. It normally refers to the
/// [BuildContext] of the widget making the call, such as an [Image] widget
/// in a [ListView].
///
/// This can be used to determine whether decoding or fetching complex data
/// for the currently visible part of the viewport should be delayed
/// to avoid doing work that will not have a chance to appear before a new
/// frame is rendered.
///
/// For example, a list of images could use this logic to delay decoding
/// images until scrolling is slow enough to actually render the decoded
/// image to the screen.
///
/// The default implementation is a heuristic that compares the current
/// scroll velocity in local logical pixels to the longest side of the window
/// in physical pixels. Implementers can change this heuristic by overriding
/// this method and providing their custom physics to the scrollable widget.
/// For example, an application that changes the local coordinate system with
/// a large perspective transform could provide a more or less aggressive
/// heuristic depending on whether the transform was increasing or decreasing
/// the overall scale between the global screen and local scrollable
/// coordinate systems.
///
/// The default implementation is stateless, and simply provides a point-in-
/// time decision about how fast the scrollable is scrolling. It would always
/// return true for a scrollable that is animating back and forth at high
/// velocity in a loop. It is assumed that callers will handle such
/// a case, or that a custom stateful implementation would be written that
/// tracks the sign of the velocity on successive calls.
///
/// Returning true from this method indicates that the current scroll velocity
/// is great enough that expensive operations impacting the UI should be
/// deferred.
bool
recommendDeferredLoading
(
double
velocity
,
ScrollMetrics
metrics
,
BuildContext
context
)
{
assert
(
velocity
!=
null
);
assert
(
metrics
!=
null
);
assert
(
context
!=
null
);
if
(
parent
==
null
)
{
final
double
maxPhysicalPixels
=
WidgetsBinding
.
instance
.
window
.
physicalSize
.
longestSide
;
return
velocity
.
abs
()
>
maxPhysicalPixels
;
}
return
parent
.
recommendDeferredLoading
(
velocity
,
metrics
,
context
);
}
/// Determines the overscroll by applying the boundary conditions.
///
/// Called by [ScrollPosition.applyBoundaryConditions], which is called by
...
...
packages/flutter/lib/src/widgets/scroll_position.dart
View file @
bcfc293c
...
...
@@ -741,6 +741,23 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
UserScrollNotification
(
metrics:
copyWith
(),
context:
context
.
notificationContext
,
direction:
direction
).
dispatch
(
context
.
notificationContext
);
}
/// Provides a heuristic to determine if expensive frame-bound tasks should be
/// deferred.
///
/// The actual work of this is delegated to the [physics] via
/// [ScrollPhysics.recommendDeferredScrolling] called with the current
/// [activity]'s [ScrollActivity.velocity].
///
/// Returning true from this method indicates that the [ScrollPhysics]
/// evaluate the current scroll velocity to be great enough that expensive
/// operations impacting the UI should be deferred.
bool
recommendDeferredLoading
(
BuildContext
context
)
{
assert
(
context
!=
null
);
assert
(
activity
!=
null
);
assert
(
activity
.
velocity
!=
null
);
return
physics
.
recommendDeferredLoading
(
activity
.
velocity
,
copyWith
(),
context
);
}
@override
void
dispose
()
{
activity
?.
dispose
();
// it will be null if it got absorbed by another ScrollPosition
...
...
packages/flutter/lib/src/widgets/scrollable.dart
View file @
bcfc293c
...
...
@@ -246,11 +246,35 @@ class Scrollable extends StatefulWidget {
/// ```dart
/// ScrollableState scrollable = Scrollable.of(context);
/// ```
///
/// Calling this method will create a dependency on the closest [Scrollable]
/// in the [context], if there is one.
static
ScrollableState
of
(
BuildContext
context
)
{
final
_ScrollableScope
widget
=
context
.
dependOnInheritedWidgetOfExactType
<
_ScrollableScope
>();
return
widget
?.
scrollable
;
}
/// Provides a heuristic to determine if expensive frame-bound tasks should be
/// deferred for the [context] at a specific point in time.
///
/// Calling this method does _not_ create a dependency on any other widget.
/// This also means that the value returned is only good for the point in time
/// when it is called, and callers will not get updated if the value changes.
///
/// The heuristic used is determined by the [physics] of this [Scrollable]
/// via [ScrollPhysics.recommendDeferredScrolling]. That method is called with
/// the current [activity]'s [ScrollActivity.velocity].
///
/// If there is no [Scrollable] in the widget tree above the [context], this
/// method returns false.
static
bool
recommendDeferredLoadingForContext
(
BuildContext
context
)
{
final
_ScrollableScope
widget
=
context
.
getElementForInheritedWidgetOfExactType
<
_ScrollableScope
>()?.
widget
as
_ScrollableScope
;
if
(
widget
==
null
)
{
return
false
;
}
return
widget
.
position
.
recommendDeferredLoading
(
context
);
}
/// Scrolls the scrollables that enclose the given context so as to make the
/// given context visible.
static
Future
<
void
>
ensureVisible
(
...
...
packages/flutter/test/widgets/scrollable_test.dart
View file @
bcfc293c
...
...
@@ -676,4 +676,213 @@ void main() {
// of Platform.isMacOS, don't skip this on web anymore.
// https://github.com/flutter/flutter/issues/31366
},
skip:
kIsWeb
);
testWidgets
(
'Can recommendDeferredLoadingForContext - animation'
,
(
WidgetTester
tester
)
async
{
final
List
<
String
>
widgetTracker
=
<
String
>[];
int
cheapWidgets
=
0
;
int
expensiveWidgets
=
0
;
final
ScrollController
controller
=
ScrollController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
ListView
.
builder
(
controller:
controller
,
itemBuilder:
(
BuildContext
context
,
int
index
)
{
if
(
Scrollable
.
recommendDeferredLoadingForContext
(
context
))
{
cheapWidgets
+=
1
;
widgetTracker
.
add
(
'cheap'
);
return
const
SizedBox
(
height:
50.0
);
}
widgetTracker
.
add
(
'expensive'
);
expensiveWidgets
+=
1
;
return
const
SizedBox
(
height:
50.0
);
},
),
));
await
tester
.
pumpAndSettle
();
expect
(
expensiveWidgets
,
17
);
expect
(
cheapWidgets
,
0
);
// The position value here is different from the maximum velocity we will
// reach, which is controlled by a combination of curve, duration, and
// position.
// This is just meant to be a pretty good simulation. A linear curve
// with these same parameters will never back off on the velocity enough
// to reset here.
controller
.
animateTo
(
5000
,
duration:
const
Duration
(
seconds:
2
),
curve:
Curves
.
linear
,
);
expect
(
expensiveWidgets
,
17
);
expect
(
widgetTracker
.
every
((
String
type
)
=>
type
==
'expensive'
),
true
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
expensiveWidgets
,
17
);
expect
(
cheapWidgets
,
25
);
expect
(
widgetTracker
.
skip
(
17
).
every
((
String
type
)
=>
type
==
'cheap'
),
true
);
await
tester
.
pumpAndSettle
();
expect
(
expensiveWidgets
,
22
);
expect
(
cheapWidgets
,
95
);
expect
(
widgetTracker
.
skip
(
17
).
skip
(
25
).
take
(
70
).
every
((
String
type
)
=>
type
==
'cheap'
),
true
);
expect
(
widgetTracker
.
skip
(
17
).
skip
(
25
).
skip
(
70
).
every
((
String
type
)
=>
type
==
'expensive'
),
true
);
});
testWidgets
(
'Can recommendDeferredLoadingForContext - ballistics'
,
(
WidgetTester
tester
)
async
{
int
cheapWidgets
=
0
;
int
expensiveWidgets
=
0
;
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
ListView
.
builder
(
itemBuilder:
(
BuildContext
context
,
int
index
)
{
if
(
Scrollable
.
recommendDeferredLoadingForContext
(
context
))
{
cheapWidgets
+=
1
;
return
const
SizedBox
(
height:
50.0
);
}
expensiveWidgets
+=
1
;
return
SizedBox
(
key:
ValueKey
<
String
>(
'Box
$index
'
),
height:
50.0
);
},
),
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 0'
)),
findsOneWidget
);
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 52'
)),
findsNothing
);
expect
(
expensiveWidgets
,
17
);
expect
(
cheapWidgets
,
0
);
// Getting the tester to simulate a life-like fling is difficult.
// Instead, just manually drive the activity with a ballistic simulation as
// if the user has flung the list.
Scrollable
.
of
(
find
.
byType
(
SizedBox
).
evaluate
().
first
).
position
.
activity
.
delegate
.
goBallistic
(
4000
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 0'
)),
findsNothing
);
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 52'
)),
findsOneWidget
);
expect
(
expensiveWidgets
,
38
);
expect
(
cheapWidgets
,
20
);
});
testWidgets
(
'Can recommendDeferredLoadingForContext - override heuristic'
,
(
WidgetTester
tester
)
async
{
int
cheapWidgets
=
0
;
int
expensiveWidgets
=
0
;
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
ListView
.
builder
(
physics:
SuperPessimisticScrollPhysics
(),
itemBuilder:
(
BuildContext
context
,
int
index
)
{
if
(
Scrollable
.
recommendDeferredLoadingForContext
(
context
))
{
cheapWidgets
+=
1
;
return
SizedBox
(
key:
ValueKey
<
String
>(
'Cheap box
$index
'
),
height:
50.0
);
}
expensiveWidgets
+=
1
;
return
SizedBox
(
key:
ValueKey
<
String
>(
'Box
$index
'
),
height:
50.0
);
},
),
));
await
tester
.
pumpAndSettle
();
final
ScrollPosition
position
=
Scrollable
.
of
(
find
.
byType
(
SizedBox
).
evaluate
().
first
).
position
;
final
SuperPessimisticScrollPhysics
physics
=
position
.
physics
as
SuperPessimisticScrollPhysics
;
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 0'
)),
findsOneWidget
);
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Cheap box 52'
)),
findsNothing
);
expect
(
physics
.
count
,
17
);
expect
(
expensiveWidgets
,
17
);
expect
(
cheapWidgets
,
0
);
// Getting the tester to simulate a life-like fling is difficult.
// Instead, just manually drive the activity with a ballistic simulation as
// if the user has flung the list.
position
.
activity
.
delegate
.
goBallistic
(
4000
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 0'
)),
findsNothing
);
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Cheap box 52'
)),
findsOneWidget
);
expect
(
expensiveWidgets
,
18
);
expect
(
cheapWidgets
,
40
);
expect
(
physics
.
count
,
40
+
18
);
});
testWidgets
(
'Can recommendDeferredLoadingForContext - override heuristic and always return true'
,
(
WidgetTester
tester
)
async
{
int
cheapWidgets
=
0
;
int
expensiveWidgets
=
0
;
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
ListView
.
builder
(
physics:
const
ExtraSuperPessimisticScrollPhysics
(),
itemBuilder:
(
BuildContext
context
,
int
index
)
{
if
(
Scrollable
.
recommendDeferredLoadingForContext
(
context
))
{
cheapWidgets
+=
1
;
return
SizedBox
(
key:
ValueKey
<
String
>(
'Cheap box
$index
'
),
height:
50.0
);
}
expensiveWidgets
+=
1
;
return
SizedBox
(
key:
ValueKey
<
String
>(
'Box
$index
'
),
height:
50.0
);
},
),
));
await
tester
.
pumpAndSettle
();
final
ScrollPosition
position
=
Scrollable
.
of
(
find
.
byType
(
SizedBox
).
evaluate
().
first
).
position
;
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Cheap box 0'
)),
findsOneWidget
);
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Cheap box 52'
)),
findsNothing
);
expect
(
expensiveWidgets
,
0
);
expect
(
cheapWidgets
,
17
);
// Getting the tester to simulate a life-like fling is difficult.
// Instead, just manually drive the activity with a ballistic simulation as
// if the user has flung the list.
position
.
activity
.
delegate
.
goBallistic
(
4000
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Cheap box 0'
)),
findsNothing
);
expect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Cheap box 52'
)),
findsOneWidget
);
expect
(
expensiveWidgets
,
0
);
expect
(
cheapWidgets
,
58
);
});
}
// ignore: must_be_immutable
class
SuperPessimisticScrollPhysics
extends
ScrollPhysics
{
SuperPessimisticScrollPhysics
({
ScrollPhysics
parent
})
:
super
(
parent:
parent
);
int
count
=
0
;
@override
bool
recommendDeferredLoading
(
double
velocity
,
ScrollMetrics
metrics
,
BuildContext
context
)
{
count
++;
return
velocity
>
1
;
}
@override
ScrollPhysics
applyTo
(
ScrollPhysics
ancestor
)
{
return
SuperPessimisticScrollPhysics
(
parent:
buildParent
(
ancestor
));
}
}
class
ExtraSuperPessimisticScrollPhysics
extends
ScrollPhysics
{
const
ExtraSuperPessimisticScrollPhysics
({
ScrollPhysics
parent
})
:
super
(
parent:
parent
);
@override
bool
recommendDeferredLoading
(
double
velocity
,
ScrollMetrics
metrics
,
BuildContext
context
)
{
return
true
;
}
@override
ScrollPhysics
applyTo
(
ScrollPhysics
ancestor
)
{
return
ExtraSuperPessimisticScrollPhysics
(
parent:
buildParent
(
ancestor
));
}
}
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