gonuit / flutter-custom-refresh-indicator Goto Github PK
View Code? Open in Web Editor NEWWidget that makes it easy to implement a custom pull to refresh gesture.
License: MIT License
Widget that makes it easy to implement a custom pull to refresh gesture.
License: MIT License
Swiping down fails to activate CustomRefreshIndicator. Issue is with CustomScrollView inside the bodyContent(), when it's removed (return Container()
) the problem goes away . Any ideas on how to get this to work?
SimpleIndicatorContent
is a copy/paste from sample project.
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
import 'package:flutter/material.dart';
import 'package:tester/indicator/simple_indicator.dart';
class Home extends StatefulWidget {
Home({Key key, this.title}) : super(key: key);
final String title;
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[300],
body: SafeArea(
child: CustomRefreshIndicator(
leadingGlowVisible: false,
offsetToArmed: 200.0,
trailingGlowVisible: false,
builder: customRefreshBuilder(),
onRefresh: () => Future.delayed(const Duration(seconds: 2)),
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text("Hello World"),
),
sliverContent()
];
},
body:bodyContent()
// body: Container(), <-- Works
// ),
),
),
),
);
}
}
customRefreshBuilder() => (context, child, controller) {
return Stack(
children: <Widget>[
child,
PositionedIndicatorContainer(
controller: controller,
child: SimpleIndicatorContent(
controller: controller,
),
),
],
);
};
bodyContent() {
final List<Widget> entries = <Widget>[
Text('1'),
Text('2'),
Text('3'),
];
SliverList sliverList = createSliverList(entries);
return Tooltip(
message: "Swipe to refresh.",
child: CustomScrollView(
scrollDirection: Axis.vertical,
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
slivers: <Widget>[SliverToBoxAdapter(), sliverList]));
}
createSliverList(entries) {
List<Widget> list = List.from(entries);
return SliverList(delegate: SliverChildListDelegate(list));
}
sliverContent() {
return SliverAppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
elevation: 0.0,
expandedHeight: 300.0,
floating: true,
snap: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Column(
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
textContainer(),
],
)
],
),
Expanded(
child: Container(child: Text("Content")),
),
Container(
child: Text("Content"),
)
],
),
),
));
}
textContainer() {
return Expanded(
child: Container(
margin: EdgeInsets.only(left: 5.0),
padding: EdgeInsets.only(left: 5.0, top: 5.0, bottom: 5.0),
child: Text("Content")));
}
_refresh() async {}
With current didStateChange
implementaion next call controller.didStateChange(from: IndicatorState.loading, to: IndicatorState.hiding)
and {controller.didStateChange(to: IndicatorState.hiding)
never return true
.
It seams then next bloc:
controller._setIndicatorState(IndicatorState.hiding);
await _animationController.animateTo(0.0,
duration: widget.loadingToIdleDuration);
execute notifyListeners
of IndicatorController
twice before builder
of AnimatedBuilder
called.
I have issue with dragging on cancelling state.
This is example from medium post:
class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});
@override
State<ExampleScreen> createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
int _itemsCount = 10;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const ExampleAppBar(),
body: CustomRefreshIndicator(
onRefresh: () => Future.delayed(const Duration(seconds: 3)),
trigger: IndicatorTrigger.leadingEdge,
child: ExampleList(
itemCount: _itemsCount,
countElements: true,
),
builder: (
BuildContext context,
Widget child,
IndicatorController controller,
) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
if (!controller.isIdle)
Positioned(
top: 35.0 * controller.value,
child: SizedBox(
height: 30,
width: 30,
child: CircularProgressIndicator(
value: !controller.isLoading ? controller.value.clamp(0.0, 1.0) : null,
),
),
),
Transform.translate(
offset: Offset(0, 100.0 * controller.value),
child: child,
),
],
);
},
),
);
}
}
The problem is - that if I start slowly dragging down, and then I drag just one pixel to the top - and container closes immediately.
I wonder whether there maybe something I missed in configuration in order to address this...
It would be nice to have an option to enable pull-to-refresh from bottom of the list and for both top and bottom
══╡ EXCEPTION CAUGHT BY FOUNDATION LIBRARY╞════
The following assertion was thrown while dispatching notifications for IndicatorController:
Build scheduled during frame.
While the widget tree was being built, laid out, and painted, a new frame was scheduled to rebuild
the widget tree.
This might be because setState() was called from a layout or paint callback. If a change is needed
to the widget tree, it should be applied as the tree is being built. Scheduling a change for the
subsequent frame instead results in an interface that lags behind by one frame. If this was done to
make your build dependent on a size measured at layout time, consider using a LayoutBuilder,
CustomSingleChildLayout, or CustomMultiChildLayout. If, on the other hand, the one frame delay is
the desired effect, for example because this is an animation, consider scheduling the frame in a
post-frame callback using SchedulerBinding.addPostFrameCallback or using an AnimationController to
trigger the animation.
When the exception was thrown, this was the stack:
#0 WidgetsBinding._handleBuildScheduled.<anonymous closure>
(package:flutter/src/widgets/binding.dart:747:9)
#1 WidgetsBinding._handleBuildScheduled (package:flutter/src/widgets/binding.dart:770:6)
#2 BuildOwner.scheduleBuildFor (package:flutter/src/widgets/framework.dart:2434:24)
#3 Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4280:12)
#4 State.setState (package:flutter/src/widgets/framework.dart:1108:15)
#5 _AnimatedState._handleChange (package:flutter/src/widgets/transitions.dart:128:5)
#6 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:308:24)
#7 IndicatorController.setIndicatorState
(package:custom_refresh_indicator/src/controller.dart:150:5)
#8 _CustomRefreshIndicatorState._start
(package:custom_refresh_indicator/src/custom_refresh_indicator.dart:293:16)
#9 _CustomRefreshIndicatorState._handleScrollEndNotification
(package:custom_refresh_indicator/src/custom_refresh_indicator.dart:212:9)
#10 _CustomRefreshIndicatorState._handleScrollNotification
(package:custom_refresh_indicator/src/custom_refresh_indicator.dart:283:14)
#11 Element.visitAncestorElements (package:flutter/src/widgets/framework.dart:4091:39)
#12 Notification.dispatch (package:flutter/src/widgets/notification_listener.dart:83:13)
#13 ScrollActivity.dispatchScrollEndNotification
(package:flutter/src/widgets/scroll_activity.dart:104:63)
#14 ScrollPosition.didEndScroll (package:flutter/src/widgets/scroll_position.dart:907:15)
#15 ScrollPosition.beginActivity (package:flutter/src/widgets/scroll_position.dart:876:9)
#16 _NestedInnerBallisticScrollActivity.applyNewDimensions
(package:extended_nested_scroll_view/src/extended_nested_scroll_view.dart:1676:14)
#17 ScrollPosition.applyNewDimensions (package:flutter/src/widgets/scroll_position.dart:623:15)
#18 _NestedScrollPosition.applyNewDimensions
(package:extended_nested_scroll_view/src/extended_nested_scroll_view.dart:1631:11)
#19 _ExtendedNestedScrollPosition.applyNewDimensions
(package:extended_nested_scroll_view/src/extended_nested_scroll_view_part.dart:261:11)
#20 ScrollPosition.applyContentDimensions
(package:flutter/src/widgets/scroll_position.dart:553:7)
#21 _ExtendedNestedScrollPosition.applyContentDimensions
(package:extended_nested_scroll_view/src/extended_nested_scroll_view_part.dart:273:18)
#22 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1493:20)
#23 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#24 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#25 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#26 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#27 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#28 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#29 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#30 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#31 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#32 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#33 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#34 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#35 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#36 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#37 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#38 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#39 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#40 RenderSliverFixedExtentBoxAdaptor.performLayout
(package:flutter/src/rendering/sliver_fixed_extent_list.dart:240:19)
#41 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#42 RenderSliverEdgeInsetsPadding.performLayout
(package:flutter/src/rendering/sliver_padding.dart:137:12)
#43 _RenderSliverFractionalPadding.performLayout
(package:flutter/src/widgets/sliver_fill.dart:167:11)
#44 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#45 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:510:13)
#46 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1580:12)
#47 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1489:20)
#48 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#49 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#50 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#51 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#52 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#53 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#54 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#55 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#56 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#57 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#58 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#59 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:116:14)
#60 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#61 ChildLayoutHelper.layoutChild (package:flutter/src/rendering/layout_helper.dart:56:11)
#62 RenderFlex._computeSizes (package:flutter/src/rendering/flex.dart:896:45)
#63 RenderFlex.performLayout (package:flutter/src/rendering/flex.dart:931:32)
#64 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#65 RenderSliverFillRemainingWithScrollable.performLayout
(package:flutter/src/rendering/sliver_fill.dart:92:14)
#66 RenderObject.layout (package:flutter/src/rendering/object.dart:1852:7)
#67 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:510:13)
#68 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1580:12)
#69 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1489:20)
#70 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1707:7)
#71 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:879:18)
#72 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:497:19)
#73 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:883:13)
#74 RendererBinding._handlePersistentFrameCallback
(package:flutter/src/rendering/binding.dart:363:5)
#75 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1145:15)
#76 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1082:9)
#77 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:996:5)
#81 _invoke (dart:ui/hooks.dart:150:10)
#82 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:270:5)
#83 _drawFrame (dart:ui/hooks.dart:114:31)
(elided 3 frames from dart:async)
The IndicatorController sending notification was:
Instance of 'IndicatorController'
════════════════════════════════════════════════
return CustomRefreshIndicator(
onRefresh: onRefresh,
builder: (context, child, controller) {
print(controller.value); // always prints 0.0
return Stack(
children: [
this,
CupertinoActivityIndicator(),
],
);
},
child: this,
);
return CustomRefreshIndicator(
onRefresh: onRefresh,
builder: MaterialIndicatorDelegate(
builder: (BuildContext context, IndicatorController controller) {
print(controller.value); // prints as expected from 0.0 to 1.0
return CupertinoActivityIndicator();
}),
child: this,
);
Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(Size.fromHeight(44),
child: AppBar(
brightness: Brightness.light,
backgroundColor: Colors.transparent,
elevation: 0,
flexibleSpace: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16.0, sigmaY: 16.0),
child: Container(
color: Colors.transparent,
),
),
),
title: Text('hello world')
),
),
body: Container(
padding: EdgeInsets.symmetric(horizontal: 16),
child: PullToRefresh(
handleRefresh: _handleRefresh,
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.only(bottom: 50),
height: 200,
child: Image.network(
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fgss0.baidu.com%2F-Po3dSag_xI4khGko9WTAnF6hhy%2Fzhidao%2Fpic%2Fitem%2F4034970a304e251fae75ad03a786c9177e3e534e.jpg&refer=http%3A%2F%2Fgss0.baidu.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1630144388&t=8eb957946c6c091d7559a9a119acfd17'),
);
}),
)
),
),
The indicator is always behind Appbar, what should I do to fix it?
Hi,
First of all, thanks for sharing with that fancy code.
When i use check mark indicator example without DecoratedBox on my ListView, the incidator is allways visible .
I saw that it can be hidden based on Indicator state, like in the plane indicator:
return Stack(
overflow: Overflow.clip,
children: <Widget>[
**if (_prevState != IndicatorState.idle)**
Container(
Is that the desired way to hide the CircularProgressIndicator for CheckMarkIdicator when using list without DecoratedBox?
To reproduce simply remove DecoratedBox from ExampleList
class ExampleList extends StatelessWidget {
final Color backgroundColor;
const ExampleList([this.backgroundColor = appBackgroundColor]);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) => const Element(),
itemCount: 4,
separatorBuilder: (BuildContext context, int index) => const Divider(
height: 0,
color: Color(0xFFe2d6ce),
thickness: 1,
),
);
}
}
Is it possible to have same structure?
Scaffold(
body: CustomScrollView(
controller: wm.scrollListController,
slivers: [
const SliverPersistentHeader(
delegate: AppBarWidget(),
pinned: true,
),
CustomRefreshIndicator(
builder: (params ) {
return AnimatedBuilder(
builder: (params) {
return child;
},
child: SliverList(
delegate: SliverChildBuilderDelegate()
)
)
}
),
]
)
);
I mean i need to put indicator between app bar and sliver list and not lose the scrollcontroller ( nested scroll view case)
On Xcode build:
: Error: Method 'addPostFrameCallback' cannot be called on 'WidgetsBinding?' because it is potentially null.
../…/src/custom_refresh_indicator.dart:209
Here's an example of bringing the onRefresh parameter to the top for warp-indicator.dart so that you can just replace the default refresh widget with this one and you still have access to the onRefresh from whichever widget you were set up in. Might be a good idea to do this for all the examples as then people can just swap out the indicators easily in their code and try the different ones. BRILLIANT project by the way - Thanks for this!
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
enum WarpAnimationState {
stopped,
playing,
}
typedef StarColorGetter = Color Function(int index);
class WarpIndicator extends StatefulWidget {
final Widget child;
final int starsCount;
final Color skyColor;
final StarColorGetter starColorGetter;
final Future Function() onRefresh; // Step 1
const WarpIndicator(
{Key? key,
required this.child,
this.starsCount = 30,
this.skyColor = Colors.black,
this.starColorGetter = _defaultStarColorGetter,
required this.onRefresh}) // Step 2
: super(key: key);
static Color _defaultStarColorGetter(int index) =>
HSLColor.fromAHSL(1, Random().nextDouble() * 360, 1, 0.98).toColor();
@override
_WarpIndicatorState createState() => _WarpIndicatorState();
}
class _WarpIndicatorState extends State<WarpIndicator>
with SingleTickerProviderStateMixin {
static const _indicatorSize = 150.0;
final _random = Random();
final _helper = IndicatorStateHelper();
WarpAnimationState _state = WarpAnimationState.stopped;
List<Star> stars = [];
final _offsetTween = Tween<Offset>(
begin: Offset.zero,
end: Offset.zero,
);
final _angleTween = Tween<double>(
begin: 0,
end: 0,
);
late AnimationController shakeController;
static final _scaleTween = Tween(begin: 1.0, end: 0.75);
static final _radiusTween = Tween(begin: 0.0, end: 16.0);
@override
void initState() {
shakeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
super.initState();
}
Offset _getRandomOffset() => Offset(
_random.nextInt(10) - 5,
_random.nextInt(10) - 5,
);
double _getRandomAngle() {
final degrees = ((_random.nextDouble() * 2) - 1);
final radians = degrees == 0 ? 0.0 : degrees / 360.0;
return radians;
}
void _shiftAndGenerateRandomShakeTransform() {
_offsetTween.begin = _offsetTween.end;
_offsetTween.end = _getRandomOffset();
_angleTween.begin = _angleTween.end;
_angleTween.end = _getRandomAngle();
}
void _startShakeAnimation() {
_shiftAndGenerateRandomShakeTransform();
shakeController.animateTo(1.0);
_state = WarpAnimationState.playing;
stars = List.generate(
widget.starsCount,
(index) => Star(initialColor: widget.starColorGetter(index)),
);
}
void _resetShakeAnimation() {
_shiftAndGenerateRandomShakeTransform();
shakeController.value = 0.0;
shakeController.animateTo(1.0);
}
void _stopShakeAnimation() {
_offsetTween.end = Offset.zero;
_angleTween.end = 0.0;
_state = WarpAnimationState.stopped;
_shiftAndGenerateRandomShakeTransform();
shakeController.stop();
shakeController.value = 0.0;
stars = [];
}
@override
Widget build(BuildContext context) {
return CustomRefreshIndicator(
offsetToArmed: _indicatorSize,
leadingGlowVisible: false,
trailingGlowVisible: false,
onRefresh: widget.onRefresh, // Step 3
child: widget.child,
builder: (
BuildContext context,
Widget child,
IndicatorController controller,
) {
final animation = Listenable.merge([controller, shakeController]);
return Stack(
children: <Widget>[
AnimatedBuilder(
animation: shakeController,
builder: (_, __) {
return LayoutBuilder(
builder:
(BuildContext context, BoxConstraints constraints) {
return CustomPaint(
painter: Sky(
stars: stars,
color: widget.skyColor,
),
child: const SizedBox.expand(),
);
},
);
}),
AnimatedBuilder(
animation: animation,
builder: (context, _) {
_helper.update(controller.state);
if (_helper.didStateChange(
to: IndicatorState.loading,
)) {
SchedulerBinding.instance
?.addPostFrameCallback((_) => _startShakeAnimation());
} else if (_helper.didStateChange(
to: IndicatorState.idle,
)) {
SchedulerBinding.instance
?.addPostFrameCallback((_) => _stopShakeAnimation());
}
return Transform.scale(
scale: _scaleTween.transform(controller.value),
child: Builder(builder: (context) {
if (shakeController.value == 1.0 &&
_state == WarpAnimationState.playing) {
SchedulerBinding.instance
?.addPostFrameCallback((_) => _resetShakeAnimation());
}
return Transform.rotate(
angle: _angleTween.transform(shakeController.value),
child: Transform.translate(
offset: _offsetTween.transform(shakeController.value),
child: ClipRRect(
child: child,
borderRadius: BorderRadius.circular(
_radiusTween.transform(controller.value),
),
),
),
);
}),
);
},
),
],
);
},
);
}
@override
void dispose() {
shakeController.dispose();
super.dispose();
}
}
class Star {
Offset? position;
Color? color;
double value;
late Offset speed;
final Color initialColor;
late double angle;
Star({
required this.initialColor,
}) : value = 0.0;
static const _minOpacity = 0.1;
static const _maxOpacity = 1.0;
void _init(Rect rect) {
position = rect.center;
value = 0.0;
final random = Random();
angle = random.nextDouble() * pi * 3;
speed = Offset(cos(angle), sin(angle));
const minSpeedScale = 20;
const maxSpeedScale = 35;
final speedScale = minSpeedScale +
random.nextInt(maxSpeedScale - minSpeedScale).toDouble();
speed = speed.scale(
speedScale,
speedScale,
);
final t = speedScale / maxSpeedScale;
final opacity = _minOpacity + (_maxOpacity - _minOpacity) * t;
color = initialColor.withOpacity(opacity);
}
draw(Canvas canvas, Rect rect) {
if (position == null) {
_init(rect);
}
value++;
final startPosition = Offset(position!.dx, position!.dy);
final endPosition = position! + (speed * (value * 0.3));
position = speed + position!;
final paint = Paint()..color = color!;
final startShiftAngle = angle + (pi / 2);
final startShift = Offset(cos(startShiftAngle), sin(startShiftAngle));
final shiftedStartPosition =
startPosition + (startShift * (0.75 + value * 0.01));
final endShiftAngle = angle + (pi / 2);
final endShift = Offset(cos(endShiftAngle), sin(endShiftAngle));
final shiftedEndPosition = endPosition + (endShift * (1.5 + value * 0.01));
final path = Path()
..moveTo(startPosition.dx, startPosition.dy)
..lineTo(startPosition.dx, startPosition.dy)
..lineTo(shiftedStartPosition.dx, shiftedStartPosition.dy)
..lineTo(shiftedEndPosition.dx, shiftedEndPosition.dy)
..lineTo(endPosition.dx, endPosition.dy);
if (!rect.contains(startPosition)) {
_init(rect);
}
canvas.drawPath(path, paint);
}
}
class Sky extends CustomPainter {
final List<Star> stars;
final Color color;
Sky({
required this.stars,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
var rect = Offset.zero & size;
canvas.drawRect(rect, Paint()..color = color);
for (final star in stars) {
star.draw(canvas, rect);
}
}
@override
SemanticsBuilderCallback get semanticsBuilder {
return (Size size) {
var rect = Offset.zero & size;
return [
CustomPainterSemantics(
rect: rect,
properties: const SemanticsProperties(
label: 'Lightspeed animation.',
textDirection: TextDirection.ltr,
),
),
];
};
}
@override
bool shouldRepaint(Sky oldDelegate) => true;
@override
bool shouldRebuildSemantics(Sky oldDelegate) => false;
}
How can I move the children list down along with the RefreshIndicator as in your examples (Plane or IceCream)?
In these examples you use the offsetToArmed property, however I didn't find it in the latest version. How can I implement child ListView movement along with an indicator?
Thanks a lot in advance!
I see that we have complete state duration which is nice, but what would be really nice is the ability to set a minimum refreshing duration. It just provides a more seamless experience while showing that actions are taking place
Hey, would it be possible to add a parameter to CustomRefreshIndicator to disable this line essentially ->
await _animationController.animateTo(1.0, duration: widget.armedToLoadingDuration);
For my use case, I want the Widgets dependent on the IndicatorController.value to stay the same until the onRefresh function completes.
CustomRefreshIndicator(
onRefresh: onRefresh,
builder: (context, child, controller) {
return Center(child: CupertinoActivityIndicator());
},
child: ListView.builder(
itemBuilder: (_, index) => Text('Item $index'),
),
)
The listView is not displayed
Hey,
when i use this code:
https://pastebin.com/cj621P44
the indicator is shown as a black circle. Is anything wrong or is this a bug?
first, thanks for this package! the default RefreshIndicator gave me really hard time and didn't feel good while yours works and feels perfect.
Is it possible to make the custom refresh indicator to animate during pull? the same way the material RefreshIndicator behaves?
Thanks!
This project looks super advanced, KUDOS!
The examples provided are very advanced and powerful.
But I'm wondering if there is a good example for a simple implementation, for example a standard iOS look using CupertinoActivityIndicator
? I'm having a hard time figuring out how to go about that
When using onRefresh, if the future returns an exception, but the CustomRefreshIndicator only has a 'Complete' state, is there any way to display the refresh error?
When building a flutter app with custom_refresh_indicator >=1.0.0, the build fails with the following message:
/C:/src/flutter_nullsafe/.pub-cache/hosted/pub.dartlang.org/custom_refresh_indicator-1.2.1/lib/src/custom_refresh_indicator.dart:199:22: Error: The method 'disallowIndicator' isn't defined for the class 'OverscrollIndicatorNotification'.
- 'OverscrollIndicatorNotification' is from 'package:flutter/src/widgets/overscroll_indicator.dart' ('/C:/src/flutter_nullsafe/packages/flutter/lib/src/widgets/overscroll_indicator.dart').
Try correcting the name to the name of an existing method, or defining a method named 'disallowIndicator'.
notification.disallowIndicator();
^^^^^^^^^^^^^^^^^
/C:/src/flutter_nullsafe/.pub-cache/hosted/pub.dartlang.org/custom_refresh_indicator-1.2.1/lib/src/custom_refresh_indicator.dart:203:22: Error: The method 'disallowIndicator' isn't defined for the class 'OverscrollIndicatorNotification'.
- 'OverscrollIndicatorNotification' is from 'package:flutter/src/widgets/overscroll_indicator.dart' ('/C:/src/flutter_nullsafe/packages/flutter/lib/src/widgets/overscroll_indicator.dart').
Try correcting the name to the name of an existing method, or defining a method named 'disallowIndicator'.
notification.disallowIndicator();
^^^^^^^^^^^^^^^^^
Flutter version:
Flutter 2.5.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 18116933e7 (9 months ago) • 2021-10-15 10:46:35 -0700
Engine • revision d3ea636dc5
Tools • Dart 2.14.4
I believe we all have encountered scenario in developing a long list of data when we want to keep requesting more data from the server while user scroll at the end of the list...
I believe that's how we can scroll indefinitely on Twitter with their seemingly infinite data list.
So, this package provide us ways to pull down the list from the top to refresh the whole list, but from the api documentation, I don't see there's way to listen to the 'drag from bottom' event.
My question is, is it available to do that feature with this package?
Thanks in advance!
It would be nice to add an optional complete state which is shown after onRefresh has completed but before the indicator is hidden again. One could show for example a checkmark.
I tried following this blog about testing using fling: https://guillaume.bernos.dev/properly-test-a-pull-to-refresh/ on how to test a RefreshIndicator which is also similar to this package, but the test fails. Can this package also be testable?
Similar to #51
The problem is different behaviour when list has one element and 100 elements.
One element
I can scroll up and down during dragging
100 elements
When I change scroll direction refresh will be hide
There is possibility to make it consistent?
There is the similar question in stackoverflow: How to show RefreshIndicator intially while waiting data from backend API?
Hi friends,
What I to integrate custom-refresh-indicator and paginate_firestore, I think this feature will be very useful for a lot of devs, can somebody guide us on how to achieve this? Thanks in advance!
Is there an out of the box icon that makes this work as a drop in replacement for the default RefreshIndicator
flutter widget?
I'm quite happy with the flutter RefreshIndicator but would quite like to make use of the trigger options to refresh from the bottom as well as the top.
How can I show a transparent background CircularProgressIndicator?
If I use the code below, the background is white.
child: CustomMaterialIndicator(
indicatorBuilder: (context, controller) => Container(
child: const CircularProgressIndicator(),
),
If I use the code below, the background is white as well.
child: CustomMaterialIndicator(
indicatorBuilder: (context, controller) => Container(
color: Colors.transparent,
child: const CircularProgressIndicator(),
),
How to cancel pull to refresh when user is going up:
_controller.position.userScrollDirection == ScrollDirection.forward
So if I have an offsetToArmed of 50 per example, it seems to me the loading state is only triggered if the user scrolls down 100 pixels. I have been trying to understand but I believe it is a bug? i am using material 3 flutter.
Hello, Is it possible to set height of box that i show indicator?
Hi, first of all, great package.
Im getting and error when I rebuild the child in CustomRefresher
. I'm using a StaggeredGridView
(or a ListView
) inside the child, but I attach a controller to it as I need to listen to the scroll to know when it reaches the end and add more items to the list. To create an infinite scroll.
The problem is that when I add those items this error appears:
═══════ Exception caught by foundation library ════════════════════════════════
The following assertion was thrown while dispatching notifications for IndicatorController:
Build scheduled during frame.
While the widget tree was being built, laid out, and painted, a new frame was scheduled to rebuild the widget tree.
This might be because setState() was called from a layout or paint callback. If a change is needed to the widget tree, it should be applied as the tree is being built. Scheduling a change for the subsequent frame instead results in an interface that lags behind by one frame. If this was done to make your build dependent on a size measured at layout time, consider using a LayoutBuilder, CustomSingleChildLayout, or CustomMultiChildLayout. If, on the other hand, the one frame delay is the desired effect, for example because this is an animation, consider scheduling the frame in a post-frame callback using SchedulerBinding.addPostFrameCallback or using an AnimationController to trigger the animation.
When the exception was thrown, this was the stack
#0 WidgetsBinding._handleBuildScheduled.<anonymous closure>
package:flutter/…/widgets/binding.dart:783
#1 WidgetsBinding._handleBuildScheduled
package:flutter/…/widgets/binding.dart:806
#2 BuildOwner.scheduleBuildFor
package:flutter/…/widgets/framework.dart:2587
#3 Element.markNeedsBuild
package:flutter/…/widgets/framework.dart:4311
#4 State.setState
package:flutter/…/widgets/framework.dart:1264
...
The IndicatorController sending notification was: Instance of 'IndicatorController'
════════════════════════════════════════════════════════════════════════════════
The app doesn't break, but it would be nice to solve it.
Hope you can shine some light into it. Thanks! ;)
It is used for NestedScrollView + TabView + ListView. controller.state is easy to get stuck in dragging, causing the pull-down refresh to fail to start.
I added the first line of _handleScrollStartNotification
, it work.
if (controller.state == IndicatorState.dragging) {
setIndicatorState(IndicatorState.idle);
}
// controller.state always IndicatorState.dragging, _canStart always false.
_canStart = controller.state == IndicatorState.idle &&
(widget.reversed
? notification.metrics.extentAfter == 0
: notification.metrics.extentBefore == 0);
Switching TabView quickly and sliding the ListView can easily trigger this problem.
When I added ScrollController to ListView and there are few elements on list (they fit on display, no scroll needed) the drag gesture is not recognized by UI and the CustomRefreshIndicator doesn't get notified about the gesture event.
I reproduced it on CheckMarkIndicatorScreen:
import 'package:example/indicators/check_mark_indicator.dart';
import 'package:example/widgets/example_app_bar.dart';
import 'package:example/widgets/example_list.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class CheckMarkIndicatorScreen extends StatefulWidget {
@override
_CheckMarkIndicatorScreenState createState() =>
_CheckMarkIndicatorScreenState();
}
class _CheckMarkIndicatorScreenState extends State<CheckMarkIndicatorScreen> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
final _scrollController = ScrollController();
@override
dispose() {
_scrollController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll(){
print("_onScroll");
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: appBackgroundColor,
appBar: const ExampleAppBar(),
body: SafeArea(
child: CheckMarkIndicator(
child: ExampleList(appBackgroundColor, _scrollController),
),
),
);
}
}
and ExampleList
import 'package:flutter/material.dart';
import 'example_app_bar.dart';
class ExampleList extends StatelessWidget {
final Color backgroundColor;
final ScrollController scrollController;
const ExampleList([this.backgroundColor = appBackgroundColor, this.scrollController ]);
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(color: appBackgroundColor, boxShadow: [
BoxShadow(
blurRadius: 2,
color: Colors.white70,
spreadRadius: 0.5,
offset: Offset(0.0, .0),
)
]),
child: ListView.separated(
controller: scrollController,
itemBuilder: (BuildContext context, int index) => const Element(),
itemCount:2,
separatorBuilder: (BuildContext context, int index) => const Divider(
height: 0,
color: Color(0xFFe2d6ce),
thickness: 1,
),
),
);
}
}
class Element extends StatelessWidget {
const Element();
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FakeBox(height: 80, width: 80),
const SizedBox(width: 20),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FakeBox(height: 8, width: double.infinity),
FakeBox(height: 8, width: double.infinity),
FakeBox(height: 8, width: 200),
],
),
),
],
),
);
}
}
class FakeBox extends StatelessWidget {
const FakeBox({
Key key,
@required this.width,
@required this.height,
}) : super(key: key);
final double width;
final double height;
static const _boxDecoration = const BoxDecoration(
color: const Color(0xFFE2D8D7),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 10),
width: width,
height: height,
decoration: _boxDecoration,
);
}
}
Hello there! I would like to suggest a minor but potentially helpful improvement to the CustomRefreshIndicator package. Currently, the property used to build the refresh indicator is named builder
. To enhance clarity and make its purpose more self-explanatory, I propose renaming it to indicatorBuilder
.
Benefits:
indicatorBuilder
will make it instantly clear that this property is responsible for creating the custom refresh indicator.I believe this simple change could enhance the overall developer experience when using CustomRefreshIndicator
. Thank you for considering this suggestion, and I'm excited to contribute to the continued improvement of the package.
Best regards.
Any ETA on stable release for null safety?
return CustomRefreshIndicator(
offsetToArmed: _indicatorSize,
onRefresh: () => Future.delayed(const Duration(seconds: 2)),
completeStateDuration: const Duration(seconds: 2),
onStateChanged: (change) {
if (change.didChange(to: IndicatorState.complete)) {
setState(() {
_loading = true;
});
/// set [_renderCompleteState] to false when controller.state become idle
} else if (change.didChange(to: IndicatorState.idle)) {
setState(() {
_loading = false;
});
}
},
child: const Text("123123"),
builder: (
BuildContext context,
Widget child,
IndicatorController controller,
) {
return Stack(
children: [
AnimatedBuilder(
animation: controller,
builder: (BuildContext context, Widget? _) {
if (controller.scrollingDirection == ScrollDirection.reverse &&
prevScrollDirection == ScrollDirection.forward) {
controller.stopDrag();
}
prevScrollDirection = controller.scrollingDirection;
final containerHeight = controller.value * _indicatorSize;
return Container(
alignment: Alignment.center,
height: containerHeight,
decoration: BoxDecoration(
color: _loading ? Colors.greenAccent : Colors.black,
shape: BoxShape.circle),
child: OverflowBox(
maxHeight: 40,
minHeight: 40,
maxWidth: 40,
minWidth: 40,
alignment: Alignment.center,
child: _loading
? const Icon(
Icons.check,
color: Colors.white,
)
: SizedBox(
height: 30,
width: 30,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: const AlwaysStoppedAnimation(Colors.white),
value: controller.isDragging || controller.isArmed
? controller.value.clamp(0.0, 1.0)
: null,
),
),
),
);
}),
AnimatedBuilder(
builder: (context, _) {
return Transform.translate(
offset: Offset(0.0, controller.value * _indicatorSize),
child: child,
);
},
animation: controller,
),
],
);
},
);
Oops, my mistake. Sorry.
When there are a few items in the list, for example, 1 or 2 items, the list doesn't scroll, nor triggers a refresh.
When you have add vertical scrolling list view with nested horizontal scrolling, if you overflow scroll one of the nested ListView
s the parent refresh indicator is triggered.
It would nice to lock the pull to an axis or direction.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.