sliver_persistent_header.dart 18.5 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 124
    Key? key,
    required this.delegate,
125 126
    this.pinned = false,
    this.floating = false,
127 128 129 130
  }) : assert(delegate != null),
       assert(pinned != null),
       assert(floating != null),
       super(key: key);
Ian Hickson's avatar
Ian Hickson committed
131

132 133 134 135 136 137 138 139 140
  /// 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.
141
  final SliverPersistentHeaderDelegate delegate;
Ian Hickson's avatar
Ian Hickson committed
142

143 144 145 146 147
  /// 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
148 149
  final bool pinned;

150 151 152 153 154 155 156 157
  /// 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
158 159 160 161
  final bool floating;

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

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

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
class _FloatingHeader extends StatefulWidget {
  const _FloatingHeader({ Key? key, required this.child }) : super(key: key);

  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();
    if (_position != null)
      _position!.isScrollingNotifier.removeListener(_isScrollingListener);
    _position = Scrollable.of(context)?.position;
    if (_position != null)
      _position!.isScrollingNotifier.addListener(_isScrollingListener);
  }

  @override
  void dispose() {
    if (_position != null)
      _position!.isScrollingNotifier.removeListener(_isScrollingListener);
    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;
}

248
class _SliverPersistentHeaderElement extends RenderObjectElement {
249 250 251 252 253 254 255
  _SliverPersistentHeaderElement(
    _SliverPersistentHeaderRenderObjectWidget widget, {
    this.floating = false,
  }) : assert(floating != null),
       super(widget);

  final bool floating;
Ian Hickson's avatar
Ian Hickson committed
256 257

  @override
258
  _SliverPersistentHeaderRenderObjectWidget get widget => super.widget as _SliverPersistentHeaderRenderObjectWidget;
Ian Hickson's avatar
Ian Hickson committed
259 260

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

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

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

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

  @override
  void performRebuild() {
288
    super.performRebuild();
Ian Hickson's avatar
Ian Hickson committed
289 290 291
    renderObject.triggerRebuild();
  }

292
  Element? child;
Ian Hickson's avatar
Ian Hickson committed
293

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

  @override
  void forgetChild(Element child) {
    assert(child == this.child);
    this.child = null;
314
    super.forgetChild(child);
Ian Hickson's avatar
Ian Hickson committed
315 316 317
  }

  @override
318
  void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
319
    assert(renderObject.debugValidateChild(child));
Ian Hickson's avatar
Ian Hickson committed
320 321 322 323
    renderObject.child = child;
  }

  @override
324
  void moveRenderObjectChild(covariant RenderObject child, Object? oldSlot, Object? newSlot) {
Ian Hickson's avatar
Ian Hickson committed
325 326 327 328
    assert(false);
  }

  @override
329
  void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
Ian Hickson's avatar
Ian Hickson committed
330 331 332 333 334
    renderObject.child = null;
  }

  @override
  void visitChildren(ElementVisitor visitor) {
335
    if (child != null)
336
      visitor(child!);
Ian Hickson's avatar
Ian Hickson committed
337 338 339
  }
}

340
abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWidget {
341
  const _SliverPersistentHeaderRenderObjectWidget({
342 343
    Key? key,
    required this.delegate,
344
    this.floating = false,
345
  }) : assert(delegate != null),
346
       assert(floating != null),
347
       super(key: key);
Ian Hickson's avatar
Ian Hickson committed
348

349
  final SliverPersistentHeaderDelegate delegate;
350
  final bool floating;
Ian Hickson's avatar
Ian Hickson committed
351 352

  @override
353
  _SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this, floating: floating);
Ian Hickson's avatar
Ian Hickson committed
354 355

  @override
356
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context);
Ian Hickson's avatar
Ian Hickson committed
357 358

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

370
mixin _RenderSliverPersistentHeaderForWidgetsMixin on RenderSliverPersistentHeader {
371
  _SliverPersistentHeaderElement? _element;
Ian Hickson's avatar
Ian Hickson committed
372

373
  @override
374
  double get minExtent => _element!.widget.delegate.minExtent;
375

Ian Hickson's avatar
Ian Hickson committed
376
  @override
377
  double get maxExtent => _element!.widget.delegate.maxExtent;
Ian Hickson's avatar
Ian Hickson committed
378 379

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

  @protected
  void triggerRebuild() {
387
    markNeedsLayout();
Ian Hickson's avatar
Ian Hickson committed
388 389 390
  }
}

391
class _SliverScrollingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
392
  const _SliverScrollingPersistentHeader({
393 394
    Key? key,
    required SliverPersistentHeaderDelegate delegate,
395 396 397 398
  }) : super(
    key: key,
    delegate: delegate,
  );
Ian Hickson's avatar
Ian Hickson committed
399 400

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

408
class _RenderSliverScrollingPersistentHeaderForWidgets extends RenderSliverScrollingPersistentHeader
409 410
  with _RenderSliverPersistentHeaderForWidgetsMixin {
  _RenderSliverScrollingPersistentHeaderForWidgets({
411 412
    RenderBox? child,
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
413 414 415 416 417
  }) : super(
    child: child,
    stretchConfiguration: stretchConfiguration,
  );
}
Ian Hickson's avatar
Ian Hickson committed
418

419
class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
420
  const _SliverPinnedPersistentHeader({
421 422
    Key? key,
    required SliverPersistentHeaderDelegate delegate,
423 424 425 426
  }) : super(
    key: key,
    delegate: delegate,
  );
Ian Hickson's avatar
Ian Hickson committed
427 428

  @override
429
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
430
    return _RenderSliverPinnedPersistentHeaderForWidgets(
431 432
      stretchConfiguration: delegate.stretchConfiguration,
      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
433
    );
Ian Hickson's avatar
Ian Hickson committed
434 435 436
  }
}

437 438 439
class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader
  with _RenderSliverPersistentHeaderForWidgetsMixin {
  _RenderSliverPinnedPersistentHeaderForWidgets({
440 441 442
    RenderBox? child,
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
    PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration,
443 444 445
  }) : super(
    child: child,
    stretchConfiguration: stretchConfiguration,
446
    showOnScreenConfiguration: showOnScreenConfiguration,
447 448
  );
}
Ian Hickson's avatar
Ian Hickson committed
449

450
class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
451
  const _SliverFloatingPersistentHeader({
452 453
    Key? key,
    required SliverPersistentHeaderDelegate delegate,
454 455 456
  }) : super(
    key: key,
    delegate: delegate,
457
    floating: true,
458
  );
Ian Hickson's avatar
Ian Hickson committed
459 460

  @override
461
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
462
    return _RenderSliverFloatingPersistentHeaderForWidgets(
463
      vsync: delegate.vsync,
464 465
      snapConfiguration: delegate.snapConfiguration,
      stretchConfiguration: delegate.stretchConfiguration,
466
      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
467
    );
468 469 470 471
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) {
472
    renderObject.vsync = delegate.vsync;
473
    renderObject.snapConfiguration = delegate.snapConfiguration;
474
    renderObject.stretchConfiguration = delegate.stretchConfiguration;
475
    renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
Ian Hickson's avatar
Ian Hickson committed
476 477 478
  }
}

479 480
class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader
  with _RenderSliverPersistentHeaderForWidgetsMixin {
481
  _RenderSliverFloatingPinnedPersistentHeaderForWidgets({
482 483 484 485 486
    RenderBox? child,
    required TickerProvider? vsync,
    FloatingHeaderSnapConfiguration? snapConfiguration,
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
    PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration,
487 488
  }) : super(
    child: child,
489
    vsync: vsync,
490 491
    snapConfiguration: snapConfiguration,
    stretchConfiguration: stretchConfiguration,
492
    showOnScreenConfiguration: showOnScreenConfiguration,
493
  );
494
}
495 496

class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget {
497
  const _SliverFloatingPinnedPersistentHeader({
498 499
    Key? key,
    required SliverPersistentHeaderDelegate delegate,
500 501 502
  }) : super(
    key: key,
    delegate: delegate,
503
    floating: true,
504
  );
505 506 507

  @override
  _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) {
508
    return _RenderSliverFloatingPinnedPersistentHeaderForWidgets(
509
      vsync: delegate.vsync,
510 511
      snapConfiguration: delegate.snapConfiguration,
      stretchConfiguration: delegate.stretchConfiguration,
512
      showOnScreenConfiguration: delegate.showOnScreenConfiguration,
513
    );
514 515 516 517
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) {
518
    renderObject.vsync = delegate.vsync;
519
    renderObject.snapConfiguration = delegate.snapConfiguration;
520
    renderObject.stretchConfiguration = delegate.stretchConfiguration;
521
    renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration;
522 523 524
  }
}

525 526
class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader
  with _RenderSliverPersistentHeaderForWidgetsMixin {
527
  _RenderSliverFloatingPersistentHeaderForWidgets({
528 529 530 531 532
    RenderBox? child,
    required TickerProvider? vsync,
    FloatingHeaderSnapConfiguration? snapConfiguration,
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
    PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration,
533 534
  }) : super(
    child: child,
535
    vsync: vsync,
536 537
    snapConfiguration: snapConfiguration,
    stretchConfiguration: stretchConfiguration,
538
    showOnScreenConfiguration: showOnScreenConfiguration,
539
  );
540
}