Skip to content

Commit

Permalink
fix: [MDS-1019] Fix carousel bugs (#373)
Browse files Browse the repository at this point in the history
Co-authored-by: Birgitt Majas <birgitt.majas@yolo.com>
  • Loading branch information
GittHub-d and Birgitt Majas committed Mar 6, 2024
1 parent bfe2e50 commit dc7f93f
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 120 deletions.
235 changes: 128 additions & 107 deletions example/lib/src/storybook/stories/carousel.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,11 +20,23 @@ class _CarouselStoryState extends State<CarouselStory> {

@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(),
);

Expand Down Expand Up @@ -72,128 +85,136 @@ class _CarouselStoryState extends State<CarouselStory> {
);

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,
),
],
),
],
),
],
),
);
},
),
);
}
Expand Down
30 changes: 22 additions & 8 deletions lib/src/widgets/carousel/carousel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,12 @@ class MoonCarousel extends StatefulWidget {
}

class _MoonCarouselState extends State<MoonCarousel> {
final Key _forwardListKey = const ValueKey<String>("moon_carousel_key");

late double _effectiveGap = 0;

late double _effectiveGap;
late int _lastReportedItemIndex;

late MoonCarouselScrollController _scrollController;

final Key _forwardListKey = const ValueKey<String>("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;
Expand All @@ -136,6 +134,17 @@ class _MoonCarouselState extends State<MoonCarousel> {
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:
Expand All @@ -159,6 +168,8 @@ class _MoonCarouselState extends State<MoonCarousel> {

_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 ??
Expand Down Expand Up @@ -209,6 +220,9 @@ class _MoonCarouselState extends State<MoonCarousel> {
_scrollController.stopAutoplay();
}
}
if (widget.gap != oldWidget.gap) {
_effectiveGap = widget.gap ?? context.moonTheme?.carouselTheme.properties.gap ?? MoonSizes.sizes.x2s;
}
}

@override
Expand All @@ -219,8 +233,6 @@ class _MoonCarouselState extends State<MoonCarousel> {
}

List<Widget> _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);
Expand Down Expand Up @@ -303,6 +315,7 @@ class _MoonCarouselState extends State<MoonCarousel> {
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double centeredAnchor = _getCenteredAnchor(constraints);
final bool clampMaxExtent = _clampMaxExtent(constraints.maxWidth);

return IconTheme(
data: IconThemeData(
Expand All @@ -314,7 +327,7 @@ class _MoonCarouselState extends State<MoonCarousel> {
anchor: centeredAnchor,
axisDirection: axisDirection,
controller: _scrollController,
clampMaxExtent: widget.clampMaxExtent,
clampMaxExtent: clampMaxExtent,
gap: _effectiveGap,
itemCount: widget.itemCount,
itemExtent: widget.itemExtent + _effectiveGap,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit dc7f93f

Please sign in to comment.