Skip to content

Commit 43e0693

Browse files
authored
Merge pull request #1 from Mairramer/feature/add-currentIndex-and-onItemChanged-to-carousel
Merge master into current branch
2 parents f8b88be + 958f770 commit 43e0693

File tree

2 files changed

+425
-14
lines changed

2 files changed

+425
-14
lines changed

packages/flutter/lib/src/material/carousel.dart

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class CarouselView extends StatefulWidget {
145145
this.enableSplash = true,
146146
required double this.itemExtent,
147147
required this.children,
148+
this.onIndexChanged,
148149
}) : consumeMaxWeight = true,
149150
flexWeights = null,
150151
itemBuilder = null,
@@ -209,6 +210,7 @@ class CarouselView extends StatefulWidget {
209210
this.enableSplash = true,
210211
required List<int> this.flexWeights,
211212
required this.children,
213+
this.onIndexChanged,
212214
}) : itemExtent = null,
213215
itemBuilder = null,
214216
itemCount = null;
@@ -252,6 +254,7 @@ class CarouselView extends StatefulWidget {
252254
required double this.itemExtent,
253255
required this.itemBuilder,
254256
this.itemCount,
257+
this.onIndexChanged,
255258
}) : consumeMaxWeight = true,
256259
flexWeights = null,
257260
children = const <Widget>[];
@@ -309,6 +312,7 @@ class CarouselView extends StatefulWidget {
309312
required List<int> this.flexWeights,
310313
required this.itemBuilder,
311314
this.itemCount,
315+
this.onIndexChanged,
312316
}) : itemExtent = null,
313317
children = const <Widget>[];
314318

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

455+
/// {@template flutter.material.CarouselView.onIndexChanged}
456+
/// A callback invoked when the leading item changes.
457+
///
458+
/// The “leading” item is the one that the carousel resolves as primary for
459+
/// the current frame according to its layout algorithm. This item can be only
460+
/// partially visible while scrolling.
461+
///
462+
/// - In a standard [CarouselView], the leading item is the one positioned at
463+
/// the leading edge of the viewport based on the current scroll offset.
464+
///
465+
/// - In a [CarouselView.weighted], the leading item is chosen by the weighted
466+
/// layout algorithm (typically the one with the greatest effective weight;
467+
/// ties are resolved using proximity to the leading edge).
468+
///
469+
/// If `itemSnapping` is enabled, scrolling settles with the resolved leading
470+
/// item fully visible when possible.
471+
///
472+
/// The callback fires only when the resolved leading index actually changes,
473+
/// whether due to user interaction or programmatic scrolling.
474+
/// {@endtemplate}
475+
///
476+
/// {@tool dartpad}
477+
/// Example:
478+
///
479+
/// ```dart
480+
/// CarouselView(
481+
/// itemExtent: 200.0,
482+
/// onIndexChanged: (int index) {
483+
/// print('Leading item changed to: $index');
484+
/// },
485+
/// children: <Widget>[
486+
/// Container(color: Colors.red),
487+
/// Container(color: Colors.green),
488+
/// Container(color: Colors.blue),
489+
/// ],
490+
/// )
491+
/// ```
492+
/// {@end-tool}
493+
final ValueChanged<int>? onIndexChanged;
494+
451495
/// Called to build carousel item on demand.
452496
///
453497
/// Will be called only for indices greater than or equal to zero and less
@@ -472,6 +516,7 @@ class _CarouselViewState extends State<CarouselView> {
472516
bool get _consumeMaxWeight => widget.consumeMaxWeight;
473517
CarouselController? _internalController;
474518
CarouselController get _controller => widget.controller ?? _internalController!;
519+
late int _lastReportedLeadingItem;
475520

476521
@override
477522
void initState() {
@@ -480,6 +525,7 @@ class _CarouselViewState extends State<CarouselView> {
480525
if (widget.controller == null) {
481526
_internalController = CarouselController();
482527
}
528+
_lastReportedLeadingItem = _getInitialLeadingItem();
483529
_controller._attach(this);
484530
}
485531

@@ -518,6 +564,15 @@ class _CarouselViewState extends State<CarouselView> {
518564
super.dispose();
519565
}
520566

567+
int _getInitialLeadingItem() {
568+
if (widget.flexWeights != null) {
569+
final int maxWeight = widget.flexWeights!.max;
570+
final int firstMaxWeightIndex = widget.flexWeights!.indexOf(maxWeight);
571+
return _controller.initialItem - firstMaxWeightIndex;
572+
}
573+
return _controller.initialItem;
574+
}
575+
521576
AxisDirection _getDirection(BuildContext context) {
522577
switch (widget.scrollDirection) {
523578
case Axis.horizontal:
@@ -636,21 +691,35 @@ class _CarouselViewState extends State<CarouselView> {
636691
_itemExtent = widget.itemExtent == null
637692
? null
638693
: clampDouble(widget.itemExtent!, 0, mainAxisExtent);
639-
640-
return Scrollable(
641-
axisDirection: axisDirection,
642-
controller: _controller,
643-
physics: physics,
644-
viewportBuilder: (BuildContext context, ViewportOffset position) {
645-
return Viewport(
646-
cacheExtent: 0.0,
647-
cacheExtentStyle: CacheExtentStyle.viewport,
648-
axisDirection: axisDirection,
649-
offset: position,
650-
clipBehavior: Clip.antiAlias,
651-
slivers: <Widget>[_buildSliverCarousel(theme)],
652-
);
694+
return NotificationListener<ScrollNotification>(
695+
onNotification: (ScrollNotification notification) {
696+
if (notification.depth == 0 &&
697+
widget.onIndexChanged != null &&
698+
notification is ScrollUpdateNotification) {
699+
final ScrollPosition position = _controller.position;
700+
final int currentLeadingIndex = (position as _CarouselPosition).leadingItem;
701+
if (currentLeadingIndex != _lastReportedLeadingItem) {
702+
_lastReportedLeadingItem = currentLeadingIndex;
703+
widget.onIndexChanged?.call(currentLeadingIndex);
704+
}
705+
}
706+
return false;
653707
},
708+
child: Scrollable(
709+
axisDirection: axisDirection,
710+
controller: _controller,
711+
physics: physics,
712+
viewportBuilder: (BuildContext context, ViewportOffset position) {
713+
return Viewport(
714+
cacheExtent: 0.0,
715+
cacheExtentStyle: CacheExtentStyle.viewport,
716+
axisDirection: axisDirection,
717+
offset: position,
718+
clipBehavior: Clip.antiAlias,
719+
slivers: <Widget>[_buildSliverCarousel(theme)],
720+
);
721+
},
722+
),
654723
);
655724
},
656725
);
@@ -1567,6 +1636,12 @@ class _CarouselPosition extends ScrollPositionWithSingleContext implements _Caro
15671636
_flexWeights = value;
15681637
}
15691638

1639+
// The index of the leading item in the carousel.
1640+
// getItemFromPixels may return a fractional value (e.g., 0.6 when mid-scroll).
1641+
// We use toInt() to truncate the fractional part, ensuring the leading item
1642+
// only advances after fully crossing the next item's boundary.
1643+
int get leadingItem => getItemFromPixels(pixels, viewportDimension).toInt();
1644+
15701645
double updateLeadingItem(List<int>? newFlexWeights, bool newConsumeMaxWeight) {
15711646
final double maxItem;
15721647
if (hasPixels && flexWeights != null) {
@@ -1704,6 +1779,22 @@ class CarouselController extends ScrollController {
17041779
/// The item that expands to the maximum size when first creating the [CarouselView].
17051780
final int initialItem;
17061781

1782+
/// The current leading item index in the [CarouselView].
1783+
///
1784+
/// {@macro flutter.material.CarouselView.onIndexChanged}
1785+
int get leadingItem {
1786+
assert(
1787+
positions.isNotEmpty,
1788+
'CarouselController.leadingItem cannot be accessed before a CarouselView is built with it.',
1789+
);
1790+
assert(
1791+
positions.length == 1,
1792+
'CarouselController.leadingItem cannot be read when multiple CarouselViews '
1793+
'are attached to the same controller.',
1794+
);
1795+
return (position as _CarouselPosition).leadingItem;
1796+
}
1797+
17071798
_CarouselViewState? _carouselState;
17081799

17091800
// ignore: use_setters_to_change_properties

0 commit comments

Comments
 (0)