Unverified Commit d3f1a3ce authored by nt4f04uNd's avatar nt4f04uNd Committed by GitHub

RefreshProgressIndicator to look native (#88301)

parent 3a3e709c
......@@ -696,7 +696,7 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter
final double radius = size.width / 2.0;
final double arrowheadPointX = radius + ux * radius + -uy * strokeWidth * 2.0 * arrowheadScale;
final double arrowheadPointY = radius + uy * radius + ux * strokeWidth * 2.0 * arrowheadScale;
final double arrowheadRadius = strokeWidth * 1.5 * arrowheadScale;
final double arrowheadRadius = strokeWidth * 2.0 * arrowheadScale;
final double innerRadius = radius - arrowheadRadius;
final double outerRadius = radius + arrowheadRadius;
......@@ -746,7 +746,7 @@ class RefreshProgressIndicator extends CircularProgressIndicator {
Color? backgroundColor,
Color? color,
Animation<Color?>? valueColor,
double strokeWidth = 2.0, // Different default than CircularProgressIndicator.
double strokeWidth = defaultStrokeWidth, // Different default than CircularProgressIndicator.
String? semanticsLabel,
String? semanticsValue,
}) : super(
......@@ -760,6 +760,9 @@ class RefreshProgressIndicator extends CircularProgressIndicator {
semanticsValue: semanticsValue,
);
/// Default stroke width.
static const double defaultStrokeWidth = 2.5;
/// {@template flutter.material.RefreshProgressIndicator.backgroundColor}
/// Background color of that fills the circle under the refresh indicator.
///
......@@ -770,33 +773,94 @@ class RefreshProgressIndicator extends CircularProgressIndicator {
/// {@endtemplate}
@override
Color? get backgroundColor => super.backgroundColor;
@override
State<CircularProgressIndicator> createState() => _RefreshProgressIndicatorState();
}
class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
static const double _indicatorSize = 40.0;
static const double _indicatorSize = 41.0;
/// Interval for arrow head to fully grow.
static const double _strokeHeadInterval = 0.33;
late final Animatable<double> _convertTween = CurveTween(
curve: const Interval(0.1, _strokeHeadInterval),
);
late final Animatable<double> _additionalRotationTween = TweenSequence<double>(
<TweenSequenceItem<double>>[
// Makes arrow to expand a little bit earlier, to match the Android look.
TweenSequenceItem<double>(
tween: Tween<double>(begin: -0.1, end: -0.2),
weight: _strokeHeadInterval,
),
// Additional rotation after the arrow expanded
TweenSequenceItem<double>(
tween: Tween<double>(begin: -0.2, end: 1.35),
weight: 1 - _strokeHeadInterval,
),
],
);
// Last value received from the widget before null.
double? _lastValue;
// Always show the indeterminate version of the circular progress indicator.
//
// When value is non-null the sweep of the progress indicator arrow's arc
// varies from 0 to about 270 degrees. When value is null the arrow animates
// starting from wherever we left it.
// varies from 0 to about 300 degrees.
//
// When value is null the arrow animation starting from wherever we left it.
@override
Widget build(BuildContext context) {
if (widget.value != null)
_controller.value = widget.value! * (1333 / 2 / _kIndeterminateCircularDuration);
else if (!_controller.isAnimating)
_controller.repeat();
final double? value = widget.value;
if (value != null) {
_lastValue = value;
_controller.value = _convertTween.transform(value)
* (1333 / 2 / _kIndeterminateCircularDuration);
}
return _buildAnimation();
}
@override
Widget _buildAnimation() {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return _buildMaterialIndicator(
context,
// Lengthen the arc a little
1.05 * _CircularProgressIndicatorState._strokeHeadTween.evaluate(_controller),
_CircularProgressIndicatorState._strokeTailTween.evaluate(_controller),
_CircularProgressIndicatorState._offsetTween.evaluate(_controller),
_CircularProgressIndicatorState._rotationTween.evaluate(_controller),
);
},
);
}
@override
Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
final double arrowheadScale = widget.value == null ? 0.0 : (widget.value! * 2.0).clamp(0.0, 1.0);
final double? value = widget.value;
final double arrowheadScale = value == null ? 0.0 : const Interval(0.1, _strokeHeadInterval).transform(value);
final double rotation;
if (value == null && _lastValue == null) {
rotation = 0.0;
} else {
rotation = math.pi * _additionalRotationTween.transform(value ?? _lastValue!);
}
Color valueColor = widget._getValueColor(context);
final double opacity = valueColor.opacity;
valueColor = valueColor.withOpacity(1.0);
final Color backgroundColor =
widget.backgroundColor ??
ProgressIndicatorTheme.of(context).refreshBackgroundColor ??
Theme.of(context).canvasColor;
return widget._buildSemanticsWrapper(
context: context,
child: Container(
......@@ -809,16 +873,22 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
elevation: 2.0,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: CustomPaint(
painter: _RefreshProgressIndicatorPainter(
valueColor: widget._getValueColor(context),
value: null, // Draw the indeterminate progress indicator.
headValue: headValue,
tailValue: tailValue,
offsetValue: offsetValue,
rotationValue: rotationValue,
strokeWidth: widget.strokeWidth,
arrowheadScale: arrowheadScale,
child: Opacity(
opacity: opacity,
child: Transform.rotate(
angle: rotation,
child: CustomPaint(
painter: _RefreshProgressIndicatorPainter(
valueColor: valueColor,
value: null, // Draw the indeterminate progress indicator.
headValue: headValue,
tailValue: tailValue,
offsetValue: offsetValue,
rotationValue: rotationValue,
strokeWidth: widget.strokeWidth,
arrowheadScale: arrowheadScale,
),
),
),
),
),
......
......@@ -120,7 +120,7 @@ class RefreshIndicator extends StatefulWidget {
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = 2.0,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : assert(child != null),
assert(onRefresh != null),
......
......@@ -534,6 +534,19 @@ void main() {
expect(tester.hasRunningAnimations, isTrue);
});
testWidgets('RefreshProgressIndicator uses expected animation', (WidgetTester tester) async {
final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(50, 50));
await tester.pumpFrames(animationSheet.record(
const _RefreshProgressIndicatorGolden(),
), const Duration(seconds: 3));
await expectLater(
await animationSheet.collate(20),
matchesGoldenFile('material.refresh_progress_indicator.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
testWidgets('Determinate CircularProgressIndicator stops the animator', (WidgetTester tester) async {
double? progressValue;
late StateSetter setState;
......@@ -863,3 +876,46 @@ void main() {
expect((wrappedTheme as ProgressIndicatorTheme).data, themeData);
});
}
class _RefreshProgressIndicatorGolden extends StatefulWidget {
const _RefreshProgressIndicatorGolden({Key? key}) : super(key: key);
@override
_RefreshProgressIndicatorGoldenState createState() => _RefreshProgressIndicatorGoldenState();
}
class _RefreshProgressIndicatorGoldenState extends State<_RefreshProgressIndicatorGolden> with SingleTickerProviderStateMixin {
late final AnimationController controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)
..forward()
..addListener(() {
setState(() {});
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
indeterminate = true;
}
});
bool indeterminate = false;
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: RefreshProgressIndicator(
value: indeterminate ? null : controller.value,
),
),
);
}
}
......@@ -470,10 +470,10 @@ void main() {
),
);
//By default the value of strokeWidth is 2.0
// Check for the default value
expect(
tester.widget<RefreshIndicator>(find.byType(RefreshIndicator)).strokeWidth,
2.0,
RefreshProgressIndicator.defaultStrokeWidth,
);
await tester.pumpWidget(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment