| // 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 'color_scheme.dart'; |
| library; |
| |
| import 'dart:math' as math; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'colors.dart'; |
| import 'ink_well.dart'; |
| import 'material.dart'; |
| import 'theme.dart'; |
| |
| // Examples can assume: |
| // late BuildContext context; |
| |
| /// A Material Design carousel widget. |
| /// |
| /// The [CarouselView] presents a scrollable list of items, each of which can dynamically |
| /// change size based on the chosen layout. |
| /// |
| /// Material Design 3 introduced 4 carousel layouts: |
| /// * Multi-browse: This layout shows at least one large, medium, and small |
| /// carousel item at a time. This layout is supported by [CarouselView.weighted]. |
| /// * Uncontained (default): This layout show items that scroll to the edge of the |
| /// container. This layout is supported by [CarouselView]. |
| /// * Hero: This layout shows at least one large and one small item at a time. |
| /// This layout is supported by [CarouselView.weighted]. |
| /// * Full-screen: This layout shows one edge-to-edge large item at a time and |
| /// scrolls vertically. The full-screen layout can be supported by both |
| /// constructors. |
| /// |
| /// The default constructor implements the uncontained layout model. It shows |
| /// items that scroll to the edge of the container, behaving similarly to a |
| /// [ListView] where all children are a uniform size. [CarouselView.weighted] |
| /// enables dynamic item sizing. Each item is assigned a weight that determines |
| /// the portion of the viewport it occupies. This constructor helps to create |
| /// layouts like multi-browse, and hero. In order to have a full-screen layout, |
| /// if [CarouselView] is used, then set the [itemExtent] to screen size; if |
| /// [CarouselView.weighted] is used, then set the [flexWeights] to only have |
| /// one integer in the array. |
| /// |
| /// {@tool snippet} |
| /// |
| /// This code snippet shows how to get a vertical full-screen carousel by using |
| /// [itemExtent] in [CarouselView]. |
| /// |
| /// ```dart |
| /// Scaffold( |
| /// body: CarouselView( |
| /// scrollDirection: Axis.vertical, |
| /// itemExtent: double.infinity, |
| /// children: List<Widget>.generate(10, (int index) { |
| /// return Center(child: Text('Item $index')); |
| /// }), |
| /// ), |
| /// ), |
| /// ``` |
| /// |
| /// This code snippet below shows how to achieve the same vertical full-screen |
| /// carousel by using [flexWeights] in [CarouselView.weighted]. |
| /// |
| /// ```dart |
| /// Scaffold( |
| /// body: CarouselView.weighted( |
| /// scrollDirection: Axis.vertical, |
| /// flexWeights: const <int>[1], // Or any positive integers as long as the length of the array is 1. |
| /// children: List<Widget>.generate(10, (int index) { |
| /// return Center(child: Text('Item $index')); |
| /// }), |
| /// ), |
| /// ), |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// In [CarouselView.weighted], weights are relative proportions. For example, |
| /// if the layout weights is `[3, 2, 1]`, it means the first visible item occupies |
| /// 3/6 of the viewport; the second visible item occupies 2/6 of the viewport; |
| /// the last visible item occupies 1/6 of the viewport. As the carousel scrolls, |
| /// the size of the latter one gradually changes to the size of the former one. |
| /// As a result, when the first visible item is completely off-screen, the |
| /// following items will follow the same layout as before. Using [CarouselView.weighted] |
| /// helps build the multi-browse, hero, center-aligned hero and full-screen layouts, |
| /// as indicated in [Carousel sepcs](https://m3.material.io/components/carousel/specs). |
| /// |
| /// The [CarouselController] is used to control the |
| /// [CarouselController.initialItem], which determines the first fully expanded |
| /// item when the [CarouselView] or [CarouselView.weighted] is initially displayed. |
| /// This is straightforward for [CarouselView] because each item in the view |
| /// has fixed size. In [CarouselView.weighted], for instance, if the layout |
| /// weights are `[1, 2, 3, 2, 1]` and the initial item is 4 (the fourth item), the |
| /// view will display items 2, 3, 4, 5, and 6 with weights 1, 2, 3, 2 and 1 |
| /// respectively. |
| /// |
| /// The [CarouselView.itemExtent] property must be non-null and defines the base |
| /// size of items. While items typically maintain this size, the first and last |
| /// visible items may be slightly compressed during scrolling. The [shrinkExtent] |
| /// property controls the minimum allowable size for these compressed items. |
| /// |
| /// {@tool dartpad} |
| /// Here is an example to show different carousel layouts that [CarouselView] |
| /// and [CarouselView.weighted] can build. |
| /// |
| /// ** See code in examples/api/lib/material/carousel/carousel.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [CarouselController], which controls the first fully visible item in the |
| /// view. |
| /// * [PageView], which is a scrollable list that works page by page. |
| class CarouselView extends StatefulWidget { |
| /// Creates a Material Design carousel. |
| const CarouselView({ |
| super.key, |
| this.padding, |
| this.backgroundColor, |
| this.elevation, |
| this.shape, |
| this.overlayColor, |
| this.itemSnapping = false, |
| this.shrinkExtent = 0.0, |
| this.controller, |
| this.scrollDirection = Axis.horizontal, |
| this.reverse = false, |
| this.onTap, |
| this.enableSplash = true, |
| required double this.itemExtent, |
| required this.children, |
| }) : consumeMaxWeight = true, |
| flexWeights = null; |
| |
| /// Creates a scrollable list where the size of each child widget is dynamically |
| /// determined by the provided [flexWeights]. |
| /// |
| /// The [flexWeights] parameter is required and defines the relative size |
| /// proportions of each child widget. |
| /// |
| /// While scrolling, the main-axis extent (size) of each visible item changes |
| /// dynamically based on the scrolling progress. The cross-axis extent is determined |
| /// by the parent constraints. As the first visible item scrolls completely |
| /// off-screen, the next item becomes the first visible item, and has the same |
| /// size as the previously first item. The rest of the visible items maintain |
| /// their relative layout. |
| /// |
| /// For example, if the layout weights are `[1, 6, 1]`, the length of [flexWeights] |
| /// indicates three items will be visible at a time. The layout of these items |
| /// would be: |
| /// * First item: Extent is (1 / (1 + 6 + 1)) * viewport extent. |
| /// * Second item: Extent is (6 / (1 + 6 + 1)) * viewport extent. |
| /// * Third item: Extent is (1 / (1 + 6 + 1)) * viewport extent. |
| /// |
| /// Assuming a viewport extent of 800 in the main axis and the first item is |
| /// item 0, there would be three visible items with extents of 100, 600, and 100. |
| /// As item 0 scrolls off-screen, the extent of item 1 smoothly decreases from 600 |
| /// to 100. For instance, if item 0 is 30% off-screen, item 1 should have decreased |
| /// its size to 30% of the difference from 600 to 100; its extent would be |
| /// 600 - 0.3 * (600 - 100). Similarly, item 2's extent would increase from 100 |
| /// to 600, becoming 100 + 0.3 * (600 - 100). |
| /// |
| /// As the initially visible items change size during scrolling, item 3 enters |
| /// the view to fill the remaining space. Its extent starts at a minimum of |
| /// [shrinkExtent] (or 0 if [shrinkExtent] is not provided) and gradually |
| /// increases to match the extent of the last visible item (100 in this example). |
| /// |
| /// When [consumeMaxWeight] is set to `true`, each child can be expanded to occupy |
| /// the maximum weight while scrolling. For example, with [flexWeights] of `[1, 7, 1]`, |
| /// the initial weight of the first item is 1. However, by enabling |
| /// [consumeMaxWeight] and scrolling forward, the first item can expand to occupy |
| /// a weight of 7, leaving a weight of 1 as some empty space before it. This feature |
| /// is particularly useful for achieving [Hero](https://m3.material.io/components/carousel/specs#b33a5579-d648-42a9-b934-98718d65454f) |
| /// and [Center-aligned hero](https://m3.material.io/components/carousel/specs#92c779ce-de8b-4dee-8201-95d3e429204f) |
| /// layouts indicated in the Material Design 3. |
| const CarouselView.weighted({ |
| super.key, |
| this.padding, |
| this.backgroundColor, |
| this.elevation, |
| this.shape, |
| this.overlayColor, |
| this.itemSnapping = false, |
| this.shrinkExtent = 0.0, |
| this.controller, |
| this.scrollDirection = Axis.horizontal, |
| this.reverse = false, |
| this.consumeMaxWeight = true, |
| this.onTap, |
| this.enableSplash = true, |
| required List<int> this.flexWeights, |
| required this.children, |
| }) : itemExtent = null; |
| |
| /// The amount of space to surround each carousel item with. |
| /// |
| /// Defaults to [EdgeInsets.all] of 4 pixels. |
| final EdgeInsets? padding; |
| |
| /// The background color for each carousel item. |
| /// |
| /// Defaults to [ColorScheme.surface]. |
| final Color? backgroundColor; |
| |
| /// The z-coordinate of each carousel item. |
| /// |
| /// Defaults to 0.0. |
| final double? elevation; |
| |
| /// The shape of each carousel item's [Material]. |
| /// |
| /// Defines each item's [Material.shape]. |
| /// |
| /// Defaults to a [RoundedRectangleBorder] with a circular corner radius |
| /// of 28.0. |
| final ShapeBorder? shape; |
| |
| /// The highlight color to indicate the carousel items are in pressed, hovered |
| /// or focused states. |
| /// |
| /// The default values are: |
| /// * [WidgetState.pressed] - [ColorScheme.onSurface] with an opacity of 0.1 |
| /// * [WidgetState.hovered] - [ColorScheme.onSurface] with an opacity of 0.08 |
| /// * [WidgetState.focused] - [ColorScheme.onSurface] with an opacity of 0.1 |
| final WidgetStateProperty<Color?>? overlayColor; |
| |
| /// The minimum allowable extent (size) in the main axis for carousel items |
| /// during scrolling transitions. |
| /// |
| /// As the carousel scrolls, the first visible item is pinned and gradually |
| /// shrinks until it reaches this minimum extent before scrolling off-screen. |
| /// Similarly, the last visible item enters the viewport at this minimum size |
| /// and expands to its full [itemExtent]. |
| /// |
| /// In cases where the remaining viewport space for the last visible item is |
| /// larger than the defined [shrinkExtent], the [shrinkExtent] is dynamically |
| /// adjusted to match this remaining space, ensuring a smooth size transition. |
| /// |
| /// Defaults to 0.0. Setting to 0.0 allows items to shrink/expand completely, |
| /// transitioning between 0.0 and the full item size. In cases where the |
| /// remaining viewport space for the last visible item is larger than the |
| /// defined [shrinkExtent], the [shrinkExtent] is dynamically adjusted to match |
| /// this remaining space, ensuring a smooth size transition. |
| final double shrinkExtent; |
| |
| /// Whether the carousel should keep scrolling to the next/previous items to |
| /// maintain the original layout. |
| /// |
| /// Defaults to false. |
| final bool itemSnapping; |
| |
| /// An object that can be used to control the position to which this scroll |
| /// view is scrolled. |
| final CarouselController? controller; |
| |
| /// The [Axis] along which the scroll view's offset increases with each item. |
| /// |
| /// Defaults to [Axis.horizontal]. |
| final Axis scrollDirection; |
| |
| /// Whether the carousel list scrolls in the reading direction. |
| /// |
| /// For example, if the reading direction is left-to-right and |
| /// [scrollDirection] is [Axis.horizontal], then the carousel scrolls from |
| /// left to right when [reverse] is false and from right to left when |
| /// [reverse] is true. |
| /// |
| /// Similarly, if [scrollDirection] is [Axis.vertical], then the carousel view |
| /// scrolls from top to bottom when [reverse] is false and from bottom to top |
| /// when [reverse] is true. |
| /// |
| /// Defaults to false. |
| final bool reverse; |
| |
| /// Whether the collapsed items are allowed to expand to the max size. |
| /// |
| /// If this is false, the layout of the carousel doesn't change. This is especially |
| /// useful when a weight list in [CarouselView.weighted] has a max item in the |
| /// middle and at least one small item on either side, such as `[1, 7, 1, 1]`. |
| /// In this case, if this is false, the first and the last two items cannot |
| /// expand to the max size. If this is true, there will be some space before |
| /// the first item or after the last item coming so every item has a chance to |
| /// be fully expanded. |
| /// |
| /// Defaults to true. |
| final bool consumeMaxWeight; |
| |
| /// Called when one of the [children] is tapped. |
| final ValueChanged<int>? onTap; |
| |
| /// Determines whether an [InkWell] will cover each Carousel item. |
| /// |
| /// If true, tapping an item will create an ink splash |
| /// as defined by the [ThemeData.splashFactory]. |
| /// |
| /// Setting this to false allows the [children] to respond to user gestures. |
| /// |
| /// Defaults to true. |
| final bool enableSplash; |
| |
| /// The extent the children are forced to have in the main axis. |
| /// |
| /// The item extent should not exceed the available space that the carousel view |
| /// occupies to ensure at least one item is fully visible. |
| /// |
| /// This is required for [CarouselView]. In [CarouselView.weighted], this is null. |
| final double? itemExtent; |
| |
| /// The weights that each visible child should occupy in the viewport. |
| /// |
| /// The length of [flexWeights] represents how many items should be visible |
| /// at a time in the viewport. For example, setting [flexWeights] to |
| /// `<int>[3, 2, 1]` means there are 3 carousel items and their extents are |
| /// 3/6, 2/6 and 1/6 of the viewport extent. |
| /// |
| /// This is a required property in [CarouselView.weighted]. This is null |
| /// for default [CarouselView]. The integers must be greater than 0. |
| final List<int>? flexWeights; |
| |
| /// The child widgets for the carousel. |
| final List<Widget> children; |
| |
| @override |
| State<CarouselView> createState() => _CarouselViewState(); |
| } |
| |
| class _CarouselViewState extends State<CarouselView> { |
| double? _itemExtent; |
| List<int>? get _flexWeights => widget.flexWeights; |
| bool get _consumeMaxWeight => widget.consumeMaxWeight; |
| CarouselController? _internalController; |
| CarouselController get _controller => widget.controller ?? _internalController!; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _itemExtent = widget.itemExtent; |
| if (widget.controller == null) { |
| _internalController = CarouselController(); |
| } |
| _controller._attach(this); |
| } |
| |
| @override |
| void didUpdateWidget(covariant CarouselView oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.controller != oldWidget.controller) { |
| oldWidget.controller?._detach(this); |
| if (widget.controller != null) { |
| _internalController?._detach(this); |
| _internalController = null; |
| widget.controller?._attach(this); |
| } else { // widget.controller == null && oldWidget.controller != null |
| assert(_internalController == null); |
| _internalController = CarouselController(); |
| _controller._attach(this); |
| } |
| } |
| if (widget.flexWeights != oldWidget.flexWeights) { |
| (_controller.position as _CarouselPosition).flexWeights = _flexWeights; |
| } |
| if (widget.itemExtent != oldWidget.itemExtent) { |
| _itemExtent = widget.itemExtent; |
| (_controller.position as _CarouselPosition).itemExtent = _itemExtent; |
| } |
| if (widget.consumeMaxWeight != oldWidget.consumeMaxWeight) { |
| (_controller.position as _CarouselPosition).consumeMaxWeight = _consumeMaxWeight; |
| } |
| } |
| |
| @override |
| void dispose() { |
| _controller._detach(this); |
| _internalController?.dispose(); |
| super.dispose(); |
| } |
| |
| AxisDirection _getDirection(BuildContext context) { |
| switch (widget.scrollDirection) { |
| case Axis.horizontal: |
| assert(debugCheckHasDirectionality(context)); |
| final TextDirection textDirection = Directionality.of(context); |
| final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection); |
| return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection; |
| case Axis.vertical: |
| return widget.reverse ? AxisDirection.up : AxisDirection.down; |
| } |
| } |
| |
| Widget _buildCarouselItem(ThemeData theme, int index) { |
| final EdgeInsets effectivePadding = widget.padding ?? const EdgeInsets.all(4.0); |
| final Color effectiveBackgroundColor = widget.backgroundColor ?? Theme.of(context).colorScheme.surface; |
| final double effectiveElevation = widget.elevation ?? 0.0; |
| final ShapeBorder effectiveShape = widget.shape |
| ?? const RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(28.0)) |
| ); |
| final WidgetStateProperty<Color?> effectiveOverlayColor = widget.overlayColor |
| ?? WidgetStateProperty.resolveWith((Set<WidgetState> states) { |
| if (states.contains(WidgetState.pressed)) { |
| return theme.colorScheme.onSurface.withOpacity(0.1); |
| } |
| if (states.contains(WidgetState.hovered)) { |
| return theme.colorScheme.onSurface.withOpacity(0.08); |
| } |
| if (states.contains(WidgetState.focused)) { |
| return theme.colorScheme.onSurface.withOpacity(0.1); |
| } |
| return null; |
| }); |
| |
| Widget contents = widget.children[index]; |
| if (widget.enableSplash) { |
| contents = Stack( |
| fit: StackFit.expand, |
| children: <Widget>[ |
| contents, |
| Material( |
| color: Colors.transparent, |
| child: InkWell( |
| onTap: () => widget.onTap?.call(index), |
| overlayColor: effectiveOverlayColor, |
| ), |
| ), |
| ], |
| ); |
| } else if (widget.onTap != null) { |
| contents = GestureDetector( |
| onTap: () => widget.onTap!(index), |
| child: contents, |
| ); |
| } |
| |
| return Padding( |
| padding: effectivePadding, |
| child: Material( |
| clipBehavior: Clip.antiAlias, |
| color: effectiveBackgroundColor, |
| elevation: effectiveElevation, |
| shape: effectiveShape, |
| child: contents, |
| ), |
| ); |
| } |
| |
| Widget _buildSliverCarousel(ThemeData theme) { |
| if (_itemExtent != null) { |
| return _SliverFixedExtentCarousel( |
| itemExtent: _itemExtent!, |
| minExtent: widget.shrinkExtent, |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| return _buildCarouselItem(theme, index); |
| }, |
| childCount: widget.children.length, |
| ), |
| ); |
| } |
| |
| assert(_flexWeights != null && _flexWeights!.every((int weight) => weight > 0), 'flexWeights is null or it contains non-positive integers'); |
| return _SliverWeightedCarousel( |
| consumeMaxWeight: _consumeMaxWeight, |
| shrinkExtent: widget.shrinkExtent, |
| weights: _flexWeights!, |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| return _buildCarouselItem(theme, index); |
| }, |
| childCount: widget.children.length, |
| ), |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData theme = Theme.of(context); |
| final AxisDirection axisDirection = _getDirection(context); |
| final ScrollPhysics physics = widget.itemSnapping |
| ? const CarouselScrollPhysics() |
| : ScrollConfiguration.of(context).getScrollPhysics(context); |
| |
| return LayoutBuilder( |
| builder: (BuildContext context, BoxConstraints constraints) { |
| final double mainAxisExtent = switch (widget.scrollDirection) { |
| Axis.horizontal => constraints.maxWidth, |
| Axis.vertical => constraints.maxHeight, |
| }; |
| _itemExtent = _itemExtent == null ? _itemExtent : clampDouble(_itemExtent!, 0, mainAxisExtent); |
| |
| return Scrollable( |
| axisDirection: axisDirection, |
| controller: _controller, |
| physics: physics, |
| viewportBuilder: (BuildContext context, ViewportOffset position) { |
| return Viewport( |
| cacheExtent: 0.0, |
| cacheExtentStyle: CacheExtentStyle.viewport, |
| axisDirection: axisDirection, |
| offset: position, |
| clipBehavior: Clip.antiAlias, |
| slivers: <Widget>[ |
| _buildSliverCarousel(theme), |
| ], |
| ); |
| }, |
| ); |
| } |
| ); |
| } |
| } |
| |
| /// A sliver that displays its box children in a linear array with a fixed extent |
| /// per item. |
| /// |
| /// _To learn more about slivers, see [CustomScrollView.slivers]._ |
| /// |
| /// This sliver list arranges its children in a line along the main axis starting |
| /// at offset zero and without gaps. Each child is constrained to a fixed extent |
| /// along the main axis and the [SliverConstraints.crossAxisExtent] |
| /// along the cross axis. The difference between this and a list view with a fixed |
| /// extent is the first item and last item can be collapsed a little during scrolling |
| /// transition. This compression is controlled by the `minExtent` property and |
| /// aligns with the [Material Design Carousel specifications] |
| /// (https://m3.material.io/components/carousel/guidelines#96c5c157-fe5b-4ee3-a9b4-72bf8efab7e9). |
| class _SliverFixedExtentCarousel extends SliverMultiBoxAdaptorWidget { |
| const _SliverFixedExtentCarousel({ |
| required super.delegate, |
| required this.minExtent, |
| required this.itemExtent, |
| }); |
| |
| final double itemExtent; |
| final double minExtent; |
| |
| @override |
| RenderSliverFixedExtentBoxAdaptor createRenderObject(BuildContext context) { |
| final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; |
| return _RenderSliverFixedExtentCarousel( |
| childManager: element, |
| minExtent: minExtent, |
| maxExtent: itemExtent, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderSliverFixedExtentCarousel renderObject) { |
| renderObject.maxExtent = itemExtent; |
| renderObject.minExtent = minExtent; |
| } |
| } |
| |
| class _RenderSliverFixedExtentCarousel extends RenderSliverFixedExtentBoxAdaptor { |
| _RenderSliverFixedExtentCarousel({ |
| required super.childManager, |
| required double maxExtent, |
| required double minExtent, |
| }) : _maxExtent = maxExtent, |
| _minExtent = minExtent; |
| |
| double get maxExtent => _maxExtent; |
| double _maxExtent; |
| set maxExtent(double value) { |
| if (_maxExtent == value) { |
| return; |
| } |
| _maxExtent = value; |
| markNeedsLayout(); |
| } |
| |
| double get minExtent => _minExtent; |
| double _minExtent; |
| set minExtent(double value) { |
| if (_minExtent == value) { |
| return; |
| } |
| _minExtent = value; |
| markNeedsLayout(); |
| } |
| |
| // This implements the [itemExtentBuilder] callback. |
| double _buildItemExtent(int index, SliverLayoutDimensions currentLayoutDimensions) { |
| final int firstVisibleIndex = (constraints.scrollOffset / maxExtent).floor(); |
| |
| // Calculate how many items have been completely scroll off screen. |
| final int offscreenItems = (constraints.scrollOffset / maxExtent).floor(); |
| |
| // If an item is partially off screen and partially on screen, |
| // `constraints.scrollOffset` must be greater than |
| // `offscreenItems * maxExtent`, so the difference between these two is how |
| // much the current first visible item is off screen. |
| final double offscreenExtent = constraints.scrollOffset - offscreenItems * maxExtent; |
| |
| // If there is not enough space to place the last visible item but the remaining |
| // space is larger than `minExtent`, the extent for last item should be at |
| // least the remaining extent to ensure a smooth size transition. |
| final double effectiveMinExtent = math.max(constraints.remainingPaintExtent % maxExtent, minExtent); |
| |
| // Two special cases are the first and last visible items. Other items' extent |
| // should all return `maxExtent`. |
| if (index == firstVisibleIndex) { |
| final double effectiveExtent = maxExtent - offscreenExtent; |
| return math.max(effectiveExtent, effectiveMinExtent); |
| } |
| |
| final double scrollOffsetForLastIndex = constraints.scrollOffset + constraints.remainingPaintExtent; |
| if (index == getMaxChildIndexForScrollOffset(scrollOffsetForLastIndex, maxExtent)) { |
| return clampDouble(scrollOffsetForLastIndex - maxExtent * index, effectiveMinExtent, maxExtent); |
| } |
| |
| return maxExtent; |
| } |
| |
| late SliverLayoutDimensions _currentLayoutDimensions; |
| |
| @override |
| void performLayout() { |
| _currentLayoutDimensions = SliverLayoutDimensions( |
| scrollOffset: constraints.scrollOffset, |
| precedingScrollExtent: constraints.precedingScrollExtent, |
| viewportMainAxisExtent: constraints.viewportMainAxisExtent, |
| crossAxisExtent: constraints.crossAxisExtent, |
| ); |
| super.performLayout(); |
| } |
| |
| /// The layout offset for the child with the given index. |
| @override |
| double indexToLayoutOffset( |
| @Deprecated( |
| 'The itemExtent is already available within the scope of this function. ' |
| 'This feature was deprecated after v3.20.0-7.0.pre.' |
| ) |
| double itemExtent, |
| int index, |
| ) { |
| final int firstVisibleIndex = (constraints.scrollOffset / maxExtent).floor(); |
| |
| // If there is not enough space to place the last visible item but the remaining |
| // space is larger than `minExtent`, the extent for last item should be at |
| // least the remaining extent to make sure a smooth size transition. |
| final double effectiveMinExtent = math.max(constraints.remainingPaintExtent % maxExtent, minExtent); |
| if (index == firstVisibleIndex) { |
| final double firstVisibleItemExtent = _buildItemExtent(index, _currentLayoutDimensions); |
| |
| // If the first item is collapsed to be less than `effectiveMinExtent`, |
| // then it should stop changing its size and should start to scroll off screen. |
| if (firstVisibleItemExtent <= effectiveMinExtent) { |
| return maxExtent * index - effectiveMinExtent + maxExtent; |
| } |
| return constraints.scrollOffset; |
| } |
| return maxExtent * index; |
| } |
| |
| /// The minimum child index that is visible at the given scroll offset. |
| @override |
| int getMinChildIndexForScrollOffset( |
| double scrollOffset, |
| @Deprecated( |
| 'The itemExtent is already available within the scope of this function. ' |
| 'This feature was deprecated after v3.20.0-7.0.pre.' |
| ) |
| double itemExtent, |
| ) { |
| final int firstVisibleIndex = (constraints.scrollOffset / maxExtent).floor(); |
| return math.max(firstVisibleIndex, 0); |
| } |
| |
| /// The maximum child index that is visible at the given scroll offset. |
| @override |
| int getMaxChildIndexForScrollOffset( |
| double scrollOffset, |
| @Deprecated( |
| 'The itemExtent is already available within the scope of this function. ' |
| 'This feature was deprecated after v3.20.0-7.0.pre.' |
| ) |
| double itemExtent, |
| ) { |
| if (maxExtent > 0.0) { |
| final double actual = scrollOffset / maxExtent - 1; |
| final int round = actual.round(); |
| if ((actual * maxExtent - round * maxExtent).abs() < precisionErrorTolerance) { |
| return math.max(0, round); |
| } |
| return math.max(0, actual.ceil()); |
| } |
| return 0; |
| } |
| |
| @override |
| double? get itemExtent => null; |
| |
| @override |
| ItemExtentBuilder? get itemExtentBuilder => _buildItemExtent; |
| } |
| |
| /// A sliver that arranges its box children in a linear array, constraining them |
| /// to specific weights determined by the [weights] property. |
| /// |
| /// _To learn more about slivers, see [CustomScrollView.slivers]._ |
| /// |
| /// This sliver arranges its children in a line along the main axis, starting |
| /// at offset zero without gaps. Each child is constrained to its corresponding |
| /// weight along the main axis and to the [SliverConstraints.crossAxisExtent] |
| /// along the cross axis. |
| /// |
| /// See [CarouselView.weighted] to get more calculation explanations. |
| class _SliverWeightedCarousel extends SliverMultiBoxAdaptorWidget { |
| const _SliverWeightedCarousel({ |
| required super.delegate, |
| required this.consumeMaxWeight, |
| required this.shrinkExtent, |
| required this.weights, |
| }); |
| |
| // Determine whether extra scroll offset should be calculate so that every |
| // item have a chance to scroll to the maximum extent. |
| // |
| // This is useful when the leading/trailing items have smaller weights, such |
| // as [1, 7], and [3, 2, 1]. |
| final bool consumeMaxWeight; |
| |
| // The starting extent for items when they gradually show on/off screen. |
| // |
| // This is useful to avoid a hairline shape. This value should also smaller |
| // than the last item extent to make sure a smooth transition. So in calculation, |
| // this is limited to [0, weight for the last visible item]. |
| final double shrinkExtent; |
| |
| // The layout arrangement. |
| // |
| // When items are laying out, each item will be arranged based on the order of |
| // the weights and the extent is based on the corresponding weight out of the |
| // sum of weights. The length of weights means how many items we can put in the |
| // view at a time. |
| final List<int> weights; |
| |
| @override |
| RenderSliverFixedExtentBoxAdaptor createRenderObject(BuildContext context) { |
| final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; |
| return _RenderSliverWeightedCarousel( |
| childManager: element, |
| consumeMaxWeight: consumeMaxWeight, |
| shrinkExtent: shrinkExtent, |
| weights: weights, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderSliverWeightedCarousel renderObject) { |
| renderObject |
| ..consumeMaxWeight = consumeMaxWeight |
| ..shrinkExtent = shrinkExtent |
| ..weights = weights; |
| } |
| } |
| |
| // A sliver that places its box children in a linear array and constrains them |
| // to have the corresponding weight which is determined by [weights]. |
| class _RenderSliverWeightedCarousel extends RenderSliverFixedExtentBoxAdaptor { |
| _RenderSliverWeightedCarousel({ |
| required super.childManager, |
| required bool consumeMaxWeight, |
| required double shrinkExtent, |
| required List<int> weights, |
| }) : _consumeMaxWeight = consumeMaxWeight, |
| _shrinkExtent = shrinkExtent, |
| _weights = weights; |
| |
| bool get consumeMaxWeight => _consumeMaxWeight; |
| bool _consumeMaxWeight; |
| set consumeMaxWeight(bool value) { |
| if (_consumeMaxWeight == value) { |
| return; |
| } |
| _consumeMaxWeight = value; |
| markNeedsLayout(); |
| } |
| |
| double get shrinkExtent => _shrinkExtent; |
| double _shrinkExtent; |
| set shrinkExtent(double value) { |
| if (_shrinkExtent == value) { |
| return; |
| } |
| _shrinkExtent = value; |
| markNeedsLayout(); |
| } |
| |
| List<int> get weights => _weights; |
| List<int> _weights; |
| set weights(List<int> value) { |
| if (_weights == value) { |
| return; |
| } |
| _weights = value; |
| markNeedsLayout(); |
| } |
| |
| late SliverLayoutDimensions _currentLayoutDimensions; |
| |
| // This is to implement the itemExtentBuilder callback to return each item extent |
| // while scrolling. |
| // |
| // The given `index` is compared with `_firstVisibleItemIndex` to know how |
| // many items are placed before the current one in the view. |
| double _buildItemExtent(int index, SliverLayoutDimensions currentLayoutDimensions) { |
| double extent; |
| if (index == _firstVisibleItemIndex) { |
| extent = math.max(_distanceToLeadingEdge, effectiveShrinkExtent); |
| } |
| |
| // Calculate the extents of items located within the range defined by the |
| // weights array relative to the first visible item. This allows us to |
| // precisely determine each item's extent based on its initial extent |
| // (calculated from the weights) and the scrolling progress (the off-screen |
| // portion of the first item). |
| else if (index > _firstVisibleItemIndex |
| && index - _firstVisibleItemIndex + 1 <= weights.length |
| ) { |
| assert(index - _firstVisibleItemIndex < weights.length); |
| final int currIndexOnWeightList = index - _firstVisibleItemIndex; |
| final int currWeight = weights[currIndexOnWeightList]; |
| extent = extentUnit * currWeight; // initial extent |
| final double progress = _firstVisibleItemOffscreenExtent / firstChildExtent; |
| |
| final int prevWeight = weights[currIndexOnWeightList - 1]; |
| final double finalIncrease = (prevWeight - currWeight) / weights.max; |
| extent = extent + finalIncrease * progress * maxChildExtent; |
| } |
| // Calculate the extents of items located beyond the range defined by the |
| // weights array relative to the first visible item. During scrolling transition, |
| // it is possible that the number of visible items is larger than the length |
| // of `weights`. The extra item extent should be calculated here to fill |
| // the remaining space. |
| else if (index > _firstVisibleItemIndex |
| && index - _firstVisibleItemIndex + 1 > weights.length) |
| { |
| double visibleItemsTotalExtent = _distanceToLeadingEdge; |
| for (int i = _firstVisibleItemIndex + 1; i < index; i++) { |
| visibleItemsTotalExtent += _buildItemExtent(i, currentLayoutDimensions); |
| } |
| extent = math.max(constraints.remainingPaintExtent - visibleItemsTotalExtent, effectiveShrinkExtent); |
| } |
| else { |
| extent = math.max(minChildExtent, effectiveShrinkExtent); |
| } |
| return extent; |
| } |
| |
| // To ge the extent unit based on the viewport extent and the sum of weights. |
| double get extentUnit => constraints.viewportMainAxisExtent / (weights.reduce((int total, int extent) => total + extent)); |
| |
| double get firstChildExtent => weights.first * extentUnit; |
| double get maxChildExtent => weights.max * extentUnit; |
| double get minChildExtent => weights.min * extentUnit; |
| |
| // The shrink extent for first and last visible items should be no larger |
| // than [minChildExtent] to ensure a smooth transition. |
| double get effectiveShrinkExtent => clampDouble(shrinkExtent, 0, minChildExtent); |
| |
| // The index of the first visible item. The returned value can be negative when |
| // the leading items with smaller weights need to be fully expanded. For example, |
| // assuming a weights [1, 7, 1], when item 0 is expanding to the maximum size |
| // (with weight 7), we leave some space before item 0 assuming there is another |
| // item -1 as the first visible item. |
| int get _firstVisibleItemIndex { |
| int smallerWeightCount = 0; |
| for (final int weight in weights) { |
| if (weight == weights.max) { |
| break; |
| } |
| smallerWeightCount += 1; |
| } |
| int index; |
| |
| final double actual = constraints.scrollOffset / firstChildExtent; |
| final int round = (constraints.scrollOffset / firstChildExtent).round(); |
| if ((actual - round).abs() < precisionErrorTolerance) { |
| index = round; |
| } else { |
| index = actual.floor(); |
| } |
| return consumeMaxWeight ? index - smallerWeightCount : index; |
| } |
| |
| // This value indicates the scrolling progress of items following the first |
| // item. It informs them how much the first item has moved off-screen, |
| // enabling them to adjust their sizes (grow or shrink) accordingly. |
| double get _firstVisibleItemOffscreenExtent { |
| int index; |
| final double actual = constraints.scrollOffset / firstChildExtent; |
| final int round = (constraints.scrollOffset / firstChildExtent).round(); |
| if ((actual - round).abs() < precisionErrorTolerance) { |
| index = round; |
| } else { |
| index = actual.floor(); |
| } |
| return constraints.scrollOffset - index * firstChildExtent; |
| } |
| |
| // Given the off-screen extent for the first visible item, we can know the |
| // on-screen extent for the first visible item. |
| double get _distanceToLeadingEdge => firstChildExtent - _firstVisibleItemOffscreenExtent; |
| |
| // Given an index, this method returns the layout offset for the item. The `index` |
| // is firstly compared to `_firstVisibleItemIndex` and compute the distance |
| // between them, then compute all the current extents for items that are located |
| // in front. |
| @override |
| double indexToLayoutOffset( |
| @Deprecated( |
| 'The itemExtent is already available within the scope of this function. ' |
| 'This feature was deprecated after v3.20.0-7.0.pre.' |
| ) |
| double itemExtent, |
| int index, |
| ) { |
| if (index == _firstVisibleItemIndex) { |
| if (_distanceToLeadingEdge <= effectiveShrinkExtent) { |
| return constraints.scrollOffset - effectiveShrinkExtent + _distanceToLeadingEdge; |
| } |
| return constraints.scrollOffset; |
| } |
| double visibleItemsTotalExtent = _distanceToLeadingEdge; |
| for (int i = _firstVisibleItemIndex + 1; i < index; i++) { |
| visibleItemsTotalExtent += _buildItemExtent(i, _currentLayoutDimensions); |
| } |
| return constraints.scrollOffset + visibleItemsTotalExtent; |
| } |
| |
| @override |
| int getMinChildIndexForScrollOffset( |
| double scrollOffset, |
| @Deprecated( |
| 'The itemExtent is already available within the scope of this function. ' |
| 'This feature was deprecated after v3.20.0-7.0.pre.' |
| ) |
| double itemExtent, |
| ) { |
| return math.max(_firstVisibleItemIndex, 0); |
| } |
| |
| @override |
| int getMaxChildIndexForScrollOffset( |
| double scrollOffset, |
| @Deprecated( |
| 'The itemExtent is already available within the scope of this function. ' |
| 'This feature was deprecated after v3.20.0-7.0.pre.' |
| ) |
| double itemExtent, |
| ) { |
| final int? childCount = childManager.estimatedChildCount; |
| if (childCount != null) { |
| double visibleItemsTotalExtent = _distanceToLeadingEdge; |
| for (int i = _firstVisibleItemIndex + 1; i < childCount; i++) { |
| visibleItemsTotalExtent += _buildItemExtent(i, _currentLayoutDimensions); |
| if (visibleItemsTotalExtent >= constraints.viewportMainAxisExtent) { |
| return i; |
| } |
| } |
| } |
| return childCount ?? 0; |
| } |
| |
| @override |
| double computeMaxScrollOffset( |
| SliverConstraints constraints, |
| @Deprecated( |
| 'The itemExtent is already available within the scope of this function. ' |
| 'This feature was deprecated after v3.20.0-7.0.pre.' |
| ) |
| double itemExtent, |
| ) { |
| return childManager.childCount * maxChildExtent; |
| } |
| |
| BoxConstraints _getChildConstraints(int index) { |
| final double extent = itemExtentBuilder!(index, _currentLayoutDimensions)!; |
| return constraints.asBoxConstraints( |
| minExtent: extent, |
| maxExtent: extent, |
| ); |
| } |
| |
| // This method is mostly the same as its parent class [RenderSliverFixedExtentList]. |
| // The difference is when we allow some space before the leading items or after |
| // the trailing items with smaller weights, we leave extra scroll offset. |
| // TODO(quncCccccc): add the calculation for the extra scroll offset on the super class to simplify the implementation here. |
| @override |
| void performLayout() { |
| assert((itemExtent != null && itemExtentBuilder == null) || |
| (itemExtent == null && itemExtentBuilder != null)); |
| assert(itemExtentBuilder != null || (itemExtent!.isFinite && itemExtent! >= 0)); |
| |
| final SliverConstraints constraints = this.constraints; |
| childManager.didStartLayout(); |
| childManager.setDidUnderflow(false); |
| |
| final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; |
| assert(scrollOffset >= 0.0); |
| final double remainingExtent = constraints.remainingCacheExtent; |
| assert(remainingExtent >= 0.0); |
| final double targetEndScrollOffset = scrollOffset + remainingExtent; |
| _currentLayoutDimensions = SliverLayoutDimensions( |
| scrollOffset: constraints.scrollOffset, |
| precedingScrollExtent: constraints.precedingScrollExtent, |
| viewportMainAxisExtent: constraints.viewportMainAxisExtent, |
| crossAxisExtent: constraints.crossAxisExtent |
| ); |
| // TODO(Piinks): Clean up when deprecation expires. |
| const double deprecatedExtraItemExtent = -1; |
| |
| final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, deprecatedExtraItemExtent); |
| final int? targetLastIndex = targetEndScrollOffset.isFinite ? |
| getMaxChildIndexForScrollOffset(targetEndScrollOffset, deprecatedExtraItemExtent) : null; |
| |
| if (firstChild != null) { |
| final int leadingGarbage = calculateLeadingGarbage(firstIndex: firstIndex); |
| final int trailingGarbage = targetLastIndex != null ? calculateTrailingGarbage(lastIndex: targetLastIndex) : 0; |
| collectGarbage(leadingGarbage, trailingGarbage); |
| } else { |
| collectGarbage(0, 0); |
| } |
| |
| if (firstChild == null) { |
| final double layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, firstIndex); |
| if (!addInitialChild(index: firstIndex, layoutOffset: layoutOffset)) { |
| // There are either no children, or we are past the end of all our children. |
| final double max; |
| if (firstIndex <= 0) { |
| max = 0.0; |
| } else { |
| max = computeMaxScrollOffset(constraints, deprecatedExtraItemExtent); |
| } |
| geometry = SliverGeometry( |
| scrollExtent: max, |
| maxPaintExtent: max, |
| ); |
| childManager.didFinishLayout(); |
| return; |
| } |
| } |
| |
| RenderBox? trailingChildWithLayout; |
| |
| for (int index = indexOf(firstChild!) - 1; index >= firstIndex; --index) { |
| final RenderBox? child = insertAndLayoutLeadingChild(_getChildConstraints(index)); |
| if (child == null) { |
| // Items before the previously first child are no longer present. |
| // Reset the scroll offset to offset all items prior and up to the |
| // missing item. Let parent re-layout everything. |
| geometry = SliverGeometry(scrollOffsetCorrection: indexToLayoutOffset(deprecatedExtraItemExtent, index)); |
| return; |
| } |
| final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
| childParentData.layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, index); |
| assert(childParentData.index == index); |
| trailingChildWithLayout ??= child; |
| } |
| |
| if (trailingChildWithLayout == null) { |
| firstChild!.layout(_getChildConstraints(indexOf(firstChild!))); |
| final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData; |
| childParentData.layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, firstIndex); |
| trailingChildWithLayout = firstChild; |
| } |
| |
| // From the last item to the firstly encountered max item |
| double extraLayoutOffset = 0; |
| if (consumeMaxWeight) { |
| for (int i = weights.length - 1; i >= 0; i--) { |
| if (weights[i] == weights.max) { |
| break; |
| } |
| extraLayoutOffset += weights[i] * extentUnit; |
| } |
| } |
| |
| double estimatedMaxScrollOffset = double.infinity; |
| // Layout visible items after the first visible item. |
| for (int index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { |
| RenderBox? child = childAfter(trailingChildWithLayout!); |
| if (child == null || indexOf(child) != index) { |
| child = insertAndLayoutChild(_getChildConstraints(index), after: trailingChildWithLayout); |
| if (child == null) { |
| // We have run out of children. |
| estimatedMaxScrollOffset = indexToLayoutOffset(deprecatedExtraItemExtent, index) + extraLayoutOffset; |
| break; |
| } |
| } else { |
| child.layout(_getChildConstraints(index)); |
| } |
| trailingChildWithLayout = child; |
| final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData; |
| assert(childParentData.index == index); |
| childParentData.layoutOffset = indexToLayoutOffset(deprecatedExtraItemExtent, childParentData.index!); |
| } |
| |
| final int lastIndex = indexOf(lastChild!); |
| final double leadingScrollOffset = indexToLayoutOffset(deprecatedExtraItemExtent, firstIndex); |
| double trailingScrollOffset; |
| |
| if (lastIndex + 1 == childManager.childCount) { |
| trailingScrollOffset = indexToLayoutOffset(deprecatedExtraItemExtent, lastIndex); |
| |
| trailingScrollOffset += math.max(weights.last * extentUnit, _buildItemExtent(lastIndex, _currentLayoutDimensions)); |
| trailingScrollOffset += extraLayoutOffset; |
| } else { |
| trailingScrollOffset = indexToLayoutOffset(deprecatedExtraItemExtent, lastIndex + 1); |
| } |
| |
| assert(debugAssertChildListIsNonEmptyAndContiguous()); |
| assert(indexOf(firstChild!) == firstIndex); |
| assert(targetLastIndex == null || lastIndex <= targetLastIndex); |
| |
| estimatedMaxScrollOffset = math.min( |
| estimatedMaxScrollOffset, |
| estimateMaxScrollOffset( |
| constraints, |
| firstIndex: firstIndex, |
| lastIndex: lastIndex, |
| leadingScrollOffset: leadingScrollOffset, |
| trailingScrollOffset: trailingScrollOffset, |
| ), |
| ); |
| |
| final double paintExtent = calculatePaintOffset( |
| constraints, |
| from: consumeMaxWeight ? 0 : leadingScrollOffset, |
| to: trailingScrollOffset, |
| ); |
| |
| final double cacheExtent = calculateCacheOffset( |
| constraints, |
| from: consumeMaxWeight ? 0 : leadingScrollOffset, |
| to: trailingScrollOffset, |
| ); |
| |
| final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; |
| final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? |
| getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint, deprecatedExtraItemExtent) : null; |
| |
| geometry = SliverGeometry( |
| scrollExtent: estimatedMaxScrollOffset, |
| paintExtent: paintExtent, |
| cacheExtent: cacheExtent, |
| maxPaintExtent: estimatedMaxScrollOffset, |
| // Conservative to avoid flickering away the clip during scroll. |
| hasVisualOverflow: (targetLastIndexForPaint != null && lastIndex >= targetLastIndexForPaint) |
| || constraints.scrollOffset > 0.0, |
| ); |
| |
| // We may have started the layout while scrolled to the end, which would not |
| // expose a new child. |
| if (estimatedMaxScrollOffset == trailingScrollOffset) { |
| childManager.setDidUnderflow(true); |
| } |
| childManager.didFinishLayout(); |
| } |
| |
| @override |
| double? get itemExtent => null; |
| |
| /// The main-axis extent builder of each item. |
| /// |
| /// If this is non-null, the [itemExtent] must be null. |
| /// If this is null, the [itemExtent] must be non-null. |
| @override |
| ItemExtentBuilder? get itemExtentBuilder => _buildItemExtent; |
| } |
| |
| /// Scroll physics used by a [CarouselView]. |
| /// |
| /// These physics cause the carousel item to snap to item boundaries. |
| /// |
| /// See also: |
| /// |
| /// * [ScrollPhysics], the base class which defines the API for scrolling |
| /// physics. |
| /// * [PageScrollPhysics], scroll physics used by a [PageView]. |
| class CarouselScrollPhysics extends ScrollPhysics { |
| /// Creates physics for a [CarouselView]. |
| const CarouselScrollPhysics({super.parent}); |
| |
| @override |
| CarouselScrollPhysics applyTo(ScrollPhysics? ancestor) { |
| return CarouselScrollPhysics(parent: buildParent(ancestor)); |
| } |
| |
| double _getTargetPixels( |
| _CarouselPosition position, |
| Tolerance tolerance, |
| double velocity, |
| ) { |
| double fraction; |
| |
| if (position.itemExtent != null) { |
| fraction = position.itemExtent! / position.viewportDimension; |
| } else { |
| assert(position.flexWeights != null); |
| fraction = position.flexWeights!.first / position.flexWeights!.sum; |
| } |
| |
| final double itemWidth = position.viewportDimension * fraction; |
| |
| final double actual = math.max(0.0, position.pixels) / itemWidth; |
| final double round = actual.roundToDouble(); |
| double item; |
| if ((actual - round).abs() < precisionErrorTolerance) { |
| item = round; |
| } else { |
| item = actual; |
| } |
| if (velocity < -tolerance.velocity) { |
| item -= 0.5; |
| } else if (velocity > tolerance.velocity) { |
| item += 0.5; |
| } |
| return item.roundToDouble() * itemWidth; |
| } |
| |
| @override |
| Simulation? createBallisticSimulation( |
| ScrollMetrics position, |
| double velocity, |
| ) { |
| assert( |
| position is _CarouselPosition, |
| 'CarouselScrollPhysics can only be used with Scrollables that uses ' |
| 'the CarouselController', |
| ); |
| |
| final _CarouselPosition metrics = position as _CarouselPosition; |
| if ((velocity <= 0.0 && metrics.pixels <= metrics.minScrollExtent) || |
| (velocity >= 0.0 && metrics.pixels >= metrics.maxScrollExtent)) { |
| return super.createBallisticSimulation(metrics, velocity); |
| } |
| |
| final Tolerance tolerance = toleranceFor(metrics); |
| final double target = _getTargetPixels(metrics, tolerance, velocity); |
| if (target != metrics.pixels) { |
| return ScrollSpringSimulation( |
| spring, |
| metrics.pixels, |
| target, |
| velocity, |
| tolerance: tolerance, |
| ); |
| } |
| return null; |
| } |
| |
| @override |
| bool get allowImplicitScrolling => true; |
| } |
| |
| /// Metrics for a [CarouselView]. |
| class _CarouselMetrics extends FixedScrollMetrics { |
| /// Creates an immutable snapshot of values associated with a [CarouselView]. |
| _CarouselMetrics({ |
| required super.minScrollExtent, |
| required super.maxScrollExtent, |
| required super.pixels, |
| required super.viewportDimension, |
| required super.axisDirection, |
| this.itemExtent, |
| this.flexWeights, |
| this.consumeMaxWeight, |
| required super.devicePixelRatio, |
| }); |
| |
| /// Extent for the carousel item. |
| /// |
| /// Used to compute the first item from the current [pixels]. |
| final double? itemExtent; |
| |
| /// The fraction of the viewport that the first item occupies. |
| /// |
| /// Used to compute the extent of each carousel item from the current [pixels], |
| /// if [itemExtent] is null. |
| final List<int>? flexWeights; |
| |
| /// Determine whether each child can be expanded to occupy the maximum weight while scrolling. |
| final bool? consumeMaxWeight; |
| |
| @override |
| _CarouselMetrics copyWith({ |
| double? minScrollExtent, |
| double? maxScrollExtent, |
| double? pixels, |
| double? viewportDimension, |
| AxisDirection? axisDirection, |
| double? itemExtent, |
| List<int>? flexWeights, |
| bool? consumeMaxWeight, |
| double? devicePixelRatio, |
| }) { |
| return _CarouselMetrics( |
| minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null), |
| maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null), |
| pixels: pixels ?? (hasPixels ? this.pixels : null), |
| viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null), |
| axisDirection: axisDirection ?? this.axisDirection, |
| itemExtent: itemExtent ?? this.itemExtent, |
| flexWeights: flexWeights ?? this.flexWeights, |
| consumeMaxWeight: consumeMaxWeight ?? this.consumeMaxWeight, |
| devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, |
| ); |
| } |
| } |
| |
| class _CarouselPosition extends ScrollPositionWithSingleContext implements _CarouselMetrics { |
| _CarouselPosition({ |
| required super.physics, |
| required super.context, |
| this.initialItem = 0, |
| double? itemExtent, |
| List<int>? flexWeights, |
| bool consumeMaxWeight = true, |
| super.oldPosition, |
| }) : assert(flexWeights != null && itemExtent == null |
| || flexWeights == null && itemExtent != null), |
| _itemToShowOnStartup = initialItem.toDouble(), |
| _consumeMaxWeight = consumeMaxWeight, |
| super( |
| initialPixels: null, |
| ); |
| |
| int initialItem; |
| final double _itemToShowOnStartup; |
| // When the viewport has a zero-size, the item can not |
| // be retrieved by `getItemFromPixels`, so we need to cache the item |
| // for use when resizing the viewport to non-zero next time. |
| double? _cachedItem; |
| |
| @override |
| bool get consumeMaxWeight => _consumeMaxWeight; |
| bool _consumeMaxWeight; |
| set consumeMaxWeight(bool value) { |
| if (_consumeMaxWeight == value) { |
| return; |
| } |
| if (hasPixels && flexWeights != null) { |
| final double leadingItem = updateLeadingItem(flexWeights, value); |
| final double newPixel = getPixelsFromItem(leadingItem, flexWeights, itemExtent); |
| forcePixels(newPixel); |
| } |
| _consumeMaxWeight = value; |
| } |
| |
| @override |
| double? get itemExtent => _itemExtent; |
| double? _itemExtent; |
| set itemExtent(double? value) { |
| if (_itemExtent == value) { |
| return; |
| } |
| if (hasPixels && _itemExtent != null) { |
| final double leadingItem = getItemFromPixels(pixels, viewportDimension); |
| final double newPixel = getPixelsFromItem(leadingItem, flexWeights, value); |
| forcePixels(newPixel); |
| } |
| _itemExtent = value; |
| } |
| |
| @override |
| List<int>? get flexWeights => _flexWeights; |
| List<int>? _flexWeights; |
| set flexWeights(List<int>? value) { |
| if (flexWeights == value) { |
| return; |
| } |
| final List<int>? oldWeights = _flexWeights; |
| if (hasPixels && oldWeights != null) { |
| final double leadingItem = updateLeadingItem(value, consumeMaxWeight); |
| final double newPixel = getPixelsFromItem(leadingItem, value, itemExtent); |
| forcePixels(newPixel); |
| } |
| _flexWeights = value; |
| } |
| |
| double updateLeadingItem(List<int>? newFlexWeights, bool newConsumeMaxWeight) { |
| final double maxItem; |
| if (hasPixels && flexWeights != null) { |
| final double leadingItem = getItemFromPixels(pixels, viewportDimension); |
| maxItem = consumeMaxWeight |
| ? leadingItem |
| : leadingItem + flexWeights!.indexOf(flexWeights!.max); |
| } else { |
| maxItem = _itemToShowOnStartup; |
| } |
| if (newFlexWeights != null && !newConsumeMaxWeight) { |
| int smallerWeights = 0; |
| for (final int weight in newFlexWeights) { |
| if (weight == newFlexWeights.max) { |
| break; |
| } |
| smallerWeights += 1; |
| } |
| return maxItem - smallerWeights; |
| } |
| return maxItem; |
| } |
| |
| double getItemFromPixels(double pixels, double viewportDimension) { |
| assert(viewportDimension > 0.0); |
| double fraction; |
| if (itemExtent != null) { |
| fraction = itemExtent! / viewportDimension; |
| } else { // If itemExtent is null, flexWeights cannot be null. |
| assert(flexWeights != null); |
| fraction = flexWeights!.first / flexWeights!.sum; |
| } |
| |
| final double actual = math.max(0.0, pixels) / (viewportDimension * fraction); |
| final double round = actual.roundToDouble(); |
| if ((actual - round).abs() < precisionErrorTolerance) { |
| return round; |
| } |
| return actual; |
| } |
| |
| double getPixelsFromItem(double item, List<int>? flexWeights, double? itemExtent) { |
| double fraction; |
| if (itemExtent != null) { |
| fraction = itemExtent / viewportDimension; |
| } else { // If itemExtent is null, flexWeights cannot be null. |
| assert(flexWeights != null); |
| fraction = flexWeights!.first / flexWeights.sum; |
| } |
| |
| return item * viewportDimension * fraction; |
| } |
| |
| @override |
| bool applyViewportDimension(double viewportDimension) { |
| final double? oldViewportDimensions = hasViewportDimension ? this.viewportDimension : null; |
| if (viewportDimension == oldViewportDimensions) { |
| return true; |
| } |
| final bool result = super.applyViewportDimension(viewportDimension); |
| final double? oldPixels = hasPixels ? pixels : null; |
| double item; |
| if (oldPixels == null) { |
| item = updateLeadingItem(flexWeights, consumeMaxWeight); |
| } else if (oldViewportDimensions == 0.0) { |
| // If resize from zero, we should use the _cachedItem to recover the state. |
| item = _cachedItem!; |
| } else { |
| item = getItemFromPixels(oldPixels, oldViewportDimensions!); |
| } |
| final double newPixels = getPixelsFromItem(item, flexWeights, itemExtent); |
| // If the viewportDimension is zero, cache the item |
| // in case the viewport is resized to be non-zero. |
| _cachedItem = (viewportDimension == 0.0) ? item : null; |
| |
| if (newPixels != oldPixels) { |
| correctPixels(newPixels); |
| return false; |
| } |
| return result; |
| } |
| |
| @override |
| _CarouselMetrics copyWith({ |
| double? minScrollExtent, |
| double? maxScrollExtent, |
| double? pixels, |
| double? viewportDimension, |
| AxisDirection? axisDirection, |
| double? itemExtent, |
| List<int>? flexWeights, |
| bool? consumeMaxWeight, |
| double? devicePixelRatio, |
| }) { |
| return _CarouselMetrics( |
| minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null), |
| maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null), |
| pixels: pixels ?? (hasPixels ? this.pixels : null), |
| viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null), |
| axisDirection: axisDirection ?? this.axisDirection, |
| itemExtent: itemExtent ?? this.itemExtent, |
| flexWeights: flexWeights ?? this.flexWeights, |
| consumeMaxWeight: consumeMaxWeight ?? this.consumeMaxWeight, |
| devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, |
| ); |
| } |
| } |
| |
| /// A controller for [CarouselView]. |
| /// |
| /// Using a carousel controller helps to show the first visible item on the |
| /// carousel list. |
| class CarouselController extends ScrollController { |
| /// Creates a carousel controller. |
| CarouselController({ |
| this.initialItem = 0, |
| }); |
| |
| /// The item that expands to the maximum size when first creating the [CarouselView]. |
| final int initialItem; |
| |
| _CarouselViewState? _carouselState; |
| |
| // ignore: use_setters_to_change_properties |
| void _attach(_CarouselViewState anchor) { |
| _carouselState = anchor; |
| } |
| |
| void _detach(_CarouselViewState anchor) { |
| if (_carouselState == anchor) { |
| _carouselState = null; |
| } |
| } |
| |
| @override |
| ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) { |
| assert(_carouselState != null); |
| return _CarouselPosition( |
| physics: physics, |
| context: context, |
| initialItem: initialItem, |
| itemExtent: _carouselState!._itemExtent, |
| consumeMaxWeight: _carouselState!._consumeMaxWeight, |
| flexWeights: _carouselState!._flexWeights, |
| oldPosition: oldPosition, |
| ); |
| } |
| |
| @override |
| void attach(ScrollPosition position) { |
| super.attach(position); |
| final _CarouselPosition carouselPosition = position as _CarouselPosition; |
| carouselPosition.flexWeights = _carouselState!._flexWeights; |
| carouselPosition.itemExtent = _carouselState!._itemExtent; |
| carouselPosition.consumeMaxWeight = _carouselState!._consumeMaxWeight; |
| } |
| } |