| 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 'notification_listener.dart'; |
| 6 | /// @docImport 'scroll_view.dart'; |
| 7 | /// @docImport 'sliver_floating_header.dart'; |
| 8 | /// @docImport 'sliver_persistent_header.dart'; |
| 9 | /// @docImport 'sliver_resizing_header.dart'; |
| 10 | library; |
| 11 | |
| 12 | import 'dart:math' as math; |
| 13 | |
| 14 | import 'package:flutter/foundation.dart'; |
| 15 | import 'package:flutter/rendering.dart'; |
| 16 | |
| 17 | import 'framework.dart'; |
| 18 | |
| 19 | /// A sliver that keeps its Widget child at the top of the a [CustomScrollView]. |
| 20 | /// |
| 21 | /// This sliver is preferable to the general purpose [SliverPersistentHeader] |
| 22 | /// for its relatively narrow use case because there's no need to create a |
| 23 | /// [SliverPersistentHeaderDelegate] or to predict the header's size. |
| 24 | /// |
| 25 | /// {@tool dartpad} |
| 26 | /// This example demonstrates that the sliver's size can change. Pressing the |
| 27 | /// floating action button replaces the one line of header text with two lines. |
| 28 | /// |
| 29 | /// ** See code in examples/api/lib/widgets/sliver/pinned_header_sliver.0.dart ** |
| 30 | /// {@end-tool} |
| 31 | /// |
| 32 | /// {@tool dartpad} |
| 33 | /// A more elaborate example which creates an app bar that's similar to the one |
| 34 | /// that appears in the iOS Settings app. In this example the pinned header |
| 35 | /// starts out transparent and the first item in the list serves as the app's |
| 36 | /// "Settings" title. When the title item has been scrolled completely behind |
| 37 | /// the pinned header, the header animates its opacity from 0 to 1 and its |
| 38 | /// (centered) "Settings" title appears. The fact that the header's opacity |
| 39 | /// depends on the height of the title item - which is unknown until the list |
| 40 | /// has been laid out - necessitates monitoring the title item's |
| 41 | /// [SliverGeometry.scrollExtent] and the header's [SliverConstraints.scrollOffset] |
| 42 | /// from a scroll [NotificationListener]. See the source code for more details. |
| 43 | /// |
| 44 | /// ** See code in examples/api/lib/widgets/sliver/pinned_header_sliver.1.dart ** |
| 45 | /// {@end-tool} |
| 46 | /// |
| 47 | /// See also: |
| 48 | /// |
| 49 | /// * [SliverResizingHeader] - which similarly pins the header at the top |
| 50 | /// of the [CustomScrollView] but reacts to scrolling by resizing the header |
| 51 | /// between its minimum and maximum extent limits. |
| 52 | /// * [SliverFloatingHeader] - which animates the header in and out of view |
| 53 | /// in response to downward and upwards scrolls. |
| 54 | /// * [SliverPersistentHeader] - a general purpose header that can be |
| 55 | /// configured as a pinned, resizing, or floating header. |
| 56 | class PinnedHeaderSliver extends SingleChildRenderObjectWidget { |
| 57 | /// Creates a sliver whose [Widget] child appears at the top of a |
| 58 | /// [CustomScrollView]. |
| 59 | const PinnedHeaderSliver({super.key, super.child}); |
| 60 | |
| 61 | @override |
| 62 | RenderObject createRenderObject(BuildContext context) { |
| 63 | return _RenderPinnedHeaderSliver(); |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | class _RenderPinnedHeaderSliver extends RenderSliverSingleBoxAdapter { |
| 68 | _RenderPinnedHeaderSliver(); |
| 69 | |
| 70 | double get childExtent { |
| 71 | if (child == null) { |
| 72 | return 0.0; |
| 73 | } |
| 74 | assert(child!.hasSize); |
| 75 | return switch (constraints.axis) { |
| 76 | Axis.vertical => child!.size.height, |
| 77 | Axis.horizontal => child!.size.width, |
| 78 | }; |
| 79 | } |
| 80 | |
| 81 | @override |
| 82 | double childMainAxisPosition(covariant RenderObject child) => 0; |
| 83 | |
| 84 | @override |
| 85 | void performLayout() { |
| 86 | final SliverConstraints constraints = this.constraints; |
| 87 | child?.layout(constraints.asBoxConstraints(), parentUsesSize: true); |
| 88 | |
| 89 | final double layoutExtent = clampDouble( |
| 90 | childExtent - constraints.scrollOffset, |
| 91 | 0, |
| 92 | constraints.remainingPaintExtent, |
| 93 | ); |
| 94 | final double paintExtent = math.min( |
| 95 | childExtent, |
| 96 | constraints.remainingPaintExtent - constraints.overlap, |
| 97 | ); |
| 98 | geometry = SliverGeometry( |
| 99 | scrollExtent: childExtent, |
| 100 | paintOrigin: constraints.overlap, |
| 101 | paintExtent: paintExtent, |
| 102 | layoutExtent: layoutExtent, |
| 103 | maxPaintExtent: childExtent, |
| 104 | maxScrollObstructionExtent: childExtent, |
| 105 | cacheExtent: calculateCacheOffset(constraints, from: 0.0, to: childExtent), |
| 106 | hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
| 107 | ); |
| 108 | } |
| 109 | } |
| 110 | |