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

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
7
import 'package:flutter/scheduler.dart' show TickerProvider;
Ian Hickson's avatar
Ian Hickson committed
8 9

import 'framework.dart';
10 11
import 'scroll_position.dart';
import 'scrollable.dart';
Ian Hickson's avatar
Ian Hickson committed
12

13
/// Delegate for configuring a [SliverPersistentHeader].
14
abstract class SliverPersistentHeaderDelegate {
Ian Hickson's avatar
Ian Hickson committed
15 16
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
17
  const SliverPersistentHeaderDelegate();
Ian Hickson's avatar
Ian Hickson committed
18

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
  /// The widget to place inside the [SliverPersistentHeader].
  ///
  /// The `context` is the [BuildContext] of the sliver.
  ///
  /// The `shrinkOffset` is a distance from [maxExtent] towards [minExtent]
  /// representing the current amount by which the sliver has been shrunk. When
  /// the `shrinkOffset` is zero, the contents will be rendered with a dimension
  /// of [maxExtent] in the main axis. When `shrinkOffset` equals the difference
  /// between [maxExtent] and [minExtent] (a positive number), the contents will
  /// be rendered with a dimension of [minExtent] in the main axis. The
  /// `shrinkOffset` will always be a positive number in that range.
  ///
  /// The `overlapsContent` argument is true if subsequent slivers (if any) will
  /// be rendered beneath this one, and false if the sliver will not have any
  /// contents below it. Typically this is used to decide whether to draw a
  /// shadow to simulate the sliver being above the contents below it. Typically
  /// this is true when `shrinkOffset` is at its greatest value and false
  /// otherwise, but that is not guaranteed. See [NestedScrollView] for an
  /// example of a case where `overlapsContent`'s value can be unrelated to
  /// `shrinkOffset`.
39 40
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);

41 42 43 44 45 46 47 48 49
  /// The smallest size to allow the header to reach, when it shrinks at the
  /// start of the viewport.
  ///
  /// This must return a value equal to or less than [maxExtent].
  ///
  /// This value should not change over the lifetime of the delegate. It should
  /// be based entirely on the constructor arguments passed to the delegate. See
  /// [shouldRebuild], which must return true if a new delegate would return a
  /// different value.
50
  double get minExtent;
Ian Hickson's avatar
Ian Hickson committed
51

52 53 54 55 56 57 58 59 60
  /// The size of the header when it is not shrinking at the top of the
  /// viewport.
  ///
  /// This must return a value equal to or greater than [minExtent].
  ///
  /// This value should not change over the lifetime of the delegate. It should
  /// be based entirely on the constructor arguments passed to the delegate. See
  /// [shouldRebuild], which must return true if a new delegate would return a
  /// different value.
Ian Hickson's avatar
Ian Hickson committed
61 62
  double get maxExtent;

63 64 65 66
  /// A [TickerProvider] to use when animating the header's size changes.
  ///
  /// Must not be null if the persistent header is a floating header, and
  /// [snapConfiguration] or [showOnScreenConfiguration] is not null.
67
  TickerProvider? get vsync => null;
68

69 70 71 72
  /// Specifies how floating headers should animate in and out of view.
  ///
  /// If the value of this property is null, then floating headers will
  /// not animate into place.
73 74 75 76 77
  ///
  /// This is only used for floating headers (those with
  /// [SliverPersistentHeader.floating] set to true).
  ///
  /// Defaults to null.
78
  FloatingHeaderSnapConfiguration? get snapConfiguration => null;
79

80 81 82 83 84 85 86 87 88
  /// Specifies an [AsyncCallback] and offset for execution.
  ///
  /// If the value of this property is null, then callback will not be
  /// triggered.
  ///
  /// This is only used for stretching headers (those with
  /// [SliverAppBar.stretch] set to true).
  ///
  /// Defaults to null.
89
  OverScrollHeaderStretchConfiguration? get stretchConfiguration => null;
90

nt4f04uNd's avatar
nt4f04uNd committed
91
  /// Specifies how floating headers and pinned headers should behave in
92 93 94
  /// response to [RenderObject.showOnScreen] calls.
  ///
  /// Defaults to null.
95
  PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => null;
96

97 98 99 100 101 102 103 104 105 106
  /// Whether this delegate is meaningfully different from the old delegate.
  ///
  /// If this returns false, then the header might not be rebuilt, even though
  /// the instance of the delegate changed.
  ///
  /// This must return true if `oldDelegate` and this object would return
  /// different values for [minExtent], [maxExtent], [snapConfiguration], or
  /// would return a meaningfully different widget tree from [build] for the
  /// same arguments.
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
Ian Hickson's avatar
Ian Hickson committed
107 108
}

109 110 111 112 113
/// A sliver whose size varies when the sliver is scrolled to the edge
/// of the viewport opposite the sliver's [GrowthDirection].
///
/// In the normal case of a [CustomScrollView] with no centered sliver, this
/// sliver will vary its size when scrolled to the leading edge of the viewport.
114 115 116
///
/// This is the layout primitive that [SliverAppBar] uses for its
/// shrinking/growing effect.
117
class SliverPersistentHeader extends StatelessWidget {
118 119 120 121
  /// Creates a sliver that varies its size when it is scrolled to the start of
  /// a viewport.
  ///
  /// The [delegate], [pinned], and [floating] arguments must not be null.
122
  const SliverPersistentHeader({
123
    super.key,
124
    required this.delegate,
125 126
    this.pinned = false,
    this.floating = false,
127 128
  }) : assert(delegate != null),
       assert(pinned != null),
129
       assert(floating != null);
Ian Hickson's avatar
Ian Hickson committed
130

131 132 133 134 135 136 137 138 139
  /// Configuration for the sliver's layout.
  ///
  /// The delegate provides the following information:
  ///
  ///  * The minimum and maximum dimensions of the sliver.
  ///
  ///  * The builder for generating the widgets of the sliver.
  ///
  ///  * The instructions for snapping the scroll offset, if [floating] is true.
140
  final SliverPersistentHeaderDelegate delegate;
Ian Hickson's avatar
Ian Hickson committed
141

142 143 144 145 146
  /// Whether to stick the header to the start of the viewport once it has
  /// reached its minimum size.
  ///
  /// If this is false, the header will continue scrolling off the screen after
  /// it has shrunk to its minimum extent.
Ian Hickson's avatar
Ian Hickson committed
147 148
  final bool pinned;

149 150 151 152 153 154 155 156
  /// Whether the header should immediately grow again if the user reverses
  /// scroll direction.
  ///
  /// If this is false, the header only grows again once the user reaches the
  /// part of the viewport that contains the sliver.
  ///
  /// The [delegate]'s [SliverPersistentHeaderDelegate.snapConfiguration] is
  /// ignored unless [floating] is true.
Ian Hickson's avatar
Ian Hickson committed
157 158 159 160
  final bool floating;

  @override
  Widget build(BuildContext context) {
161
    if (floating && pinned) {
162
      return _SliverFloatingPinnedPersistentHeader(delegate: delegate);
163 164
    }
    if (pinned) {
165
      return _SliverPinnedPersistentHeader(delegate: delegate);
166 167
    }
    if (floating) {
168
      return _SliverFloatingPersistentHeader(delegate: delegate);
169
    }
170
    return _SliverScrollingPersistentHeader(delegate: delegate);
Ian Hickson's avatar
Ian Hickson committed
171 172 173
  }

  @override
174 175
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
176 177 178 179
    properties.add(
      DiagnosticsProperty<SliverPersistentHeaderDelegate>(
        'delegate',
        delegate,
180
      ),
181
    );
182 183 184 185
    final List<String> flags = <String>[
      if (pinned) 'pinned',
      if (floating) 'floating',
    ];
186
    if (flags.isEmpty) {
Ian Hickson's avatar
Ian Hickson committed
187
      flags.add('normal');
188
    }
189
    properties.add(IterableProperty<String>('mode', flags));
Ian Hickson's avatar
Ian Hickson committed
190 191 192
  }
}

193
class _FloatingHeader extends StatefulWidget {
194
  const _FloatingHeader({ required this.child });
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211

  final Widget child;

  @override
  _FloatingHeaderState createState() => _FloatingHeaderState();
}

// A wrapper for the widget created by _SliverPersistentHeaderElement that
// starts and stops the floating app bar's snap-into-view or snap-out-of-view
// animation. It also informs the float when pointer scrolling by updating the
// last known ScrollDirection when scrolling began.
class _FloatingHeaderState extends State<_FloatingHeader> {
  ScrollPosition? _position;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
212
    if (_position != null) {
213
      _position!.isScrollingNotifier.removeListener(_isScrollingListener);
214
    }
215
    _position = Scrollable.of(context)?.position;
216
    if (_position != null) {
217
      _position!.isScrollingNotifier.addListener(_isScrollingListener);
218
    }
219 220 221 222
  }

  @override
  void dispose() {
223
    if (_position != null) {
224
      _position!.isScrollingNotifier.removeListener(_isScrollingListener);
225
    }
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
    super.dispose();
  }

  RenderSliverFloatingPersistentHeader? _headerRenderer() {
    return context.findAncestorRenderObjectOfType<RenderSliverFloatingPersistentHeader>();
  }

  void _isScrollingListener() {
    assert(_position != null);

    // When a scroll stops, then maybe snap the app bar into view.
    // Similarly, when a scroll starts, then maybe stop the snap animation.
    // Update the scrolling direction as well for pointer scrolling updates.
    final RenderSliverFloatingPersistentHeader? header = _headerRenderer();
    if (_position!.isScrollingNotifier.value) {
      header?.updateScrollStartDirection(_position!.userScrollDirection);
      // Only SliverAppBars support snapping, headers will not snap.
      header?.maybeStopSnapAnimation(_position!.userScrollDirection);
    } else {
      // Only SliverAppBars support snapping, headers will not snap.
      header?.maybeStartSnapAnimation(_position!.userScrollDirection);
    }
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

254
class _SliverPersistentHeaderElement extends RenderObjectElement {
255
  _SliverPersistentHeaderElement(
256
    _SliverPersistentHeaderRenderObjectWidget super.widget, {
257
    this.floating = false,
258
  }) : assert(floating != null);
259 260

  final bool floating;
Ian Hickson's avatar
Ian Hickson committed
261 262

  @override
263
  _RenderSliverPersistentHeaderForWidgetsMixin get renderObject => super.renderObject as _RenderSliverPersistentHeaderForWidgetsMixin;
Ian Hickson's avatar
Ian Hickson committed
264 265

  @override
266
  void mount(Element? parent, Object? newSlot) {
Ian Hickson's avatar
Ian Hickson committed
267 268 269 270 271 272
    super.mount(parent, newSlot);
    renderObject._element = this;
  }

  @override
  void unmount() {
273
    renderObject._element = null;
274
    super.unmount();
Ian Hickson's avatar
Ian Hickson committed
275 276 277
  }

  @override
278
  void update(_SliverPersistentHeaderRenderObjectWidget newWidget) {
279
    final _SliverPersistentHeaderRenderObjectWidget oldWidget = widget as _SliverPersistentHeaderRenderObjectWidget;
Ian Hickson's avatar
Ian Hickson committed
280
    super.update(newWidget);
281 282
    final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate;
    final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate;
Ian Hickson's avatar
Ian Hickson committed
283
    if (newDelegate != oldDelegate &&
284
        (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) {
Ian Hickson's avatar
Ian Hickson committed
285
      renderObject.triggerRebuild();
286
    }
Ian Hickson's avatar
Ian Hickson committed
287 288 289 290
  }

  @override
  void performRebuild() {
291
    super.performRebuild();
Ian Hickson's avatar
Ian Hickson committed
292 293 294
    renderObject.triggerRebuild();
  }

295
  Element? child;
Ian Hickson's avatar
Ian Hickson committed
296

297
  void _build(double shrinkOffset, bool overlapsContent) {
298
    owner!.buildScope(this, () {
299
      final _SliverPersistentHeaderRenderObjectWidget sliverPersistentHeaderRenderObjectWidget = widget as _SliverPersistentHeaderRenderObjectWidget;
300 301
      child = updateChild(
        child,
302
        floating
303
          ? _FloatingHeader(child: sliverPersistentHeaderRenderObjectWidget.delegate.build(
304 305 306 307
            this,
            shrinkOffset,
            overlapsContent
          ))
308
          : sliverPersistentHeaderRenderObjectWidget.delegate.build(this, shrinkOffset, overlapsContent),
309 310
        null,
      );
Ian Hickson's avatar
Ian Hickson committed
311 312 313 314 315 316 317
    });
  }

  @override
  void forgetChild(Element child) {
    assert(child == this.child);
    this.child = null;
318
    super.forgetChild(child);
Ian Hickson's avatar
Ian Hickson committed
319 320 321
  }

  @override
322
  void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
323
    assert(renderObject.debugValidateChild(child));
Ian Hickson's avatar
Ian Hickson committed
324 325 326 327
    renderObject.child = child;
  }

  @override
328
  void moveRenderObjectChild(covariant RenderObject child, Object? oldSlot, Object? newSlot) {
Ian Hickson's avatar
Ian Hickson committed
329 330 331 332
    assert(false);
  }

  @override
333
  void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
Ian Hickson's avatar
Ian Hickson committed
334 335 336 337 338
    renderObject.child = null;
  }

  @override
  void visitChildren(ElementVisitor visitor) {
339
    if (child != null) {
340
      visitor(child!);
341
    }
Ian Hickson's avatar
Ian Hickson committed
342 343 344
  }
}

345
abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWidget {
346
  const _SliverPersistentHeaderRenderObjectWidget({
347
    required this.delegate,
348
    this.floating = false,
349
  }) : assert(delegate != null),
350
       assert(floating != null);
Ian Hickson's avatar
Ian Hickson committed
351

352
  final SliverPersistentHeaderDelegate delegate;
353
  final bool floating;
Ian Hickson's avatar
Ian Hickson committed
354 355

  @override
356
  _SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this, floating: floating);
Ian Hickson's avatar
Ian Hickson committed
357 358

  @override
359
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context);
Ian Hickson's avatar
Ian Hickson committed
360 361

  @override
362
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
363
    super.debugFillProperties(description);
364 365 366 367
    description.add(
      DiagnosticsProperty<SliverPersistentHeaderDelegate>(
        'delegate',
        delegate,
368
      ),
369
    );
Ian Hickson's avatar
Ian Hickson committed
370 371 372
  }
}

373
mixin _RenderSliverPersistentHeaderForWidgetsMixin on RenderSliverPersistentHeader {
374
  _SliverPersistentHeaderElement? _element;
Ian Hickson's avatar
Ian Hickson committed
375

376
  @override
377
  double get minExtent => (_element!.widget as _SliverPersistentHeaderRenderObjectWidget).delegate.minExtent;
378

Ian Hickson's avatar
Ian Hickson committed
379
  @override
380
  double get maxExtent => (_element!.widget as _SliverPersistentHeaderRenderObjectWidget).delegate.maxExtent;
Ian Hickson's avatar
Ian Hickson committed
381 382

  @override
383
  void updateChild(double shrinkOffset, bool overlapsContent) {
Ian Hickson's avatar
Ian Hickson committed
384
    assert(_element != null);
385
    _element!._build(shrinkOffset, overlapsContent);
Ian Hickson's avatar
Ian Hickson committed
386 387 388 389
  }

  @protected
  void triggerRebuild() {
390
    markNeedsLayout();
Ian Hickson's avatar
Ian Hickson committed
391 392 393
  }
}

394
class _SliverScrollingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
395
  const _SliverScrollingPersistentHeader({
396 397
    required super.delegate,
  });
Ian Hickson's avatar
Ian Hickson committed
398 399

  @override
400
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
401
    return _RenderSliverScrollingPersistentHeaderForWidgets(
402
      stretchConfiguration: delegate.stretchConfiguration,
403
    );
Ian Hickson's avatar
Ian Hickson committed
404 405 406
  }
}

407
class _RenderSliverScrollingPersistentHeaderForWidgets extends RenderSliverScrollingPersistentHeader
408 409
  with _RenderSliverPersistentHeaderForWidgetsMixin {
  _RenderSliverScrollingPersistentHeaderForWidgets({
410 411
    super.stretchConfiguration,
  });
412
}
Ian Hickson's avatar
Ian Hickson committed
413

414
class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
415
  const _SliverPinnedPersistentHeader({
416 417
    required super.delegate,
  });
Ian Hickson's avatar
Ian Hickson committed
418 419

  @override
420
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
421
    return _RenderSliverPinnedPersistentHeaderForWidgets(
422 423
      stretchConfiguration: delegate.stretchConfiguration,
      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
424
    );
Ian Hickson's avatar
Ian Hickson committed
425 426 427
  }
}

428 429 430
class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader
  with _RenderSliverPersistentHeaderForWidgetsMixin {
  _RenderSliverPinnedPersistentHeaderForWidgets({
431 432 433
    super.stretchConfiguration,
    super.showOnScreenConfiguration,
  });
434
}
Ian Hickson's avatar
Ian Hickson committed
435

436
class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
437
  const _SliverFloatingPersistentHeader({
438
    required super.delegate,
439
  }) : super(
440
    floating: true,
441
  );
Ian Hickson's avatar
Ian Hickson committed
442 443

  @override
444
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
445
    return _RenderSliverFloatingPersistentHeaderForWidgets(
446
      vsync: delegate.vsync,
447 448
      snapConfiguration: delegate.snapConfiguration,
      stretchConfiguration: delegate.stretchConfiguration,
449
      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
450
    );
451 452 453 454
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) {
455
    renderObject.vsync = delegate.vsync;
456
    renderObject.snapConfiguration = delegate.snapConfiguration;
457
    renderObject.stretchConfiguration = delegate.stretchConfiguration;
458
    renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
Ian Hickson's avatar
Ian Hickson committed
459 460 461
  }
}

462 463
class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader
  with _RenderSliverPersistentHeaderForWidgetsMixin {
464
  _RenderSliverFloatingPinnedPersistentHeaderForWidgets({
465 466 467 468 469
    required super.vsync,
    super.snapConfiguration,
    super.stretchConfiguration,
    super.showOnScreenConfiguration,
  });
470
}
471 472

class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
473
  const _SliverFloatingPinnedPersistentHeader({
474
    required super.delegate,
475
  }) : super(
476
    floating: true,
477
  );
478 479 480

  @override
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
481
    return _RenderSliverFloatingPinnedPersistentHeaderForWidgets(
482
      vsync: delegate.vsync,
483 484
      snapConfiguration: delegate.snapConfiguration,
      stretchConfiguration: delegate.stretchConfiguration,
485
      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
486
    );
487 488 489 490
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) {
491
    renderObject.vsync = delegate.vsync;
492
    renderObject.snapConfiguration = delegate.snapConfiguration;
493
    renderObject.stretchConfiguration = delegate.stretchConfiguration;
494
    renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
495 496 497
  }
}

498 499
class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader
  with _RenderSliverPersistentHeaderForWidgetsMixin {
500
  _RenderSliverFloatingPersistentHeaderForWidgets({
501 502 503 504 505
    required super.vsync,
    super.snapConfiguration,
    super.stretchConfiguration,
    super.showOnScreenConfiguration,
  });
506
}