New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
iOS mid-drag activity indicator #58392
iOS mid-drag activity indicator #58392
Conversation
I'm not sure why
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @edwardaux welcome! Thank you for the contribution!
The failing golden file test in the customer_testing-windows
shard is unrelated to your change. I believe if you update your branch with the latest from master it will pass.
I am happy to help land the golden files you are adding in this PR. I think @xster or @LongCatIsLooong may have some thoughts here on this change.
This is a breaking change since a pre-existing test is altered here and the default behavior is changing. Please see our wiki on making breaking changes. I recommend creating a design document (the template is linked there) so you can gather feedback from other contributors in successfully making this change. :)
testWidgets('buildSimpleRefreshIndicator dark mode', (WidgetTester tester) async { | ||
const CupertinoDynamicColor color = CupertinoColors.inactiveGray; | ||
|
||
testWidgets('buildAppleRefreshIndicator progress', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why has this test changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old test used to check for the presence of an icon (the down-arrow) and make sure that the colour of the icon was correct. The change introduced in this PR (which matches iOS implementation) doesn't have an icon any more, so now the test makes sure that the in-progress calculations are correct.
/// 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 animated back to closed. | ||
static Widget buildAppleRefreshIndicator( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the name change? I think we avoid using 'Apple' explicitly in the framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old name was buildSimpleRefreshIndicator
which, to me, was accurate in that it was a simple implementation that didn't deal with the complexities of how iOS renders its indicator. Having the full implementation labelled "simple" didn't feel quite right. Your point is taken about referencing Apple, though, so I've renamed it to simply buildRefreshIndicator
1f05d3c
to
24852d3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be made non-breaking by introducing a flag on the refresh indicator. This flag could specify which refresh indicator to use. It would use the previous behavior by default and the new behavior as an opt-in.
I did consider adding a flag, however I guess my concern with this is that by having the existing behaviour as the default, we're actively promoting the incorrect user experience. I feel like all developers who have explicitly chosen to use Perhaps it comes down to the definition of "breaking". To me, it feels like we're fixing a defect in the current implementation. |
FYI, design doc is located here: http://flutter.dev/go/ios-pull-to-refresh |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the high quality contribution! Most of my comments are just small code style changes. The design doc looks good.
}) : assert(animating != null), | ||
assert(radius != null), | ||
assert(radius > 0), | ||
assert(progress != null), | ||
assert(progress >= 0), | ||
assert(progress <= 1), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: We usually include the .0
for doubles, just to clarify that it is a double.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Also updated the radius
variable above.
@@ -128,13 +143,17 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter { | |||
canvas.save(); | |||
canvas.translate(size.width / 2.0, size.height / 2.0); | |||
|
|||
// The standard iOS implementation has the top tick appearing first, | |||
// so need to rotate so that that is the first one that gets drawn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs a period at the end of the sentence. If you want you could probably even simplify this to:
// Rotate so that the top tick is the first to be drawn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
/// the refresh is done. | ||
static Widget buildSimpleRefreshIndicator( | ||
/// Builds a refresh indicator that reflects the standard iOS pull-to-refresh | ||
/// behaviour. Specifically, this entails presenting an activity indicator that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use the American spelling "behavior" per the style guide: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#spell-words-in-identifiers-and-comments-correctly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
/// 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 animated back to closed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"animated" => "animates"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
/// behaviour. 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Would it help to include references to the states like [RefreshIndicatorMode.armed]
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a couple of goes at trying this, but each attempt resulted in an unwieldy mouthful.
At the moment, the function description is more like an overview, rather than an explanation of the internal mechanics. As a consumer of this function, I'm not sure if I would really need the gory details, which seem reasonably clear in the switch
statement further down, but I'm happy to try further if you feel it is warranted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, I'm happy with leaving it as-is. Thanks for taking a look.
), | ||
); | ||
} | ||
|
||
static Widget _buildIndicatorForRefreshState(RefreshIndicatorMode refreshState, double radius, double percentageComplete) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I've been called out on my PRs before for using static methods that return widgets and told to just make a new StatelessWidget instead. In this case I realize that buildSimpleRefreshIndicator
was already guilty of this though :) Just something to keep in mind if you see a better way to do this, otherwise it's fine as-is.
); | ||
case RefreshIndicatorMode.armed: | ||
case RefreshIndicatorMode.refresh: | ||
// Once we're armed or performing the refresh, we just show the normal spinner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Period at the end of the sentence here and two more comments below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
// Once we're armed or performing the refresh, we just show the normal spinner | ||
return CupertinoActivityIndicator(radius: radius); | ||
case RefreshIndicatorMode.done: | ||
// When the user let's go, the standard transition is to shrink the spinner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"let's" => "lets"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -1327,48 +1327,40 @@ void main() { | |||
); | |||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); | |||
|
|||
testWidgets('buildSimpleRefreshIndicator dark mode', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this still be tested in dark mode one way or another?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent question. We don't do anything specific depending on what mode we're in... not sure if the standard practice is to always do a light and dark mode test, or only if the widget actually does something different. Do you know if there's any specific guidance one way or another?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's not necessary if the change doesn't have any special dark mode behavior. But why was this test replaced? Could you keep the original test and add your new one, or does the original test conflict with the new behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm on board with this being a breaking change rather than opt-in. It seems worth the effort to make this look correct, and hopefully it's just a couple of visual diff tests that need to be updated. |
I concur! :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome, thank you!
Is the golden change the only breaking change? If that's the case since the const List<int> _alphaValues = <int>[147, 131, 114, 97, 81, 64, 47, 47, 47, 47, 47, 47];
array is not used when progress < 1
, can we just shift this array so when progress == 1
the first frame looks the same?
@@ -107,6 +120,7 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter { | |||
@required this.position, | |||
@required this.activeColor, | |||
double radius, | |||
@required this.progress, | |||
}) : tickFundamentalRRect = RRect.fromLTRBXY( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it would make the paint logic slightly easier to understand if tickFundamentalRRect
starts off from a vertical position, instead of making the adjustment later using canvas.rotate(math.pi / 2);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
/// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems when progress is under 1, setting animating
to true or false makes no difference visually? Maybe we should assert(!animating)
if progress < 1 , since progress
basically takes control from the state's animation controller and it doesn't really make too much sense to have the animation running when progress is manually set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. This is implicitly controlling whether it's static or animating. It should definitely be pointed out in the docs. Perhaps allow null and make it mutually exclusive with animating? Since it's a bit harder to explain that 1 specific value of this double has a secondary meaning.
I also strongly encourage consistency with https://api.flutter.dev/flutter/material/ProgressIndicator/value.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it is worth creating an explicit constructor for the "progressive release" version? That might help with trying to keep the mutually exclusive values separate... not sure if there's any specific guidelines around when we should/shouldn't create a separate constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a great idea! Many widgets have more than one constructor so it doesn't look like a problem to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have added a new constructor called CupertinoActivityIndicator.partiallyRevealed()
but am open to suggestions about better naming (I'm not super-thrilled about what I have now). Also updated some of the internal comments to be more accurate.
@@ -42,6 +46,14 @@ class CupertinoActivityIndicator extends StatefulWidget { | |||
/// Defaults to 10px. Must be positive and cannot be null. | |||
final double radius; | |||
|
|||
/// Determines the number of spinner segments that will be show. Typical usage would |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the percentage of spinner segments? The value ranges from 0 to 1. Also I think most people would expect the segments to distribute evenly when progress < 1, or even mistake this parameter for something that can be used to configure the total number of segments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -120,6 +134,7 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter { | |||
final Animation<double> position; | |||
final RRect tickFundamentalRRect; | |||
final Color activeColor; | |||
final double progress; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldRepaint
should return true when progress changes. Not sure why it's repainting, maybe it's because the indicator is inside a LayoutBuilder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
canvas.drawRRect(tickFundamentalRRect, paint); | ||
canvas.rotate(-_kTwoPI / _kTickCount); | ||
canvas.rotate(_kTwoPI / _kTickCount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you change the rotate direction here, doesn't it counteract the iteration direction change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's three things all going on here at once: 1) looping through rendering each tick when spinning, 2) pulling out an opacity for each tick, and 3) incrementally revealing each tick when progress < 1
.
The first two don't really matter what sequence you do them in (or rotate) as long as you get the indexes correct for the colours (this is because in the finish, all 12 of them need to be drawn). However, the last case /must/ reveal the ticks in specific order (clockwise).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank for cleaning this up! Now the _alphaValues
array is a lot easier to reason about.
final int activeTick = (_kTickCount * position.value).floor(); | ||
|
||
for (int i = 0; i < _kTickCount; ++ i) { | ||
for (int i = ((_kTickCount - 1) * progress).toInt(); i >= 0; --i) { | ||
final int t = (i + activeTick) % _kTickCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should this be
final int t = (i - activeTick) % _kTickCount;
so the segment sitting at i == activeTick
becomes the most visible one, when progress == 1
(But watch out this changes the direction of the opacity gradient too)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've adjusted most of the maths to be simpler (well, as simple as it can be)
final int t = (i + activeTick) % _kTickCount; | ||
paint.color = activeColor.withAlpha(_alphaValues[t]); | ||
paint.color = activeColor.withAlpha(progress < 1 ? _alphaValues[0] : _alphaValues[t]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe mention this color change in the API documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've extracted that out into a private constant to be clearer
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: native UIRefreshIndicator
s dismiss with a 0.3s shrink + fadeout animation (independent of percentageComplete
)
@@ -128,13 +143,16 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter { | |||
canvas.save(); | |||
canvas.translate(size.width / 2.0, size.height / 2.0); | |||
|
|||
// Rotate so that the top tick is the first to be drawn. | |||
canvas.rotate(math.pi / 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optional: but this function would look cleaner if we just drew from the top first (adjust the tickFundamentalRRect if needed) instead of having to rotate twice in the paint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I had to update a testcase to reflect this change because the testcase assumes that the 9 o'clock tick is the one that gets drawn first. I added a comment in the testcase for clarity.
I think this is ready for review again now. There are still a couple of minor gaps in fidelity that I'm aware of:
How vital is it that we implement these? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just some nits below.
I checked out the code and tried it and it looks and feels good to me.
I also tried some native iOS apps to see the differences mentioned above and I do see what you mean. In my opinion it's ok for us to do these kinds of things incrementally. Could you create a new issue (or two) for those?
case RefreshIndicatorMode.drag: | ||
// While we're dragging, we draw individual segments of the spinner while simultaneously | ||
// easing the opacity in. | ||
const Curve opacityCurve = Interval(0.0, 0.2, curve: Curves.easeInOut); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want to answer for @xster, but I would say it's definitely valuable to document that. "These values were obtained by stepping through a screen recording of <native widget> on <device> running <iOS version>."
I have lots of comments floating around stating that I just loosely eyeballed some value. It's valuable because if someone comes along with a value they got with a more precise method, they can be confident that their value is better than mine and they should update it.
// 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 early implementation for the activity indicator starting drawing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// An early implementation for the activity indicator starting drawing | |
// An earlier implementation for the activity indicator started drawing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With
const List<int> _alphaValues = <int>[47, 47, 47, 47, 64, 81, 97, 114, 131, 147, 47, 47];
the starting position of the activity indicator remains the same (as the current behavior on master, the most visible segment appears at 9 o'clock initially). Since the native activity indicator also starts at 9 o'clock, I think we should keep it as is to avoid introducing breaking changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've modified the order of the values in the list so that the most prominent tick starts at 9 o'clock, and have added some comments explaining the meaning of the values.
Regarding the "breaking" test case... there's a bit of subtlety here. This test case is designed to confirm that the rounded rectangle has been generated correctly and it happens to do so by inspecting the /first/ rrect that is painted.
My implementation draws the first rrect at 12 o'clock, whereas the old implementation used to be at 9 o'clock. However, both implementations still draw all 12 ticks... the only thing that has changed is the sequence that they get drawn in.
There are no user-facing breakages at all. This test case just happens to be very much a white-box test, and we've now changed the internal implementation details.
Hope that makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. I don't think having to change this specific test makes the PR a breaking change, it was meant to ensure the corner radius scales with the activity indicator.
What I meant by "breaking change" is the angle of the most prominent segment when animating
is false. It used to be 9 o'clock and now its 11 o'clock. If we shift the alpha array to [47, 47, 47, 47, 64, 81, 97, 114, 131, 147, 47, 47]
then it can go back to 9 o'clock. I doubt it will break anyone for real but it would be best if the appearance can remain exactly the same imo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I think I shuffled those entries in an earlier commit to reflect the original behaviour. All good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is already a good enough fidelity improvement to the existing refresh indicator as-is. I can open a new issue to keep track of the further improvements we wish to make in the future once the PR is merged.
case RefreshIndicatorMode.drag: | ||
// While we're dragging, we draw individual segments of the spinner while simultaneously | ||
// easing the opacity in. | ||
const Curve opacityCurve = Interval(0.0, 0.2, curve: Curves.easeInOut); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the end value (0.2) correct? From what I'm seeing in Xcode the opacity seems to go up to 1.0 based on the drag distance.
|
||
@override | ||
void paint(Canvas canvas, Size size) { | ||
final Paint paint = Paint(); | ||
|
||
canvas.save(); | ||
canvas.translate(size.width / 2.0, size.height / 2.0); | ||
canvas.translate(size.width / 2.0, radius); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This moves the center of the spinner to the top center from the center of the canvas, if the canvas has a big enough size. Is it because the activity indicator needs to be pinned to the top when it's in done
state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, precisely. Once the drag is fully complete, size.height / 2.0
should be the same as radius
, however where we were coming unstuck was that during the initial stages of the drag, size.height
is less than the diameter of the spinner and changes as the user drags. This has the undesirable effect of moving the spinner as the user drags down.
By using radius, we ensure that the translation is constant and the spinner doesn't move. I've added some comments to this effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Activity indicator is a public widget so we probably shouldn't change its behavior just to suit our needs. I think we might want to change the SliverGeometry
the _RenderCupertinoSliverRefresh
reports to its viewport (or we can add it to the list of things we want to improve later).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, of course ... that makes total sense. I've reverted that change and pushed a change that manipulates the sliver geometry.
While I was in there, I realised that the standard iOS pull-to-refresh interaction has a super-subtle effect where the first few pixels of the top tick get drawn out of the clipping bounds over the top of the content being dragged down. By adding the margin to the paintOrigin
I achieve the same effect... however, it has the knock-on effect of impacting a couple of testcases.
At the time those test cases were written, the widget bounds that it was checking were correct, but I've had to adjust them slightly to accommodate these sliver geometry changes.
Let me know if I'm on the wrong path, or if there's a better way.
canvas.drawRRect(tickFundamentalRRect, paint); | ||
canvas.rotate(-_kTwoPI / _kTickCount); | ||
canvas.rotate(_kTwoPI / _kTickCount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank for cleaning this up! Now the _alphaValues
array is a lot easier to reason about.
// 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 early implementation for the activity indicator starting drawing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With
const List<int> _alphaValues = <int>[47, 47, 47, 47, 64, 81, 97, 114, 131, 147, 47, 47];
the starting position of the activity indicator remains the same (as the current behavior on master, the most visible segment appears at 9 o'clock initially). Since the native activity indicator also starts at 9 o'clock, I think we should keep it as is to avoid introducing breaking changes.
geometry = SliverGeometry( | ||
scrollExtent: layoutExtent, | ||
paintOrigin: -overscrolledExtent - constraints.scrollOffset, | ||
paintOrigin: topMargin + internalPainterDelta - overscrolledExtent - constraints.scrollOffset, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay. Thank you for putting detailed comments in the code!
Is it possible to add a padding to the activity indicator, instead of applying the paintOrigin
change, and achieve the same thing? My concern is it might hurt the customizability of this widget, if the sliver has a hard-coded top margin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, here's a gif showing the native behavior:
The native refresh control seems to become fully visible on screen (in terms of extent) as soon as the drag begins, and fades in as the user keeps dragging the content down.
When the refresh control becomes active (armed
or refresh
), it stays at the top unless the user drags the content up far enough, at which point it simply disappears. It reappears when the content goes back down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove the height constraint from the BoxConstriants
we apply one the child
:
flutter/packages/flutter/lib/src/cupertino/refresh.dart
Lines 136 to 143 in a0d8fdf
child.layout( | |
constraints.asBoxConstraints( | |
maxExtent: layoutExtent | |
// Plus only the overscrolled portion immediately preceding this | |
// sliver. | |
+ overscrolledExtent, | |
), | |
parentUsesSize: true, |
OK, this is looking much nicer. I've pulled the margin calculations out of the sliver into the widget that wraps the spinner. These last changes also simplify a lot of the maths by removing the dependency on the internal implementation of the activity indicator (the padding calculations were previously a real pain to try and cater for the internal translation that the activity indicator does). It also means I can revert the test case changes which were, again, dependent on those calculations. Much cleaner now. |
|
||
// If the user has dragged less than the threshold, there is no activity indicator | ||
// returned. Instead, an empty container is returned until the threshold is passed. | ||
expect(find.byType(Container), findsOneWidget); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One test is failing because the Container
is now gone. Could you fix it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm... that's weird. Somehow I didn't push that change up. Sorry... changes incoming.
// dragged widget, hence the use of Overflow.visible. | ||
return Center( | ||
child: Stack( | ||
overflow: Overflow.visible, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow this is brilliant 👍
👍 LGTM modulo the test failure. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an amazing contribution! 🎉 Thank you @edwardaux for your work on this. I had a few questions below about constants, otherwise LGTM!
I've prepped Gold for the updated files and will make sure all the goldens (new and updated) get checked in once this lands
import 'colors.dart'; | ||
import 'icons.dart'; | ||
|
||
const double _activityIndicatorRadius = 14.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit for constant:
const double _activityIndicatorRadius = 14.0; | |
const double _kActivityIndicatorRadius = 14.0; |
I also noticed that in packages/flutter/lib/src/cupertino/activity_indicator.dart above, _kDefaultIndicatorRadius = 10.0
, is this expected? I wasn't sure if they were intentionally different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, thanks. Have updated this constant, plus also the margin one you mentioned further down.
The default radius for the activity indicator has been set to 10 since the very initial commit (and it used as a default in the constructor, so I guess forms part of the public API)... any apps that just use the activity indicator (outside of the pull-to-refresh context) would expect for that to remain the same.
In the pull-to-refresh context, though, the default radius is set to 14 (since the original commit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool! Thanks for checking!
overflow: Overflow.visible, | ||
children: <Widget>[ | ||
Positioned( | ||
top: 16.0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this also a constant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM after a re-review 👍
Nice merge! 🎉 For more information about working with golden files, see the wiki page Writing a Golden File Test for package:flutter/flutter. |
iOS mid-drag activity indicator
@edwardaux Thank you very much for your contribution, I really like this animation. |
I just noticed this difference in behavior, a couple of days ago when I had to use it in my ongoing-app. Thank you so much for this contribution! |
Description
The current iOS pull-to-refresh action renders a down-arrow as the user is pulling downwards, but this is not how iOS behaves. Rather, it should progressively reveal the activity ticks one-by-one until the refresh action is triggered, at which point the activity indicator starts animating.
This PR modifies
CupertinoActivityIndicator
to add the ability to pass aprogress
value so that the indicator ticks can be revealed one-by-one.I'm currently asserting that the value of progress is non-null and between 0.0 and 1.0 (inclusive), however I'm not sure whether a better approach could be to just ensure that it is non-null and clamp internally.
It also modifies the default builder in
CupertinoSliverRefreshControl
to selectively render a different version of the activity indicator depending on the current pull-to-refresh state.Note that there is a subtle change in the way the activity indicator is drawn that results in a breaking golden image. In the old code, the first frame of the animation used to draw the brightest tick at 9 o'clock. However, the iOS in-progress tick reveal starts at 12 o'clock, so I added an initial 90 degree rotation to make the maths cleaner in the loop. See the different golden images below:
As part of my tests, I also added three new golden images:
One last comment... the original code use to render all 12 ticks anti-clockwise, however, the iOS in-progress ticks are revealed clockwise. This is why I switched the loop from ascending to descending.
Related Issues
There is (at least) one issue tracking this at #29159
Tests
I added the following tests:
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]
). This will ensure a smooth and quick review process.///
).flutter analyze --flutter-repo
) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read [Handling breaking changes].
I've ticked that it is breaking, because of the golden image change. However, given this image is part of a rotating animation, I don't believe that it is truly "breaking".
@Piinks This is my first contribution using golden files, so looping you in per guidance in the Golden Test page. Apologies if I have missed any steps or done them incorrectly.