diff --git a/lib/screens/shared/media/carousel_banner.dart b/lib/screens/shared/media/carousel_banner.dart index 7bcd8cc..e9cad90 100644 --- a/lib/screens/shared/media/carousel_banner.dart +++ b/lib/screens/shared/media/carousel_banner.dart @@ -11,7 +11,6 @@ import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/themes_data.dart'; -import 'package:fladder/widgets/shared/fladder_carousel.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; @@ -42,119 +41,126 @@ class _CarouselBannerState extends ConsumerState { (MediaQuery.sizeOf(context).shortestSide * 0.75).clamp(251.0, double.maxFinite), ); final border = BorderRadius.circular(18); - return FladderCarousel( - elevation: 3, - shrinkExtent: 0, - shape: RoundedRectangleBorder(borderRadius: border), - itemPadding: + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), - padding: const EdgeInsets.symmetric(horizontal: 6), - itemExtent: widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent, - children: [ - ...widget.items.mapIndexed( - (index, item) => LayoutBuilder(builder: (context, constraints) { - final opacity = (constraints.maxWidth / maxExtent); - return Stack( - clipBehavior: Clip.none, - children: [ - FladderImage(image: item.bannerImage), - Opacity( - opacity: opacity.clamp(0, 1), - child: Stack( - children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomLeft, - end: Alignment.topCenter, - colors: [ - ThemesData.of(context).dark.colorScheme.primaryContainer.withValues(alpha: 0.85), - Colors.transparent, - ], + child: CarouselView( + elevation: 3, + shrinkExtent: 0, + shape: RoundedRectangleBorder(borderRadius: border), + padding: const EdgeInsets.symmetric(horizontal: 6), + enableSplash: false, + itemExtent: widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent, + children: [ + ...widget.items.mapIndexed( + (index, item) => LayoutBuilder(builder: (context, constraints) { + final opacity = (constraints.maxWidth / maxExtent); + return Stack( + clipBehavior: Clip.none, + children: [ + FladderImage(image: item.bannerImage), + Opacity( + opacity: opacity.clamp(0, 1), + child: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topCenter, + colors: [ + ThemesData.of(context) + .dark + .colorScheme + .primaryContainer + .withValues(alpha: 0.85), + Colors.transparent, + ], + ), ), ), ), - ), - ], - ), - ), - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: const EdgeInsets.all(16.0).copyWith(right: constraints.maxWidth * 0.2), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - maxLines: 2, - softWrap: item.title.length > 25, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), - ), - if (item.label(context) != null || item.subText != null) - Text( - item.label(context) ?? item.subText ?? "", - maxLines: 2, - softWrap: false, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white), - ), - ].addInBetween(const SizedBox(height: 4)), + ], ), ), - ), - FlatButton( - onTap: () => widget.items[index].navigateTo(context), - onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer - ? null - : () { - final poster = widget.items[index]; - showBottomSheetPill( - context: context, - item: poster, - content: (scrollContext, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: poster - .generateActions(context, ref) - .listTileItems(scrollContext, useIcons: true), + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(16.0).copyWith(right: constraints.maxWidth * 0.2), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 2, + softWrap: item.title.length > 25, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), + ), + if (item.label(context) != null || item.subText != null) + Text( + item.label(context) ?? item.subText ?? "", + maxLines: 2, + softWrap: false, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white), ), - ); - }, - onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch - ? null - : (details) async { - Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); - final poster = widget.items[index]; - - await showMenu( - context: context, - position: position, - items: poster.generateActions(context, ref).popupMenuItems(useIcons: true), - ); - }, - ), - BannerPlayButton(item: widget.items[index]), - IgnorePointer( - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.white.withValues(alpha: 0.1), - width: 1.0, - ), - borderRadius: border), + ].addInBetween(const SizedBox(height: 4)), + ), + ), ), - ), - ], - ); - }), - ) - ], + FlatButton( + onTap: () => widget.items[index].navigateTo(context), + onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer + ? null + : () { + final poster = widget.items[index]; + showBottomSheetPill( + context: context, + item: poster, + content: (scrollContext, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: poster + .generateActions(context, ref) + .listTileItems(scrollContext, useIcons: true), + ), + ); + }, + onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch + ? null + : (details) async { + Offset localPosition = details.globalPosition; + RelativeRect position = RelativeRect.fromLTRB( + localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); + final poster = widget.items[index]; + + await showMenu( + context: context, + position: position, + items: poster.generateActions(context, ref).popupMenuItems(useIcons: true), + ); + }, + ), + BannerPlayButton(item: widget.items[index]), + IgnorePointer( + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.white.withValues(alpha: 0.1), + width: 1.0, + ), + borderRadius: border), + ), + ), + ], + ); + }), + ) + ], + ), ); }, ), diff --git a/lib/screens/shared/media/media_banner.dart b/lib/screens/shared/media/media_banner.dart index d8e4ca7..f8f2c3d 100644 --- a/lib/screens/shared/media/media_banner.dart +++ b/lib/screens/shared/media/media_banner.dart @@ -12,7 +12,6 @@ import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/themes_data.dart'; -import 'package:fladder/widgets/shared/fladder_carousel.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; @@ -81,8 +80,6 @@ class _MediaBannerState extends ConsumerState { timer.reset(); } - final controller = FladderCarouselController(); - @override Widget build(BuildContext context) { final overlayColor = ThemesData.of(context).dark.colorScheme.primaryContainer; diff --git a/lib/widgets/shared/fladder_carousel.dart b/lib/widgets/shared/fladder_carousel.dart deleted file mode 100644 index f6671a4..0000000 --- a/lib/widgets/shared/fladder_carousel.dart +++ /dev/null @@ -1,760 +0,0 @@ -// 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. - -//This is a copy of the CarouselView widget with some minor changes. - -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class MyCustomScrollBehavior extends MaterialScrollBehavior { - @override - Set get dragDevices => PointerDeviceKind.values.toSet(); -} - -/// A Material Design carousel widget. -/// -/// The [FladderCarousel] present a scrollable list of items, each of which can dynamically -/// change size based on the chosen layout. -/// -/// This widget supports uncontained carousel layout. It shows items that scroll -/// to the edge of the container, behaving similarly to a [ListView] where all -/// children are a uniform size. -/// -/// The [FladderCarouselController] is used to control the [FladderCarouselController.initialItem]. -/// -/// The [FladderCarousel.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 of [FladderCarousel] to show the uncontained layout. Each carousel -/// item has the same size but can be "squished" to the [shrinkExtent] when they -/// are show on the view and out of view. -/// -/// ** See code in examples/api/lib/material/carousel/carousel.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [FladderCarouselController], which controls the first visible item in the carousel. -/// * [PageView], which is a scrollable list that works page by page. -class FladderCarousel extends StatefulWidget { - /// Creates a Material Design carousel. - const FladderCarousel({ - super.key, - this.itemPadding, - this.padding = EdgeInsets.zero, - 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.onLongPress, - this.onSecondaryTap, - required this.itemExtent, - required this.children, - }); - - /// The amount of space to surround each carousel item with. - /// - /// Defaults to [EdgeInsets.all] of 4 pixels. - final EdgeInsets? itemPadding; - - 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? 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 [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. - 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 FladderCarouselController? 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; - - /// Called when one of the [children] is tapped. - final ValueChanged? onTap; - - /// Called when one of the [children] is longPressed. - final ValueChanged? onLongPress; - - final ValueChanged<(int, TapDownDetails)>? onSecondaryTap; - - /// The extent the children are forced to have in the main axis. - /// - /// The item extent should not exceed the available space that the carousel - /// occupies to ensure at least one item is fully visible. - /// - /// This must be non-null. - final double itemExtent; - - /// The child widgets for the carousel. - final List children; - - @override - State createState() => _CarouselViewState(); -} - -class _CarouselViewState extends State { - late double _itemExtent; - FladderCarouselController? _internalController; - FladderCarouselController get _controller => widget.controller ?? _internalController!; - - @override - void initState() { - super.initState(); - if (widget.controller == null) { - _internalController = FladderCarouselController(); - } - _controller._attach(this); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _itemExtent = widget.itemExtent; - } - - @override - void didUpdateWidget(covariant FladderCarousel 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 = FladderCarouselController(); - _controller._attach(this); - } - } - if (widget.itemExtent != oldWidget.itemExtent) { - _itemExtent = widget.itemExtent; - } - } - - @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; - } - } - - @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); - final EdgeInsets effectivePadding = widget.itemPadding ?? 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))); - - return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - final double mainAxisExtent = switch (widget.scrollDirection) { - Axis.horizontal => constraints.maxWidth, - Axis.vertical => constraints.maxHeight, - }; - _itemExtent = clampDouble(_itemExtent, 0, mainAxisExtent); - - return Scrollable( - axisDirection: axisDirection, - scrollBehavior: MyCustomScrollBehavior(), - 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: [ - _SliverFixedExtentCarousel( - itemExtent: _itemExtent, - minExtent: widget.shrinkExtent, - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return Padding( - padding: effectivePadding.add(EdgeInsets.only( - left: index == 0 ? widget.padding.left : 0, - right: index == widget.children.length - 1 ? widget.padding.right : 0, - )), - child: Material( - clipBehavior: Clip.antiAlias, - color: effectiveBackgroundColor, - elevation: effectiveElevation, - shape: effectiveShape, - child: Stack( - fit: StackFit.expand, - children: [ - widget.children.elementAt(index), - if (widget.onTap != null || widget.onSecondaryTap != null || widget.onLongPress != null) - Material( - color: Colors.transparent, - child: InkWell( - onTap: widget.onTap != null ? () => widget.onTap!.call(index) : null, - onLongPress: - widget.onLongPress != null ? () => widget.onLongPress!.call(index) : null, - onSecondaryTapDown: widget.onSecondaryTap != null - ? (details) => widget.onSecondaryTap!.call((index, details)) - : null, - overlayColor: widget.overlayColor ?? - WidgetStateProperty.resolveWith((Set states) { - if (states.contains(WidgetState.pressed)) { - return theme.colorScheme.onSurface.withValues(alpha: 0.1); - } - if (states.contains(WidgetState.hovered)) { - return theme.colorScheme.onSurface.withValues(alpha: 0.08); - } - if (states.contains(WidgetState.focused)) { - return theme.colorScheme.onSurface.withValues(alpha: 0.1); - } - return null; - }), - ), - ), - ], - ), - ), - ); - }, - childCount: widget.children.length, - ), - ), - ], - ); - }, - ); - }); - } -} - -/// 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 squished 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 squished to be less than `effectievMinExtent`, - // then it should stop changinng 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; -} - -/// Scroll physics used by a [FladderCarousel]. -/// -/// 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 [FladderCarousel]. - const CarouselScrollPhysics({super.parent}); - - @override - CarouselScrollPhysics applyTo(ScrollPhysics? ancestor) { - return CarouselScrollPhysics(parent: buildParent(ancestor)); - } - - double _getTargetPixels( - _CarouselPosition position, - Tolerance tolerance, - double velocity, - ) { - double fraction; - fraction = position.itemExtent! / position.viewportDimension; - - 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 FladderCarouselController', - ); - - 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 [FladderCarousel]. -class _CarouselMetrics extends FixedScrollMetrics { - /// Creates an immutable snapshot of values associated with a [FladderCarousel]. - _CarouselMetrics({ - required super.minScrollExtent, - required super.maxScrollExtent, - required super.pixels, - required super.viewportDimension, - required super.axisDirection, - this.itemExtent, - required super.devicePixelRatio, - }); - - /// Extent for the carousel item. - /// - /// Used to compute the first item from the current [pixels]. - final double? itemExtent; - - @override - _CarouselMetrics copyWith({ - double? minScrollExtent, - double? maxScrollExtent, - double? pixels, - double? viewportDimension, - AxisDirection? axisDirection, - double? itemExtent, - 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, - devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, - ); - } -} - -class _CarouselPosition extends ScrollPositionWithSingleContext implements _CarouselMetrics { - _CarouselPosition({ - required super.physics, - required super.context, - this.initialItem = 0, - required this.itemExtent, - super.oldPosition, - }) : _itemToShowOnStartup = initialItem.toDouble(), - super(initialPixels: null); - - final 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 - double? itemExtent; - - double getItemFromPixels(double pixels, double viewportDimension) { - assert(viewportDimension > 0.0); - final double fraction = itemExtent! / viewportDimension; - - 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) { - final double fraction = itemExtent! / viewportDimension; - - 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 = _itemToShowOnStartup; - } 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); - // 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? layoutWeights, - 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, - devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, - ); - } -} - -/// A controller for [FladderCarousel]. -/// -/// Using a carousel controller helps to show the first visible item on the -/// carousel list. -class FladderCarouselController extends ScrollController { - /// Creates a carousel controller. - FladderCarouselController({ - this.initialItem = 0, - }); - - /// The item that expands to the maximum size when first creating the [FladderCarousel]. - 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); - final double itemExtent = _carouselState!._itemExtent; - - return _CarouselPosition( - physics: physics, - context: context, - initialItem: initialItem, - itemExtent: itemExtent, - oldPosition: oldPosition, - ); - } - - @override - void attach(ScrollPosition position) { - super.attach(position); - final _CarouselPosition carouselPosition = position as _CarouselPosition; - carouselPosition.itemExtent = _carouselState!._itemExtent; - } -}