blob: 9638cfcbc57d2855c1322b300fb2c871420d2547 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'pinned_header_sliver.dart';
/// @docImport 'scroll_view.dart';
/// @docImport 'sliver_floating_header.dart';
/// @docImport 'sliver_persistent_header.dart';
library;
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'slotted_render_object_widget.dart';
/// A sliver that is pinned to the start of its [CustomScrollView] and
/// reacts to scrolling by resizing between the intrinsic sizes of its
/// min and max extent prototypes.
///
/// The minimum and maximum sizes of this sliver are defined by [minExtentPrototype]
/// and [maxExtentPrototype], a pair of widgets that are laid out once. You can
/// use [SizedBox] widgets to define the size limits.
///
/// If the [minExtentPrototype] is null, then the default minimum extent is 0. If
/// [maxExtentPrototype] is null then the default maximum extent is based on the child's
/// intrinsic size.
///
/// This sliver is preferable to the general purpose [SliverPersistentHeader]
/// for its relatively narrow use case because there's no need to create a
/// [SliverPersistentHeaderDelegate] or to predict the header's minimum or
/// maximum size.
///
/// {@tool dartpad}
/// This sample shows how this sliver's two extent prototype properties can be used to
/// create a resizing header whose minimum and maximum sizes match small and large
/// configurations of the same header widget.
///
/// ** See code in examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [PinnedHeaderSliver] - which just pins the header at the top
/// of the [CustomScrollView].
/// * [SliverFloatingHeader] - which animates the header in and out of view
/// in response to downward and upwards scrolls.
/// * [SliverPersistentHeader] - a general purpose header that can be
/// configured as a pinned, resizing, or floating header.
class SliverResizingHeader extends StatelessWidget {
/// Create a pinned header sliver that reacts to scrolling by resizing between
/// the intrinsic sizes of the min and max extent prototypes.
const SliverResizingHeader({
super.key,
this.minExtentPrototype,
this.maxExtentPrototype,
this.child,
});
/// Laid out once to define the minimum size of this sliver along the
/// [CustomScrollView.scrollDirection] axis.
///
/// If null, the minimum size of the sliver is 0.
///
/// This widget is never made visible.
final Widget? minExtentPrototype;
/// Laid out once to define the maximum size of this sliver along the
/// [CustomScrollView.scrollDirection] axis.
///
/// If null, the maximum extent of the sliver is based on the child's
/// intrinsic size.
///
/// This widget is never made visible.
final Widget? maxExtentPrototype;
/// The widget contained by this sliver.
final Widget? child;
Widget? _excludeFocus(Widget? extentPrototype) {
return extentPrototype != null ? ExcludeFocus(child: extentPrototype) : null;
}
@override
Widget build(BuildContext context) {
return _SliverResizingHeader(
minExtentPrototype: _excludeFocus(minExtentPrototype),
maxExtentPrototype: _excludeFocus(maxExtentPrototype),
child: child ?? const SizedBox.shrink(),
);
}
}
enum _Slot {
minExtent,
maxExtent,
child,
}
class _SliverResizingHeader extends SlottedMultiChildRenderObjectWidget<_Slot, RenderBox> {
const _SliverResizingHeader({
this.minExtentPrototype,
this.maxExtentPrototype,
required this.child,
});
final Widget? minExtentPrototype;
final Widget? maxExtentPrototype;
final Widget child;
@override
Iterable<_Slot> get slots => _Slot.values;
@override
Widget? childForSlot(_Slot slot) {
return switch (slot) {
_Slot.minExtent => minExtentPrototype,
_Slot.maxExtent => maxExtentPrototype,
_Slot.child => child,
};
}
@override
_RenderSliverResizingHeader createRenderObject(BuildContext context) {
return _RenderSliverResizingHeader();
}
}
class _RenderSliverResizingHeader extends RenderSliver with SlottedContainerRenderObjectMixin<_Slot, RenderBox>, RenderSliverHelpers {
RenderBox? get minExtentPrototype => childForSlot(_Slot.minExtent);
RenderBox? get maxExtentPrototype => childForSlot(_Slot.maxExtent);
RenderBox? get child => childForSlot(_Slot.child);
@override
Iterable<RenderBox> get children => <RenderBox>[
if (minExtentPrototype != null) minExtentPrototype!,
if (maxExtentPrototype != null) maxExtentPrototype!,
if (child != null) child!,
];
double boxExtent(RenderBox box) {
assert(box.hasSize);
return switch (constraints.axis) {
Axis.vertical => box.size.height,
Axis.horizontal => box.size.width,
};
}
double get childExtent => child == null ? 0 : boxExtent(child!);
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalParentData) {
child.parentData = SliverPhysicalParentData();
}
}
@protected
void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) {
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
final AxisDirection direction = applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection);
childParentData.paintOffset = switch (direction) {
AxisDirection.up => Offset(0.0, -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset))),
AxisDirection.right => Offset(-constraints.scrollOffset, 0.0),
AxisDirection.down => Offset(0.0, -constraints.scrollOffset),
AxisDirection.left => Offset(-(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)), 0.0),
};
}
@override
double childMainAxisPosition(covariant RenderObject child) => 0;
@override
void performLayout() {
final SliverConstraints constraints = this.constraints;
final BoxConstraints prototypeBoxConstraints = constraints.asBoxConstraints();
double minExtent = 0;
if (minExtentPrototype != null) {
minExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true);
minExtent = boxExtent(minExtentPrototype!);
}
late final double maxExtent;
if (maxExtentPrototype != null) {
maxExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true);
maxExtent = boxExtent(maxExtentPrototype!);
} else {
final Size childSize = child!.getDryLayout(prototypeBoxConstraints);
maxExtent = switch (constraints.axis) {
Axis.vertical => childSize.height,
Axis.horizontal => childSize.width,
};
}
final double scrollOffset = constraints.scrollOffset;
final double shrinkOffset = math.min(scrollOffset, maxExtent);
final BoxConstraints boxConstraints = constraints.asBoxConstraints(
minExtent: minExtent,
maxExtent: math.max(minExtent, maxExtent - shrinkOffset),
);
child?.layout(boxConstraints, parentUsesSize: true);
final double remainingPaintExtent = constraints.remainingPaintExtent;
final double layoutExtent = math.min(childExtent, maxExtent - scrollOffset);
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: constraints.overlap,
paintExtent: math.min(childExtent, remainingPaintExtent),
layoutExtent: clampDouble(layoutExtent, 0, remainingPaintExtent),
maxPaintExtent: childExtent,
maxScrollObstructionExtent: childExtent,
cacheExtent: calculateCacheOffset(constraints, from: 0.0, to: childExtent),
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
childParentData.applyPaintTransform(transform);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
context.paintChild(child!, offset + childParentData.paintOffset);
}
}
@override
bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
assert(geometry!.hitTestExtent > 0.0);
if (child != null) {
return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
}
return false;
}
}