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
159557eb
Unverified
Commit
159557eb
authored
Jun 23, 2020
by
Craig Edwards
Committed by
GitHub
Jun 23, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
iOS mid-drag activity indicator (#58392)
parent
2a1c078b
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
208 additions
and
79 deletions
+208
-79
activity_indicator.dart
packages/flutter/lib/src/cupertino/activity_indicator.dart
+49
-13
refresh.dart
packages/flutter/lib/src/cupertino/refresh.dart
+56
-33
activity_indicator_test.dart
packages/flutter/test/cupertino/activity_indicator_test.dart
+65
-1
refresh_test.dart
packages/flutter/test/cupertino/refresh_test.dart
+38
-32
No files found.
packages/flutter/lib/src/cupertino/activity_indicator.dart
View file @
159557eb
...
...
@@ -31,7 +31,26 @@ class CupertinoActivityIndicator extends StatefulWidget {
this
.
radius
=
_kDefaultIndicatorRadius
,
})
:
assert
(
animating
!=
null
),
assert
(
radius
!=
null
),
assert
(
radius
>
0
),
assert
(
radius
>
0.0
),
progress
=
1.0
,
super
(
key:
key
);
/// Creates a non-animated iOS-style activity indicator that displays
/// a partial count of ticks based on the value of [progress].
///
/// When provided, the value of [progress] must be between 0.0 (zero ticks
/// will be shown) and 1.0 (all ticks will be shown) inclusive. Defaults
/// to 1.0.
const
CupertinoActivityIndicator
.
partiallyRevealed
({
Key
key
,
this
.
radius
=
_kDefaultIndicatorRadius
,
this
.
progress
=
1.0
,
})
:
assert
(
radius
!=
null
),
assert
(
radius
>
0.0
),
assert
(
progress
!=
null
),
assert
(
progress
>=
0.0
),
assert
(
progress
<=
1.0
),
animating
=
false
,
super
(
key:
key
);
/// Whether the activity indicator is running its animation.
...
...
@@ -44,6 +63,14 @@ class CupertinoActivityIndicator extends StatefulWidget {
/// Defaults to 10px. Must be positive and cannot be null.
final
double
radius
;
/// Determines the percentage of spinner ticks that will be shown. Typical usage would
/// display all ticks, however, this allows for more fine-grained control such as
/// during pull-to-refresh when the drag-down action shows one tick at a time as
/// the user continues to drag down.
///
/// Defaults to 1.0. Must be between 0.0 and 1.0 inclusive, and cannot be null.
final
double
progress
;
@override
_CupertinoActivityIndicatorState
createState
()
=>
_CupertinoActivityIndicatorState
();
}
...
...
@@ -91,6 +118,7 @@ class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
position:
_controller
,
activeColor:
CupertinoDynamicColor
.
resolve
(
_kActiveTickColor
,
context
),
radius:
widget
.
radius
,
progress:
widget
.
progress
,
),
),
);
...
...
@@ -100,20 +128,26 @@ class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
const
double
_kTwoPI
=
math
.
pi
*
2.0
;
const
int
_kTickCount
=
12
;
// Alpha values extracted from the native component (for both dark and light mode).
// The list has a length of 12.
const
List
<
int
>
_alphaValues
=
<
int
>[
147
,
131
,
114
,
97
,
81
,
64
,
47
,
47
,
47
,
47
,
47
,
47
];
// Alpha values extracted from the native component (for both dark and light mode) to
// draw the spinning ticks. The list must have a length of _kTickCount. The order of
// these values is designed to match the first frame of the iOS activity indicator which
// has the most prominent tick at 9 o'clock.
const
List
<
int
>
_alphaValues
=
<
int
>[
47
,
47
,
47
,
47
,
64
,
81
,
97
,
114
,
131
,
147
,
47
,
47
];
// The alpha value that is used to draw the partially revealed ticks.
const
int
_partiallyRevealedAlpha
=
147
;
class
_CupertinoActivityIndicatorPainter
extends
CustomPainter
{
_CupertinoActivityIndicatorPainter
({
@required
this
.
position
,
@required
this
.
activeColor
,
double
radius
,
@required
this
.
radius
,
@required
this
.
progress
,
})
:
tickFundamentalRRect
=
RRect
.
fromLTRBXY
(
-
radius
,
radius
/
_kDefaultIndicatorRadius
,
-
radius
/
2.0
,
-
radius
/
_kDefaultIndicatorRadius
,
-
radius
/
2.0
,
radius
/
_kDefaultIndicatorRadius
,
-
radius
,
radius
/
_kDefaultIndicatorRadius
,
radius
/
_kDefaultIndicatorRadius
,
),
...
...
@@ -122,6 +156,8 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter {
final
Animation
<
double
>
position
;
final
RRect
tickFundamentalRRect
;
final
Color
activeColor
;
final
double
radius
;
final
double
progress
;
@override
void
paint
(
Canvas
canvas
,
Size
size
)
{
...
...
@@ -132,11 +168,11 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter {
final
int
activeTick
=
(
_kTickCount
*
position
.
value
).
floor
();
for
(
int
i
=
0
;
i
<
_kTickCount
;
++
i
)
{
final
int
t
=
(
i
+
activeTick
)
%
_kTickCount
;
paint
.
color
=
activeColor
.
withAlpha
(
_alphaValues
[
t
]);
for
(
int
i
=
0
;
i
<
_kTickCount
*
progress
;
++
i
)
{
final
int
t
=
(
i
-
activeTick
)
%
_kTickCount
;
paint
.
color
=
activeColor
.
withAlpha
(
progress
<
1
?
_partiallyRevealedAlpha
:
_alphaValues
[
t
]);
canvas
.
drawRRect
(
tickFundamentalRRect
,
paint
);
canvas
.
rotate
(
-
_kTwoPI
/
_kTickCount
);
canvas
.
rotate
(
_kTwoPI
/
_kTickCount
);
}
canvas
.
restore
();
...
...
@@ -144,6 +180,6 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter {
@override
bool
shouldRepaint
(
_CupertinoActivityIndicatorPainter
oldPainter
)
{
return
oldPainter
.
position
!=
position
||
oldPainter
.
activeColor
!=
activeColor
;
return
oldPainter
.
position
!=
position
||
oldPainter
.
activeColor
!=
activeColor
||
oldPainter
.
progress
!=
progress
;
}
}
packages/flutter/lib/src/cupertino/refresh.dart
View file @
159557eb
...
...
@@ -13,8 +13,9 @@ import 'package:flutter/services.dart';
import
'package:flutter/widgets.dart'
;
import
'activity_indicator.dart'
;
import
'colors.dart'
;
import
'icons.dart'
;
const
double
_kActivityIndicatorRadius
=
14.0
;
const
double
_kActivityIndicatorMargin
=
16.0
;
class
_CupertinoSliverRefresh
extends
SingleChildRenderObjectWidget
{
const
_CupertinoSliverRefresh
({
...
...
@@ -294,7 +295,7 @@ class CupertinoSliverRefreshControl extends StatefulWidget {
Key
key
,
this
.
refreshTriggerPullDistance
=
_defaultRefreshTriggerPullDistance
,
this
.
refreshIndicatorExtent
=
_defaultRefreshIndicatorExtent
,
this
.
builder
=
build
Simple
RefreshIndicator
,
this
.
builder
=
buildRefreshIndicator
,
this
.
onRefresh
,
})
:
assert
(
refreshTriggerPullDistance
!=
null
),
assert
(
refreshTriggerPullDistance
>
0.0
),
...
...
@@ -330,9 +331,6 @@ class CupertinoSliverRefreshControl extends StatefulWidget {
/// A builder that's called as this sliver's size changes, and as the state
/// changes.
///
/// A default simple Twitter-style pull-to-refresh indicator is provided if
/// not specified.
///
/// Can be set to null, in which case nothing will be drawn in the overscrolled
/// space.
///
...
...
@@ -361,43 +359,68 @@ class CupertinoSliverRefreshControl extends StatefulWidget {
return
state
.
refreshState
;
}
/// Builds a simple refresh indicator that fades in a bottom aligned down
/// arrow before the refresh is triggered, a [CupertinoActivityIndicator]
/// during the refresh and fades the [CupertinoActivityIndicator] away when
/// the refresh is done.
static
Widget
buildSimpleRefreshIndicator
(
/// Builds a refresh indicator that reflects the standard iOS pull-to-refresh
/// behavior. Specifically, this entails presenting an activity indicator that
/// changes depending on the current refreshState. As the user initially drags
/// down, the indicator will gradually reveal individual ticks until the refresh
/// becomes armed. At this point, the animated activity indicator will begin rotating.
/// Once the refresh has completed, the activity indicator shrinks away as the
/// space allocation animates back to closed.
static
Widget
buildRefreshIndicator
(
BuildContext
context
,
RefreshIndicatorMode
refreshState
,
double
pulledExtent
,
double
refreshTriggerPullDistance
,
double
refreshIndicatorExtent
,
)
{
const
Curve
opacityCurve
=
Interval
(
0.4
,
0.8
,
curve:
Curves
.
easeInOut
);
return
Align
(
alignment:
Alignment
.
bottomCenter
,
child:
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
16.0
),
child:
refreshState
==
RefreshIndicatorMode
.
drag
?
Opacity
(
opacity:
opacityCurve
.
transform
(
min
(
pulledExtent
/
refreshTriggerPullDistance
,
1.0
)
),
child:
Icon
(
CupertinoIcons
.
down_arrow
,
color:
CupertinoDynamicColor
.
resolve
(
CupertinoColors
.
inactiveGray
,
context
),
size:
36.0
,
),
)
:
Opacity
(
opacity:
opacityCurve
.
transform
(
min
(
pulledExtent
/
refreshIndicatorExtent
,
1.0
)
),
child:
const
CupertinoActivityIndicator
(
radius:
14.0
),
),
final
double
percentageComplete
=
pulledExtent
/
refreshTriggerPullDistance
;
// Place the indicator at the top of the sliver that opens up. Note that we're using
// a Stack/Positioned widget because the CupertinoActivityIndicator does some internal
// translations based on the current size (which grows as the user drags) that makes
// Padding calculations difficult. Rather than be reliant on the internal implementation
// of the activity indicator, the Positioned widget allows us to be explicit where the
// widget gets placed. Also note that the indicator should appear over the top of the
// dragged widget, hence the use of Overflow.visible.
return
Center
(
child:
Stack
(
overflow:
Overflow
.
visible
,
children:
<
Widget
>[
Positioned
(
top:
_kActivityIndicatorMargin
,
left:
0.0
,
right:
0.0
,
child:
_buildIndicatorForRefreshState
(
refreshState
,
_kActivityIndicatorRadius
,
percentageComplete
),
),
],
),
);
}
static
Widget
_buildIndicatorForRefreshState
(
RefreshIndicatorMode
refreshState
,
double
radius
,
double
percentageComplete
)
{
switch
(
refreshState
)
{
case
RefreshIndicatorMode
.
drag
:
// While we're dragging, we draw individual ticks of the spinner while simultaneously
// easing the opacity in. Note that the opacity curve values here were derived using
// Xcode through inspecting a native app running on iOS 13.5.
const
Curve
opacityCurve
=
Interval
(
0.0
,
0.35
,
curve:
Curves
.
easeInOut
);
return
Opacity
(
opacity:
opacityCurve
.
transform
(
percentageComplete
),
child:
CupertinoActivityIndicator
.
partiallyRevealed
(
radius:
radius
,
progress:
percentageComplete
),
);
case
RefreshIndicatorMode
.
armed
:
case
RefreshIndicatorMode
.
refresh
:
// Once we're armed or performing the refresh, we just show the normal spinner.
return
CupertinoActivityIndicator
(
radius:
radius
);
case
RefreshIndicatorMode
.
done
:
// When the user lets go, the standard transition is to shrink the spinner.
return
CupertinoActivityIndicator
(
radius:
radius
*
percentageComplete
);
default
:
// Anything else doesn't show anything.
return
Container
();
}
}
@override
_CupertinoSliverRefreshControlState
createState
()
=>
_CupertinoSliverRefreshControlState
();
}
...
...
packages/flutter/test/cupertino/activity_indicator_test.dart
View file @
159557eb
...
...
@@ -71,15 +71,79 @@ void main() {
);
});
testWidgets
(
'Activity indicator 0% in progress'
,
(
WidgetTester
tester
)
async
{
final
Key
key
=
UniqueKey
();
await
tester
.
pumpWidget
(
Center
(
child:
RepaintBoundary
(
key:
key
,
child:
Container
(
color:
CupertinoColors
.
white
,
child:
const
CupertinoActivityIndicator
.
partiallyRevealed
(
progress:
0
),
),
),
),
);
await
expectLater
(
find
.
byKey
(
key
),
matchesGoldenFile
(
'activityIndicator.inprogress.0.0.png'
),
);
});
testWidgets
(
'Activity indicator 30% in progress'
,
(
WidgetTester
tester
)
async
{
final
Key
key
=
UniqueKey
();
await
tester
.
pumpWidget
(
Center
(
child:
RepaintBoundary
(
key:
key
,
child:
Container
(
color:
CupertinoColors
.
white
,
child:
const
CupertinoActivityIndicator
.
partiallyRevealed
(
progress:
0.5
),
),
),
),
);
await
expectLater
(
find
.
byKey
(
key
),
matchesGoldenFile
(
'activityIndicator.inprogress.0.3.png'
),
);
});
testWidgets
(
'Activity indicator 100% in progress'
,
(
WidgetTester
tester
)
async
{
final
Key
key
=
UniqueKey
();
await
tester
.
pumpWidget
(
Center
(
child:
RepaintBoundary
(
key:
key
,
child:
Container
(
color:
CupertinoColors
.
white
,
child:
const
CupertinoActivityIndicator
.
partiallyRevealed
(
progress:
1
),
),
),
),
);
await
expectLater
(
find
.
byKey
(
key
),
matchesGoldenFile
(
'activityIndicator.inprogress.1.0.png'
),
);
});
// Regression test for https://github.com/flutter/flutter/issues/41345.
testWidgets
(
'has the correct corner radius'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
CupertinoActivityIndicator
(
animating:
false
,
radius:
100
),
);
// An earlier implementation for the activity indicator started drawing
// the ticks at 9 o'clock, however, in order to support partially revealed
// indicator (https://github.com/flutter/flutter/issues/29159), the
// first tick was changed to be at 12 o'clock.
expect
(
find
.
byType
(
CupertinoActivityIndicator
),
paints
..
rrect
(
rrect:
const
RRect
.
fromLTRBXY
(-
10
0
,
10
,
-
50
,
-
1
0
,
10
,
10
)),
paints
..
rrect
(
rrect:
const
RRect
.
fromLTRBXY
(-
10
,
-
50
,
10
,
-
10
0
,
10
,
10
)),
);
});
}
...
...
packages/flutter/test/cupertino/refresh_test.dart
View file @
159557eb
...
...
@@ -1329,48 +1329,54 @@ void main() {
);
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
testWidgets
(
'buildSimpleRefreshIndicator dark mode'
,
(
WidgetTester
tester
)
async
{
const
CupertinoDynamicColor
color
=
CupertinoColors
.
inactiveGray
;
testWidgets
(
'buildRefreshIndicator progress'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MediaQuery
(
data:
const
MediaQueryData
(
platformBrightness:
Brightness
.
light
),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
CupertinoSliverRefreshControl
.
buildSimpleRefreshIndicator
(
context
,
RefreshIndicatorMode
.
drag
,
10
,
10
,
10
,
);
},
),
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
CupertinoSliverRefreshControl
.
buildRefreshIndicator
(
context
,
RefreshIndicatorMode
.
drag
,
10
,
100
,
10
,
);
},
),
),
);
expect
(
tester
.
widget
<
Icon
>(
find
.
byType
(
Icon
)).
color
.
value
,
color
.
color
.
value
);
expect
(
tester
.
widget
<
CupertinoActivityIndicator
>(
find
.
byType
(
CupertinoActivityIndicator
)).
progress
,
10.0
/
100.0
);
await
tester
.
pumpWidget
(
MediaQuery
(
data:
const
MediaQueryData
(
platformBrightness:
Brightness
.
dark
),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
CupertinoSliverRefreshControl
.
buildSimpleRefreshIndicator
(
context
,
RefreshIndicatorMode
.
drag
,
10
,
10
,
10
,
);
},
),
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
CupertinoSliverRefreshControl
.
buildRefreshIndicator
(
context
,
RefreshIndicatorMode
.
drag
,
26
,
100
,
10
,
);
},
),
),
);
expect
(
tester
.
widget
<
CupertinoActivityIndicator
>(
find
.
byType
(
CupertinoActivityIndicator
)).
progress
,
26.0
/
100.0
);
expect
(
tester
.
widget
<
Icon
>(
find
.
byType
(
Icon
)).
color
.
value
,
color
.
darkColor
.
value
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
CupertinoSliverRefreshControl
.
buildRefreshIndicator
(
context
,
RefreshIndicatorMode
.
drag
,
100
,
100
,
10
,
);
},
),
),
);
expect
(
tester
.
widget
<
CupertinoActivityIndicator
>(
find
.
byType
(
CupertinoActivityIndicator
)).
progress
,
100.0
/
100.0
);
});
};
...
...
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