From eb0fafcc41d9187201cf600fe7ba9aa5289e18eb Mon Sep 17 00:00:00 2001 From: Birgitt Majas Date: Wed, 6 Mar 2024 12:54:41 +0200 Subject: [PATCH] [MDS-1019] Fix carousel bugs --- .../lib/src/storybook/stories/carousel.dart | 235 ++++++++++-------- lib/src/widgets/carousel/carousel.dart | 30 ++- 2 files changed, 150 insertions(+), 115 deletions(-) diff --git a/example/lib/src/storybook/stories/carousel.dart b/example/lib/src/storybook/stories/carousel.dart index 8d31bab8..b34909bc 100644 --- a/example/lib/src/storybook/stories/carousel.dart +++ b/example/lib/src/storybook/stories/carousel.dart @@ -1,3 +1,4 @@ +import 'package:example/src/storybook/common/color_options.dart'; import 'package:example/src/storybook/common/widgets/text_divider.dart'; import 'package:flutter/material.dart'; import 'package:moon_design/moon_design.dart'; @@ -19,11 +20,23 @@ class _CarouselStoryState extends State { @override Widget build(BuildContext context) { + final backgroundColorKnob = context.knobs.nullable.options( + label: "Background color", + description: "MoonColors variants for MoonCarousel item background.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final backgroundColor = colorTable(context)[backgroundColorKnob ?? 40]; + final itemExtentKnob = context.knobs.nullable.sliderInt( label: "itemExtent", description: "Extent for MoonCarousel item.", enabled: false, initial: 114, + min: 1, max: MediaQuery.of(context).size.width.round(), ); @@ -72,128 +85,136 @@ class _CarouselStoryState extends State { ); return Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 64.0, horizontal: 16.0), - child: Column( - children: [ - const TextDivider( - text: "MoonCarousel", - paddingTop: 0, - ), - SizedBox( - height: 114, - child: OverflowBox( - maxWidth: MediaQuery.of(context).size.width, - child: MoonCarousel( - velocityFactor: velocityFactorKnob ?? 0.5, - gap: gapKnob?.toDouble() ?? 8, - autoPlay: autoPlayKnob, - itemCount: 10, - itemExtent: itemExtentKnob?.toDouble() ?? 114, - isCentered: isCenteredKnob, - anchor: anchorKnob ?? 0.041, - loop: isLoopedKnob, - clampMaxExtent: clampMaxExtentKnob, - itemBuilder: (BuildContext context, int itemIndex, int realIndex) => Container( - decoration: ShapeDecoration( - color: context.moonColors!.goku, - shape: MoonSquircleBorder( - borderRadius: BorderRadius.circular(12).squircleBorderRadius(context), - ), - ), - child: Center( - child: Text("${itemIndex + 1}"), - ), - ), - ), - ), - ), - const TextDivider(text: "Custom MoonCarousel with extras"), - Column( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 64.0, horizontal: 16.0), + child: Column( children: [ + const TextDivider( + text: "MoonCarousel", + paddingTop: 0, + ), SizedBox( - height: 180, + height: 114, child: OverflowBox( - maxWidth: MediaQuery.of(context).size.width, - child: Stack( - children: [ - MoonCarousel( - gap: 48, - controller: carouselController, - autoPlay: autoPlayKnob, - itemCount: 5, - itemExtent: MediaQuery.of(context).size.width - 64, - loop: isLoopedKnob, - onIndexChanged: (int index) => setState(() => selectedDot = index), - itemBuilder: (BuildContext context, int itemIndex, int realIndex) => Container( - decoration: ShapeDecoration( - color: context.moonColors!.goku, - shape: MoonSquircleBorder( - borderRadius: BorderRadius.circular(12).squircleBorderRadius(context), - ), - ), - child: Center( - child: Text("${itemIndex + 1}"), - ), + maxWidth: constraints.maxWidth, + child: MoonCarousel( + velocityFactor: velocityFactorKnob ?? 0.5, + gap: gapKnob?.toDouble() ?? 8, + autoPlay: autoPlayKnob, + itemCount: 10, + itemExtent: itemExtentKnob?.toDouble() ?? 114, + isCentered: isCenteredKnob, + anchor: anchorKnob ?? 16 / (constraints.maxWidth - 16), + loop: isLoopedKnob, + clampMaxExtent: clampMaxExtentKnob, + itemBuilder: (BuildContext context, int itemIndex, int _) => Container( + decoration: ShapeDecoration( + color: backgroundColor ?? context.moonColors!.goku, + shape: MoonSquircleBorder( + borderRadius: BorderRadius.circular(12).squircleBorderRadius(context), ), ), - Align( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MoonButton.icon( - buttonSize: MoonButtonSize.sm, - showBorder: true, - icon: Icon( - Directionality.of(context) == TextDirection.ltr - ? MoonIcons.controls_chevron_left_small_24_light - : MoonIcons.controls_chevron_right_small_24_light, - ), - decoration: ShapeDecorationWithPremultipliedAlpha( - color: context.moonColors!.goku, - shadows: context.moonShadows!.sm, - shape: MoonSquircleBorder( - borderRadius: BorderRadius.circular(8).squircleBorderRadius(context), - ), + child: Center( + child: Text("${itemIndex + 1}"), + ), + ), + ), + ), + ), + const TextDivider(text: "Custom MoonCarousel with extras"), + Column( + children: [ + SizedBox( + height: 180, + child: OverflowBox( + maxWidth: constraints.maxWidth, + child: Stack( + children: [ + MoonCarousel( + gap: 48, + controller: carouselController, + autoPlay: autoPlayKnob, + itemCount: 5, + itemExtent: constraints.maxWidth - 64, + loop: isLoopedKnob, + onIndexChanged: (int index) => setState(() => selectedDot = index), + itemBuilder: (BuildContext context, int itemIndex, int _) => Container( + decoration: ShapeDecoration( + color: backgroundColor ?? context.moonColors!.goku, + shape: MoonSquircleBorder( + borderRadius: BorderRadius.circular(12).squircleBorderRadius(context), ), - onTap: selectedDot == 0 ? null : () => carouselController.previousItem(), ), - MoonButton.icon( - buttonSize: MoonButtonSize.sm, - showBorder: true, - icon: Icon( - Directionality.of(context) == TextDirection.ltr - ? MoonIcons.controls_chevron_right_small_24_light - : MoonIcons.controls_chevron_left_small_24_light, - ), - decoration: ShapeDecorationWithPremultipliedAlpha( - color: context.moonColors!.goku, - shadows: context.moonShadows!.sm, - shape: MoonSquircleBorder( - borderRadius: BorderRadius.circular(8).squircleBorderRadius(context), + child: Center( + child: Text("${itemIndex + 1}"), + ), + ), + ), + Align( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MoonButton.icon( + buttonSize: MoonButtonSize.sm, + showBorder: true, + icon: Icon( + Directionality.of(context) == TextDirection.ltr + ? MoonIcons.controls_chevron_left_small_24_light + : MoonIcons.controls_chevron_right_small_24_light, + ), + decoration: ShapeDecorationWithPremultipliedAlpha( + color: context.moonColors!.goku, + shadows: context.moonShadows!.sm, + shape: MoonSquircleBorder( + borderRadius: BorderRadius.circular(8).squircleBorderRadius(context), + ), + ), + onTap: selectedDot == 0 && !isLoopedKnob + ? null + : () => carouselController.previousItem(), ), - ), - onTap: selectedDot == 4 ? null : () => carouselController.nextItem(), + MoonButton.icon( + buttonSize: MoonButtonSize.sm, + showBorder: true, + icon: Icon( + Directionality.of(context) == TextDirection.ltr + ? MoonIcons.controls_chevron_right_small_24_light + : MoonIcons.controls_chevron_left_small_24_light, + ), + decoration: ShapeDecorationWithPremultipliedAlpha( + color: context.moonColors!.goku, + shadows: context.moonShadows!.sm, + shape: MoonSquircleBorder( + borderRadius: BorderRadius.circular(8).squircleBorderRadius(context), + ), + ), + onTap: selectedDot == 4 && !isLoopedKnob + ? null + : () => carouselController.nextItem(), + ), + ], ), - ], + ), ), - ), + ], ), - ], + ), ), - ), - ), - const SizedBox(height: 16), - MoonDotIndicator( - selectedDot: selectedDot, - dotCount: 5, + const SizedBox(height: 16), + MoonDotIndicator( + selectedDot: selectedDot, + dotCount: 5, + ), + ], ), ], ), - ], - ), + ); + }, ), ); } diff --git a/lib/src/widgets/carousel/carousel.dart b/lib/src/widgets/carousel/carousel.dart index e6f0907a..4efc990c 100644 --- a/lib/src/widgets/carousel/carousel.dart +++ b/lib/src/widgets/carousel/carousel.dart @@ -119,14 +119,12 @@ class MoonCarousel extends StatefulWidget { } class _MoonCarouselState extends State { - final Key _forwardListKey = const ValueKey("moon_carousel_key"); - - late double _effectiveGap = 0; - + late double _effectiveGap; late int _lastReportedItemIndex; - late MoonCarouselScrollController _scrollController; + final Key _forwardListKey = const ValueKey("moon_carousel_key"); + // Calculates the anchor position for the viewport to center the selected item when 'isCentered' is true. double _getCenteredAnchor(BoxConstraints constraints) { if (!widget.isCentered) return widget.anchor; @@ -136,6 +134,17 @@ class _MoonCarouselState extends State { return ((maxExtent / 2) - (widget.itemExtent / 2)) / maxExtent; } + // Determines whether the carousel's anchored content surpasses the viewport's width. + // If not, clamping is not applicable. + bool _clampMaxExtent(double viewportWidth) { + final double itemsWidth = widget.itemCount * widget.itemExtent; + final double gapWidth = (widget.itemCount - 1) * _effectiveGap; + final double anchor = viewportWidth * widget.anchor * 2; + final double totalWidth = itemsWidth + gapWidth + anchor; + + return totalWidth >= viewportWidth && widget.clampMaxExtent; + } + AxisDirection _getDirection(BuildContext context) { switch (widget.axisDirection) { case Axis.horizontal: @@ -159,6 +168,8 @@ class _MoonCarouselState extends State { _lastReportedItemIndex = _scrollController.initialItem; + _effectiveGap = widget.gap ?? context.moonTheme?.carouselTheme.properties.gap ?? MoonSizes.sizes.x2s; + if (widget.autoPlay) { WidgetsBinding.instance.addPostFrameCallback((Duration _) { final Duration effectiveAutoPlayDelay = widget.autoPlayDelay ?? @@ -209,6 +220,9 @@ class _MoonCarouselState extends State { _scrollController.stopAutoplay(); } } + if (widget.gap != oldWidget.gap) { + _effectiveGap = widget.gap ?? context.moonTheme?.carouselTheme.properties.gap ?? MoonSizes.sizes.x2s; + } } @override @@ -219,8 +233,6 @@ class _MoonCarouselState extends State { } List _buildSlivers(BuildContext context, {required AxisDirection axisDirection}) { - _effectiveGap = widget.gap ?? context.moonTheme?.carouselTheme.properties.gap ?? MoonSizes.sizes.x2s; - final EdgeInsetsDirectional resolvedPadding = widget.axisDirection == Axis.horizontal ? EdgeInsetsDirectional.only(end: _effectiveGap) : EdgeInsetsDirectional.only(bottom: _effectiveGap); @@ -303,6 +315,7 @@ class _MoonCarouselState extends State { child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final double centeredAnchor = _getCenteredAnchor(constraints); + final bool clampMaxExtent = _clampMaxExtent(constraints.maxWidth); return IconTheme( data: IconThemeData( @@ -314,7 +327,7 @@ class _MoonCarouselState extends State { anchor: centeredAnchor, axisDirection: axisDirection, controller: _scrollController, - clampMaxExtent: widget.clampMaxExtent, + clampMaxExtent: clampMaxExtent, gap: _effectiveGap, itemCount: widget.itemCount, itemExtent: widget.itemExtent + _effectiveGap, @@ -422,6 +435,7 @@ class MoonCarouselScrollController extends ScrollController { super.dispose(); } + /// Returns the index of the currently selected item. If [MoonCarousel.loop] is true it provides the modded index value. int get selectedItem => _getTrueIndex( (position as _MoonCarouselScrollPosition).itemIndex,