Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
05d6e89
feat(carousel): add currentIndex to CarouselController and onItemChan…
Mairramer May 22, 2025
34ab347
fix docs
Mairramer May 22, 2025
59a0819
fix docs
Mairramer May 22, 2025
b9607f7
improve docs
Mairramer May 31, 2025
8c9209e
improves
Mairramer Jun 2, 2025
a50ab3d
improves docs
Mairramer Jun 5, 2025
e19d243
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Jun 9, 2025
7d3d2d1
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Jun 20, 2025
9ca3320
small nit
Mairramer Jun 25, 2025
0da4a8c
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Jun 25, 2025
2766ab3
improve tests
Mairramer Jun 28, 2025
4518dc8
fix
Mairramer Jun 28, 2025
f0266ae
small adjust
Mairramer Jun 28, 2025
d6bd04c
improve code
Mairramer Jun 29, 2025
88c0623
improve
Mairramer Jun 29, 2025
97ea420
small fix in doc
Mairramer Jun 30, 2025
552e4d5
refactor
Mairramer Jul 1, 2025
235ecd7
improve docs
Mairramer Jul 9, 2025
e234b01
fix small nit
Mairramer Jul 9, 2025
b7526e9
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Jul 22, 2025
bca206e
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Jul 23, 2025
d87d5c4
fix
Mairramer Jul 23, 2025
d5ea0ee
fixes logic
Mairramer Jul 26, 2025
492c3b8
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Jul 26, 2025
6a877b7
docs:: update
Mairramer Jul 28, 2025
c7602d6
improves
Mairramer Aug 5, 2025
bcb5f34
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Aug 5, 2025
e39651e
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Aug 5, 2025
2d5235c
Trigger Build
Mairramer Aug 5, 2025
106f14c
small fix
Mairramer Aug 5, 2025
b9123e7
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Nov 10, 2025
6ccb494
small adjustments
Mairramer Nov 10, 2025
f64dd2e
Add tests for CarouselView's onIndexChanged callback and visibility l…
Mairramer Nov 10, 2025
5f97911
fix
Mairramer Nov 10, 2025
c99dda3
Add tests for CarouselView's leadingIndex updates during animations a…
Mairramer Nov 12, 2025
d214b4e
fix
Mairramer Nov 12, 2025
3d96eb2
Improve documentation for CarouselController's leadingIndex property
Mairramer Nov 12, 2025
64bcd21
nit
Mairramer Nov 12, 2025
1adeda2
improve docs and fix some behavior
Mairramer Nov 15, 2025
3c3e375
nit
Mairramer Nov 18, 2025
b4dea44
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Nov 18, 2025
25db08e
improve docs
Mairramer Nov 18, 2025
e99a057
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Nov 25, 2025
365bda5
small improves
Mairramer Nov 25, 2025
969e481
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Dec 1, 2025
d9b2020
small improves
Mairramer Dec 2, 2025
217231f
fix
Mairramer Dec 2, 2025
b55e410
improve docs
Mairramer Dec 2, 2025
2487749
fix docs
Mairramer Dec 2, 2025
0fde232
fix doc
Mairramer Dec 8, 2025
c86c40c
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Dec 8, 2025
958f770
Merge branch 'master' into feature/add-currentIndex-and-onItemChanged…
Mairramer Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 105 additions & 14 deletions packages/flutter/lib/src/material/carousel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class CarouselView extends StatefulWidget {
this.enableSplash = true,
required double this.itemExtent,
required this.children,
this.onIndexChanged,
}) : consumeMaxWeight = true,
flexWeights = null,
itemBuilder = null,
Expand Down Expand Up @@ -209,6 +210,7 @@ class CarouselView extends StatefulWidget {
this.enableSplash = true,
required List<int> this.flexWeights,
required this.children,
this.onIndexChanged,
}) : itemExtent = null,
itemBuilder = null,
itemCount = null;
Expand Down Expand Up @@ -252,6 +254,7 @@ class CarouselView extends StatefulWidget {
required double this.itemExtent,
required this.itemBuilder,
this.itemCount,
this.onIndexChanged,
}) : consumeMaxWeight = true,
flexWeights = null,
children = const <Widget>[];
Expand Down Expand Up @@ -309,6 +312,7 @@ class CarouselView extends StatefulWidget {
required List<int> this.flexWeights,
required this.itemBuilder,
this.itemCount,
this.onIndexChanged,
}) : itemExtent = null,
children = const <Widget>[];

Expand Down Expand Up @@ -448,6 +452,46 @@ class CarouselView extends StatefulWidget {
/// The child widgets for the carousel.
final List<Widget> children;

/// {@template flutter.material.CarouselView.onIndexChanged}
/// A callback invoked when the leading item changes.
///
/// The “leading” item is the one that the carousel resolves as primary for
/// the current frame according to its layout algorithm. This item can be only
/// partially visible while scrolling.
///
/// - In a standard [CarouselView], the leading item is the one positioned at
/// the leading edge of the viewport based on the current scroll offset.
///
/// - In a [CarouselView.weighted], the leading item is chosen by the weighted
/// layout algorithm (typically the one with the greatest effective weight;
/// ties are resolved using proximity to the leading edge).
Comment on lines +465 to +467

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The documentation for onIndexChanged in a CarouselView.weighted is misleading. It describes the "primary" item (the one with the greatest weight), but the implementation and the leadingItem property refer to the item at the leading edge of the viewport, which might not be the primary one. This can cause confusion for developers using this callback. The documentation should be updated to accurately reflect that onIndexChanged reports the index of the item at the leading edge of the viewport.

  /// - In a [CarouselView.weighted], the leading item is also the one
  ///   positioned at the leading edge of the viewport. The primary item, which
  ///   is the one with the largest extent, may be a different item depending on
  ///   the `flexWeights`.

///
/// If `itemSnapping` is enabled, scrolling settles with the resolved leading
/// item fully visible when possible.
///
/// The callback fires only when the resolved leading index actually changes,
/// whether due to user interaction or programmatic scrolling.
/// {@endtemplate}
///
/// {@tool dartpad}
/// Example:
///
/// ```dart
/// CarouselView(
/// itemExtent: 200.0,
/// onIndexChanged: (int index) {
/// print('Leading item changed to: $index');
/// },
/// children: <Widget>[
/// Container(color: Colors.red),
/// Container(color: Colors.green),
/// Container(color: Colors.blue),
/// ],
/// )
/// ```
/// {@end-tool}
final ValueChanged<int>? onIndexChanged;

/// Called to build carousel item on demand.
///
/// Will be called only for indices greater than or equal to zero and less
Expand All @@ -472,6 +516,7 @@ class _CarouselViewState extends State<CarouselView> {
bool get _consumeMaxWeight => widget.consumeMaxWeight;
CarouselController? _internalController;
CarouselController get _controller => widget.controller ?? _internalController!;
late int _lastReportedLeadingItem;

@override
void initState() {
Expand All @@ -480,6 +525,7 @@ class _CarouselViewState extends State<CarouselView> {
if (widget.controller == null) {
_internalController = CarouselController();
}
_lastReportedLeadingItem = _getInitialLeadingItem();
_controller._attach(this);
}

Expand Down Expand Up @@ -518,6 +564,15 @@ class _CarouselViewState extends State<CarouselView> {
super.dispose();
}

int _getInitialLeadingItem() {
if (widget.flexWeights != null) {
final int maxWeight = widget.flexWeights!.max;
final int firstMaxWeightIndex = widget.flexWeights!.indexOf(maxWeight);
return _controller.initialItem - firstMaxWeightIndex;
}
return _controller.initialItem;
}

AxisDirection _getDirection(BuildContext context) {
switch (widget.scrollDirection) {
case Axis.horizontal:
Expand Down Expand Up @@ -636,21 +691,35 @@ class _CarouselViewState extends State<CarouselView> {
_itemExtent = widget.itemExtent == null
? null
: clampDouble(widget.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)],
);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
widget.onIndexChanged != null &&
notification is ScrollUpdateNotification) {
final ScrollPosition position = _controller.position;
final int currentLeadingIndex = (position as _CarouselPosition).leadingItem;
if (currentLeadingIndex != _lastReportedLeadingItem) {
_lastReportedLeadingItem = currentLeadingIndex;
widget.onIndexChanged?.call(currentLeadingIndex);
}
}
return false;
},
child: 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)],
);
},
),
);
},
);
Expand Down Expand Up @@ -1567,6 +1636,12 @@ class _CarouselPosition extends ScrollPositionWithSingleContext implements _Caro
_flexWeights = value;
}

// The index of the leading item in the carousel.
// getItemFromPixels may return a fractional value (e.g., 0.6 when mid-scroll).
// We use toInt() to truncate the fractional part, ensuring the leading item
// only advances after fully crossing the next item's boundary.
int get leadingItem => getItemFromPixels(pixels, viewportDimension).toInt();

double updateLeadingItem(List<int>? newFlexWeights, bool newConsumeMaxWeight) {
final double maxItem;
if (hasPixels && flexWeights != null) {
Expand Down Expand Up @@ -1704,6 +1779,22 @@ class CarouselController extends ScrollController {
/// The item that expands to the maximum size when first creating the [CarouselView].
final int initialItem;

/// The current leading item index in the [CarouselView].
///
/// {@macro flutter.material.CarouselView.onIndexChanged}
int get leadingItem {
assert(
positions.isNotEmpty,
'CarouselController.leadingItem cannot be accessed before a CarouselView is built with it.',
);
assert(
positions.length == 1,
'CarouselController.leadingItem cannot be read when multiple CarouselViews '
'are attached to the same controller.',
);
return (position as _CarouselPosition).leadingItem;
}

_CarouselViewState? _carouselState;

// ignore: use_setters_to_change_properties
Expand Down
Loading