1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'package:flutter/material.dart'; |
6 | /// |
7 | /// @docImport 'app.dart'; |
8 | /// @docImport 'nav_bar.dart'; |
9 | library; |
10 | |
11 | import 'dart:math'; |
12 | |
13 | import 'package:flutter/foundation.dart' show clampDouble; |
14 | import 'package:flutter/rendering.dart'; |
15 | import 'package:flutter/scheduler.dart'; |
16 | import 'package:flutter/services.dart'; |
17 | import 'package:flutter/widgets.dart'; |
18 | |
19 | import 'activity_indicator.dart'; |
20 | |
21 | const double _kActivityIndicatorRadius = 14.0; |
22 | const double _kActivityIndicatorMargin = 16.0; |
23 | |
24 | class _CupertinoSliverRefresh extends SingleChildRenderObjectWidget { |
25 | const _CupertinoSliverRefresh({ |
26 | this.refreshIndicatorLayoutExtent = 0.0, |
27 | this.hasLayoutExtent = false, |
28 | super.child, |
29 | }) : assert(refreshIndicatorLayoutExtent >= 0.0); |
30 | |
31 | // The amount of space the indicator should occupy in the sliver in a |
32 | // resting state when in the refreshing mode. |
33 | final double refreshIndicatorLayoutExtent; |
34 | |
35 | // _RenderCupertinoSliverRefresh will paint the child in the available |
36 | // space either way but this instructs the _RenderCupertinoSliverRefresh |
37 | // on whether to also occupy any layoutExtent space or not. |
38 | final bool hasLayoutExtent; |
39 | |
40 | @override |
41 | _RenderCupertinoSliverRefresh createRenderObject(BuildContext context) { |
42 | return _RenderCupertinoSliverRefresh( |
43 | refreshIndicatorExtent: refreshIndicatorLayoutExtent, |
44 | hasLayoutExtent: hasLayoutExtent, |
45 | ); |
46 | } |
47 | |
48 | @override |
49 | void updateRenderObject( |
50 | BuildContext context, |
51 | covariant _RenderCupertinoSliverRefresh renderObject, |
52 | ) { |
53 | renderObject |
54 | ..refreshIndicatorLayoutExtent = refreshIndicatorLayoutExtent |
55 | ..hasLayoutExtent = hasLayoutExtent; |
56 | } |
57 | } |
58 | |
59 | // RenderSliver object that gives its child RenderBox object space to paint |
60 | // in the overscrolled gap and may or may not hold that overscrolled gap |
61 | // around the RenderBox depending on whether [layoutExtent] is set. |
62 | // |
63 | // The [layoutExtentOffsetCompensation] field keeps internal accounting to |
64 | // prevent scroll position jumps as the [layoutExtent] is set and unset. |
65 | class _RenderCupertinoSliverRefresh extends RenderSliver |
66 | with RenderObjectWithChildMixin<RenderBox> { |
67 | _RenderCupertinoSliverRefresh({ |
68 | required double refreshIndicatorExtent, |
69 | required bool hasLayoutExtent, |
70 | RenderBox? child, |
71 | }) : assert(refreshIndicatorExtent >= 0.0), |
72 | _refreshIndicatorExtent = refreshIndicatorExtent, |
73 | _hasLayoutExtent = hasLayoutExtent { |
74 | this.child = child; |
75 | } |
76 | |
77 | // The amount of layout space the indicator should occupy in the sliver in a |
78 | // resting state when in the refreshing mode. |
79 | double get refreshIndicatorLayoutExtent => _refreshIndicatorExtent; |
80 | double _refreshIndicatorExtent; |
81 | set refreshIndicatorLayoutExtent(double value) { |
82 | assert(value >= 0.0); |
83 | if (value == _refreshIndicatorExtent) { |
84 | return; |
85 | } |
86 | _refreshIndicatorExtent = value; |
87 | markNeedsLayout(); |
88 | } |
89 | |
90 | // The child box will be laid out and painted in the available space either |
91 | // way but this determines whether to also occupy any |
92 | // [SliverGeometry.layoutExtent] space or not. |
93 | bool get hasLayoutExtent => _hasLayoutExtent; |
94 | bool _hasLayoutExtent; |
95 | set hasLayoutExtent(bool value) { |
96 | if (value == _hasLayoutExtent) { |
97 | return; |
98 | } |
99 | _hasLayoutExtent = value; |
100 | markNeedsLayout(); |
101 | } |
102 | |
103 | // This keeps track of the previously applied scroll offsets to the scrollable |
104 | // so that when [refreshIndicatorLayoutExtent] or [hasLayoutExtent] changes, |
105 | // the appropriate delta can be applied to keep everything in the same place |
106 | // visually. |
107 | double layoutExtentOffsetCompensation = 0.0; |
108 | |
109 | @override |
110 | void performLayout() { |
111 | final SliverConstraints constraints = this.constraints; |
112 | // Only pulling to refresh from the top is currently supported. |
113 | assert(constraints.axisDirection == AxisDirection.down); |
114 | assert(constraints.growthDirection == GrowthDirection.forward); |
115 | |
116 | // The new layout extent this sliver should now have. |
117 | final double layoutExtent = (_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent; |
118 | // If the new layoutExtent instructive changed, the SliverGeometry's |
119 | // layoutExtent will take that value (on the next performLayout run). Shift |
120 | // the scroll offset first so it doesn't make the scroll position suddenly jump. |
121 | if (layoutExtent != layoutExtentOffsetCompensation) { |
122 | geometry = SliverGeometry( |
123 | scrollOffsetCorrection: layoutExtent - layoutExtentOffsetCompensation, |
124 | ); |
125 | layoutExtentOffsetCompensation = layoutExtent; |
126 | // Return so we don't have to do temporary accounting and adjusting the |
127 | // child's constraints accounting for this one transient frame using a |
128 | // combination of existing layout extent, new layout extent change and |
129 | // the overlap. |
130 | return; |
131 | } |
132 | |
133 | final bool active = constraints.overlap < 0.0 || layoutExtent > 0.0; |
134 | final double overscrolledExtent = constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0; |
135 | // Layout the child giving it the space of the currently dragged overscroll |
136 | // which may or may not include a sliver layout extent space that it will |
137 | // keep after the user lets go during the refresh process. |
138 | child!.layout( |
139 | constraints.asBoxConstraints( |
140 | maxExtent: |
141 | layoutExtent |
142 | // Plus only the overscrolled portion immediately preceding this |
143 | // sliver. |
144 | + |
145 | overscrolledExtent, |
146 | ), |
147 | parentUsesSize: true, |
148 | ); |
149 | if (active) { |
150 | geometry = SliverGeometry( |
151 | scrollExtent: layoutExtent, |
152 | paintOrigin: -overscrolledExtent - constraints.scrollOffset, |
153 | paintExtent: max( |
154 | // Check child size (which can come from overscroll) because |
155 | // layoutExtent may be zero. Check layoutExtent also since even |
156 | // with a layoutExtent, the indicator builder may decide to not |
157 | // build anything. |
158 | max(child!.size.height, layoutExtent) - constraints.scrollOffset, |
159 | 0.0, |
160 | ), |
161 | maxPaintExtent: max(max(child!.size.height, layoutExtent) - constraints.scrollOffset, 0.0), |
162 | layoutExtent: max(layoutExtent - constraints.scrollOffset, 0.0), |
163 | ); |
164 | } else { |
165 | // If we never started overscrolling, return no geometry. |
166 | geometry = SliverGeometry.zero; |
167 | } |
168 | } |
169 | |
170 | @override |
171 | void paint(PaintingContext paintContext, Offset offset) { |
172 | if (constraints.overlap < 0.0 || constraints.scrollOffset + child!.size.height > 0) { |
173 | paintContext.paintChild(child!, offset); |
174 | } |
175 | } |
176 | |
177 | // Nothing special done here because this sliver always paints its child |
178 | // exactly between paintOrigin and paintExtent. |
179 | @override |
180 | void applyPaintTransform(RenderObject child, Matrix4 transform) {} |
181 | } |
182 | |
183 | /// The current state of the refresh control. |
184 | /// |
185 | /// Passed into the [RefreshControlIndicatorBuilder] builder function so |
186 | /// users can show different UI in different modes. |
187 | enum RefreshIndicatorMode { |
188 | /// Initial state, when not being overscrolled into, or after the overscroll |
189 | /// is canceled or after done and the sliver retracted away. |
190 | inactive, |
191 | |
192 | /// While being overscrolled but not far enough yet to trigger the refresh. |
193 | drag, |
194 | |
195 | /// Dragged far enough that the onRefresh callback will run and the dragged |
196 | /// displacement is not yet at the final refresh resting state. |
197 | armed, |
198 | |
199 | /// While the onRefresh task is running. |
200 | refresh, |
201 | |
202 | /// While the indicator is animating away after refreshing. |
203 | done, |
204 | } |
205 | |
206 | /// Signature for a builder that can create a different widget to show in the |
207 | /// refresh indicator space depending on the current state of the refresh |
208 | /// control and the space available. |
209 | /// |
210 | /// The `refreshTriggerPullDistance` and `refreshIndicatorExtent` parameters are |
211 | /// the same values passed into the [CupertinoSliverRefreshControl]. |
212 | /// |
213 | /// The `pulledExtent` parameter is the currently available space either from |
214 | /// overscrolling or as held by the sliver during refresh. |
215 | typedef RefreshControlIndicatorBuilder = |
216 | Widget Function( |
217 | BuildContext context, |
218 | RefreshIndicatorMode refreshState, |
219 | double pulledExtent, |
220 | double refreshTriggerPullDistance, |
221 | double refreshIndicatorExtent, |
222 | ); |
223 | |
224 | /// A callback function that's invoked when the [CupertinoSliverRefreshControl] is |
225 | /// pulled a `refreshTriggerPullDistance`. Must return a [Future]. Upon |
226 | /// completion of the [Future], the [CupertinoSliverRefreshControl] enters the |
227 | /// [RefreshIndicatorMode.done] state and will start to go away. |
228 | typedef RefreshCallback = Future<void> Function(); |
229 | |
230 | /// A sliver widget implementing the iOS-style pull to refresh content control. |
231 | /// |
232 | /// When inserted as the first sliver in a scroll view or behind other slivers |
233 | /// that still lets the scrollable overscroll in front of this sliver (such as |
234 | /// the [CupertinoSliverNavigationBar], this widget will: |
235 | /// |
236 | /// * Let the user draw inside the overscrolled area via the passed in [builder]. |
237 | /// * Trigger the provided [onRefresh] function when overscrolled far enough to |
238 | /// pass [refreshTriggerPullDistance]. |
239 | /// * Continue to hold [refreshIndicatorExtent] amount of space for the [builder] |
240 | /// to keep drawing inside of as the [Future] returned by [onRefresh] processes. |
241 | /// * Scroll away once the [onRefresh] [Future] completes. |
242 | /// |
243 | /// The [builder] function will be informed of the current [RefreshIndicatorMode] |
244 | /// when invoking it, except in the [RefreshIndicatorMode.inactive] state when |
245 | /// no space is available and nothing needs to be built. The [builder] function |
246 | /// will otherwise be continuously invoked as the amount of space available |
247 | /// changes from overscroll, as the sliver scrolls away after the [onRefresh] |
248 | /// task is done, etc. |
249 | /// |
250 | /// Only one refresh can be triggered until the previous refresh has completed |
251 | /// and the indicator sliver has retracted at least 90% of the way back. |
252 | /// |
253 | /// Can only be used in downward-scrolling vertical lists that overscrolls. In |
254 | /// other words, refreshes can't be triggered with [Scrollable]s using |
255 | /// [ClampingScrollPhysics] which is the default on Android. To allow overscroll |
256 | /// on Android, use an overscrolling physics such as [BouncingScrollPhysics]. |
257 | /// This can be done via: |
258 | /// |
259 | /// * Providing a [BouncingScrollPhysics] (possibly in combination with a |
260 | /// [AlwaysScrollableScrollPhysics]) while constructing the scrollable. |
261 | /// * By inserting a [ScrollConfiguration] with [BouncingScrollPhysics] above |
262 | /// the scrollable. |
263 | /// * By using [CupertinoApp], which always uses a [ScrollConfiguration] |
264 | /// with [BouncingScrollPhysics] regardless of platform. |
265 | /// |
266 | /// In a typical application, this sliver should be inserted between the app bar |
267 | /// sliver such as [CupertinoSliverNavigationBar] and your main scrollable |
268 | /// content's sliver. |
269 | /// |
270 | /// {@tool dartpad} |
271 | /// When the user scrolls past [refreshTriggerPullDistance], |
272 | /// this sample shows the default iOS pull to refresh indicator for 1 second and |
273 | /// adds a new item to the top of the list view. |
274 | /// |
275 | /// ** See code in examples/api/lib/cupertino/refresh/cupertino_sliver_refresh_control.0.dart ** |
276 | /// {@end-tool} |
277 | /// |
278 | /// See also: |
279 | /// |
280 | /// * [CustomScrollView], a typical sliver holding scroll view this control |
281 | /// should go into. |
282 | /// * <https://developer.apple.com/ios/human-interface-guidelines/controls/refresh-content-controls/> |
283 | /// * [RefreshIndicator], a Material Design version of the pull-to-refresh |
284 | /// paradigm. This widget works differently than [RefreshIndicator] because |
285 | /// instead of being an overlay on top of the scrollable, the |
286 | /// [CupertinoSliverRefreshControl] is part of the scrollable and actively occupies |
287 | /// scrollable space. |
288 | class CupertinoSliverRefreshControl extends StatefulWidget { |
289 | /// Create a new refresh control for inserting into a list of slivers. |
290 | /// |
291 | /// The [refreshTriggerPullDistance] and [refreshIndicatorExtent] arguments |
292 | /// must be greater than or equal to 0. |
293 | /// |
294 | /// The [builder] argument may be null, in which case no indicator UI will be |
295 | /// shown but the [onRefresh] will still be invoked. By default, [builder] |
296 | /// shows a [CupertinoActivityIndicator]. |
297 | /// |
298 | /// The [onRefresh] argument will be called when pulled far enough to trigger |
299 | /// a refresh. |
300 | const CupertinoSliverRefreshControl({ |
301 | super.key, |
302 | this.refreshTriggerPullDistance = _defaultRefreshTriggerPullDistance, |
303 | this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent, |
304 | this.builder = buildRefreshIndicator, |
305 | this.onRefresh, |
306 | }) : assert(refreshTriggerPullDistance > 0.0), |
307 | assert(refreshIndicatorExtent >= 0.0), |
308 | assert( |
309 | refreshTriggerPullDistance >= refreshIndicatorExtent, |
310 | 'The refresh indicator cannot take more space in its final state ' |
311 | 'than the amount initially created by overscrolling.', |
312 | ); |
313 | |
314 | /// The amount of overscroll the scrollable must be dragged to trigger a reload. |
315 | /// |
316 | /// Must be larger than zero and larger than [refreshIndicatorExtent]. |
317 | /// Defaults to 100 pixels when not specified. |
318 | /// |
319 | /// When overscrolled past this distance, [onRefresh] will be called if not |
320 | /// null and the [builder] will build in the [RefreshIndicatorMode.armed] state. |
321 | final double refreshTriggerPullDistance; |
322 | |
323 | /// The amount of space the refresh indicator sliver will keep holding while |
324 | /// [onRefresh]'s [Future] is still running. |
325 | /// |
326 | /// Must be a positive number, but can be zero, in which case the sliver will |
327 | /// start retracting back to zero as soon as the refresh is started. Defaults |
328 | /// to 60 pixels when not specified. |
329 | /// |
330 | /// Must be smaller than [refreshTriggerPullDistance], since the sliver |
331 | /// shouldn't grow further after triggering the refresh. |
332 | final double refreshIndicatorExtent; |
333 | |
334 | /// A builder that's called as this sliver's size changes, and as the state |
335 | /// changes. |
336 | /// |
337 | /// Can be set to null, in which case nothing will be drawn in the overscrolled |
338 | /// space. |
339 | /// |
340 | /// Will not be called when the available space is zero such as before any |
341 | /// overscroll. |
342 | final RefreshControlIndicatorBuilder? builder; |
343 | |
344 | /// Callback invoked when pulled by [refreshTriggerPullDistance]. |
345 | /// |
346 | /// If provided, must return a [Future] which will keep the indicator in the |
347 | /// [RefreshIndicatorMode.refresh] state until the [Future] completes. |
348 | /// |
349 | /// Can be null, in which case a single frame of [RefreshIndicatorMode.armed] |
350 | /// state will be drawn before going immediately to the [RefreshIndicatorMode.done] |
351 | /// where the sliver will start retracting. |
352 | final RefreshCallback? onRefresh; |
353 | |
354 | static const double _defaultRefreshTriggerPullDistance = 100.0; |
355 | static const double _defaultRefreshIndicatorExtent = 60.0; |
356 | |
357 | /// Retrieve the current state of the CupertinoSliverRefreshControl. The same as the |
358 | /// state that gets passed into the [builder] function. Used for testing. |
359 | @visibleForTesting |
360 | static RefreshIndicatorMode state(BuildContext context) { |
361 | final _CupertinoSliverRefreshControlState state = |
362 | context.findAncestorStateOfType<_CupertinoSliverRefreshControlState>()!; |
363 | return state.refreshState; |
364 | } |
365 | |
366 | /// Builds a refresh indicator that reflects the standard iOS pull-to-refresh |
367 | /// behavior. Specifically, this entails presenting an activity indicator that |
368 | /// changes depending on the current refreshState. As the user initially drags |
369 | /// down, the indicator will gradually reveal individual ticks until the refresh |
370 | /// becomes armed. At this point, the animated activity indicator will begin rotating. |
371 | /// Once the refresh has completed, the activity indicator shrinks away as the |
372 | /// space allocation animates back to closed. |
373 | static Widget buildRefreshIndicator( |
374 | BuildContext context, |
375 | RefreshIndicatorMode refreshState, |
376 | double pulledExtent, |
377 | double refreshTriggerPullDistance, |
378 | double refreshIndicatorExtent, |
379 | ) { |
380 | final double percentageComplete = clampDouble( |
381 | pulledExtent / refreshTriggerPullDistance, |
382 | 0.0, |
383 | 1.0, |
384 | ); |
385 | |
386 | // Place the indicator at the top of the sliver that opens up. We're using a |
387 | // Stack/Positioned widget because the CupertinoActivityIndicator does some |
388 | // internal translations based on the current size (which grows as the user drags) |
389 | // that makes Padding calculations difficult. Rather than be reliant on the |
390 | // internal implementation of the activity indicator, the Positioned widget allows |
391 | // us to be explicit where the widget gets placed. The indicator should appear |
392 | // over the top of the dragged widget, hence the use of Clip.none. |
393 | return Center( |
394 | child: Stack( |
395 | clipBehavior: Clip.none, |
396 | children: <Widget>[ |
397 | Positioned( |
398 | top: _kActivityIndicatorMargin, |
399 | left: 0.0, |
400 | right: 0.0, |
401 | child: _buildIndicatorForRefreshState( |
402 | refreshState, |
403 | _kActivityIndicatorRadius, |
404 | percentageComplete, |
405 | ), |
406 | ), |
407 | ], |
408 | ), |
409 | ); |
410 | } |
411 | |
412 | static Widget _buildIndicatorForRefreshState( |
413 | RefreshIndicatorMode refreshState, |
414 | double radius, |
415 | double percentageComplete, |
416 | ) { |
417 | switch (refreshState) { |
418 | case RefreshIndicatorMode.drag: |
419 | // While we're dragging, we draw individual ticks of the spinner while simultaneously |
420 | // easing the opacity in. The opacity curve values here were derived using |
421 | // Xcode through inspecting a native app running on iOS 13.5. |
422 | const Curve opacityCurve = Interval(0.0, 0.35, curve: Curves.easeInOut); |
423 | return Opacity( |
424 | opacity: opacityCurve.transform(percentageComplete), |
425 | child: CupertinoActivityIndicator.partiallyRevealed( |
426 | radius: radius, |
427 | progress: percentageComplete, |
428 | ), |
429 | ); |
430 | case RefreshIndicatorMode.armed: |
431 | case RefreshIndicatorMode.refresh: |
432 | // Once we're armed or performing the refresh, we just show the normal spinner. |
433 | return CupertinoActivityIndicator(radius: radius); |
434 | case RefreshIndicatorMode.done: |
435 | // When the user lets go, the standard transition is to shrink the spinner. |
436 | return CupertinoActivityIndicator(radius: radius * percentageComplete); |
437 | case RefreshIndicatorMode.inactive: |
438 | // Anything else doesn't show anything. |
439 | return const SizedBox.shrink(); |
440 | } |
441 | } |
442 | |
443 | @override |
444 | State<CupertinoSliverRefreshControl> createState() => _CupertinoSliverRefreshControlState(); |
445 | } |
446 | |
447 | class _CupertinoSliverRefreshControlState extends State<CupertinoSliverRefreshControl> { |
448 | // Reset the state from done to inactive when only this fraction of the |
449 | // original `refreshTriggerPullDistance` is left. |
450 | static const double _inactiveResetOverscrollFraction = 0.1; |
451 | |
452 | late RefreshIndicatorMode refreshState; |
453 | // [Future] returned by the widget's `onRefresh`. |
454 | Future<void>? refreshTask; |
455 | // The amount of space available from the inner indicator box's perspective. |
456 | // |
457 | // The value is the sum of the sliver's layout extent and the overscroll |
458 | // (which partially gets transferred into the layout extent when the refresh |
459 | // triggers). |
460 | // |
461 | // The value of latestIndicatorBoxExtent doesn't change when the sliver scrolls |
462 | // away without retracting; it is independent from the sliver's scrollOffset. |
463 | double latestIndicatorBoxExtent = 0.0; |
464 | bool hasSliverLayoutExtent = false; |
465 | |
466 | @override |
467 | void initState() { |
468 | super.initState(); |
469 | refreshState = RefreshIndicatorMode.inactive; |
470 | } |
471 | |
472 | // A state machine transition calculator. Multiple states can be transitioned |
473 | // through per single call. |
474 | RefreshIndicatorMode transitionNextState() { |
475 | RefreshIndicatorMode nextState; |
476 | |
477 | void goToDone() { |
478 | nextState = RefreshIndicatorMode.done; |
479 | // Either schedule the RenderSliver to re-layout on the next frame |
480 | // when not currently in a frame or schedule it on the next frame. |
481 | if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { |
482 | setState(() => hasSliverLayoutExtent = false); |
483 | } else { |
484 | SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { |
485 | setState(() => hasSliverLayoutExtent = false); |
486 | }, debugLabel: 'Refresh.goToDone'); |
487 | } |
488 | } |
489 | |
490 | switch (refreshState) { |
491 | case RefreshIndicatorMode.inactive: |
492 | if (latestIndicatorBoxExtent <= 0) { |
493 | return RefreshIndicatorMode.inactive; |
494 | } else { |
495 | nextState = RefreshIndicatorMode.drag; |
496 | } |
497 | continue drag; |
498 | drag: |
499 | case RefreshIndicatorMode.drag: |
500 | if (latestIndicatorBoxExtent == 0) { |
501 | return RefreshIndicatorMode.inactive; |
502 | } else if (latestIndicatorBoxExtent < widget.refreshTriggerPullDistance) { |
503 | return RefreshIndicatorMode.drag; |
504 | } else { |
505 | if (widget.onRefresh != null) { |
506 | HapticFeedback.mediumImpact(); |
507 | // Call onRefresh after this frame finished since the function is |
508 | // user supplied and we're always here in the middle of the sliver's |
509 | // performLayout. |
510 | SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { |
511 | refreshTask = |
512 | widget.onRefresh!()..whenComplete(() { |
513 | if (mounted) { |
514 | setState(() => refreshTask = null); |
515 | // Trigger one more transition because by this time, BoxConstraint's |
516 | // maxHeight might already be resting at 0 in which case no |
517 | // calls to [transitionNextState] will occur anymore and the |
518 | // state may be stuck in a non-inactive state. |
519 | refreshState = transitionNextState(); |
520 | } |
521 | }); |
522 | setState(() => hasSliverLayoutExtent = true); |
523 | }, debugLabel: 'Refresh.transition'); |
524 | } |
525 | return RefreshIndicatorMode.armed; |
526 | } |
527 | case RefreshIndicatorMode.armed: |
528 | if (refreshState == RefreshIndicatorMode.armed && refreshTask == null) { |
529 | goToDone(); |
530 | continue done; |
531 | } |
532 | |
533 | if (latestIndicatorBoxExtent > widget.refreshIndicatorExtent) { |
534 | return RefreshIndicatorMode.armed; |
535 | } else { |
536 | nextState = RefreshIndicatorMode.refresh; |
537 | } |
538 | continue refresh; |
539 | refresh: |
540 | case RefreshIndicatorMode.refresh: |
541 | if (refreshTask != null) { |
542 | return RefreshIndicatorMode.refresh; |
543 | } else { |
544 | goToDone(); |
545 | } |
546 | continue done; |
547 | done: |
548 | case RefreshIndicatorMode.done: |
549 | // Let the transition back to inactive trigger before strictly going |
550 | // to 0.0 since the last bit of the animation can take some time and |
551 | // can feel sluggish if not going all the way back to 0.0 prevented |
552 | // a subsequent pull-to-refresh from starting. |
553 | if (latestIndicatorBoxExtent > |
554 | widget.refreshTriggerPullDistance * _inactiveResetOverscrollFraction) { |
555 | return RefreshIndicatorMode.done; |
556 | } else { |
557 | nextState = RefreshIndicatorMode.inactive; |
558 | } |
559 | } |
560 | |
561 | return nextState; |
562 | } |
563 | |
564 | @override |
565 | Widget build(BuildContext context) { |
566 | return _CupertinoSliverRefresh( |
567 | refreshIndicatorLayoutExtent: widget.refreshIndicatorExtent, |
568 | hasLayoutExtent: hasSliverLayoutExtent, |
569 | // A LayoutBuilder lets the sliver's layout changes be fed back out to |
570 | // its owner to trigger state changes. |
571 | child: LayoutBuilder( |
572 | builder: (BuildContext context, BoxConstraints constraints) { |
573 | latestIndicatorBoxExtent = constraints.maxHeight; |
574 | refreshState = transitionNextState(); |
575 | if (widget.builder != null && latestIndicatorBoxExtent > 0) { |
576 | return widget.builder!( |
577 | context, |
578 | refreshState, |
579 | latestIndicatorBoxExtent, |
580 | widget.refreshTriggerPullDistance, |
581 | widget.refreshIndicatorExtent, |
582 | ); |
583 | } |
584 | return const LimitedBox(maxWidth: 0.0, maxHeight: 0.0, child: SizedBox.expand()); |
585 | }, |
586 | ), |
587 | ); |
588 | } |
589 | } |
590 |
Definitions
- _kActivityIndicatorRadius
- _kActivityIndicatorMargin
- _CupertinoSliverRefresh
- _CupertinoSliverRefresh
- createRenderObject
- updateRenderObject
- _RenderCupertinoSliverRefresh
- _RenderCupertinoSliverRefresh
- refreshIndicatorLayoutExtent
- refreshIndicatorLayoutExtent
- hasLayoutExtent
- hasLayoutExtent
- performLayout
- paint
- applyPaintTransform
- RefreshIndicatorMode
- CupertinoSliverRefreshControl
- CupertinoSliverRefreshControl
- state
- buildRefreshIndicator
- _buildIndicatorForRefreshState
- createState
- _CupertinoSliverRefreshControlState
- initState
- transitionNextState
- goToDone
Learn more about Flutter for embedded and desktop on industrialflutter.com