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
1f0df545
Unverified
Commit
1f0df545
authored
Nov 11, 2020
by
Kate Lovett
Committed by
GitHub
Nov 11, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Default Keyboard ScrollActions with PrimaryScrollController (#69795)
parent
273efff0
Changes
13
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
261 additions
and
141 deletions
+261
-141
page_scaffold.dart
packages/flutter/lib/src/cupertino/page_scaffold.dart
+9
-6
scaffold.dart
packages/flutter/lib/src/material/scaffold.dart
+24
-29
primary_scroll_controller.dart
...es/flutter/lib/src/widgets/primary_scroll_controller.dart
+13
-0
routes.dart
packages/flutter/lib/src/widgets/routes.dart
+41
-35
scroll_view.dart
packages/flutter/lib/src/widgets/scroll_view.dart
+5
-0
scrollable.dart
packages/flutter/lib/src/widgets/scrollable.dart
+25
-2
shortcuts.dart
packages/flutter/lib/src/widgets/shortcuts.dart
+1
-1
single_child_scroll_view.dart
...ges/flutter/lib/src/widgets/single_child_scroll_view.dart
+5
-0
app_bar_test.dart
packages/flutter/test/material/app_bar_test.dart
+25
-36
debug_test.dart
packages/flutter/test/material/debug_test.dart
+1
-0
scaffold_test.dart
packages/flutter/test/material/scaffold_test.dart
+1
-2
scroll_view_test.dart
packages/flutter/test/widgets/scroll_view_test.dart
+45
-0
scrollable_test.dart
packages/flutter/test/widgets/scrollable_test.dart
+66
-30
No files found.
packages/flutter/lib/src/cupertino/page_scaffold.dart
View file @
1f0df545
...
...
@@ -12,6 +12,12 @@ import 'theme.dart';
/// The scaffold lays out the navigation bar on top and the content between or
/// behind the navigation bar.
///
/// When tapping a status bar at the top of the CupertinoPageScaffold, an
/// animation will complete for the current primary [ScrollView], scrolling to
/// the beginning. This is done using the [PrimaryScrollController] that
/// encloses the [ScrollView]. The [ScrollView.primary] flag is used to connect
/// a [ScrollView] to the enclosing [PrimaryScrollController].
///
/// See also:
///
/// * [CupertinoTabScaffold], a similar widget for tabbed applications.
...
...
@@ -75,11 +81,11 @@ class CupertinoPageScaffold extends StatefulWidget {
}
class
_CupertinoPageScaffoldState
extends
State
<
CupertinoPageScaffold
>
{
final
ScrollController
_primaryScrollController
=
ScrollController
();
void
_handleStatusBarTap
()
{
final
ScrollController
?
_primaryScrollController
=
PrimaryScrollController
.
of
(
context
);
// Only act on the scroll controller if it has any attached scroll positions.
if
(
_primaryScrollController
.
hasClients
)
{
if
(
_primaryScrollController
!=
null
&&
_primaryScrollController
.
hasClients
)
{
_primaryScrollController
.
animateTo
(
0.0
,
// Eyeballed from iOS.
...
...
@@ -163,10 +169,7 @@ class _CupertinoPageScaffoldState extends State<CupertinoPageScaffold> {
child:
Stack
(
children:
<
Widget
>[
// The main content being at the bottom is added to the stack first.
PrimaryScrollController
(
controller:
_primaryScrollController
,
child:
paddedContent
,
),
paddedContent
,
if
(
widget
.
navigationBar
!=
null
)
Positioned
(
top:
0.0
,
...
...
packages/flutter/lib/src/material/scaffold.dart
View file @
1f0df545
...
...
@@ -2670,13 +2670,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// iOS FEATURES - status bar tap, back gesture
// On iOS, tapping the status bar scrolls the app's primary scrollable to the
// top. We implement this by
providing a
primary scroll controller and
// top. We implement this by
looking up the
primary scroll controller and
// scrolling it to the top when tapped.
final
ScrollController
_primaryScrollController
=
ScrollController
();
void
_handleStatusBarTap
()
{
if
(
_primaryScrollController
.
hasClients
)
{
final
ScrollController
?
_primaryScrollController
=
PrimaryScrollController
.
of
(
context
);
if
(
_primaryScrollController
!=
null
&&
_primaryScrollController
.
hasClients
)
{
_primaryScrollController
.
animateTo
(
0.0
,
duration:
const
Duration
(
milliseconds:
300
),
...
...
@@ -3160,30 +3158,27 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
return
_ScaffoldScope
(
hasDrawer:
hasDrawer
,
geometryNotifier:
_geometryNotifier
,
child:
PrimaryScrollController
(
controller:
_primaryScrollController
,
child:
Material
(
color:
widget
.
backgroundColor
??
themeData
.
scaffoldBackgroundColor
,
child:
AnimatedBuilder
(
animation:
_floatingActionButtonMoveController
,
builder:
(
BuildContext
context
,
Widget
?
child
)
{
return
CustomMultiChildLayout
(
children:
children
,
delegate:
_ScaffoldLayout
(
extendBody:
_extendBody
,
extendBodyBehindAppBar:
widget
.
extendBodyBehindAppBar
,
minInsets:
minInsets
,
minViewPadding:
minViewPadding
,
currentFloatingActionButtonLocation:
_floatingActionButtonLocation
!,
floatingActionButtonMoveAnimationProgress:
_floatingActionButtonMoveController
.
value
,
floatingActionButtonMotionAnimator:
_floatingActionButtonAnimator
,
geometryNotifier:
_geometryNotifier
,
previousFloatingActionButtonLocation:
_previousFloatingActionButtonLocation
!,
textDirection:
textDirection
,
isSnackBarFloating:
isSnackBarFloating
,
snackBarWidth:
snackBarWidth
,
),
);
}),
),
child:
Material
(
color:
widget
.
backgroundColor
??
themeData
.
scaffoldBackgroundColor
,
child:
AnimatedBuilder
(
animation:
_floatingActionButtonMoveController
,
builder:
(
BuildContext
context
,
Widget
?
child
)
{
return
CustomMultiChildLayout
(
children:
children
,
delegate:
_ScaffoldLayout
(
extendBody:
_extendBody
,
extendBodyBehindAppBar:
widget
.
extendBodyBehindAppBar
,
minInsets:
minInsets
,
minViewPadding:
minViewPadding
,
currentFloatingActionButtonLocation:
_floatingActionButtonLocation
!,
floatingActionButtonMoveAnimationProgress:
_floatingActionButtonMoveController
.
value
,
floatingActionButtonMotionAnimator:
_floatingActionButtonAnimator
,
geometryNotifier:
_geometryNotifier
,
previousFloatingActionButtonLocation:
_previousFloatingActionButtonLocation
!,
textDirection:
textDirection
,
isSnackBarFloating:
isSnackBarFloating
,
snackBarWidth:
snackBarWidth
,
),
);
}),
),
);
}
...
...
packages/flutter/lib/src/widgets/primary_scroll_controller.dart
View file @
1f0df545
...
...
@@ -16,6 +16,19 @@ import 'scroll_controller.dart';
/// This mechanism can be used to provide default behavior for scroll views in a
/// subtree. For example, the [Scaffold] uses this mechanism to implement the
/// scroll-to-top gesture on iOS.
///
/// Another default behavior handled by the PrimaryScrollController is default
/// [ScrollAction]s. If a ScrollAction is not handled by an otherwise focused
/// part of the application, the ScrollAction will be evaluated using the scroll
/// view associated with a PrimaryScrollController, for example, when executing
/// [Shortcuts] key events like page up and down.
///
/// See also:
/// * [ScrollAction], an [Action] that scrolls the [Scrollable] that encloses
/// the current [primaryFocus] or is attached to the PrimaryScrollController.
/// * [Shortcuts], a widget that establishes a [ShortcutManager] to be used
/// by its descendants when invoking an [Action] via a keyboard key
/// combination that maps to an [Intent].
class
PrimaryScrollController
extends
InheritedWidget
{
/// Creates a widget that associates a [ScrollController] with a subtree.
const
PrimaryScrollController
({
...
...
packages/flutter/lib/src/widgets/routes.dart
View file @
1f0df545
...
...
@@ -18,7 +18,9 @@ import 'modal_barrier.dart';
import
'navigator.dart'
;
import
'overlay.dart'
;
import
'page_storage.dart'
;
import
'primary_scroll_controller.dart'
;
import
'restoration.dart'
;
import
'scroll_controller.dart'
;
import
'transitions.dart'
;
// Examples can assume:
...
...
@@ -712,6 +714,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
/// The node this scope will use for its root [FocusScope] widget.
final
FocusScopeNode
focusScopeNode
=
FocusScopeNode
(
debugLabel:
'
$_ModalScopeState
Focus Scope'
);
final
ScrollController
primaryScrollController
=
ScrollController
();
@override
void
initState
()
{
...
...
@@ -792,44 +795,47 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
bucket:
widget
.
route
.
_storageBucket
,
// immutable
child:
Actions
(
actions:
_actionMap
,
child:
FocusScope
(
node:
focusScopeNode
,
// immutable
child:
RepaintBoundary
(
child:
AnimatedBuilder
(
animation:
_listenable
,
// immutable
builder:
(
BuildContext
context
,
Widget
?
child
)
{
return
widget
.
route
.
buildTransitions
(
context
,
widget
.
route
.
animation
!,
widget
.
route
.
secondaryAnimation
!,
// This additional AnimatedBuilder is include because if the
// value of the userGestureInProgressNotifier changes, it's
// only necessary to rebuild the IgnorePointer widget and set
// the focus node's ability to focus.
AnimatedBuilder
(
animation:
widget
.
route
.
navigator
?.
userGestureInProgressNotifier
??
ValueNotifier
<
bool
>(
false
),
builder:
(
BuildContext
context
,
Widget
?
child
)
{
final
bool
ignoreEvents
=
_shouldIgnoreFocusRequest
;
focusScopeNode
.
canRequestFocus
=
!
ignoreEvents
;
return
IgnorePointer
(
ignoring:
ignoreEvents
,
child:
child
,
child:
PrimaryScrollController
(
controller:
primaryScrollController
,
child:
FocusScope
(
node:
focusScopeNode
,
// immutable
child:
RepaintBoundary
(
child:
AnimatedBuilder
(
animation:
_listenable
,
// immutable
builder:
(
BuildContext
context
,
Widget
?
child
)
{
return
widget
.
route
.
buildTransitions
(
context
,
widget
.
route
.
animation
!,
widget
.
route
.
secondaryAnimation
!,
// This additional AnimatedBuilder is include because if the
// value of the userGestureInProgressNotifier changes, it's
// only necessary to rebuild the IgnorePointer widget and set
// the focus node's ability to focus.
AnimatedBuilder
(
animation:
widget
.
route
.
navigator
?.
userGestureInProgressNotifier
??
ValueNotifier
<
bool
>(
false
),
builder:
(
BuildContext
context
,
Widget
?
child
)
{
final
bool
ignoreEvents
=
_shouldIgnoreFocusRequest
;
focusScopeNode
.
canRequestFocus
=
!
ignoreEvents
;
return
IgnorePointer
(
ignoring:
ignoreEvents
,
child:
child
,
);
},
child:
child
,
),
);
},
child:
_page
??=
RepaintBoundary
(
key:
widget
.
route
.
_subtreeKey
,
// immutable
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
widget
.
route
.
buildPage
(
context
,
widget
.
route
.
animation
!,
widget
.
route
.
secondaryAnimation
!,
);
},
child:
child
,
),
);
},
child:
_page
??=
RepaintBoundary
(
key:
widget
.
route
.
_subtreeKey
,
// immutable
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
widget
.
route
.
buildPage
(
context
,
widget
.
route
.
animation
!,
widget
.
route
.
secondaryAnimation
!,
);
},
),
),
),
...
...
packages/flutter/lib/src/widgets/scroll_view.dart
View file @
1f0df545
...
...
@@ -149,6 +149,11 @@ abstract class ScrollView extends StatelessWidget {
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
///
...
...
packages/flutter/lib/src/widgets/scrollable.dart
View file @
1f0df545
...
...
@@ -17,6 +17,7 @@ import 'focus_manager.dart';
import
'framework.dart'
;
import
'gesture_detector.dart'
;
import
'notification_listener.dart'
;
import
'primary_scroll_controller.dart'
;
import
'restoration.dart'
;
import
'restoration_properties.dart'
;
import
'scroll_configuration.dart'
;
...
...
@@ -959,6 +960,10 @@ class ScrollIntent extends Intent {
/// An [Action] that scrolls the [Scrollable] that encloses the current
/// [primaryFocus] by the amount configured in the [ScrollIntent] given to it.
///
/// If a Scrollable cannot be found above the current [primaryFocus], the
/// [PrimaryScrollController] will be considered for default handling of
/// [ScrollAction]s.
///
/// If [Scrollable.incrementCalculator] is null for the scrollable, the default
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
...
...
@@ -967,7 +972,21 @@ class ScrollAction extends Action<ScrollIntent> {
@override
bool
isEnabled
(
ScrollIntent
intent
)
{
final
FocusNode
?
focus
=
primaryFocus
;
return
focus
!=
null
&&
focus
.
context
!=
null
&&
Scrollable
.
of
(
focus
.
context
!)
!=
null
;
final
bool
contextIsValid
=
focus
!=
null
&&
focus
.
context
!=
null
;
if
(
contextIsValid
)
{
// Check for primary scrollable within the current context
if
(
Scrollable
.
of
(
focus
!.
context
!)
!=
null
)
return
true
;
// Check for fallback scrollable with context from PrimaryScrollController
if
(
PrimaryScrollController
.
of
(
focus
.
context
!)
!=
null
)
{
final
ScrollController
?
primaryScrollController
=
PrimaryScrollController
.
of
(
focus
.
context
!);
return
primaryScrollController
!=
null
&&
primaryScrollController
.
hasClients
&&
primaryScrollController
.
position
.
context
.
notificationContext
!=
null
&&
Scrollable
.
of
(
primaryScrollController
.
position
.
context
.
notificationContext
!)
!=
null
;
}
}
return
false
;
}
// Returns the scroll increment for a single scroll request, for use when
...
...
@@ -1051,7 +1070,11 @@ class ScrollAction extends Action<ScrollIntent> {
@override
void
invoke
(
ScrollIntent
intent
)
{
final
ScrollableState
?
state
=
Scrollable
.
of
(
primaryFocus
!.
context
!);
ScrollableState
?
state
=
Scrollable
.
of
(
primaryFocus
!.
context
!);
if
(
state
==
null
)
{
final
ScrollController
?
primaryScrollController
=
PrimaryScrollController
.
of
(
primaryFocus
!.
context
!);
state
=
Scrollable
.
of
(
primaryScrollController
!.
position
.
context
.
notificationContext
!);
}
assert
(
state
!=
null
,
'
$ScrollAction
was invoked on a context that has no scrollable parent'
);
assert
(
state
!.
position
.
hasPixels
,
'Scrollable must be laid out before it can be scrolled via a ScrollAction'
);
assert
(
state
!.
position
.
viewportDimension
!=
null
);
...
...
packages/flutter/lib/src/widgets/shortcuts.dart
View file @
1f0df545
...
...
@@ -382,7 +382,7 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
}
}
/// A widget that establishes a
n
[ShortcutManager] to be used by its descendants
/// A widget that establishes a [ShortcutManager] to be used by its descendants
/// when invoking an [Action] via a keyboard key combination that maps to an
/// [Intent].
///
...
...
packages/flutter/lib/src/widgets/single_child_scroll_view.dart
View file @
1f0df545
...
...
@@ -270,6 +270,11 @@ class SingleChildScrollView extends StatelessWidget {
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
///
...
...
packages/flutter/test/material/app_bar_test.dart
View file @
1f0df545
...
...
@@ -19,43 +19,32 @@ Widget buildSliverAppBarApp({
bool
snap
=
false
,
double
toolbarHeight
=
kToolbarHeight
,
})
{
return
Localizations
(
locale:
const
Locale
(
'en'
,
'US'
),
delegates:
const
<
LocalizationsDelegate
<
dynamic
>>[
DefaultMaterialLocalizations
.
delegate
,
DefaultWidgetsLocalizations
.
delegate
,
],
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Scaffold
(
body:
DefaultTabController
(
length:
3
,
child:
CustomScrollView
(
primary:
true
,
slivers:
<
Widget
>[
SliverAppBar
(
title:
const
Text
(
'AppBar Title'
),
floating:
floating
,
pinned:
pinned
,
collapsedHeight:
collapsedHeight
,
expandedHeight:
expandedHeight
,
toolbarHeight:
toolbarHeight
,
snap:
snap
,
bottom:
TabBar
(
tabs:
<
String
>[
'A'
,
'B'
,
'C'
].
map
<
Widget
>((
String
t
)
=>
Tab
(
text:
'TAB
$t
'
)).
toList
(),
),
),
SliverToBoxAdapter
(
child:
Container
(
height:
1200.0
,
color:
Colors
.
orange
[
400
],
),
),
],
return
MaterialApp
(
home:
Scaffold
(
body:
DefaultTabController
(
length:
3
,
child:
CustomScrollView
(
primary:
true
,
slivers:
<
Widget
>[
SliverAppBar
(
title:
const
Text
(
'AppBar Title'
),
floating:
floating
,
pinned:
pinned
,
collapsedHeight:
collapsedHeight
,
expandedHeight:
expandedHeight
,
toolbarHeight:
toolbarHeight
,
snap:
snap
,
bottom:
TabBar
(
tabs:
<
String
>[
'A'
,
'B'
,
'C'
].
map
<
Widget
>((
String
t
)
=>
Tab
(
text:
'TAB
$t
'
)).
toList
(),
),
),
),
SliverToBoxAdapter
(
child:
Container
(
height:
1200.0
,
color:
Colors
.
orange
[
400
],
),
),
],
),
),
),
...
...
packages/flutter/test/material/debug_test.dart
View file @
1f0df545
...
...
@@ -135,6 +135,7 @@ void main() {
' _FocusMarker
\n
'
' Semantics
\n
'
' FocusScope
\n
'
' PrimaryScrollController
\n
'
' _ActionsMarker
\n
'
' Actions
\n
'
' PageStorage
\n
'
...
...
packages/flutter/test/material/scaffold_test.dart
View file @
1f0df545
...
...
@@ -2128,12 +2128,11 @@ void main() {
' AnimatedBuilder
\n
'
' DefaultTextStyle
\n
'
' AnimatedDefaultTextStyle
\n
'
' _InkFeatures-[GlobalKey#
342d
0 ink renderer]
\n
'
' _InkFeatures-[GlobalKey#
0000
0 ink renderer]
\n
'
' NotificationListener<LayoutChangedNotification>
\n
'
' PhysicalModel
\n
'
' AnimatedPhysicalModel
\n
'
' Material
\n
'
' PrimaryScrollController
\n
'
' _ScaffoldScope
\n
'
' Scaffold
\n
'
' MediaQuery
\n
'
...
...
packages/flutter/test/widgets/scroll_view_test.dart
View file @
1f0df545
...
...
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/services.dart'
show
LogicalKeyboardKey
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
...
...
@@ -1259,4 +1260,48 @@ void main() {
semanticChildCount:
4
,
),
throwsAssertionError
);
});
testWidgets
(
'PrimaryScrollController provides fallback ScrollActions'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
home:
CustomScrollView
(
primary:
true
,
slivers:
List
<
Widget
>.
generate
(
20
,
(
int
index
)
{
return
SliverToBoxAdapter
(
child:
Focus
(
autofocus:
index
==
0
,
child:
SizedBox
(
key:
ValueKey
<
String
>(
'Box
$index
'
),
height:
50.0
),
),
);
},
),
),
),
);
final
ScrollController
controller
=
PrimaryScrollController
.
of
(
tester
.
element
(
find
.
byType
(
CustomScrollView
))
)!;
await
tester
.
pumpAndSettle
();
expect
(
controller
.
position
.
pixels
,
equals
(
0.0
));
expect
(
tester
.
getRect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 0'
),
skipOffstage:
false
)),
equals
(
const
Rect
.
fromLTRB
(
0.0
,
0.0
,
800.0
,
50.0
)),
);
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
pageDown
);
await
tester
.
pumpAndSettle
();
expect
(
controller
.
position
.
pixels
,
equals
(
400.0
));
expect
(
tester
.
getRect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 0'
),
skipOffstage:
false
)),
equals
(
const
Rect
.
fromLTRB
(
0.0
,
-
400.0
,
800.0
,
-
350.0
)),
);
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
pageUp
);
await
tester
.
pumpAndSettle
();
expect
(
controller
.
position
.
pixels
,
equals
(
0.0
));
expect
(
tester
.
getRect
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Box 0'
),
skipOffstage:
false
)),
equals
(
const
Rect
.
fromLTRB
(
0.0
,
0.0
,
800.0
,
50.0
)),
);
});
}
packages/flutter/test/widgets/scrollable_test.dart
View file @
1f0df545
This diff is collapsed.
Click to expand it.
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