refresh.dart 23.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Ian Hickson's avatar
Ian Hickson committed
4

5 6
import 'dart:math';

7
import 'package:flutter/foundation.dart' show clampDouble;
8 9 10 11
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
12

13
import 'activity_indicator.dart';
14 15 16

const double _kActivityIndicatorRadius = 14.0;
const double _kActivityIndicatorMargin = 16.0;
17

18 19
class _CupertinoSliverRefresh extends SingleChildRenderObjectWidget {
  const _CupertinoSliverRefresh({
20 21
    this.refreshIndicatorLayoutExtent = 0.0,
    this.hasLayoutExtent = false,
22
    super.child,
23
  }) : assert(refreshIndicatorLayoutExtent >= 0.0);
24 25 26 27

  // The amount of space the indicator should occupy in the sliver in a
  // resting state when in the refreshing mode.
  final double refreshIndicatorLayoutExtent;
28

29 30
  // _RenderCupertinoSliverRefresh will paint the child in the available
  // space either way but this instructs the _RenderCupertinoSliverRefresh
31 32 33 34
  // on whether to also occupy any layoutExtent space or not.
  final bool hasLayoutExtent;

  @override
35
  _RenderCupertinoSliverRefresh createRenderObject(BuildContext context) {
36
    return _RenderCupertinoSliverRefresh(
37 38 39 40 41 42
      refreshIndicatorExtent: refreshIndicatorLayoutExtent,
      hasLayoutExtent: hasLayoutExtent,
    );
  }

  @override
43
  void updateRenderObject(BuildContext context, covariant _RenderCupertinoSliverRefresh renderObject) {
44 45 46 47 48 49 50 51 52 53 54 55
    renderObject
      ..refreshIndicatorLayoutExtent = refreshIndicatorLayoutExtent
      ..hasLayoutExtent = hasLayoutExtent;
  }
}

// RenderSliver object that gives its child RenderBox object space to paint
// in the overscrolled gap and may or may not hold that overscrolled gap
// around the RenderBox depending on whether [layoutExtent] is set.
//
// The [layoutExtentOffsetCompensation] field keeps internal accounting to
// prevent scroll position jumps as the [layoutExtent] is set and unset.
56
class _RenderCupertinoSliverRefresh extends RenderSliver
57
    with RenderObjectWithChildMixin<RenderBox> {
58
  _RenderCupertinoSliverRefresh({
59 60 61
    required double refreshIndicatorExtent,
    required bool hasLayoutExtent,
    RenderBox? child,
62
  }) : assert(refreshIndicatorExtent >= 0.0),
63 64 65 66 67 68 69 70 71 72 73
       _refreshIndicatorExtent = refreshIndicatorExtent,
       _hasLayoutExtent = hasLayoutExtent {
    this.child = child;
  }

  // The amount of layout space the indicator should occupy in the sliver in a
  // resting state when in the refreshing mode.
  double get refreshIndicatorLayoutExtent => _refreshIndicatorExtent;
  double _refreshIndicatorExtent;
  set refreshIndicatorLayoutExtent(double value) {
    assert(value >= 0.0);
74
    if (value == _refreshIndicatorExtent) {
75
      return;
76
    }
77 78 79 80 81
    _refreshIndicatorExtent = value;
    markNeedsLayout();
  }

  // The child box will be laid out and painted in the available space either
82 83
  // way but this determines whether to also occupy any
  // [SliverGeometry.layoutExtent] space or not.
84 85 86
  bool get hasLayoutExtent => _hasLayoutExtent;
  bool _hasLayoutExtent;
  set hasLayoutExtent(bool value) {
87
    if (value == _hasLayoutExtent) {
88
      return;
89
    }
90 91 92 93 94 95 96 97 98 99 100 101
    _hasLayoutExtent = value;
    markNeedsLayout();
  }

  // This keeps track of the previously applied scroll offsets to the scrollable
  // so that when [refreshIndicatorLayoutExtent] or [hasLayoutExtent] changes,
  // the appropriate delta can be applied to keep everything in the same place
  // visually.
  double layoutExtentOffsetCompensation = 0.0;

  @override
  void performLayout() {
102
    final SliverConstraints constraints = this.constraints;
103 104 105 106 107 108 109 110 111 112 113
    // Only pulling to refresh from the top is currently supported.
    assert(constraints.axisDirection == AxisDirection.down);
    assert(constraints.growthDirection == GrowthDirection.forward);

    // The new layout extent this sliver should now have.
    final double layoutExtent =
        (_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent;
    // If the new layoutExtent instructive changed, the SliverGeometry's
    // layoutExtent will take that value (on the next performLayout run). Shift
    // the scroll offset first so it doesn't make the scroll position suddenly jump.
    if (layoutExtent != layoutExtentOffsetCompensation) {
114
      geometry = SliverGeometry(
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
        scrollOffsetCorrection: layoutExtent - layoutExtentOffsetCompensation,
      );
      layoutExtentOffsetCompensation = layoutExtent;
      // Return so we don't have to do temporary accounting and adjusting the
      // child's constraints accounting for this one transient frame using a
      // combination of existing layout extent, new layout extent change and
      // the overlap.
      return;
    }

    final bool active = constraints.overlap < 0.0 || layoutExtent > 0.0;
    final double overscrolledExtent =
        constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
    // Layout the child giving it the space of the currently dragged overscroll
    // which may or may not include a sliver layout extent space that it will
    // keep after the user lets go during the refresh process.
131
    child!.layout(
132 133 134 135 136 137 138 139 140
      constraints.asBoxConstraints(
        maxExtent: layoutExtent
            // Plus only the overscrolled portion immediately preceding this
            // sliver.
            + overscrolledExtent,
      ),
      parentUsesSize: true,
    );
    if (active) {
141
      geometry = SliverGeometry(
142 143 144 145 146 147 148
        scrollExtent: layoutExtent,
        paintOrigin: -overscrolledExtent - constraints.scrollOffset,
        paintExtent: max(
          // Check child size (which can come from overscroll) because
          // layoutExtent may be zero. Check layoutExtent also since even
          // with a layoutExtent, the indicator builder may decide to not
          // build anything.
149
          max(child!.size.height, layoutExtent) - constraints.scrollOffset,
150 151 152
          0.0,
        ),
        maxPaintExtent: max(
153
          max(child!.size.height, layoutExtent) - constraints.scrollOffset,
154 155 156 157 158 159 160 161 162 163 164 165 166
          0.0,
        ),
        layoutExtent: max(layoutExtent - constraints.scrollOffset, 0.0),
      );
    } else {
      // If we never started overscrolling, return no geometry.
      geometry = SliverGeometry.zero;
    }
  }

  @override
  void paint(PaintingContext paintContext, Offset offset) {
    if (constraints.overlap < 0.0 ||
167 168
        constraints.scrollOffset + child!.size.height > 0) {
      paintContext.paintChild(child!, offset);
169 170 171 172 173 174
    }
  }

  // Nothing special done here because this sliver always paints its child
  // exactly between paintOrigin and paintExtent.
  @override
175
  void applyPaintTransform(RenderObject child, Matrix4 transform) { }
176 177 178 179 180 181 182 183 184 185
}

/// The current state of the refresh control.
///
/// Passed into the [RefreshControlIndicatorBuilder] builder function so
/// users can show different UI in different modes.
enum RefreshIndicatorMode {
  /// Initial state, when not being overscrolled into, or after the overscroll
  /// is canceled or after done and the sliver retracted away.
  inactive,
186

187 188
  /// While being overscrolled but not far enough yet to trigger the refresh.
  drag,
189

190 191 192
  /// Dragged far enough that the onRefresh callback will run and the dragged
  /// displacement is not yet at the final refresh resting state.
  armed,
193

194 195
  /// While the onRefresh task is running.
  refresh,
196

197 198 199 200 201 202 203 204 205
  /// While the indicator is animating away after refreshing.
  done,
}

/// Signature for a builder that can create a different widget to show in the
/// refresh indicator space depending on the current state of the refresh
/// control and the space available.
///
/// The `refreshTriggerPullDistance` and `refreshIndicatorExtent` parameters are
206
/// the same values passed into the [CupertinoSliverRefreshControl].
207 208 209
///
/// The `pulledExtent` parameter is the currently available space either from
/// overscrolling or as held by the sliver during refresh.
210
typedef RefreshControlIndicatorBuilder = Widget Function(
211 212 213 214 215 216 217
  BuildContext context,
  RefreshIndicatorMode refreshState,
  double pulledExtent,
  double refreshTriggerPullDistance,
  double refreshIndicatorExtent,
);

218
/// A callback function that's invoked when the [CupertinoSliverRefreshControl] is
219
/// pulled a `refreshTriggerPullDistance`. Must return a [Future]. Upon
220
/// completion of the [Future], the [CupertinoSliverRefreshControl] enters the
221
/// [RefreshIndicatorMode.done] state and will start to go away.
222
typedef RefreshCallback = Future<void> Function();
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246

/// A sliver widget implementing the iOS-style pull to refresh content control.
///
/// When inserted as the first sliver in a scroll view or behind other slivers
/// that still lets the scrollable overscroll in front of this sliver (such as
/// the [CupertinoSliverNavigationBar], this widget will:
///
///  * Let the user draw inside the overscrolled area via the passed in [builder].
///  * Trigger the provided [onRefresh] function when overscrolled far enough to
///    pass [refreshTriggerPullDistance].
///  * Continue to hold [refreshIndicatorExtent] amount of space for the [builder]
///    to keep drawing inside of as the [Future] returned by [onRefresh] processes.
///  * Scroll away once the [onRefresh] [Future] completes.
///
/// The [builder] function will be informed of the current [RefreshIndicatorMode]
/// when invoking it, except in the [RefreshIndicatorMode.inactive] state when
/// no space is available and nothing needs to be built. The [builder] function
/// will otherwise be continuously invoked as the amount of space available
/// changes from overscroll, as the sliver scrolls away after the [onRefresh]
/// task is done, etc.
///
/// Only one refresh can be triggered until the previous refresh has completed
/// and the indicator sliver has retracted at least 90% of the way back.
///
247
/// Can only be used in downward-scrolling vertical lists that overscrolls. In
248 249 250 251 252 253 254 255 256 257 258
/// other words, refreshes can't be triggered with [Scrollable]s using
/// [ClampingScrollPhysics] which is the default on Android. To allow overscroll
/// on Android, use an overscrolling physics such as [BouncingScrollPhysics].
/// This can be done via:
///
///  * Providing a [BouncingScrollPhysics] (possibly in combination with a
///    [AlwaysScrollableScrollPhysics]) while constructing the scrollable.
///  * By inserting a [ScrollConfiguration] with [BouncingScrollPhysics] above
///    the scrollable.
///  * By using [CupertinoApp], which always uses a [ScrollConfiguration]
///    with [BouncingScrollPhysics] regardless of platform.
259
///
260 261 262 263
/// In a typical application, this sliver should be inserted between the app bar
/// sliver such as [CupertinoSliverNavigationBar] and your main scrollable
/// content's sliver.
///
264
/// {@tool dartpad}
265
/// When the user scrolls past [refreshTriggerPullDistance],
266
/// this sample shows the default iOS pull to refresh indicator for 1 second and
267 268
/// adds a new item to the top of the list view.
///
269
/// ** See code in examples/api/lib/cupertino/refresh/cupertino_sliver_refresh_control.0.dart **
270 271
/// {@end-tool}
///
272 273 274 275 276 277 278 279
/// See also:
///
///  * [CustomScrollView], a typical sliver holding scroll view this control
///    should go into.
///  * <https://developer.apple.com/ios/human-interface-guidelines/controls/refresh-content-controls/>
///  * [RefreshIndicator], a Material Design version of the pull-to-refresh
///    paradigm. This widget works differently than [RefreshIndicator] because
///    instead of being an overlay on top of the scrollable, the
280
///    [CupertinoSliverRefreshControl] is part of the scrollable and actively occupies
281
///    scrollable space.
282 283
class CupertinoSliverRefreshControl extends StatefulWidget {
  /// Create a new refresh control for inserting into a list of slivers.
284
  ///
285
  /// The [refreshTriggerPullDistance] and [refreshIndicatorExtent] arguments
286
  /// must be greater than or equal to 0.
287
  ///
288 289 290
  /// The [builder] argument may be null, in which case no indicator UI will be
  /// shown but the [onRefresh] will still be invoked. By default, [builder]
  /// shows a [CupertinoActivityIndicator].
291
  ///
292 293 294
  /// The [onRefresh] argument will be called when pulled far enough to trigger
  /// a refresh.
  const CupertinoSliverRefreshControl({
295
    super.key,
296 297
    this.refreshTriggerPullDistance = _defaultRefreshTriggerPullDistance,
    this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent,
298
    this.builder = buildRefreshIndicator,
299
    this.onRefresh,
300
  }) : assert(refreshTriggerPullDistance > 0.0),
301 302 303 304
       assert(refreshIndicatorExtent >= 0.0),
       assert(
         refreshTriggerPullDistance >= refreshIndicatorExtent,
         'The refresh indicator cannot take more space in its final state '
305
         'than the amount initially created by overscrolling.',
306
       );
307 308 309

  /// The amount of overscroll the scrollable must be dragged to trigger a reload.
  ///
310 311
  /// Must be larger than zero and larger than [refreshIndicatorExtent].
  /// Defaults to 100 pixels when not specified.
312 313 314 315 316 317 318 319
  ///
  /// When overscrolled past this distance, [onRefresh] will be called if not
  /// null and the [builder] will build in the [RefreshIndicatorMode.armed] state.
  final double refreshTriggerPullDistance;

  /// The amount of space the refresh indicator sliver will keep holding while
  /// [onRefresh]'s [Future] is still running.
  ///
320 321 322
  /// Must be a positive number, but can be zero, in which case the sliver will
  /// start retracting back to zero as soon as the refresh is started. Defaults
  /// to 60 pixels when not specified.
323 324 325 326 327 328 329 330 331 332 333 334 335
  ///
  /// Must be smaller than [refreshTriggerPullDistance], since the sliver
  /// shouldn't grow further after triggering the refresh.
  final double refreshIndicatorExtent;

  /// A builder that's called as this sliver's size changes, and as the state
  /// changes.
  ///
  /// Can be set to null, in which case nothing will be drawn in the overscrolled
  /// space.
  ///
  /// Will not be called when the available space is zero such as before any
  /// overscroll.
336
  final RefreshControlIndicatorBuilder? builder;
337 338 339 340 341 342 343 344 345

  /// Callback invoked when pulled by [refreshTriggerPullDistance].
  ///
  /// If provided, must return a [Future] which will keep the indicator in the
  /// [RefreshIndicatorMode.refresh] state until the [Future] completes.
  ///
  /// Can be null, in which case a single frame of [RefreshIndicatorMode.armed]
  /// state will be drawn before going immediately to the [RefreshIndicatorMode.done]
  /// where the sliver will start retracting.
346
  final RefreshCallback? onRefresh;
347

348 349
  static const double _defaultRefreshTriggerPullDistance = 100.0;
  static const double _defaultRefreshIndicatorExtent = 60.0;
350

351
  /// Retrieve the current state of the CupertinoSliverRefreshControl. The same as the
352 353 354
  /// state that gets passed into the [builder] function. Used for testing.
  @visibleForTesting
  static RefreshIndicatorMode state(BuildContext context) {
355
    final _CupertinoSliverRefreshControlState state = context.findAncestorStateOfType<_CupertinoSliverRefreshControlState>()!;
356 357 358
    return state.refreshState;
  }

359 360 361 362 363 364 365 366
  /// Builds a refresh indicator that reflects the standard iOS pull-to-refresh
  /// behavior. 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.
  /// Once the refresh has completed, the activity indicator shrinks away as the
  /// space allocation animates back to closed.
  static Widget buildRefreshIndicator(
367
    BuildContext context,
368 369 370 371 372
    RefreshIndicatorMode refreshState,
    double pulledExtent,
    double refreshTriggerPullDistance,
    double refreshIndicatorExtent,
  ) {
373
    final double percentageComplete = clampDouble(pulledExtent / refreshTriggerPullDistance, 0.0, 1.0);
374

375 376 377 378 379 380 381
    // Place the indicator at the top of the sliver that opens up. We're using a
    // Stack/Positioned widget because the CupertinoActivityIndicator does some
    // internal translations based on the current size (which grows as the user drags)
    // that makes Padding calculations difficult. Rather than be reliant on the
    // internal implementation of the activity indicator, the Positioned widget allows
    // us to be explicit where the widget gets placed. The indicator should appear
    // over the top of the dragged widget, hence the use of Clip.none.
382 383
    return Center(
      child: Stack(
384
        clipBehavior: Clip.none,
385 386 387 388 389 390 391 392
        children: <Widget>[
          Positioned(
            top: _kActivityIndicatorMargin,
            left: 0.0,
            right: 0.0,
            child: _buildIndicatorForRefreshState(refreshState, _kActivityIndicatorRadius, percentageComplete),
          ),
        ],
393 394 395 396
      ),
    );
  }

397 398 399 400
  static Widget _buildIndicatorForRefreshState(RefreshIndicatorMode refreshState, double radius, double percentageComplete) {
    switch (refreshState) {
      case RefreshIndicatorMode.drag:
        // While we're dragging, we draw individual ticks of the spinner while simultaneously
401
        // easing the opacity in. The opacity curve values here were derived using
402 403 404 405 406 407 408 409 410 411 412 413 414
        // Xcode through inspecting a native app running on iOS 13.5.
        const Curve opacityCurve = Interval(0.0, 0.35, curve: Curves.easeInOut);
        return Opacity(
          opacity: opacityCurve.transform(percentageComplete),
          child: CupertinoActivityIndicator.partiallyRevealed(radius: radius, progress: percentageComplete),
        );
      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 lets go, the standard transition is to shrink the spinner.
        return CupertinoActivityIndicator(radius: radius * percentageComplete);
415
      case RefreshIndicatorMode.inactive:
416
        // Anything else doesn't show anything.
417
        return const SizedBox.shrink();
418 419 420
    }
  }

421
  @override
422
  State<CupertinoSliverRefreshControl> createState() => _CupertinoSliverRefreshControlState();
423 424
}

425
class _CupertinoSliverRefreshControlState extends State<CupertinoSliverRefreshControl> {
426 427
  // Reset the state from done to inactive when only this fraction of the
  // original `refreshTriggerPullDistance` is left.
428
  static const double _inactiveResetOverscrollFraction = 0.1;
429

430
  late RefreshIndicatorMode refreshState;
431
  // [Future] returned by the widget's `onRefresh`.
432
  Future<void>? refreshTask;
433 434 435
  // The amount of space available from the inner indicator box's perspective.
  //
  // The value is the sum of the sliver's layout extent and the overscroll
436
  // (which partially gets transferred into the layout extent when the refresh
437 438
  // triggers).
  //
439
  // The value of latestIndicatorBoxExtent doesn't change when the sliver scrolls
440
  // away without retracting; it is independent from the sliver's scrollOffset.
441
  double latestIndicatorBoxExtent = 0.0;
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
  bool hasSliverLayoutExtent = false;

  @override
  void initState() {
    super.initState();
    refreshState = RefreshIndicatorMode.inactive;
  }

  // A state machine transition calculator. Multiple states can be transitioned
  // through per single call.
  RefreshIndicatorMode transitionNextState() {
    RefreshIndicatorMode nextState;

    void goToDone() {
      nextState = RefreshIndicatorMode.done;
      // Either schedule the RenderSliver to re-layout on the next frame
      // when not currently in a frame or schedule it on the next frame.
459
      if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
460 461
        setState(() => hasSliverLayoutExtent = false);
      } else {
462
        SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
463
          setState(() => hasSliverLayoutExtent = false);
464
        }, debugLabel: 'Refresh.goToDone');
465 466 467 468 469
      }
    }

    switch (refreshState) {
      case RefreshIndicatorMode.inactive:
470
        if (latestIndicatorBoxExtent <= 0) {
471 472 473 474 475 476 477
          return RefreshIndicatorMode.inactive;
        } else {
          nextState = RefreshIndicatorMode.drag;
        }
        continue drag;
      drag:
      case RefreshIndicatorMode.drag:
478
        if (latestIndicatorBoxExtent == 0) {
479
          return RefreshIndicatorMode.inactive;
480
        } else if (latestIndicatorBoxExtent < widget.refreshTriggerPullDistance) {
481 482 483 484 485 486 487
          return RefreshIndicatorMode.drag;
        } else {
          if (widget.onRefresh != null) {
            HapticFeedback.mediumImpact();
            // Call onRefresh after this frame finished since the function is
            // user supplied and we're always here in the middle of the sliver's
            // performLayout.
488
            SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
489
              refreshTask = widget.onRefresh!()..whenComplete(() {
490 491 492 493 494 495 496 497 498 499
                if (mounted) {
                  setState(() => refreshTask = null);
                  // Trigger one more transition because by this time, BoxConstraint's
                  // maxHeight might already be resting at 0 in which case no
                  // calls to [transitionNextState] will occur anymore and the
                  // state may be stuck in a non-inactive state.
                  refreshState = transitionNextState();
                }
              });
              setState(() => hasSliverLayoutExtent = true);
500
            }, debugLabel: 'Refresh.transition');
501 502 503 504 505 506 507 508 509
          }
          return RefreshIndicatorMode.armed;
        }
      case RefreshIndicatorMode.armed:
        if (refreshState == RefreshIndicatorMode.armed && refreshTask == null) {
          goToDone();
          continue done;
        }

510
        if (latestIndicatorBoxExtent > widget.refreshIndicatorExtent) {
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
          return RefreshIndicatorMode.armed;
        } else {
          nextState = RefreshIndicatorMode.refresh;
        }
        continue refresh;
      refresh:
      case RefreshIndicatorMode.refresh:
        if (refreshTask != null) {
          return RefreshIndicatorMode.refresh;
        } else {
          goToDone();
        }
        continue done;
      done:
      case RefreshIndicatorMode.done:
        // Let the transition back to inactive trigger before strictly going
        // to 0.0 since the last bit of the animation can take some time and
        // can feel sluggish if not going all the way back to 0.0 prevented
        // a subsequent pull-to-refresh from starting.
530
        if (latestIndicatorBoxExtent >
531
            widget.refreshTriggerPullDistance * _inactiveResetOverscrollFraction) {
532 533 534 535 536 537 538 539 540 541 542
          return RefreshIndicatorMode.done;
        } else {
          nextState = RefreshIndicatorMode.inactive;
        }
    }

    return nextState;
  }

  @override
  Widget build(BuildContext context) {
543
    return _CupertinoSliverRefresh(
544 545 546 547
      refreshIndicatorLayoutExtent: widget.refreshIndicatorExtent,
      hasLayoutExtent: hasSliverLayoutExtent,
      // A LayoutBuilder lets the sliver's layout changes be fed back out to
      // its owner to trigger state changes.
548
      child: LayoutBuilder(
549
        builder: (BuildContext context, BoxConstraints constraints) {
550
          latestIndicatorBoxExtent = constraints.maxHeight;
551
          refreshState = transitionNextState();
552
          if (widget.builder != null && latestIndicatorBoxExtent > 0) {
553
            return widget.builder!(
554 555
              context,
              refreshState,
556
              latestIndicatorBoxExtent,
557 558 559 560
              widget.refreshTriggerPullDistance,
              widget.refreshIndicatorExtent,
            );
          }
561
          return Container();
562
        },
563
      ),
564 565 566
    );
  }
}