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
770a9b25
Unverified
Commit
770a9b25
authored
Dec 11, 2020
by
Kate Lovett
Committed by
GitHub
Dec 11, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Updated Interactive Scrollbars (#71664)
parent
e92df4ff
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1737 additions
and
754 deletions
+1737
-754
scrollbar.dart
packages/flutter/lib/src/cupertino/scrollbar.dart
+70
-453
scrollbar.dart
packages/flutter/lib/src/material/scrollbar.dart
+180
-179
scrollbar.dart
packages/flutter/lib/src/widgets/scrollbar.dart
+880
-33
scrollbar_test.dart
packages/flutter/test/cupertino/scrollbar_test.dart
+59
-0
scrollbar_paint_test.dart
packages/flutter/test/material/scrollbar_paint_test.dart
+5
-5
scrollbar_test.dart
packages/flutter/test/material/scrollbar_test.dart
+294
-84
scrollbar_test.dart
packages/flutter/test/widgets/scrollbar_test.dart
+249
-0
No files found.
packages/flutter/lib/src/cupertino/scrollbar.dart
View file @
770a9b25
...
...
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:async'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
...
...
@@ -32,180 +30,70 @@ const double _kScrollbarCrossAxisMargin = 3.0;
/// An iOS style scrollbar.
///
/// A scrollbar indicates which portion of a [Scrollable] widget is actually
/// visible.
///
/// To add a scrollbar to a [ScrollView], simply wrap the scroll view widget in
/// a [CupertinoScrollbar] widget.
///
/// By default, the CupertinoScrollbar will be draggable (a feature introduced
/// in iOS 13), it uses the PrimaryScrollController. For multiple scrollbars, or
/// other more complicated situations, see the [controller] parameter.
/// {@macro flutter.widgets.Scrollbar}
///
/// When dragging a [CupertinoScrollbar] thumb, the thickness and radius will
/// animate from [thickness] and [radius] to [thicknessWhileDragging] and
/// [radiusWhileDragging], respectively.
///
// TODO(Piinks): Add code sample
///
/// See also:
///
/// * [ListView], which display a linear, scrollable list of children.
/// * [GridView], which display a 2 dimensional, scrollable array of children.
/// * [Scrollbar], a Material Design scrollbar that dynamically adapts to the
/// platform showing either an Android style or iOS style scrollbar.
class
CupertinoScrollbar
extends
StatefulWidget
{
/// * [ListView], which displays a linear, scrollable list of children.
/// * [GridView], which displays a 2 dimensional, scrollable array of children.
/// * [Scrollbar], a Material Design scrollbar.
/// * [RawScrollbar], a basic scrollbar that fades in and out, extended
/// by this class to add more animations and behaviors.
class
CupertinoScrollbar
extends
RawScrollbar
{
/// Creates an iOS style scrollbar that wraps the given [child].
///
/// The [child] should be a source of [ScrollNotification] notifications,
/// typically a [Scrollable] widget.
const
CupertinoScrollbar
({
Key
?
key
,
this
.
controller
,
this
.
isAlwaysShown
=
false
,
this
.
thickness
=
defaultThickness
,
required
Widget
child
,
ScrollController
?
controller
,
bool
isAlwaysShown
=
false
,
double
thickness
=
defaultThickness
,
this
.
thicknessWhileDragging
=
defaultThicknessWhileDragging
,
this
.
radius
=
defaultRadius
,
Radius
radius
=
defaultRadius
,
this
.
radiusWhileDragging
=
defaultRadiusWhileDragging
,
required
this
.
child
,
})
:
assert
(
thickness
!=
null
),
assert
(
thickness
<
double
.
infinity
),
assert
(
thicknessWhileDragging
!=
null
),
assert
(
thicknessWhileDragging
<
double
.
infinity
),
assert
(
radius
!=
null
),
assert
(
radiusWhileDragging
!=
null
),
assert
(!
isAlwaysShown
||
controller
!=
null
,
'When isAlwaysShown is true, must pass a controller that is attached to a scroll view'
),
super
(
key:
key
);
super
(
key:
key
,
child:
child
,
controller:
controller
,
isAlwaysShown:
isAlwaysShown
,
thickness:
thickness
,
radius:
radius
,
fadeDuration:
_kScrollbarFadeDuration
,
timeToFade:
_kScrollbarTimeToFade
,
pressDuration:
const
Duration
(
milliseconds:
100
),
);
/// Default value for [thickness] if it's not specified in [
new
CupertinoScrollbar].
/// Default value for [thickness] if it's not specified in [CupertinoScrollbar].
static
const
double
defaultThickness
=
3
;
/// Default value for [thicknessWhileDragging] if it's not specified in [new CupertinoScrollbar].
/// Default value for [thicknessWhileDragging] if it's not specified in
/// [CupertinoScrollbar].
static
const
double
defaultThicknessWhileDragging
=
8.0
;
/// Default value for [radius] if it's not specified in [
new
CupertinoScrollbar].
/// Default value for [radius] if it's not specified in [CupertinoScrollbar].
static
const
Radius
defaultRadius
=
Radius
.
circular
(
1.5
);
/// Default value for [radiusWhileDragging] if it's not specified in [new CupertinoScrollbar].
/// Default value for [radiusWhileDragging] if it's not specified in
/// [CupertinoScrollbar].
static
const
Radius
defaultRadiusWhileDragging
=
Radius
.
circular
(
4.0
);
/// The subtree to place inside the [CupertinoScrollbar].
///
/// This should include a source of [ScrollNotification] notifications,
/// typically a [Scrollable] widget.
final
Widget
child
;
/// {@template flutter.cupertino.cupertinoScrollbar.controller}
/// The [ScrollController] used to implement Scrollbar dragging.
///
/// introduced in iOS 13.
///
/// If nothing is passed to controller, the default behavior is to automatically
/// enable scrollbar dragging on the nearest ScrollController using
/// [PrimaryScrollController.of].
///
/// If a ScrollController is passed, then scrollbar dragging will be enabled on
/// the given ScrollController. A stateful ancestor of this CupertinoScrollbar
/// needs to manage the ScrollController and either pass it to a scrollable
/// descendant or use a PrimaryScrollController to share it.
///
/// Here is an example of using the `controller` parameter to enable
/// scrollbar dragging for multiple independent ListViews:
///
/// {@tool snippet}
///
/// ```dart
/// final ScrollController _controllerOne = ScrollController();
/// final ScrollController _controllerTwo = ScrollController();
///
/// build(BuildContext context) {
/// return Column(
/// children: <Widget>[
/// Container(
/// height: 200,
/// child: CupertinoScrollbar(
/// controller: _controllerOne,
/// child: ListView.builder(
/// controller: _controllerOne,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index) => Text('item $index'),
/// ),
/// ),
/// ),
/// Container(
/// height: 200,
/// child: CupertinoScrollbar(
/// controller: _controllerTwo,
/// child: ListView.builder(
/// controller: _controllerTwo,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index) => Text('list 2 item $index'),
/// ),
/// ),
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
/// {@endtemplate}
final
ScrollController
?
controller
;
/// {@template flutter.cupertino.cupertinoScrollbar.isAlwaysShown}
/// Indicates whether the [Scrollbar] should always be visible.
///
/// When false, the scrollbar will be shown during scrolling
/// and will fade out otherwise.
///
/// When true, the scrollbar will always be visible and never fade out.
///
/// The [controller] property must be set in this case.
/// It should be passed the relevant [Scrollable]'s [ScrollController].
///
/// Defaults to false.
///
/// {@tool snippet}
///
/// ```dart
/// final ScrollController _controllerOne = ScrollController();
/// final ScrollController _controllerTwo = ScrollController();
///
/// build(BuildContext context) {
/// return Column(
/// children: <Widget>[
/// Container(
/// height: 200,
/// child: Scrollbar(
/// isAlwaysShown: true,
/// controller: _controllerOne,
/// child: ListView.builder(
/// controller: _controllerOne,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index)
/// => Text('item $index'),
/// ),
/// ),
/// ),
/// Container(
/// height: 200,
/// child: CupertinoScrollbar(
/// isAlwaysShown: true,
/// controller: _controllerTwo,
/// child: SingleChildScrollView(
/// controller: _controllerTwo,
/// child: SizedBox(height: 2000, width: 500,),
/// ),
/// ),
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
/// {@endtemplate}
final
bool
isAlwaysShown
;
/// The thickness of the scrollbar when it's not being dragged by the user.
///
/// When the user starts dragging the scrollbar, the thickness will animate
/// to [thicknessWhileDragging], then animate back when the user stops
/// dragging the scrollbar.
final
double
thickness
;
/// The thickness of the scrollbar when it's being dragged by the user.
///
/// When the user starts dragging the scrollbar, the thickness will animate
...
...
@@ -213,14 +101,6 @@ class CupertinoScrollbar extends StatefulWidget {
/// dragging the scrollbar.
final
double
thicknessWhileDragging
;
/// The radius of the scrollbar edges when the scrollbar is not being dragged
/// by the user.
///
/// When the user starts dragging the scrollbar, the radius will animate
/// to [radiusWhileDragging], then animate back when the user stops dragging
/// the scrollbar.
final
Radius
radius
;
/// The radius of the scrollbar edges when the scrollbar is being dragged by
/// the user.
///
...
...
@@ -233,363 +113,100 @@ class CupertinoScrollbar extends StatefulWidget {
_CupertinoScrollbarState
createState
()
=>
_CupertinoScrollbarState
();
}
class
_CupertinoScrollbarState
extends
State
<
CupertinoScrollbar
>
with
TickerProviderStateMixin
{
final
GlobalKey
_customPaintKey
=
GlobalKey
();
ScrollbarPainter
?
_painter
;
late
AnimationController
_fadeoutAnimationController
;
late
Animation
<
double
>
_fadeoutOpacityAnimation
;
class
_CupertinoScrollbarState
extends
RawScrollbarState
<
CupertinoScrollbar
>
{
late
AnimationController
_thicknessAnimationController
;
Timer
?
_fadeoutTimer
;
double
?
_dragScrollbarAxisPosition
;
Drag
?
_drag
;
double
get
_thickness
{
return
widget
.
thickness
+
_thicknessAnimationController
.
value
*
(
widget
.
thicknessWhileDragging
-
widget
.
thickness
);
return
widget
.
thickness
!
+
_thicknessAnimationController
.
value
*
(
widget
.
thicknessWhileDragging
-
widget
.
thickness
!
);
}
Radius
get
_radius
{
return
Radius
.
lerp
(
widget
.
radius
,
widget
.
radiusWhileDragging
,
_thicknessAnimationController
.
value
)!;
}
ScrollController
?
_currentController
;
ScrollController
?
get
_controller
=>
widget
.
controller
??
PrimaryScrollController
.
of
(
context
);
@override
void
initState
()
{
super
.
initState
();
_fadeoutAnimationController
=
AnimationController
(
vsync:
this
,
duration:
_kScrollbarFadeDuration
,
);
_fadeoutOpacityAnimation
=
CurvedAnimation
(
parent:
_fadeoutAnimationController
,
curve:
Curves
.
fastOutSlowIn
,
);
_thicknessAnimationController
=
AnimationController
(
vsync:
this
,
duration:
_kScrollbarResizeDuration
,
);
_thicknessAnimationController
.
addListener
(()
{
_painter
!.
updateThickness
(
_thickness
,
_radius
);
updateScrollbarPainter
(
);
});
}
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
if
(
_painter
==
null
)
{
_painter
=
_buildCupertinoScrollbarPainter
(
context
);
}
else
{
_painter
!
..
textDirection
=
Directionality
.
of
(
context
)
void
updateScrollbarPainter
()
{
scrollbarPainter
..
color
=
CupertinoDynamicColor
.
resolve
(
_kScrollbarColor
,
context
)
..
padding
=
MediaQuery
.
of
(
context
).
padding
;
}
_triggerScrollbar
();
}
@override
void
didUpdateWidget
(
CupertinoScrollbar
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
assert
(
_painter
!=
null
);
_painter
!.
updateThickness
(
_thickness
,
_radius
);
if
(
widget
.
isAlwaysShown
!=
oldWidget
.
isAlwaysShown
)
{
if
(
widget
.
isAlwaysShown
==
true
)
{
_triggerScrollbar
();
_fadeoutAnimationController
.
animateTo
(
1.0
);
}
else
{
_fadeoutAnimationController
.
reverse
();
}
}
}
/// Returns a [ScrollbarPainter] visually styled like the iOS scrollbar.
ScrollbarPainter
_buildCupertinoScrollbarPainter
(
BuildContext
context
)
{
return
ScrollbarPainter
(
color:
CupertinoDynamicColor
.
resolve
(
_kScrollbarColor
,
context
),
textDirection:
Directionality
.
of
(
context
),
thickness:
_thickness
,
fadeoutOpacityAnimation:
_fadeoutOpacityAnimation
,
mainAxisMargin:
_kScrollbarMainAxisMargin
,
crossAxisMargin:
_kScrollbarCrossAxisMargin
,
radius:
_radius
,
padding:
MediaQuery
.
of
(
context
).
padding
,
minLength:
_kScrollbarMinLength
,
minOverscrollLength:
_kScrollbarMinOverscrollLength
,
);
}
// Wait one frame and cause an empty scroll event. This allows the thumb to
// show immediately when isAlwaysShown is true. A scroll event is required in
// order to paint the thumb.
void
_triggerScrollbar
()
{
WidgetsBinding
.
instance
!.
addPostFrameCallback
((
Duration
duration
)
{
if
(
widget
.
isAlwaysShown
)
{
_fadeoutTimer
?.
cancel
();
widget
.
controller
!.
position
.
didUpdateScrollPositionBy
(
0
);
}
});
}
// Handle a gesture that drags the scrollbar by the given amount.
void
_dragScrollbar
(
double
primaryDelta
)
{
assert
(
_currentController
!=
null
);
// Convert primaryDelta, the amount that the scrollbar moved since the last
// time _dragScrollbar was called, into the coordinate space of the scroll
// position, and create/update the drag event with that position.
final
double
scrollOffsetLocal
=
_painter
!.
getTrackToScroll
(
primaryDelta
);
final
double
scrollOffsetGlobal
=
scrollOffsetLocal
+
_currentController
!.
position
.
pixels
;
final
Axis
direction
=
_currentController
!.
position
.
axis
;
if
(
_drag
==
null
)
{
_drag
=
_currentController
!.
position
.
drag
(
DragStartDetails
(
globalPosition:
direction
==
Axis
.
vertical
?
Offset
(
0.0
,
scrollOffsetGlobal
)
:
Offset
(
scrollOffsetGlobal
,
0.0
),
),
()
{},
);
}
else
{
_drag
!.
update
(
DragUpdateDetails
(
globalPosition:
direction
==
Axis
.
vertical
?
Offset
(
0.0
,
scrollOffsetGlobal
)
:
Offset
(
scrollOffsetGlobal
,
0.0
),
delta:
direction
==
Axis
.
vertical
?
Offset
(
0.0
,
-
scrollOffsetLocal
)
:
Offset
(-
scrollOffsetLocal
,
0.0
),
primaryDelta:
-
scrollOffsetLocal
,
));
}
}
void
_startFadeoutTimer
()
{
if
(!
widget
.
isAlwaysShown
)
{
_fadeoutTimer
?.
cancel
();
_fadeoutTimer
=
Timer
(
_kScrollbarTimeToFade
,
()
{
_fadeoutAnimationController
.
reverse
();
_fadeoutTimer
=
null
;
});
}
}
Axis
?
_getDirection
()
{
try
{
return
_currentController
!.
position
.
axis
;
}
catch
(
_
)
{
// Ignore the gesture if we cannot determine the direction.
return
null
;
}
..
textDirection
=
Directionality
.
of
(
context
)
..
thickness
=
_thickness
..
mainAxisMargin
=
_kScrollbarMainAxisMargin
..
crossAxisMargin
=
_kScrollbarCrossAxisMargin
..
radius
=
_radius
..
padding
=
MediaQuery
.
of
(
context
).
padding
..
minLength
=
_kScrollbarMinLength
..
minOverscrollLength
=
_kScrollbarMinOverscrollLength
;
}
double
_pressStartAxisPosition
=
0.0
;
// Long press event callbacks handle the gesture where the user long presses
// on the scrollbar thumb and then drags the scrollbar without releasing.
void
_handleLongPressStart
(
LongPressStartDetails
details
)
{
_currentController
=
_controller
;
final
Axis
?
direction
=
_getDirection
();
if
(
direction
==
null
)
{
return
;
}
_fadeoutTimer
?.
cancel
();
_fadeoutAnimationController
.
forward
();
@override
void
handleThumbPressStart
(
Offset
localPosition
)
{
super
.
handleThumbPressStart
(
localPosition
);
final
Axis
direction
=
getScrollbarDirection
()!;
switch
(
direction
)
{
case
Axis
.
vertical
:
_pressStartAxisPosition
=
details
.
localPosition
.
dy
;
_dragScrollbar
(
details
.
localPosition
.
dy
);
_dragScrollbarAxisPosition
=
details
.
localPosition
.
dy
;
_pressStartAxisPosition
=
localPosition
.
dy
;
break
;
case
Axis
.
horizontal
:
_pressStartAxisPosition
=
details
.
localPosition
.
dx
;
_dragScrollbar
(
details
.
localPosition
.
dx
);
_dragScrollbarAxisPosition
=
details
.
localPosition
.
dx
;
_pressStartAxisPosition
=
localPosition
.
dx
;
break
;
}
}
void
_handleLongPress
()
{
if
(
_getDirection
()
==
null
)
{
@override
void
handleThumbPress
()
{
if
(
getScrollbarDirection
()
==
null
)
{
return
;
}
_fadeoutTimer
?.
cancel
();
super
.
handleThumbPress
();
_thicknessAnimationController
.
forward
().
then
<
void
>(
(
_
)
=>
HapticFeedback
.
mediumImpact
(),
);
}
void
_handleLongPressMoveUpdate
(
LongPressMoveUpdateDetails
details
)
{
final
Axis
?
direction
=
_getDirection
();
if
(
direction
==
null
)
{
return
;
}
switch
(
direction
)
{
case
Axis
.
vertical
:
_dragScrollbar
(
details
.
localPosition
.
dy
-
_dragScrollbarAxisPosition
!);
_dragScrollbarAxisPosition
=
details
.
localPosition
.
dy
;
break
;
case
Axis
.
horizontal
:
_dragScrollbar
(
details
.
localPosition
.
dx
-
_dragScrollbarAxisPosition
!);
_dragScrollbarAxisPosition
=
details
.
localPosition
.
dx
;
break
;
}
}
void
_handleLongPressEnd
(
LongPressEndDetails
details
)
{
final
Axis
?
direction
=
_getDirection
();
@override
void
handleThumbPressEnd
(
Offset
localPosition
,
Velocity
velocity
)
{
final
Axis
?
direction
=
getScrollbarDirection
();
if
(
direction
==
null
)
{
return
;
}
_thicknessAnimationController
.
reverse
();
super
.
handleThumbPressEnd
(
localPosition
,
velocity
);
switch
(
direction
)
{
case
Axis
.
vertical
:
_handleDragScrollEnd
(
details
.
velocity
.
pixelsPerSecond
.
dy
,
direction
);
if
(
details
.
velocity
.
pixelsPerSecond
.
dy
.
abs
()
<
10
&&
(
details
.
localPosition
.
dy
-
_pressStartAxisPosition
).
abs
()
>
0
)
{
if
(
velocity
.
pixelsPerSecond
.
dy
.
abs
()
<
10
&&
(
localPosition
.
dy
-
_pressStartAxisPosition
).
abs
()
>
0
)
{
HapticFeedback
.
mediumImpact
();
}
break
;
case
Axis
.
horizontal
:
_handleDragScrollEnd
(
details
.
velocity
.
pixelsPerSecond
.
dx
,
direction
);
if
(
details
.
velocity
.
pixelsPerSecond
.
dx
.
abs
()
<
10
&&
(
details
.
localPosition
.
dx
-
_pressStartAxisPosition
).
abs
()
>
0
)
{
if
(
velocity
.
pixelsPerSecond
.
dx
.
abs
()
<
10
&&
(
localPosition
.
dx
-
_pressStartAxisPosition
).
abs
()
>
0
)
{
HapticFeedback
.
mediumImpact
();
}
break
;
}
_currentController
=
null
;
}
void
_handleDragScrollEnd
(
double
trackVelocity
,
Axis
direction
)
{
_startFadeoutTimer
();
_thicknessAnimationController
.
reverse
();
_dragScrollbarAxisPosition
=
null
;
final
double
scrollVelocity
=
_painter
!.
getTrackToScroll
(
trackVelocity
);
_drag
?.
end
(
DragEndDetails
(
primaryVelocity:
-
scrollVelocity
,
velocity:
Velocity
(
pixelsPerSecond:
direction
==
Axis
.
vertical
?
Offset
(
0.0
,
-
scrollVelocity
)
:
Offset
(-
scrollVelocity
,
0.0
),
),
));
_drag
=
null
;
}
bool
_handleScrollNotification
(
ScrollNotification
notification
)
{
final
ScrollMetrics
metrics
=
notification
.
metrics
;
if
(
metrics
.
maxScrollExtent
<=
metrics
.
minScrollExtent
)
{
return
false
;
}
if
(
notification
is
ScrollUpdateNotification
||
notification
is
OverscrollNotification
)
{
// Any movements always makes the scrollbar start showing up.
if
(
_fadeoutAnimationController
.
status
!=
AnimationStatus
.
forward
)
{
_fadeoutAnimationController
.
forward
();
}
_fadeoutTimer
?.
cancel
();
_painter
!.
update
(
notification
.
metrics
,
notification
.
metrics
.
axisDirection
);
}
else
if
(
notification
is
ScrollEndNotification
)
{
// On iOS, the scrollbar can only go away once the user lifted the finger.
if
(
_dragScrollbarAxisPosition
==
null
)
{
_startFadeoutTimer
();
}
}
return
false
;
}
// Get the GestureRecognizerFactories used to detect gestures on the scrollbar
// thumb.
Map
<
Type
,
GestureRecognizerFactory
>
get
_gestures
{
final
Map
<
Type
,
GestureRecognizerFactory
>
gestures
=
<
Type
,
GestureRecognizerFactory
>{};
gestures
[
_ThumbPressGestureRecognizer
]
=
GestureRecognizerFactoryWithHandlers
<
_ThumbPressGestureRecognizer
>(
()
=>
_ThumbPressGestureRecognizer
(
debugOwner:
this
,
customPaintKey:
_customPaintKey
,
),
(
_ThumbPressGestureRecognizer
instance
)
{
instance
..
onLongPressStart
=
_handleLongPressStart
..
onLongPress
=
_handleLongPress
..
onLongPressMoveUpdate
=
_handleLongPressMoveUpdate
..
onLongPressEnd
=
_handleLongPressEnd
;
},
);
return
gestures
;
}
@override
void
dispose
()
{
_fadeoutAnimationController
.
dispose
();
_thicknessAnimationController
.
dispose
();
_fadeoutTimer
?.
cancel
();
_painter
!.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
NotificationListener
<
ScrollNotification
>(
onNotification:
_handleScrollNotification
,
child:
RepaintBoundary
(
child:
RawGestureDetector
(
gestures:
_gestures
,
child:
CustomPaint
(
key:
_customPaintKey
,
foregroundPainter:
_painter
,
child:
RepaintBoundary
(
child:
widget
.
child
),
),
),
),
);
}
}
// A longpress gesture detector that only responds to events on the scrollbar's
// thumb and ignores everything else.
class
_ThumbPressGestureRecognizer
extends
LongPressGestureRecognizer
{
_ThumbPressGestureRecognizer
({
double
?
postAcceptSlopTolerance
,
PointerDeviceKind
?
kind
,
required
Object
debugOwner
,
required
GlobalKey
customPaintKey
,
})
:
_customPaintKey
=
customPaintKey
,
super
(
postAcceptSlopTolerance:
postAcceptSlopTolerance
,
kind:
kind
,
debugOwner:
debugOwner
,
duration:
const
Duration
(
milliseconds:
100
),
);
final
GlobalKey
_customPaintKey
;
@override
bool
isPointerAllowed
(
PointerDownEvent
event
)
{
if
(!
_hitTestInteractive
(
_customPaintKey
,
event
.
position
))
{
return
false
;
}
return
super
.
isPointerAllowed
(
event
);
}
}
// foregroundPainter also hit tests its children by default, but the
// scrollbar should only respond to a gesture directly on its thumb, so
// manually check for a hit on the thumb here.
bool
_hitTestInteractive
(
GlobalKey
customPaintKey
,
Offset
offset
)
{
if
(
customPaintKey
.
currentContext
==
null
)
{
return
false
;
}
final
CustomPaint
customPaint
=
customPaintKey
.
currentContext
!.
widget
as
CustomPaint
;
final
ScrollbarPainter
painter
=
customPaint
.
foregroundPainter
!
as
ScrollbarPainter
;
final
RenderBox
renderBox
=
customPaintKey
.
currentContext
!.
findRenderObject
()!
as
RenderBox
;
final
Offset
localOffset
=
renderBox
.
globalToLocal
(
offset
);
return
painter
.
hitTestInteractive
(
localOffset
);
}
packages/flutter/lib/src/material/scrollbar.dart
View file @
770a9b25
...
...
@@ -2,236 +2,237 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:async'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/widgets.dart'
;
import
'color_scheme.dart'
;
import
'material_state.dart'
;
import
'theme.dart'
;
const
double
_kScrollbarThickness
=
6.0
;
const
double
_kScrollbarThickness
=
8.0
;
const
double
_kScrollbarThicknessWithTrack
=
12.0
;
const
double
_kScrollbarMargin
=
2.0
;
const
double
_kScrollbarMinLength
=
48.0
;
const
Radius
_kScrollbarRadius
=
Radius
.
circular
(
8.0
);
const
Duration
_kScrollbarFadeDuration
=
Duration
(
milliseconds:
300
);
const
Duration
_kScrollbarTimeToFade
=
Duration
(
milliseconds:
600
);
/// A material design scrollbar.
///
/// A scrollbar indicates which portion of a [Scrollable] widget is actually
/// visible.
/// To add a scrollbar thumb to a [ScrollView], simply wrap the scroll view
/// widget in a [Scrollbar] widget.
///
/// {@macro flutter.widgets.Scrollbar}
///
/// Dynamically changes to an iOS style scrollbar that looks like
/// [CupertinoScrollbar] on the iOS platform.
/// The color of the Scrollbar will change when dragged, as well as when
/// hovered over. A scrollbar track can also been drawn when triggered by a
/// hover event, which is controlled by [showTrackOnHover]. The thickness of the
/// track and scrollbar thumb will become larger when hovering, unless
/// overridden by [hoverThickness].
///
/// To add a scrollbar to a [ScrollView], simply wrap the scroll view widget in
/// a [Scrollbar] widget.
// TODO(Piinks): Add code sample
///
/// See also:
///
/// * [ListView], which display a linear, scrollable list of children.
/// * [GridView], which display a 2 dimensional, scrollable array of children.
class
Scrollbar
extends
StatefulWidget
{
/// Creates a material design scrollbar that wraps the given [child].
/// * [RawScrollbar], a basic scrollbar that fades in and out, extended
/// by this class to add more animations and behaviors.
/// * [CupertinoScrollbar], an iOS style scrollbar.
/// * [ListView], which displays a linear, scrollable list of children.
/// * [GridView], which displays a 2 dimensional, scrollable array of children.
class
Scrollbar
extends
RawScrollbar
{
/// Creates a material design scrollbar that by default will connect to the
/// closest Scrollable descendant of [child].
///
/// The [child] should be a source of [ScrollNotification] notifications,
/// typically a [Scrollable] widget.
const
Scrollbar
({
Key
?
key
,
required
this
.
child
,
this
.
controller
,
this
.
isAlwaysShown
=
false
,
this
.
thickness
,
this
.
radius
,
})
:
assert
(!
isAlwaysShown
||
controller
!=
null
,
'When isAlwaysShown is true, must pass a controller that is attached to a scroll view'
),
super
(
key:
key
);
/// The widget below this widget in the tree.
///
///
The scrollbar will be stacked on top of this child. This child (and its
///
subtree) should include a source of [ScrollNotification] notifications
.
///
If the [controller] is null, the default behavior is to
///
enable scrollbar dragging using the [PrimaryScrollController]
.
///
/// Typically a [ListView] or [CustomScrollView].
final
Widget
child
;
/// {@macro flutter.cupertino.cupertinoScrollbar.controller}
final
ScrollController
?
controller
;
/// {@macro flutter.cupertino.cupertinoScrollbar.isAlwaysShown}
final
bool
isAlwaysShown
;
/// When null, [thickness] and [radius] defaults will result in a rounded
/// rectangular thumb that is 8.0 dp wide with a radius of 8.0 pixels.
const
Scrollbar
({
Key
?
key
,
required
Widget
child
,
ScrollController
?
controller
,
bool
isAlwaysShown
=
false
,
this
.
showTrackOnHover
=
false
,
this
.
hoverThickness
,
double
?
thickness
,
Radius
?
radius
,
})
:
super
(
key:
key
,
child:
child
,
controller:
controller
,
isAlwaysShown:
isAlwaysShown
,
thickness:
thickness
??
_kScrollbarThickness
,
radius:
radius
,
fadeDuration:
_kScrollbarFadeDuration
,
timeToFade:
_kScrollbarTimeToFade
,
pressDuration:
Duration
.
zero
,
);
///
The thickness of the scrollbar
.
///
Controls if the track will show on hover and remain, including during drag
.
///
/// If this is non-null, it will be used as the thickness of the scrollbar on
/// all platforms, whether the scrollbar is being dragged by the user or not.
/// By default (if this is left null), each platform will get a thickness
/// that matches the look and feel of the platform, and the thickness may
/// grow while the scrollbar is being dragged if the platform look and feel
/// calls for such behavior.
final
double
?
thickness
;
/// The radius of the corners of the scrollbar.
/// Defaults to false, cannot be null.
final
bool
showTrackOnHover
;
/// The thickness of the scrollbar when a hover state is active and
/// [showTrackOnHover] is true.
///
/// If this is non-null, it will be used as the fixed radius of the scrollbar
/// on all platforms, whether the scrollbar is being dragged by the user or
/// not. By default (if this is left null), each platform will get a radius
/// that matches the look and feel of the platform, and the radius may
/// change while the scrollbar is being dragged if the platform look and feel
/// calls for such behavior.
final
Radius
?
radius
;
/// Defaults to 12.0 dp when null.
final
double
?
hoverThickness
;
@override
_ScrollbarState
createState
()
=>
_ScrollbarState
();
}
class
_ScrollbarState
extends
State
<
Scrollbar
>
with
SingleTickerProviderStateMixin
{
ScrollbarPainter
?
_materialPainter
;
late
TextDirection
_textDirection
;
late
Color
_themeColor
;
late
bool
_useCupertinoScrollbar
;
late
AnimationController
_fadeoutAnimationController
;
late
Animation
<
double
>
_fadeoutOpacityAnimation
;
Timer
?
_fadeoutTimer
;
@override
void
initState
()
{
super
.
initState
();
_fadeoutAnimationController
=
AnimationController
(
vsync:
this
,
duration:
_kScrollbarFadeDuration
,
);
_fadeoutOpacityAnimation
=
CurvedAnimation
(
parent:
_fadeoutAnimationController
,
curve:
Curves
.
fastOutSlowIn
,
);
}
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
final
ThemeData
theme
=
Theme
.
of
(
context
);
switch
(
theme
.
platform
)
{
case
TargetPlatform
.
iOS
:
case
TargetPlatform
.
macOS
:
// On iOS, stop all local animations. CupertinoScrollbar has its own
// animations.
_fadeoutTimer
?.
cancel
();
_fadeoutTimer
=
null
;
_fadeoutAnimationController
.
reset
();
_useCupertinoScrollbar
=
true
;
class
_ScrollbarState
extends
RawScrollbarState
<
Scrollbar
>
{
late
AnimationController
_hoverAnimationController
;
bool
_dragIsActive
=
false
;
bool
_hoverIsActive
=
false
;
late
ColorScheme
_colorScheme
;
Set
<
MaterialState
>
get
_states
=>
<
MaterialState
>{
if
(
_dragIsActive
)
MaterialState
.
dragged
,
if
(
_hoverIsActive
)
MaterialState
.
hovered
,
};
MaterialStateProperty
<
Color
>
get
_thumbColor
{
final
Color
onSurface
=
_colorScheme
.
onSurface
;
final
Brightness
brightness
=
_colorScheme
.
brightness
;
late
Color
dragColor
;
late
Color
hoverColor
;
late
Color
idleColor
;
switch
(
brightness
)
{
case
Brightness
.
light
:
dragColor
=
onSurface
.
withOpacity
(
0.6
);
hoverColor
=
onSurface
.
withOpacity
(
0.5
);
idleColor
=
onSurface
.
withOpacity
(
0.1
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
windows
:
_themeColor
=
theme
.
highlightColor
.
withOpacity
(
1.0
);
_textDirection
=
Directionality
.
of
(
context
);
_materialPainter
=
_buildMaterialScrollbarPainter
();
_useCupertinoScrollbar
=
false
;
_triggerScrollbar
();
case
Brightness
.
dark
:
dragColor
=
onSurface
.
withOpacity
(
0.75
);
hoverColor
=
onSurface
.
withOpacity
(
0.65
);
idleColor
=
onSurface
.
withOpacity
(
0.3
);
break
;
}
return
MaterialStateProperty
.
resolveWith
((
Set
<
MaterialState
>
states
)
{
if
(
states
.
contains
(
MaterialState
.
dragged
))
return
dragColor
;
// If the track is visible, the thumb color hover animation is ignored and
// changes immediately.
if
(
states
.
contains
(
MaterialState
.
hovered
)
&&
widget
.
showTrackOnHover
)
return
hoverColor
;
return
Color
.
lerp
(
idleColor
,
hoverColor
,
_hoverAnimationController
.
value
,
)!;
});
}
@override
void
didUpdateWidget
(
Scrollbar
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
isAlwaysShown
!=
oldWidget
.
isAlwaysShown
)
{
if
(
widget
.
isAlwaysShown
==
false
)
{
_fadeoutAnimationController
.
reverse
();
}
else
{
_triggerScrollbar
();
_fadeoutAnimationController
.
animateTo
(
1.0
);
MaterialStateProperty
<
Color
>
get
_trackColor
{
final
Color
onSurface
=
_colorScheme
.
onSurface
;
final
Brightness
brightness
=
_colorScheme
.
brightness
;
return
MaterialStateProperty
.
resolveWith
((
Set
<
MaterialState
>
states
)
{
if
(
states
.
contains
(
MaterialState
.
hovered
)
&&
widget
.
showTrackOnHover
)
{
return
brightness
==
Brightness
.
light
?
onSurface
.
withOpacity
(
0.03
)
:
onSurface
.
withOpacity
(
0.05
);
}
return
const
Color
(
0x00000000
);
});
}
if
(!
_useCupertinoScrollbar
)
{
_materialPainter
!
..
thickness
=
widget
.
thickness
??
_kScrollbarThickness
..
radius
=
widget
.
radius
;
MaterialStateProperty
<
Color
>
get
_trackBorderColor
{
final
Color
onSurface
=
_colorScheme
.
onSurface
;
final
Brightness
brightness
=
_colorScheme
.
brightness
;
return
MaterialStateProperty
.
resolveWith
((
Set
<
MaterialState
>
states
)
{
if
(
states
.
contains
(
MaterialState
.
hovered
)
&&
widget
.
showTrackOnHover
)
{
return
brightness
==
Brightness
.
light
?
onSurface
.
withOpacity
(
0.1
)
:
onSurface
.
withOpacity
(
0.25
);
}
return
const
Color
(
0x00000000
);
});
}
// Wait one frame and cause an empty scroll event. This allows the thumb to
// show immediately when isAlwaysShown is true. A scroll event is required in
// order to paint the thumb.
void
_triggerScrollbar
()
{
WidgetsBinding
.
instance
!.
addPostFrameCallback
((
Duration
duration
)
{
if
(
widget
.
isAlwaysShown
)
{
_fadeoutTimer
?.
cancel
();
widget
.
controller
!.
position
.
didUpdateScrollPositionBy
(
0
);
}
MaterialStateProperty
<
double
>
get
_thickness
{
return
MaterialStateProperty
.
resolveWith
((
Set
<
MaterialState
>
states
)
{
if
(
states
.
contains
(
MaterialState
.
hovered
)
&&
widget
.
showTrackOnHover
)
return
widget
.
hoverThickness
??
_kScrollbarThicknessWithTrack
;
return
widget
.
thickness
??
_kScrollbarThickness
;
});
}
ScrollbarPainter
_buildMaterialScrollbarPainter
()
{
return
ScrollbarPainter
(
color:
_themeColor
,
textDirection:
_textDirection
,
thickness:
widget
.
thickness
??
_kScrollbarThickness
,
radius:
widget
.
radius
,
fadeoutOpacityAnimation:
_fadeoutOpacityAnimation
,
padding:
MediaQuery
.
of
(
context
).
padding
,
@override
void
initState
()
{
super
.
initState
();
_hoverAnimationController
=
AnimationController
(
vsync:
this
,
duration:
const
Duration
(
milliseconds:
200
),
);
_hoverAnimationController
.
addListener
(()
{
updateScrollbarPainter
();
});
}
bool
_handleScrollNotification
(
ScrollNotification
notification
)
{
final
ScrollMetrics
metrics
=
notification
.
metrics
;
if
(
metrics
.
maxScrollExtent
<=
metrics
.
minScrollExtent
)
{
return
false
;
@override
void
updateScrollbarPainter
()
{
_colorScheme
=
Theme
.
of
(
context
).
colorScheme
;
scrollbarPainter
..
color
=
_thumbColor
.
resolve
(
_states
)
..
trackColor
=
_trackColor
.
resolve
(
_states
)
..
trackBorderColor
=
_trackBorderColor
.
resolve
(
_states
)
..
textDirection
=
Directionality
.
of
(
context
)
..
thickness
=
_thickness
.
resolve
(
_states
)
..
radius
=
widget
.
radius
??
_kScrollbarRadius
..
crossAxisMargin
=
_kScrollbarMargin
..
minLength
=
_kScrollbarMinLength
..
padding
=
MediaQuery
.
of
(
context
).
padding
;
}
// iOS sub-delegates to the CupertinoScrollbar instead and doesn't handle
// scroll notifications here.
if
(!
_useCupertinoScrollbar
&&
(
notification
is
ScrollUpdateNotification
||
notification
is
OverscrollNotification
))
{
if
(
_fadeoutAnimationController
.
status
!=
AnimationStatus
.
forward
)
{
_fadeoutAnimationController
.
forward
();
@override
void
handleThumbPressStart
(
Offset
localPosition
)
{
super
.
handleThumbPressStart
(
localPosition
);
setState
(()
{
_dragIsActive
=
true
;
});
}
_materialPainter
!.
update
(
notification
.
metrics
,
notification
.
metrics
.
axisDirection
,
);
if
(!
widget
.
isAlwaysShown
)
{
_fadeoutTimer
?.
cancel
();
_fadeoutTimer
=
Timer
(
_kScrollbarTimeToFade
,
()
{
_fadeoutAnimationController
.
reverse
();
_fadeoutTimer
=
null
;
});
@override
void
handleThumbPressEnd
(
Offset
localPosition
,
Velocity
velocity
)
{
super
.
handleThumbPressEnd
(
localPosition
,
velocity
);
setState
(()
{
_dragIsActive
=
false
;
});
}
@override
void
handleHover
(
PointerHoverEvent
event
)
{
super
.
handleHover
(
event
);
// Check if the position of the pointer falls over the painted scrollbar
if
(
isPointerOverScrollbar
(
event
.
position
))
{
// Pointer is hovering over the scrollbar
setState
(()
{
_hoverIsActive
=
true
;
});
_hoverAnimationController
.
forward
();
}
else
if
(
_hoverIsActive
)
{
// Pointer was, but is no longer over painted scrollbar.
setState
(()
{
_hoverIsActive
=
false
;
});
_hoverAnimationController
.
reverse
();
}
return
false
;
}
@override
void
dispose
()
{
_fadeoutAnimationController
.
dispose
();
_fadeoutTimer
?.
cancel
();
_materialPainter
?.
dispose
();
super
.
dispose
();
void
handleHoverExit
(
PointerExitEvent
event
)
{
super
.
handleHoverExit
(
event
);
setState
(()
{
_hoverIsActive
=
false
;
});
_hoverAnimationController
.
reverse
();
}
@override
Widget
build
(
BuildContext
context
)
{
if
(
_useCupertinoScrollbar
)
{
return
CupertinoScrollbar
(
child:
widget
.
child
,
isAlwaysShown:
widget
.
isAlwaysShown
,
thickness:
widget
.
thickness
??
CupertinoScrollbar
.
defaultThickness
,
thicknessWhileDragging:
widget
.
thickness
??
CupertinoScrollbar
.
defaultThicknessWhileDragging
,
radius:
widget
.
radius
??
CupertinoScrollbar
.
defaultRadius
,
radiusWhileDragging:
widget
.
radius
??
CupertinoScrollbar
.
defaultRadiusWhileDragging
,
controller:
widget
.
controller
,
);
}
return
NotificationListener
<
ScrollNotification
>(
onNotification:
_handleScrollNotification
,
child:
RepaintBoundary
(
child:
CustomPaint
(
foregroundPainter:
_materialPainter
,
child:
RepaintBoundary
(
child:
widget
.
child
,
),
),
),
);
void
dispose
()
{
_hoverAnimationController
.
dispose
();
super
.
dispose
();
}
}
packages/flutter/lib/src/widgets/scrollbar.dart
View file @
770a9b25
...
...
@@ -2,18 +2,34 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:async'
;
import
'dart:math'
as
math
;
import
'package:flutter/animation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/rendering.dart'
;
import
'basic.dart'
;
import
'binding.dart'
;
import
'framework.dart'
;
import
'gesture_detector.dart'
;
import
'media_query.dart'
;
import
'notification_listener.dart'
;
import
'primary_scroll_controller.dart'
;
import
'scroll_controller.dart'
;
import
'scroll_metrics.dart'
;
import
'scroll_notification.dart'
;
import
'scrollable.dart'
;
import
'ticker_provider.dart'
;
const
double
_kMinThumbExtent
=
18.0
;
const
double
_kMinInteractiveSize
=
48.0
;
const
double
_kScrollbarThickness
=
6.0
;
const
Duration
_kScrollbarFadeDuration
=
Duration
(
milliseconds:
300
);
const
Duration
_kScrollbarTimeToFade
=
Duration
(
milliseconds:
600
);
///
A [CustomPainter] for painting scrollbars
.
///
Paints a scrollbar's track and thumb
.
///
/// The size of the scrollbar along its scroll direction is typically
/// proportional to the percentage of content completely visible on screen,
...
...
@@ -45,17 +61,18 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// Creates a scrollbar with customizations given by construction arguments.
ScrollbarPainter
({
required
Color
color
,
required
TextDirection
textDirection
,
required
this
.
thickness
,
required
this
.
fadeoutOpacityAnimation
,
Color
trackColor
=
const
Color
(
0x00000000
),
Color
trackBorderColor
=
const
Color
(
0x00000000
),
TextDirection
?
textDirection
,
double
thickness
=
_kScrollbarThickness
,
EdgeInsets
padding
=
EdgeInsets
.
zero
,
this
.
mainAxisMargin
=
0.0
,
this
.
crossAxisMargin
=
0.0
,
this
.
radius
,
this
.
minLength
=
_kMinThumbExtent
,
double
mainAxisMargin
=
0.0
,
double
crossAxisMargin
=
0.0
,
Radius
?
radius
,
double
minLength
=
_kMinThumbExtent
,
double
?
minOverscrollLength
,
})
:
assert
(
color
!=
null
),
assert
(
textDirection
!=
null
),
assert
(
thickness
!=
null
),
assert
(
fadeoutOpacityAnimation
!=
null
),
assert
(
mainAxisMargin
!=
null
),
...
...
@@ -68,8 +85,15 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
assert
(
padding
.
isNonNegative
),
_color
=
color
,
_textDirection
=
textDirection
,
_thickness
=
thickness
,
_radius
=
radius
,
_padding
=
padding
,
minOverscrollLength
=
minOverscrollLength
??
minLength
{
_mainAxisMargin
=
mainAxisMargin
,
_crossAxisMargin
=
crossAxisMargin
,
_minLength
=
minLength
,
_trackColor
=
trackColor
,
_trackBorderColor
=
trackBorderColor
,
_minOverscrollLength
=
minOverscrollLength
??
minLength
{
fadeoutOpacityAnimation
.
addListener
(
notifyListeners
);
}
...
...
@@ -85,11 +109,36 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
notifyListeners
();
}
/// [Color] of the track. Mustn't be null.
Color
get
trackColor
=>
_trackColor
;
Color
_trackColor
;
set
trackColor
(
Color
value
)
{
assert
(
value
!=
null
);
if
(
trackColor
==
value
)
return
;
_trackColor
=
value
;
notifyListeners
();
}
/// [Color] of the track border. Mustn't be null.
Color
get
trackBorderColor
=>
_trackBorderColor
;
Color
_trackBorderColor
;
set
trackBorderColor
(
Color
value
)
{
assert
(
value
!=
null
);
if
(
trackBorderColor
==
value
)
return
;
_trackBorderColor
=
value
;
notifyListeners
();
}
/// [TextDirection] of the [BuildContext] which dictates the side of the
/// screen the scrollbar appears in (the trailing side). Mustn't be null.
TextDirection
get
textDirection
=>
_textDirection
;
TextDirection
_textDirection
;
set
textDirection
(
TextDirection
value
)
{
/// screen the scrollbar appears in (the trailing side). Must be set prior to
/// calling paint.
TextDirection
?
get
textDirection
=>
_textDirection
;
TextDirection
?
_textDirection
;
set
textDirection
(
TextDirection
?
value
)
{
assert
(
value
!=
null
);
if
(
textDirection
==
value
)
return
;
...
...
@@ -99,7 +148,16 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
}
/// Thickness of the scrollbar in its cross-axis in logical pixels. Mustn't be null.
double
thickness
;
double
get
thickness
=>
_thickness
;
double
_thickness
;
set
thickness
(
double
value
)
{
assert
(
value
!=
null
);
if
(
thickness
==
value
)
return
;
_thickness
=
value
;
notifyListeners
();
}
/// An opacity [Animation] that dictates the opacity of the thumb.
/// Changes in value of this [Listenable] will automatically trigger repaints.
...
...
@@ -110,17 +168,43 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// in logical pixels. It affects the amount of available paint area.
///
/// Mustn't be null and defaults to 0.
final
double
mainAxisMargin
;
double
get
mainAxisMargin
=>
_mainAxisMargin
;
double
_mainAxisMargin
;
set
mainAxisMargin
(
double
value
)
{
assert
(
value
!=
null
);
if
(
mainAxisMargin
==
value
)
return
;
_mainAxisMargin
=
value
;
notifyListeners
();
}
/// Distance from the scrollbar's side to the nearest edge in logical pixels.
///
/// Must not be null and defaults to 0.
final
double
crossAxisMargin
;
double
get
crossAxisMargin
=>
_crossAxisMargin
;
double
_crossAxisMargin
;
set
crossAxisMargin
(
double
value
)
{
assert
(
value
!=
null
);
if
(
crossAxisMargin
==
value
)
return
;
_crossAxisMargin
=
value
;
notifyListeners
();
}
/// [Radius] of corners if the scrollbar should have rounded corners.
///
/// Scrollbar will be rectangular if [radius] is null.
Radius
?
radius
;
Radius
?
get
radius
=>
_radius
;
Radius
?
_radius
;
set
radius
(
Radius
?
value
)
{
if
(
radius
==
value
)
return
;
_radius
=
value
;
notifyListeners
();
}
/// The amount of space by which to inset the scrollbar's start and end, as
/// well as its side to the nearest edge, in logical pixels.
...
...
@@ -154,7 +238,16 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
///
/// Mustn't be null and the value has to be within the range of 0 to
/// [minOverscrollLength], inclusive. Defaults to 18.0.
final
double
minLength
;
double
get
minLength
=>
_minLength
;
double
_minLength
;
set
minLength
(
double
value
)
{
assert
(
value
!=
null
);
if
(
minLength
==
value
)
return
;
_minLength
=
value
;
notifyListeners
();
}
/// The preferred smallest size the scrollbar can shrink to when viewport is
/// overscrolled.
...
...
@@ -166,11 +259,22 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
///
/// The value is less than or equal to [minLength] and greater than or equal to 0.
/// If unspecified or set to null, it will defaults to the value of [minLength].
final
double
minOverscrollLength
;
double
get
minOverscrollLength
=>
_minOverscrollLength
;
double
_minOverscrollLength
;
set
minOverscrollLength
(
double
value
)
{
assert
(
value
!=
null
);
if
(
minOverscrollLength
==
value
)
return
;
_minOverscrollLength
=
value
;
notifyListeners
();
}
ScrollMetrics
?
_lastMetrics
;
AxisDirection
?
_lastAxisDirection
;
Rect
?
_thumbRect
;
Rect
?
_trackRect
;
late
double
_thumbOffset
;
/// Update with new [ScrollMetrics]. The scrollbar will show and redraw itself
/// based on these new metrics.
...
...
@@ -189,50 +293,82 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
void
updateThickness
(
double
nextThickness
,
Radius
nextRadius
)
{
thickness
=
nextThickness
;
radius
=
nextRadius
;
notifyListeners
();
}
Paint
get
_paint
{
Paint
get
_paint
Thumb
{
return
Paint
()
..
color
=
color
.
withOpacity
(
color
.
opacity
*
fadeoutOpacityAnimation
.
value
);
}
void
_paintThumbCrossAxis
(
Canvas
canvas
,
Size
size
,
double
thumbOffset
,
double
thumbExtent
,
AxisDirection
direction
)
{
Paint
_paintTrack
({
bool
isBorder
=
false
})
{
if
(
isBorder
)
{
return
Paint
()
..
color
=
trackBorderColor
.
withOpacity
(
trackBorderColor
.
opacity
*
fadeoutOpacityAnimation
.
value
)
..
style
=
PaintingStyle
.
stroke
..
strokeWidth
=
1.0
;
}
return
Paint
()
..
color
=
trackColor
.
withOpacity
(
trackColor
.
opacity
*
fadeoutOpacityAnimation
.
value
);
}
void
_paintScrollbar
(
Canvas
canvas
,
Size
size
,
double
thumbExtent
,
AxisDirection
direction
)
{
assert
(
textDirection
!=
null
,
'A TextDirection must be provided before a Scrollbar can be painted.'
,
);
final
double
x
,
y
;
final
Size
thumbSize
;
final
Size
thumbSize
,
trackSize
;
final
Offset
trackOffset
;
switch
(
direction
)
{
case
AxisDirection
.
down
:
thumbSize
=
Size
(
thickness
,
thumbExtent
);
trackSize
=
Size
(
thickness
+
2
*
crossAxisMargin
,
_trackExtent
);
x
=
textDirection
==
TextDirection
.
rtl
?
crossAxisMargin
+
padding
.
left
:
size
.
width
-
thickness
-
crossAxisMargin
-
padding
.
right
;
y
=
thumbOffset
;
y
=
_thumbOffset
;
trackOffset
=
Offset
(
x
-
crossAxisMargin
,
0.0
);
break
;
case
AxisDirection
.
up
:
thumbSize
=
Size
(
thickness
,
thumbExtent
);
trackSize
=
Size
(
thickness
+
2
*
crossAxisMargin
,
_trackExtent
);
x
=
textDirection
==
TextDirection
.
rtl
?
crossAxisMargin
+
padding
.
left
:
size
.
width
-
thickness
-
crossAxisMargin
-
padding
.
right
;
y
=
thumbOffset
;
y
=
_thumbOffset
;
trackOffset
=
Offset
(
x
-
crossAxisMargin
,
0.0
);
break
;
case
AxisDirection
.
left
:
thumbSize
=
Size
(
thumbExtent
,
thickness
);
x
=
thumbOffset
;
x
=
_
thumbOffset
;
y
=
size
.
height
-
thickness
-
crossAxisMargin
-
padding
.
bottom
;
trackSize
=
Size
(
_trackExtent
,
thickness
+
2
*
crossAxisMargin
);
trackOffset
=
Offset
(
0.0
,
y
-
crossAxisMargin
);
break
;
case
AxisDirection
.
right
:
thumbSize
=
Size
(
thumbExtent
,
thickness
);
x
=
thumbOffset
;
trackSize
=
Size
(
_trackExtent
,
thickness
+
2
*
crossAxisMargin
);
x
=
_thumbOffset
;
y
=
size
.
height
-
thickness
-
crossAxisMargin
-
padding
.
bottom
;
trackOffset
=
Offset
(
0.0
,
y
-
crossAxisMargin
);
break
;
}
_trackRect
=
trackOffset
&
trackSize
;
canvas
.
drawRect
(
_trackRect
!,
_paintTrack
());
canvas
.
drawLine
(
trackOffset
,
Offset
(
trackOffset
.
dx
,
trackOffset
.
dy
+
_trackExtent
),
_paintTrack
(
isBorder:
true
),
);
_thumbRect
=
Offset
(
x
,
y
)
&
thumbSize
;
if
(
radius
==
null
)
canvas
.
drawRect
(
_thumbRect
!,
_paint
);
canvas
.
drawRect
(
_thumbRect
!,
_paint
Thumb
);
else
canvas
.
drawRRect
(
RRect
.
fromRectAndRadius
(
_thumbRect
!,
radius
!),
_paint
);
canvas
.
drawRRect
(
RRect
.
fromRectAndRadius
(
_thumbRect
!,
radius
!),
_paint
Thumb
);
}
double
_thumbExtent
()
{
...
...
@@ -332,14 +468,39 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
final
double
beforePadding
=
_isVertical
?
padding
.
top
:
padding
.
left
;
final
double
thumbExtent
=
_thumbExtent
();
final
double
thumbOffsetLocal
=
_getScrollToTrack
(
_lastMetrics
!,
thumbExtent
);
final
double
thumbOffset
=
thumbOffsetLocal
+
mainAxisMargin
+
beforePadding
;
_thumbOffset
=
thumbOffsetLocal
+
mainAxisMargin
+
beforePadding
;
// Do not paint a scrollbar if the scroll view is infinitely long.
// TODO(Piinks): Special handling for infinite scroll views, https://github.com/flutter/flutter/issues/41434
if
(
_lastMetrics
!.
maxScrollExtent
.
isInfinite
)
return
;
return
_paint
ThumbCrossAxis
(
canvas
,
size
,
thumbOffset
,
thumbExtent
,
_lastAxisDirection
!);
return
_paint
Scrollbar
(
canvas
,
size
,
thumbExtent
,
_lastAxisDirection
!);
}
/// Same as hitTest, but includes some padding to make sure that the region
/// isn't too small to be interacted with by the user.
bool
hitTestInteractive
(
Offset
position
)
{
if
(
_thumbRect
==
null
)
{
return
false
;
}
// The scrollbar is not able to be hit when transparent.
if
(
fadeoutOpacityAnimation
.
value
==
0.0
)
{
return
false
;
}
final
Rect
interactiveScrollbarRect
=
_trackRect
==
null
?
_thumbRect
!.
expandToInclude
(
Rect
.
fromCircle
(
center:
_thumbRect
!.
center
,
radius:
_kMinInteractiveSize
/
2
),
)
:
_trackRect
!.
expandToInclude
(
Rect
.
fromCircle
(
center:
_thumbRect
!.
center
,
radius:
_kMinInteractiveSize
/
2
),
);
return
interactiveScrollbarRect
.
contains
(
position
);
}
/// Same as hitTestInteractive, but excludes the track portion of the scrollbar.
/// Used to evaluate interactions with only the scrollbar thumb.
bool
hitTestOnlyThumbInteractive
(
Offset
position
)
{
if
(
_thumbRect
==
null
)
{
return
false
;
}
...
...
@@ -353,7 +514,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
return
interactiveThumbRect
.
contains
(
position
);
}
// Scrollbars
can be interactive in Cupertino
.
// Scrollbars
are interactive
.
@override
bool
?
hitTest
(
Offset
?
position
)
{
if
(
_thumbRect
==
null
)
{
...
...
@@ -370,6 +531,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
bool
shouldRepaint
(
ScrollbarPainter
old
)
{
// Should repaint if any properties changed.
return
color
!=
old
.
color
||
trackColor
!=
old
.
trackColor
||
trackBorderColor
!=
old
.
trackBorderColor
||
textDirection
!=
old
.
textDirection
||
thickness
!=
old
.
thickness
||
fadeoutOpacityAnimation
!=
old
.
fadeoutOpacityAnimation
...
...
@@ -377,7 +540,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
||
crossAxisMargin
!=
old
.
crossAxisMargin
||
radius
!=
old
.
radius
||
minLength
!=
old
.
minLength
||
padding
!=
old
.
padding
;
||
padding
!=
old
.
padding
||
minOverscrollLength
!=
old
.
minOverscrollLength
;
}
@override
...
...
@@ -386,3 +550,686 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
@override
SemanticsBuilderCallback
?
get
semanticsBuilder
=>
null
;
}
/// An extendable base class for building scrollbars that fade in and out.
///
/// To add a scrollbar to a [ScrollView], like a [ListView] or a
/// [CustomScrollView], wrap the scroll view widget in a [RawScrollbar] widget.
///
/// {@template flutter.widgets.Scrollbar}
/// A scrollbar thumb indicates which portion of a [ScrollView] is actually
/// visible.
///
/// By default, the thumb will fade in and out as the child scroll view
/// scrolls. When [isAlwaysShown] is true, and a [controller] is specified, the
/// scrollbar thumb will remain visible without the fade animation.
///
/// Scrollbars are interactive and will use the [PrimaryScrollController] if
/// a [controller] is not set. Scrollbar thumbs can be dragged along the main axis
/// of the [ScrollView] to change the [ScrollPosition]. Tapping along the track
/// exclusive of the thumb will trigger a [ScrollIncrementType.page] based on
/// the relative position to the thumb.
///
/// If the child [ScrollView] is infinitely long, the [RawScrollbar] will not be
/// painted. In this case, the scrollbar cannot accurately represent the
/// relative location of the visible area, or calculate the accurate delta to
/// apply when dragging on the thumb or tapping on the track.
/// {@endtemplate}
///
// TODO(Piinks): Add code sample
///
/// See also:
///
/// * [ListView], which displays a linear, scrollable list of children.
/// * [GridView], which displays a 2 dimensional, scrollable array of children.
// TODO(Piinks): Add support for passing a shape instead of thickness/radius.
// Will need to update painter to support as well.
// Also, expose helpful properties like main/crossAxis margins, minThumbLength,
// etc. on the RawScrollbar in follow-up changes
// part of https://github.com/flutter/flutter/issues/13253
class
RawScrollbar
extends
StatefulWidget
{
/// Creates a basic raw scrollbar that wraps the given [child].
///
/// The [child], or a descendant of the [child], should be a source of
/// [ScrollNotification] notifications, typically a [Scrollable] widget.
///
/// The [child], [thickness], [thumbColor], [isAlwaysShown], [fadeDuration],
/// and [timeToFade] arguments must not be null.
const
RawScrollbar
({
Key
?
key
,
required
this
.
child
,
this
.
controller
,
this
.
isAlwaysShown
=
false
,
this
.
radius
,
this
.
thickness
,
this
.
thumbColor
,
this
.
fadeDuration
=
_kScrollbarFadeDuration
,
this
.
timeToFade
=
_kScrollbarTimeToFade
,
this
.
pressDuration
=
Duration
.
zero
,
})
:
assert
(
!
isAlwaysShown
||
controller
!=
null
,
'When isAlwaysShown is true, a ScrollController must be provided.'
,
),
assert
(
child
!=
null
),
assert
(
fadeDuration
!=
null
),
assert
(
timeToFade
!=
null
),
assert
(
pressDuration
!=
null
),
super
(
key:
key
);
/// The widget below this widget in the tree.
///
/// The scrollbar will be stacked on top of this child. This child (and its
/// subtree) should include a source of [ScrollNotification] notifications.
///
/// Typically a [ListView] or [CustomScrollView].
final
Widget
child
;
/// The [ScrollController] used to implement Scrollbar dragging.
///
/// If nothing is passed to controller, the default behavior is to automatically
/// enable scrollbar dragging on the nearest ScrollController using
/// [PrimaryScrollController.of].
///
/// If a ScrollController is passed, then dragging on the scrollbar thumb will
/// update the [ScrollPosition] attached to the controller. A stateful ancestor
/// of this widget needs to manage the ScrollController and either pass it to
/// a scrollable descendant or use a PrimaryScrollController to share it.
///
/// {@tool snippet}
/// Here is an example of using the `controller` parameter to enable
/// scrollbar dragging for multiple independent ListViews:
///
/// ```dart
/// final ScrollController _controllerOne = ScrollController();
/// final ScrollController _controllerTwo = ScrollController();
///
/// build(BuildContext context) {
/// return Column(
/// children: <Widget>[
/// Container(
/// height: 200,
/// child: CupertinoScrollbar(
/// controller: _controllerOne,
/// child: ListView.builder(
/// controller: _controllerOne,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index) => Text('item $index'),
/// ),
/// ),
/// ),
/// Container(
/// height: 200,
/// child: CupertinoScrollbar(
/// controller: _controllerTwo,
/// child: ListView.builder(
/// controller: _controllerTwo,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index) => Text('list 2 item $index'),
/// ),
/// ),
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
final
ScrollController
?
controller
;
/// Indicates that the scrollbar should be visible, even when a scroll is not
/// underway.
///
/// When false, the scrollbar will be shown during scrolling
/// and will fade out otherwise.
///
/// When true, the scrollbar will always be visible and never fade out. The
/// [controller] property must be set in this case.
///
/// Defaults to false.
///
/// {@tool snippet}
///
/// ```dart
/// final ScrollController _controllerOne = ScrollController();
/// final ScrollController _controllerTwo = ScrollController();
///
/// build(BuildContext context) {
/// return Column(
/// children: <Widget>[
/// SizedBox(
/// height: 200,
/// child: Scrollbar(
/// isAlwaysShown: true,
/// controller: _controllerOne,
/// child: ListView.builder(
/// controller: _controllerOne,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index) {
/// return Text('item $index');
/// },
/// ),
/// ),
/// ),
/// SizedBox(
/// height: 200,
/// child: CupertinoScrollbar(
/// isAlwaysShown: true,
/// controller: _controllerTwo,
/// child: SingleChildScrollView(
/// controller: _controllerTwo,
/// child: SizedBox(
/// height: 2000,
/// width: 500,
/// child: Placeholder(),
/// ),
/// ),
/// ),
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
final
bool
isAlwaysShown
;
/// The [Radius] of the scrollbar thumb's rounded rectangle corners.
///
/// Scrollbar will be rectangular if [radius] is null, which is the default
/// behavior.
final
Radius
?
radius
;
/// The thickness of the scrollbar in the cross axis of the scrollable.
///
/// If null, will default to 6.0 pixels.
final
double
?
thickness
;
/// The color of the scrollbar thumb.
///
/// If null, defaults to Color(0x66BCBCBC).
final
Color
?
thumbColor
;
/// The [Duration] of the fade animation.
///
/// Cannot be null, defaults to a [Duration] of 300 milliseconds.
final
Duration
fadeDuration
;
/// The [Duration] of time until the fade animation begins.
///
/// Cannot be null, defaults to a [Duration] of 600 milliseconds.
final
Duration
timeToFade
;
/// The [Duration] of time that a LongPress will trigger the drag gesture of
/// the scrollbar thumb.
///
/// Cannot be null, defaults to [Duration.zero].
final
Duration
pressDuration
;
@override
RawScrollbarState
<
RawScrollbar
>
createState
()
=>
RawScrollbarState
<
RawScrollbar
>();
}
/// The state for a [RawScrollbar] widget, also shared by the [Scrollbar] and
/// [CupertinoScrollbar] widgets.
///
/// Controls the animation that fades a scrollbar's thumb in and out of view.
///
/// Provides defaults gestures for dragging the scrollbar thumb and tapping on the
/// scrollbar track.
class
RawScrollbarState
<
T
extends
RawScrollbar
>
extends
State
<
T
>
with
TickerProviderStateMixin
<
T
>
{
double
?
_dragScrollbarAxisPosition
;
ScrollController
?
_currentController
;
Timer
?
_fadeoutTimer
;
late
AnimationController
_fadeoutAnimationController
;
late
Animation
<
double
>
_fadeoutOpacityAnimation
;
final
GlobalKey
_scrollbarPainterKey
=
GlobalKey
();
bool
_hoverIsActive
=
false
;
/// Used to paint the scrollbar.
///
/// Can be customized by subclasses to change scrollbar behavior by overriding
/// [updateScrollbarPainter].
@protected
late
final
ScrollbarPainter
scrollbarPainter
;
@override
void
initState
()
{
super
.
initState
();
_fadeoutAnimationController
=
AnimationController
(
vsync:
this
,
duration:
widget
.
fadeDuration
,
);
_fadeoutOpacityAnimation
=
CurvedAnimation
(
parent:
_fadeoutAnimationController
,
curve:
Curves
.
fastOutSlowIn
,
);
scrollbarPainter
=
ScrollbarPainter
(
color:
widget
.
thumbColor
??
const
Color
(
0x66BCBCBC
),
thickness:
widget
.
thickness
??
_kScrollbarThickness
,
fadeoutOpacityAnimation:
_fadeoutOpacityAnimation
,
);
}
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
_maybeTriggerScrollbar
();
}
// Waits one frame and cause an empty scroll event (zero delta pixels).
//
// This allows the thumb to show immediately when isAlwaysShown is true.
// A scroll event is required in order to paint the thumb.
void
_maybeTriggerScrollbar
()
{
WidgetsBinding
.
instance
!.
addPostFrameCallback
((
Duration
duration
)
{
if
(
widget
.
isAlwaysShown
)
{
_fadeoutTimer
?.
cancel
();
// Wait one frame and cause an empty scroll event. This allows the
// thumb to show immediately when isAlwaysShown is true. A scroll
// event is required in order to paint the thumb.
widget
.
controller
!.
position
.
didUpdateScrollPositionBy
(
0
);
}
});
}
/// This method is responsible for configuring the [scrollbarPainter]
/// according to the [widget]'s properties and any inherited widgets the
/// painter depends on, like [Directionality] and [MediaQuery].
///
/// Subclasses can override to configure the [scrollbarPainter].
@protected
void
updateScrollbarPainter
()
{
scrollbarPainter
..
color
=
widget
.
thumbColor
??
const
Color
(
0x66BCBCBC
)
..
textDirection
=
Directionality
.
of
(
context
)
..
thickness
=
widget
.
thickness
??
_kScrollbarThickness
..
radius
=
widget
.
radius
..
padding
=
MediaQuery
.
of
(
context
).
padding
;
}
@override
void
didUpdateWidget
(
T
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
isAlwaysShown
!=
oldWidget
.
isAlwaysShown
)
{
if
(
widget
.
isAlwaysShown
==
true
)
{
_maybeTriggerScrollbar
();
_fadeoutAnimationController
.
animateTo
(
1.0
);
}
else
{
_fadeoutAnimationController
.
reverse
();
}
}
}
void
_updateScrollPosition
(
double
primaryDelta
)
{
assert
(
_currentController
!=
null
);
// Convert primaryDelta, the amount that the scrollbar moved since the last
// time _dragScrollbar was called, into the coordinate space of the scroll
// position, and jump to that position.
final
double
scrollOffsetLocal
=
scrollbarPainter
.
getTrackToScroll
(
primaryDelta
);
final
double
scrollOffsetGlobal
=
scrollOffsetLocal
+
_currentController
!.
position
.
pixels
;
_currentController
!.
position
.
jumpTo
(
scrollOffsetGlobal
);
}
void
_maybeStartFadeoutTimer
()
{
if
(!
widget
.
isAlwaysShown
)
{
_fadeoutTimer
?.
cancel
();
_fadeoutTimer
=
Timer
(
widget
.
timeToFade
,
()
{
_fadeoutAnimationController
.
reverse
();
_fadeoutTimer
=
null
;
});
}
}
/// Returns the [Axis] of the child scroll view, or null if the current scroll
/// controller does not have any attached positions.
@protected
Axis
?
getScrollbarDirection
()
{
assert
(
_currentController
!=
null
);
if
(
_currentController
!.
hasClients
)
return
_currentController
!.
position
.
axis
;
return
null
;
}
/// Handler called when a press on the scrollbar thumb has been recognized.
///
/// Cancels the [Timer] associated with the fade animation of the scrollbar.
@protected
@mustCallSuper
void
handleThumbPress
()
{
if
(
getScrollbarDirection
()
==
null
)
{
return
;
}
_fadeoutTimer
?.
cancel
();
}
/// Handler called when a long press gesture has started.
///
/// Begins the fade out animation and initializes dragging the scrollbar thumb.
@protected
@mustCallSuper
void
handleThumbPressStart
(
Offset
localPosition
)
{
_currentController
=
widget
.
controller
??
PrimaryScrollController
.
of
(
context
);
final
Axis
?
direction
=
getScrollbarDirection
();
if
(
direction
==
null
)
{
return
;
}
_fadeoutTimer
?.
cancel
();
_fadeoutAnimationController
.
forward
();
switch
(
direction
)
{
case
Axis
.
vertical
:
_dragScrollbarAxisPosition
=
localPosition
.
dy
;
break
;
case
Axis
.
horizontal
:
_dragScrollbarAxisPosition
=
localPosition
.
dx
;
break
;
}
}
/// Handler called when a currently active long press gesture moves.
///
/// Updates the position of the child scrollable.
@protected
@mustCallSuper
void
handleThumbPressUpdate
(
Offset
localPosition
)
{
final
Axis
?
direction
=
getScrollbarDirection
();
if
(
direction
==
null
)
{
return
;
}
switch
(
direction
)
{
case
Axis
.
vertical
:
_updateScrollPosition
(
localPosition
.
dy
-
_dragScrollbarAxisPosition
!);
_dragScrollbarAxisPosition
=
localPosition
.
dy
;
break
;
case
Axis
.
horizontal
:
_updateScrollPosition
(
localPosition
.
dx
-
_dragScrollbarAxisPosition
!);
_dragScrollbarAxisPosition
=
localPosition
.
dx
;
break
;
}
}
/// Handler called when a long press has ended.
@protected
@mustCallSuper
void
handleThumbPressEnd
(
Offset
localPosition
,
Velocity
velocity
)
{
final
Axis
?
direction
=
getScrollbarDirection
();
if
(
direction
==
null
)
return
;
_maybeStartFadeoutTimer
();
_dragScrollbarAxisPosition
=
null
;
_currentController
=
null
;
}
void
_handleTrackTapDown
(
TapDownDetails
details
)
{
// The Scrollbar should page towards the position of the tap on the track.
_currentController
=
widget
.
controller
??
PrimaryScrollController
.
of
(
context
);
double
scrollIncrement
;
// Is an increment calculator available?
final
ScrollIncrementCalculator
?
calculator
=
Scrollable
.
of
(
_currentController
!.
position
.
context
.
notificationContext
!
)?.
widget
.
incrementCalculator
;
if
(
calculator
!=
null
)
{
scrollIncrement
=
calculator
(
ScrollIncrementDetails
(
type:
ScrollIncrementType
.
page
,
metrics:
_currentController
!.
position
,
)
);
}
else
{
// Default page increment
scrollIncrement
=
0.8
*
_currentController
!.
position
.
viewportDimension
;
}
// Adjust scrollIncrement for direction
switch
(
_currentController
!.
position
.
axisDirection
)
{
case
AxisDirection
.
up
:
if
(
details
.
localPosition
.
dy
>
scrollbarPainter
.
_thumbOffset
)
scrollIncrement
=
-
scrollIncrement
;
break
;
case
AxisDirection
.
down
:
if
(
details
.
localPosition
.
dy
<
scrollbarPainter
.
_thumbOffset
)
scrollIncrement
=
-
scrollIncrement
;
break
;
case
AxisDirection
.
right
:
if
(
details
.
localPosition
.
dx
<
scrollbarPainter
.
_thumbOffset
)
scrollIncrement
=
-
scrollIncrement
;
break
;
case
AxisDirection
.
left
:
if
(
details
.
localPosition
.
dx
>
scrollbarPainter
.
_thumbOffset
)
scrollIncrement
=
-
scrollIncrement
;
break
;
}
_currentController
!.
position
.
moveTo
(
_currentController
!.
position
.
pixels
+
scrollIncrement
,
duration:
const
Duration
(
milliseconds:
100
),
curve:
Curves
.
easeInOut
,
);
}
bool
_handleScrollNotification
(
ScrollNotification
notification
)
{
final
ScrollMetrics
metrics
=
notification
.
metrics
;
if
(
metrics
.
maxScrollExtent
<=
metrics
.
minScrollExtent
)
return
false
;
if
(
notification
is
ScrollUpdateNotification
||
notification
is
OverscrollNotification
)
{
// Any movements always makes the scrollbar start showing up.
if
(
_fadeoutAnimationController
.
status
!=
AnimationStatus
.
forward
)
_fadeoutAnimationController
.
forward
();
_fadeoutTimer
?.
cancel
();
scrollbarPainter
.
update
(
notification
.
metrics
,
notification
.
metrics
.
axisDirection
);
}
else
if
(
notification
is
ScrollEndNotification
)
{
if
(
_dragScrollbarAxisPosition
==
null
)
_maybeStartFadeoutTimer
();
}
return
false
;
}
Map
<
Type
,
GestureRecognizerFactory
>
get
_gestures
{
final
Map
<
Type
,
GestureRecognizerFactory
>
gestures
=
<
Type
,
GestureRecognizerFactory
>{};
final
ScrollController
?
controller
=
widget
.
controller
??
PrimaryScrollController
.
of
(
context
);
if
(
controller
==
null
)
return
gestures
;
gestures
[
_ThumbPressGestureRecognizer
]
=
GestureRecognizerFactoryWithHandlers
<
_ThumbPressGestureRecognizer
>(
()
=>
_ThumbPressGestureRecognizer
(
debugOwner:
this
,
customPaintKey:
_scrollbarPainterKey
,
pressDuration:
widget
.
pressDuration
,
),
(
_ThumbPressGestureRecognizer
instance
)
{
instance
.
onLongPress
=
handleThumbPress
;
instance
.
onLongPressStart
=
(
LongPressStartDetails
details
)
=>
handleThumbPressStart
(
details
.
localPosition
);
instance
.
onLongPressMoveUpdate
=
(
LongPressMoveUpdateDetails
details
)
=>
handleThumbPressUpdate
(
details
.
localPosition
);
instance
.
onLongPressEnd
=
(
LongPressEndDetails
details
)
=>
handleThumbPressEnd
(
details
.
localPosition
,
details
.
velocity
);
},
);
gestures
[
_TrackTapGestureRecognizer
]
=
GestureRecognizerFactoryWithHandlers
<
_TrackTapGestureRecognizer
>(
()
=>
_TrackTapGestureRecognizer
(
debugOwner:
this
,
customPaintKey:
_scrollbarPainterKey
,
),
(
_TrackTapGestureRecognizer
instance
)
{
instance
.
onTapDown
=
_handleTrackTapDown
;
},
);
return
gestures
;
}
/// Returns true if the provided [Offset] is located over the track of the
/// [RawScrollbar].
///
/// Excludes the [RawScrollbar] thumb.
@protected
bool
isPointerOverTrack
(
Offset
position
)
{
if
(
_scrollbarPainterKey
.
currentContext
==
null
)
{
return
false
;
}
final
Offset
localOffset
=
_getLocalOffset
(
_scrollbarPainterKey
,
position
);
return
scrollbarPainter
.
hitTestInteractive
(
localOffset
)
&&
!
scrollbarPainter
.
hitTestOnlyThumbInteractive
(
localOffset
);
}
/// Returns true if the provided [Offset] is located over the thumb of the
/// [RawScrollbar].
@protected
bool
isPointerOverThumb
(
Offset
position
)
{
if
(
_scrollbarPainterKey
.
currentContext
==
null
)
{
return
false
;
}
final
Offset
localOffset
=
_getLocalOffset
(
_scrollbarPainterKey
,
position
);
return
scrollbarPainter
.
hitTestOnlyThumbInteractive
(
localOffset
);
}
/// Returns true if the provided [Offset] is located over the track or thumb
/// of the [RawScrollbar].
@protected
bool
isPointerOverScrollbar
(
Offset
position
)
{
if
(
_scrollbarPainterKey
.
currentContext
==
null
)
{
return
false
;
}
final
Offset
localOffset
=
_getLocalOffset
(
_scrollbarPainterKey
,
position
);
return
scrollbarPainter
.
hitTestInteractive
(
localOffset
);
}
/// Cancels the fade out animation so the scrollbar will remain visible for
/// interaction.
///
/// Can be overridden by subclasses to respond to a [PointerHoverEvent].
///
/// Helper methods [isPointerOverScrollbar], [isPointerOverThumb], and
/// [isPointerOverTrack] can be used to determine the location of the pointer
/// relative to the painter scrollbar elements.
@protected
@mustCallSuper
void
handleHover
(
PointerHoverEvent
event
)
{
// Check if the position of the pointer falls over the painted scrollbar
if
(
isPointerOverScrollbar
(
event
.
position
))
{
_hoverIsActive
=
true
;
_fadeoutTimer
?.
cancel
();
}
else
if
(
_hoverIsActive
)
{
// Pointer is not over painted scrollbar.
_hoverIsActive
=
false
;
_maybeStartFadeoutTimer
();
}
}
/// Initiates the fade out animation.
///
/// Can be overridden by subclasses to respond to a [PointerExitEvent].
@protected
@mustCallSuper
void
handleHoverExit
(
PointerExitEvent
event
)
{
_hoverIsActive
=
false
;
_maybeStartFadeoutTimer
();
}
@override
void
dispose
()
{
_fadeoutAnimationController
.
dispose
();
_fadeoutTimer
?.
cancel
();
scrollbarPainter
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
updateScrollbarPainter
();
return
NotificationListener
<
ScrollNotification
>(
onNotification:
_handleScrollNotification
,
child:
RepaintBoundary
(
child:
RawGestureDetector
(
gestures:
_gestures
,
child:
MouseRegion
(
onExit:
handleHoverExit
,
onHover:
handleHover
,
child:
CustomPaint
(
key:
_scrollbarPainterKey
,
foregroundPainter:
scrollbarPainter
,
child:
RepaintBoundary
(
child:
widget
.
child
),
),
),
),
),
);
}
}
// A long press gesture detector that only responds to events on the scrollbar's
// thumb and ignores everything else.
class
_ThumbPressGestureRecognizer
extends
LongPressGestureRecognizer
{
_ThumbPressGestureRecognizer
({
double
?
postAcceptSlopTolerance
,
PointerDeviceKind
?
kind
,
required
Object
debugOwner
,
required
GlobalKey
customPaintKey
,
required
Duration
pressDuration
,
})
:
_customPaintKey
=
customPaintKey
,
super
(
postAcceptSlopTolerance:
postAcceptSlopTolerance
,
kind:
kind
,
debugOwner:
debugOwner
,
duration:
pressDuration
,
);
final
GlobalKey
_customPaintKey
;
@override
bool
isPointerAllowed
(
PointerDownEvent
event
)
{
if
(!
_hitTestInteractive
(
_customPaintKey
,
event
.
position
))
{
return
false
;
}
return
super
.
isPointerAllowed
(
event
);
}
bool
_hitTestInteractive
(
GlobalKey
customPaintKey
,
Offset
offset
)
{
if
(
customPaintKey
.
currentContext
==
null
)
{
return
false
;
}
final
CustomPaint
customPaint
=
customPaintKey
.
currentContext
!.
widget
as
CustomPaint
;
final
ScrollbarPainter
painter
=
customPaint
.
foregroundPainter
!
as
ScrollbarPainter
;
final
Offset
localOffset
=
_getLocalOffset
(
customPaintKey
,
offset
);
return
painter
.
hitTestOnlyThumbInteractive
(
localOffset
);
}
}
// A tap gesture detector that only responds to events on the scrollbar's
// track and ignores everything else, including the thumb.
class
_TrackTapGestureRecognizer
extends
TapGestureRecognizer
{
_TrackTapGestureRecognizer
({
required
Object
debugOwner
,
required
GlobalKey
customPaintKey
,
})
:
_customPaintKey
=
customPaintKey
,
super
(
debugOwner:
debugOwner
);
final
GlobalKey
_customPaintKey
;
@override
bool
isPointerAllowed
(
PointerDownEvent
event
)
{
if
(!
_hitTestInteractive
(
_customPaintKey
,
event
.
position
))
{
return
false
;
}
return
super
.
isPointerAllowed
(
event
);
}
bool
_hitTestInteractive
(
GlobalKey
customPaintKey
,
Offset
offset
)
{
if
(
customPaintKey
.
currentContext
==
null
)
{
return
false
;
}
final
CustomPaint
customPaint
=
customPaintKey
.
currentContext
!.
widget
as
CustomPaint
;
final
ScrollbarPainter
painter
=
customPaint
.
foregroundPainter
!
as
ScrollbarPainter
;
final
Offset
localOffset
=
_getLocalOffset
(
customPaintKey
,
offset
);
// We only receive track taps that are not on the thumb.
return
painter
.
hitTestInteractive
(
localOffset
)
&&
!
painter
.
hitTestOnlyThumbInteractive
(
localOffset
);
}
}
Offset
_getLocalOffset
(
GlobalKey
scrollbarPainterKey
,
Offset
position
)
{
final
RenderBox
renderBox
=
scrollbarPainterKey
.
currentContext
!.
findRenderObject
()!
as
RenderBox
;
return
renderBox
.
globalToLocal
(
position
);
}
packages/flutter/test/cupertino/scrollbar_test.dart
View file @
770a9b25
...
...
@@ -680,4 +680,63 @@ void main() {
await
tester
.
pump
(
_kScrollbarTimeToFade
);
await
tester
.
pump
(
_kScrollbarFadeDuration
);
});
testWidgets
(
'Tapping the track area pages the Scroll View'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
MediaQuery
(
data:
const
MediaQueryData
(),
child:
CupertinoScrollbar
(
isAlwaysShown:
true
,
controller:
scrollController
,
child:
SingleChildScrollView
(
controller:
scrollController
,
child:
const
SizedBox
(
width:
1000.0
,
height:
1000.0
),
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
CupertinoScrollbar
),
paints
..
rrect
(
color:
_kScrollbarColor
.
color
,
rrect:
RRect
.
fromLTRBR
(
794.0
,
3.0
,
797.0
,
359.4
,
const
Radius
.
circular
(
1.5
)),
)
);
// Tap on the track area below the thumb.
await
tester
.
tapAt
(
const
Offset
(
796.0
,
550.0
));
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
400.0
);
expect
(
find
.
byType
(
CupertinoScrollbar
),
paints
..
rrect
(
color:
_kScrollbarColor
.
color
,
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
794.0
,
240.6
,
797.0
,
597.0
),
const
Radius
.
circular
(
1.5
),
),
)
);
// Tap on the track area above the thumb.
await
tester
.
tapAt
(
const
Offset
(
796.0
,
50.0
));
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
CupertinoScrollbar
),
paints
..
rrect
(
color:
_kScrollbarColor
.
color
,
rrect:
RRect
.
fromLTRBR
(
794.0
,
3.0
,
797.0
,
359.4
,
const
Radius
.
circular
(
1.5
)),
)
);
});
}
packages/flutter/test/material/scrollbar_paint_test.dart
View file @
770a9b25
...
...
@@ -30,7 +30,7 @@ void main() {
));
expect
(
find
.
byType
(
Scrollbar
),
isNot
(
paints
..
rect
()));
await
tester
.
fling
(
find
.
byType
(
SingleChildScrollView
),
const
Offset
(
0.0
,
-
10.0
),
10.0
);
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
800.0
-
6.0
,
1.5
,
800.0
,
91.5
)));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
800.0
-
12.0
,
0.0
,
800.0
,
600.0
)));
});
testWidgets
(
'Viewport basic test (RTL)'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -40,7 +40,7 @@ void main() {
));
expect
(
find
.
byType
(
Scrollbar
),
isNot
(
paints
..
rect
()));
await
tester
.
fling
(
find
.
byType
(
SingleChildScrollView
),
const
Offset
(
0.0
,
-
10.0
),
10.0
);
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
0.0
,
1.5
,
6.0
,
91.5
)));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
0.0
,
0.0
,
12.0
,
600.0
)));
});
testWidgets
(
'works with MaterialApp and Scaffold'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -69,11 +69,11 @@ void main() {
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTWH
(
800.0
-
6
,
// screen width - thickness
800.0
-
12
,
// screen width - default thickness and margin
0
,
// the paint area starts from the bottom of the app bar
6
,
// thickness
12
,
// thickness
// 56 being the height of the app bar
(
600.0
-
56
-
34
-
20
)
/
4000
*
(
600
-
56
-
34
-
20
)
,
600.0
-
56
-
34
-
20
,
),
));
});
...
...
packages/flutter/test/material/scrollbar_test.dart
View file @
770a9b25
...
...
@@ -2,14 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'
package:flutter/cupertino.dart'
;
import
'package:flutter/foundation.dart'
;
import
'
dart:ui'
as
ui
;
import
'package:flutter/material.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'../rendering/mock_canvas.dart'
;
const
Duration
_kScrollbarFadeDuration
=
Duration
(
milliseconds:
300
);
const
Duration
_kScrollbarTimeToFade
=
Duration
(
milliseconds:
600
);
class
TestCanvas
implements
Canvas
{
final
List
<
Invocation
>
invocations
=
<
Invocation
>[];
...
...
@@ -119,85 +122,6 @@ void main() {
expect
(
canvas
.
invocations
.
isEmpty
,
isTrue
);
});
testWidgets
(
'Adaptive scrollbar'
,
(
WidgetTester
tester
)
async
{
Widget
viewWithScroll
(
TargetPlatform
platform
)
{
return
_buildBoilerplate
(
child:
Theme
(
data:
ThemeData
(
platform:
platform
),
child:
const
Scrollbar
(
child:
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
),
),
),
),
);
}
await
tester
.
pumpWidget
(
viewWithScroll
(
TargetPlatform
.
android
));
await
tester
.
drag
(
find
.
byType
(
SingleChildScrollView
),
const
Offset
(
0.0
,
-
10.0
));
await
tester
.
pump
();
// Scrollbar fully showing
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
());
await
tester
.
pumpWidget
(
viewWithScroll
(
TargetPlatform
.
iOS
));
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
byType
(
SingleChildScrollView
))
);
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
10.0
));
await
tester
.
drag
(
find
.
byType
(
SingleChildScrollView
),
const
Offset
(
0.0
,
-
10.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
());
expect
(
find
.
byType
(
CupertinoScrollbar
),
paints
..
rrect
());
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
await
tester
.
pumpWidget
(
viewWithScroll
(
TargetPlatform
.
macOS
));
await
gesture
.
down
(
tester
.
getCenter
(
find
.
byType
(
SingleChildScrollView
)),
);
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
10.0
));
await
tester
.
drag
(
find
.
byType
(
SingleChildScrollView
),
const
Offset
(
0.0
,
-
10.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
());
expect
(
find
.
byType
(
CupertinoScrollbar
),
paints
..
rrect
());
});
testWidgets
(
'Scrollbar passes controller to CupertinoScrollbar'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
controller
=
ScrollController
();
Widget
viewWithScroll
(
TargetPlatform
?
platform
)
{
return
_buildBoilerplate
(
child:
Theme
(
data:
ThemeData
(
platform:
platform
),
child:
Scrollbar
(
controller:
controller
,
child:
const
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
),
),
),
),
);
}
await
tester
.
pumpWidget
(
viewWithScroll
(
debugDefaultTargetPlatformOverride
));
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
byType
(
SingleChildScrollView
))
);
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
10.0
));
await
tester
.
drag
(
find
.
byType
(
SingleChildScrollView
),
const
Offset
(
0.0
,
-
10.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
expect
(
find
.
byType
(
CupertinoScrollbar
),
paints
..
rrect
());
final
CupertinoScrollbar
scrollbar
=
find
.
byType
(
CupertinoScrollbar
).
evaluate
().
first
.
widget
as
CupertinoScrollbar
;
expect
(
scrollbar
.
controller
,
isNotNull
);
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
testWidgets
(
'When isAlwaysShown is true, must pass a controller'
,
(
WidgetTester
tester
)
async
{
Widget
viewWithScroll
()
{
...
...
@@ -546,14 +470,300 @@ void main() {
await
tester
.
pump
();
// Long press on the scrollbar thumb and expect it to grow
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
(
r
ect:
const
Rect
.
fromLTWH
(
780
,
0
,
20
,
300
),
expect
(
find
.
byType
(
Scrollbar
),
paints
..
r
r
ect
(
r
rect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTWH
(
778
,
0
,
20
,
300
),
const
Radius
.
circular
(
8
)
),
));
await
tester
.
pumpWidget
(
viewWithScroll
(
radius:
const
Radius
.
circular
(
10
)));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTWH
(
7
80
,
0
,
20
,
300
),
const
Radius
.
circular
(
10
)),
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTWH
(
7
78
,
0
,
20
,
300
),
const
Radius
.
circular
(
10
)),
));
await
tester
.
pumpAndSettle
();
});
testWidgets
(
'Tapping the track area pages the Scroll View'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Scrollbar
(
isAlwaysShown:
true
,
controller:
scrollController
,
child:
SingleChildScrollView
(
controller:
scrollController
,
child:
const
SizedBox
(
width:
1000.0
,
height:
1000.0
),
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromLTRBR
(
790.0
,
0.0
,
798.0
,
360.0
,
const
Radius
.
circular
(
8.0
)),
)
);
// Tap on the track area below the thumb.
await
tester
.
tapAt
(
const
Offset
(
796.0
,
550.0
));
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
400.0
);
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
240.0
,
798.0
,
600.0
),
const
Radius
.
circular
(
8.0
),
),
)
);
// Tap on the track area above the thumb.
await
tester
.
tapAt
(
const
Offset
(
796.0
,
50.0
));
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromLTRBR
(
790.0
,
0.0
,
798.0
,
360.0
,
const
Radius
.
circular
(
8.0
)),
)
);
});
testWidgets
(
'Scrollbar never goes away until finger lift'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
MaterialApp
(
home:
Scrollbar
(
child:
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
)
),
),
),
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
byType
(
SingleChildScrollView
)));
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
20.0
));
await
tester
.
pump
();
// Scrollbar fully showing
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
3.0
,
798.0
,
93.0
),
const
Radius
.
circular
(
8.0
),
),
color:
const
Color
(
0x1a000000
),
),
);
await
tester
.
pump
(
const
Duration
(
seconds:
3
));
await
tester
.
pump
(
const
Duration
(
seconds:
3
));
// Still there.
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
3.0
,
798.0
,
93.0
),
const
Radius
.
circular
(
8.0
),
),
color:
const
Color
(
0x1a000000
),
),
);
await
gesture
.
up
();
await
tester
.
pump
(
_kScrollbarTimeToFade
);
await
tester
.
pump
(
_kScrollbarFadeDuration
*
0.5
);
// Opacity going down now.
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
3.0
,
798.0
,
93.0
),
const
Radius
.
circular
(
8.0
),
),
color:
const
Color
(
0x14000000
),
),
);
});
testWidgets
(
'Scrollbar thumb can be dragged'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
PrimaryScrollController
(
controller:
scrollController
,
child:
Scrollbar
(
isAlwaysShown:
true
,
controller:
scrollController
,
child:
const
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
)
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
0.0
,
798.0
,
90.0
),
const
Radius
.
circular
(
8.0
),
),
color:
const
Color
(
0x1a000000
),
),
);
// Drag the thumb down to scroll down.
const
double
scrollAmount
=
10.0
;
final
TestGesture
dragScrollbarGesture
=
await
tester
.
startGesture
(
const
Offset
(
797.0
,
45.0
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
0.0
,
798.0
,
90.0
),
const
Radius
.
circular
(
8.0
),
),
// Drag color
color:
const
Color
(
0x99000000
),
),
);
await
dragScrollbarGesture
.
moveBy
(
const
Offset
(
0.0
,
scrollAmount
));
await
tester
.
pumpAndSettle
();
await
dragScrollbarGesture
.
up
();
await
tester
.
pumpAndSettle
();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect
(
scrollController
.
offset
,
greaterThan
(
scrollAmount
*
2
));
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
10.0
,
798.0
,
100.0
),
const
Radius
.
circular
(
8.0
),
),
color:
const
Color
(
0x1a000000
),
),
);
});
testWidgets
(
'Scrollbar thumb color completes a hover animation'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
PrimaryScrollController
(
controller:
scrollController
,
child:
Scrollbar
(
isAlwaysShown:
true
,
controller:
scrollController
,
child:
const
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
)
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
0.0
,
798.0
,
90.0
),
const
Radius
.
circular
(
8.0
),
),
color:
const
Color
(
0x1a000000
),
),
);
final
TestGesture
gesture
=
await
tester
.
createGesture
(
kind:
ui
.
PointerDeviceKind
.
mouse
);
await
gesture
.
addPointer
();
addTearDown
(
gesture
.
removePointer
);
await
gesture
.
moveTo
(
const
Offset
(
794.0
,
5.0
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
0.0
,
798.0
,
90.0
),
const
Radius
.
circular
(
8.0
),
),
// Hover color
color:
const
Color
(
0x80000000
),
),
);
});
testWidgets
(
'Scrollbar showTrackOnHover'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
PrimaryScrollController
(
controller:
scrollController
,
child:
Scrollbar
(
isAlwaysShown:
true
,
showTrackOnHover:
true
,
controller:
scrollController
,
child:
const
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
)
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
const
Rect
.
fromLTRB
(
790.0
,
0.0
,
798.0
,
90.0
),
const
Radius
.
circular
(
8.0
),
),
color:
const
Color
(
0x1a000000
),
),
);
final
TestGesture
gesture
=
await
tester
.
createGesture
(
kind:
ui
.
PointerDeviceKind
.
mouse
);
await
gesture
.
addPointer
();
addTearDown
(
gesture
.
removePointer
);
await
gesture
.
moveTo
(
const
Offset
(
794.0
,
5.0
));
await
tester
.
pump
();
expect
(
find
.
byType
(
Scrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
784.0
,
0.0
,
800.0
,
600.0
),
color:
const
Color
(
0x08000000
),
)
..
line
(
p1:
const
Offset
(
784.0
,
0.0
),
p2:
const
Offset
(
784.0
,
600.0
),
strokeWidth:
1.0
,
color:
const
Color
(
0x1a000000
),
)
..
rrect
(
rrect:
RRect
.
fromRectAndRadius
(
// Scrollbar thumb is larger
const
Rect
.
fromLTRB
(
786.0
,
0.0
,
798.0
,
90.0
),
const
Radius
.
circular
(
8.0
),
),
// Hover color
color:
const
Color
(
0x80000000
),
),
);
});
}
packages/flutter/test/widgets/scrollbar_test.dart
View file @
770a9b25
...
...
@@ -2,15 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
as
ui
;
import
'package:flutter/src/physics/utils.dart'
show
nearEqual
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'../flutter_test_alternative.dart'
show
Fake
;
import
'../rendering/mock_canvas.dart'
;
const
Color
_kScrollbarColor
=
Color
(
0xFF123456
);
const
double
_kThickness
=
2.5
;
const
double
_kMinThumbExtent
=
18.0
;
const
Duration
_kScrollbarFadeDuration
=
Duration
(
milliseconds:
300
);
const
Duration
_kScrollbarTimeToFade
=
Duration
(
milliseconds:
600
);
ScrollbarPainter
_buildPainter
(
{
TextDirection
textDirection
=
TextDirection
.
ltr
,
...
...
@@ -45,6 +50,9 @@ class _DrawRectOnceCanvas extends Fake implements Canvas {
void
drawRect
(
Rect
rect
,
Paint
paint
)
{
rects
.
add
(
rect
);
}
@override
void
drawLine
(
Offset
p1
,
Offset
p2
,
Paint
paint
)
{}
}
void
main
(
)
{
...
...
@@ -503,4 +511,245 @@ void main() {
}
},
);
testWidgets
(
'ScrollbarPainter asserts if no TextDirection has been provided'
,
(
WidgetTester
tester
)
async
{
final
ScrollbarPainter
painter
=
ScrollbarPainter
(
color:
_kScrollbarColor
,
fadeoutOpacityAnimation:
kAlwaysCompleteAnimation
,
);
const
Size
size
=
Size
(
60
,
80
);
final
ScrollMetrics
scrollMetrics
=
defaultMetrics
.
copyWith
(
maxScrollExtent:
100000
,
viewportDimension:
size
.
height
,
);
painter
.
update
(
scrollMetrics
,
scrollMetrics
.
axisDirection
);
// Try to paint the scrollbar
try
{
painter
.
paint
(
testCanvas
,
size
);
}
on
AssertionError
catch
(
error
)
{
expect
(
error
.
message
,
'A TextDirection must be provided before a Scrollbar can be painted.'
);
}
});
testWidgets
(
'Tapping the track area pages the Scroll View'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
MediaQuery
(
data:
const
MediaQueryData
(),
child:
RawScrollbar
(
isAlwaysShown:
true
,
controller:
scrollController
,
child:
SingleChildScrollView
(
controller:
scrollController
,
child:
const
SizedBox
(
width:
1000.0
,
height:
1000.0
),
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
360.0
),
color:
const
Color
(
0x66BCBCBC
),
)
);
// Tap on the track area below the thumb.
await
tester
.
tapAt
(
const
Offset
(
796.0
,
550.0
));
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
400.0
);
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
240.0
,
800.0
,
600.0
),
color:
const
Color
(
0x66BCBCBC
),
)
);
// Tap on the track area above the thumb.
await
tester
.
tapAt
(
const
Offset
(
796.0
,
50.0
));
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
360.0
),
color:
const
Color
(
0x66BCBCBC
),
)
);
});
testWidgets
(
'Scrollbar never goes away until finger lift'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
MediaQuery
(
data:
MediaQueryData
(),
child:
RawScrollbar
(
child:
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
)
),
),
),
),
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
byType
(
SingleChildScrollView
)));
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
20.0
));
await
tester
.
pump
();
// Scrollbar fully showing
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
3.0
,
800.0
,
93.0
),
color:
const
Color
(
0x66BCBCBC
),
),
);
await
tester
.
pump
(
const
Duration
(
seconds:
3
));
await
tester
.
pump
(
const
Duration
(
seconds:
3
));
// Still there.
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
3.0
,
800.0
,
93.0
),
color:
const
Color
(
0x66BCBCBC
),
),
);
await
gesture
.
up
();
await
tester
.
pump
(
_kScrollbarTimeToFade
);
await
tester
.
pump
(
_kScrollbarFadeDuration
*
0.5
);
// Opacity going down now.
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
3.0
,
800.0
,
93.0
),
color:
const
Color
(
0x4fbcbcbc
),
),
);
});
testWidgets
(
'Scrollbar does not fade away while hovering'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
MediaQuery
(
data:
MediaQueryData
(),
child:
RawScrollbar
(
child:
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
)
),
),
),
),
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
byType
(
SingleChildScrollView
)));
await
gesture
.
moveBy
(
const
Offset
(
0.0
,
-
20.0
));
await
tester
.
pump
();
// Scrollbar fully showing
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
3.0
,
800.0
,
93.0
),
color:
const
Color
(
0x66BCBCBC
),
),
);
final
TestPointer
testPointer
=
TestPointer
(
1
,
ui
.
PointerDeviceKind
.
mouse
);
// Hover over the thumb to prevent the scrollbar from fading out.
testPointer
.
hover
(
const
Offset
(
790.0
,
5.0
));
await
gesture
.
up
();
await
tester
.
pump
(
const
Duration
(
seconds:
3
));
// Still there.
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
3.0
,
800.0
,
93.0
),
color:
const
Color
(
0x66BCBCBC
),
),
);
});
testWidgets
(
'Scrollbar thumb can be dragged'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
MediaQuery
(
data:
const
MediaQueryData
(),
child:
PrimaryScrollController
(
controller:
scrollController
,
child:
RawScrollbar
(
isAlwaysShown:
true
,
controller:
scrollController
,
child:
const
SingleChildScrollView
(
child:
SizedBox
(
width:
4000.0
,
height:
4000.0
)
),
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
scrollController
.
offset
,
0.0
);
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
90.0
),
color:
const
Color
(
0x66BCBCBC
),
),
);
// Drag the thumb down to scroll down.
const
double
scrollAmount
=
10.0
;
final
TestGesture
dragScrollbarGesture
=
await
tester
.
startGesture
(
const
Offset
(
797.0
,
45.0
));
await
tester
.
pumpAndSettle
();
await
dragScrollbarGesture
.
moveBy
(
const
Offset
(
0.0
,
scrollAmount
));
await
tester
.
pumpAndSettle
();
await
dragScrollbarGesture
.
up
();
await
tester
.
pumpAndSettle
();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect
(
scrollController
.
offset
,
greaterThan
(
scrollAmount
*
2
));
expect
(
find
.
byType
(
RawScrollbar
),
paints
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
0.0
,
800.0
,
600.0
))
..
rect
(
rect:
const
Rect
.
fromLTRB
(
794.0
,
10.0
,
800.0
,
100.0
),
color:
const
Color
(
0x66BCBCBC
),
),
);
});
}
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