Skip to content

Instantly share code, notes, and snippets.

@edwardaux
Last active August 27, 2020 16:45
Show Gist options
  • Save edwardaux/eb481793aff880b8917d1445000058ad to your computer and use it in GitHub Desktop.
Save edwardaux/eb481793aff880b8917d1445000058ad to your computer and use it in GitHub Desktop.
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
// This function can be passed to the [CupertinoSliverRefreshControl] builder function
// in order to achieve a much more natural pull-to-refresh spinner. The out-of-the-box
// implementation provided by Flutter uses a down-arrow which is very unfamiliar to iOS
// users. This implementation is an attempt to make a more faithful implementation.
//
// It isn't perfect (in iOS, the spinner stays pinned to the top, whereas this implementation
// allows it to scroll down with the content), but the spinner implementation is much more
// like the standard iOS implementation which does the following:
//
// * As you drag down, the spinner fades in
// * As it fades in, it draws the ticks one at a time (ie. it isn't just the spinner rotating)
// * Once you're refreshing, the standard spinner shows
// * When you're done, the spinner shrinks out of view
//
// You can use it like so:
// ```
// CupertinoSliverRefreshControl(
// ...
// builder: buildHackyAppleRefreshIndicator,
// )
// ```
//
// See https://github.com/flutter/flutter/issues/29159 which is tracking the real
// implementation.
//
// The starting point for this class was looking at the [CupertinoActivityIndicator] that
// is provided with the Flutter SDK.
//
// IMPORTANT: Many of the constants (such as the default radius and colours) are copied
// from the SDK at a certain point in time so may diverge from the standard implementation.
// Use at your own risk.
// The radius of the spinner
const double kDefaultRadius = 14.0;
// Used as a reference size in order to calculate the segment sizes
const double _kDefaultIndicatorRadius = 10.0;
// How many ticks are in the spinner
const int _kTickCount = 12;
// Colours for the ticks
const Color _kActiveTickColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFF3C3C44),
darkColor: Color(0xFFEBEBF5),
);
Widget buildHackyAppleRefreshIndicator(
BuildContext context,
RefreshIndicatorMode refreshState,
double pulledExtent,
double refreshTriggerPullDistance,
double refreshIndicatorExtent,
) {
final percentageComplete = min(pulledExtent / refreshIndicatorExtent, 1.0);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildIndicator(refreshState, kDefaultRadius, percentageComplete),
),
);
}
Widget _buildIndicator(RefreshIndicatorMode refreshState, double radius, double percentageComplete) {
switch (refreshState) {
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.8, curve: Curves.easeInOut);
return Opacity(
opacity: opacityCurve.transform(percentageComplete),
child: _DraggingActivityIndicator(percentageComplete: percentageComplete, radius: radius),
);
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 let's go, the standard transition is to shrink the spinner
return CupertinoActivityIndicator(radius: radius * percentageComplete);
default:
// Anything else doesn't show anything
return Container();
}
}
class _DraggingActivityIndicator extends StatelessWidget {
final double percentageComplete;
final double radius;
_DraggingActivityIndicator({@required this.percentageComplete, @required this.radius});
@override
Widget build(BuildContext context) {
return SizedBox(
height: radius * 2,
width: radius * 2,
child: CustomPaint(
painter: _DraggingActivityIndicatorPainter(
percentageComplete: percentageComplete,
activeColor: CupertinoDynamicColor.resolve(_kActiveTickColor, context),
radius: radius,
),
),
);
}
}
class _DraggingActivityIndicatorPainter extends CustomPainter {
_DraggingActivityIndicatorPainter({
@required this.percentageComplete,
@required this.activeColor,
double radius,
}) : tickFundamentalRRect = RRect.fromLTRBXY(
-radius,
radius / _kDefaultIndicatorRadius,
-radius / 2.0,
-radius / _kDefaultIndicatorRadius,
radius / _kDefaultIndicatorRadius,
radius / _kDefaultIndicatorRadius,
);
final double percentageComplete;
final RRect tickFundamentalRRect;
final Color activeColor;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint();
canvas.save();
canvas.translate(size.width / 2.0, size.height / 2);
// The standard implementation has the top tick appearing first, so
// need to rotate so that that is the first one that gets drawn
canvas.rotate(pi / 2);
for (var i = 0; i < (percentageComplete * _kTickCount); ++i) {
paint.color = activeColor.withAlpha(147);
canvas.drawRRect(tickFundamentalRRect, paint);
canvas.rotate(pi * 2.0 / _kTickCount);
}
canvas.restore();
}
@override
bool shouldRepaint(_DraggingActivityIndicatorPainter oldPainter) {
return oldPainter.percentageComplete != percentageComplete || oldPainter.activeColor != activeColor;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment