Create CarouselView widget - Part 2 (#149775)

This PR is to create `Carousel.weighted` so the size of each carousel item is based on a list of weights. While scrolling, item sizes are changing dynamically based on the scrolling progress.

https://github.com/flutter/flutter/assets/36861262/181472b0-6f8b-48e7-b191-ab5f7c88c0c8
This commit is contained in:
Qun Cheng 2024-07-17 12:56:01 -07:00 committed by GitHub
parent 6d0bb94456
commit 844eb2fe76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1761 additions and 118 deletions

View File

@ -17,7 +17,14 @@ class CarouselExampleApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Carousel Sample'),
leading: const Icon(Icons.cast),
title: const Text('Flutter TV'),
actions: const <Widget>[
Padding(
padding: EdgeInsetsDirectional.only(end: 16.0),
child: CircleAvatar(child: Icon(Icons.account_circle)),
),
],
),
body: const CarouselExample(),
),
@ -33,19 +40,140 @@ class CarouselExample extends StatefulWidget {
}
class _CarouselExampleState extends State<CarouselExample> {
final CarouselController controller = CarouselController(initialItem: 1);
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView(
itemExtent: 330,
shrinkExtent: 200,
children: List<Widget>.generate(20, (int index) {
return UncontainedLayoutCard(index: index, label: 'Item $index');
}),
final double height = MediaQuery.sizeOf(context).height;
return ListView(
children: <Widget>[
ConstrainedBox(
constraints: BoxConstraints(maxHeight: height / 2),
child: CarouselView.weighted(
controller: controller,
itemSnapping: true,
flexWeights: const <int>[1, 7, 1],
children: ImageInfo.values.map((ImageInfo image) {
return HeroLayoutCard(imageInfo: image);
}).toList(),
),
),
),
const SizedBox(height: 20),
const Padding(
padding: EdgeInsetsDirectional.only(top: 8.0, start: 8.0),
child: Text('Multi-browse layout'),
),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 50),
child: CarouselView.weighted(
flexWeights: const <int>[1, 2, 3, 2, 1],
consumeMaxWeight: false,
children: List<Widget>.generate(20, (int index) {
return ColoredBox(
color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.8),
child: const SizedBox.expand(),
);
}),
),
),
const SizedBox(height: 20),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView.weighted(
flexWeights: const <int>[3, 3, 3, 2, 1],
consumeMaxWeight: false,
children: CardInfo.values.map((CardInfo info) {
return ColoredBox(
color: info.backgroundColor,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(info.icon, color: info.color, size: 32.0),
Text(info.label, style: const TextStyle(fontWeight: FontWeight.bold), overflow: TextOverflow.clip, softWrap: false),
],
),
),
);
}).toList()
),
),
const SizedBox(height: 20),
const Padding(
padding: EdgeInsetsDirectional.only(top: 8.0, start: 8.0),
child: Text('Uncontained layout'),
),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView(
itemExtent: 330,
shrinkExtent: 200,
children: List<Widget>.generate(20, (int index){
return UncontainedLayoutCard(index: index, label: 'Show $index');
}),
),
)
],
);
}
}
class HeroLayoutCard extends StatelessWidget {
const HeroLayoutCard({
super.key,
required this.imageInfo,
});
final ImageInfo imageInfo;
@override
Widget build(BuildContext context) {
final double width = MediaQuery.sizeOf(context).width;
return Stack(
alignment: AlignmentDirectional.bottomStart,
children: <Widget>[
ClipRect(
child: OverflowBox(
maxWidth: width * 7 / 8,
minWidth: width * 7 / 8,
child: Image(
fit: BoxFit.cover,
image: NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/material/${imageInfo.url}'
),
),
),
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
imageInfo.title,
overflow: TextOverflow.clip,
softWrap: false,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(color: Colors.white),
),
const SizedBox(height: 10),
Text(
imageInfo.subtitle,
overflow: TextOverflow.clip,
softWrap: false,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white),
)
],
),
),
]
);
}
}
@ -75,3 +203,34 @@ class UncontainedLayoutCard extends StatelessWidget {
);
}
}
enum CardInfo {
camera('Cameras', Icons.video_call, Color(0xff2354C7), Color(0xffECEFFD)),
lighting('Lighting', Icons.lightbulb, Color(0xff806C2A), Color(0xffFAEEDF)),
climate('Climate', Icons.thermostat, Color(0xffA44D2A), Color(0xffFAEDE7)),
wifi('Wifi', Icons.wifi, Color(0xff417345), Color(0xffE5F4E0)),
media('Media', Icons.library_music, Color(0xff2556C8), Color(0xffECEFFD)),
security('Security', Icons.crisis_alert, Color(0xff794C01), Color(0xffFAEEDF)),
safety('Safety', Icons.medical_services, Color(0xff2251C5), Color(0xffECEFFD)),
more('', Icons.add, Color(0xff201D1C), Color(0xffE3DFD8));
const CardInfo(this.label, this.icon, this.color, this.backgroundColor);
final String label;
final IconData icon;
final Color color;
final Color backgroundColor;
}
enum ImageInfo {
image0('The Flow', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_1.png'),
image1('Through the Pane', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_2.png'),
image2('Iridescence', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_3.png'),
image3('Sea Change', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_4.png'),
image4('Blue Symphony', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_5.png'),
image5('When It Rains', 'Sponsored | Season 1 Now Streaming', 'content_based_color_scheme_6.png');
const ImageInfo(this.title, this.subtitle, this.url);
final String title;
final String subtitle;
final String url;
}

View File

@ -2,18 +2,44 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/carousel/carousel.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
// The app being tested loads images via HTTP which the test
// framework defeats by default.
setUpAll(() {
HttpOverrides.global = null;
});
testWidgets('Carousel Smoke Test', (WidgetTester tester) async {
await tester.pumpWidget(
const example.CarouselExampleApp(),
);
expect(find.byType(CarouselView), findsOneWidget);
expect(find.widgetWithText(example.UncontainedLayoutCard, 'Item 0'), findsOneWidget);
expect(find.widgetWithText(example.UncontainedLayoutCard, 'Item 1'), findsOneWidget);
expect(find.widgetWithText(example.HeroLayoutCard, 'Through the Pane'), findsOneWidget);
final Finder firstCarousel = find.byType(CarouselView).first;
await tester.drag(firstCarousel, const Offset(150, 0));
await tester.pumpAndSettle();
expect(find.widgetWithText(example.HeroLayoutCard, 'The Flow'), findsOneWidget);
await tester.drag(firstCarousel, const Offset(0, -200));
await tester.pumpAndSettle();
expect(find.widgetWithText(CarouselView, 'Cameras'), findsOneWidget);
expect(find.widgetWithText(CarouselView, 'Lighting'), findsOneWidget);
expect(find.widgetWithText(CarouselView, 'Climate'), findsOneWidget);
expect(find.widgetWithText(CarouselView, 'Wifi'), findsOneWidget);
await tester.drag(find.widgetWithText(CarouselView, 'Cameras'), const Offset(0, -200));
await tester.pumpAndSettle();
expect(find.text('Uncontained layout'), findsOneWidget);
expect(find.widgetWithText(CarouselView, 'Show 0'), findsOneWidget);
expect(find.widgetWithText(CarouselView, 'Show 1'), findsOneWidget);
});
}

File diff suppressed because it is too large Load Diff

View File

@ -26,12 +26,12 @@ void main() {
),
);
final Finder carouselMaterial = find.descendant(
final Finder carouselViewMaterial = find.descendant(
of: find.byType(CarouselView),
matching: find.byType(Material),
).first;
final Material material = tester.widget<Material>(carouselMaterial);
final Material material = tester.widget<Material>(carouselViewMaterial);
expect(material.clipBehavior, Clip.antiAlias);
expect(material.color, colorScheme.surface);
expect(material.elevation, 0.0);
@ -80,13 +80,13 @@ void main() {
),
);
final Finder carouselMaterial = find.descendant(
final Finder carouselViewMaterial = find.descendant(
of: find.byType(CarouselView),
matching: find.byType(Material),
).first;
expect(tester.getSize(carouselMaterial).width, 200 - 20 - 20); // Padding is 20 on both side.
final Material material = tester.widget<Material>(carouselMaterial);
expect(tester.getSize(carouselViewMaterial).width, 200 - 20 - 20); // Padding is 20 on both side.
final Material material = tester.widget<Material>(carouselViewMaterial);
expect(material.color, Colors.amber);
expect(material.elevation, 10.0);
expect(material.shape, const StadiumBorder());
@ -110,7 +110,7 @@ void main() {
await gesture.removePointer();
// On focused.
final Element inkWellElement = tester.element(find.descendant(of: carouselMaterial, matching: find.byType(InkWell)));
final Element inkWellElement = tester.element(find.descendant(of: carouselViewMaterial, matching: find.byType(InkWell)));
expect(inkWellElement.widget, isA<InkWell>());
final InkWell inkWell = inkWellElement.widget as InkWell;
@ -145,12 +145,12 @@ void main() {
),
);
final Finder item1 = find.byKey(keys.elementAt(1));
final Finder item1 = find.byKey(keys[1]);
await tester.tap(find.ancestor(of: item1, matching: find.byType(Stack)));
await tester.pump();
expect(tapIndex, 1);
final Finder item2 = find.byKey(keys.elementAt(2));
final Finder item2 = find.byKey(keys[2]);
await tester.tap(find.ancestor(of: item2, matching: find.byType(Stack)));
await tester.pump();
expect(tapIndex, 2);
@ -194,15 +194,73 @@ void main() {
expect(find.text('Item 4'), findsNothing);
});
testWidgets('CarouselController initialItem', (WidgetTester tester) async {
final CarouselController controller = CarouselController(initialItem: 5);
addTearDown(controller.dispose);
testWidgets('CarouselView.weighted layout', (WidgetTester tester) async {
Widget buildCarouselView({ required List<int> weights }) {
return MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
flexWeights: weights,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
);
}
await tester.pumpWidget(buildCarouselView(weights: <int>[4, 3, 2, 1]));
final Size viewportSize = MediaQuery.of(tester.element(find.byType(CarouselView))).size;
expect(viewportSize, const Size(800, 600));
expect(find.text('Item 0'), findsOneWidget);
Rect rect0 = tester.getRect(getItem(0));
// Item width is 4/10 of the viewport.
expect(rect0, const Rect.fromLTRB(0.0, 0.0, 320.0, 600.0));
expect(find.text('Item 1'), findsOneWidget);
Rect rect1 = tester.getRect(getItem(1));
// Item width is 3/10 of the viewport.
expect(rect1, const Rect.fromLTRB(320.0, 0.0, 560.0, 600.0));
expect(find.text('Item 2'), findsOneWidget);
final Rect rect2 = tester.getRect(getItem(2));
// Item width is 2/10 of the viewport.
expect(rect2, const Rect.fromLTRB(560.0, 0.0, 720.0, 600.0));
expect(find.text('Item 3'), findsOneWidget);
final Rect rect3 = tester.getRect(getItem(3));
// Item width is 1/10 of the viewport.
expect(rect3, const Rect.fromLTRB(720.0, 0.0, 800.0, 600.0));
expect(find.text('Item 4'), findsNothing);
// Test shorter weight list.
await tester.pumpWidget(buildCarouselView(weights: <int>[7, 1]));
await tester.pumpAndSettle();
expect(viewportSize, const Size(800, 600));
expect(find.text('Item 0'), findsOneWidget);
rect0 = tester.getRect(getItem(0));
// Item width is 7/8 of the viewport.
expect(rect0, const Rect.fromLTRB(0.0, 0.0, 700.0, 600.0));
expect(find.text('Item 1'), findsOneWidget);
rect1 = tester.getRect(getItem(1));
// Item width is 1/8 of the viewport.
expect(rect1, const Rect.fromLTRB(700.0, 0.0, 800.0, 600.0));
expect(find.text('Item 2'), findsNothing);
});
testWidgets('CarouselController initialItem', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView(
controller: controller,
controller: CarouselController(initialItem: 5),
itemExtent: 400,
children: List<Widget>.generate(10, (int index) {
return Center(
@ -231,6 +289,79 @@ void main() {
expect(find.text('Item 7'), findsNothing);
});
testWidgets('CarouselView.weighted respects CarouselController.initialItem', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
controller: CarouselController(initialItem: 5),
flexWeights: const <int>[7, 1],
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
final Size viewportSize = MediaQuery.of(tester.element(find.byType(CarouselView))).size;
expect(viewportSize, const Size(800, 600));
expect(find.text('Item 5'), findsOneWidget);
final Rect rect5 = tester.getRect(getItem(5));
// Item width is 7/8 of the viewport.
expect(rect5, const Rect.fromLTRB(0.0, 0.0, 700.0, 600.0));
expect(find.text('Item 6'), findsOneWidget);
final Rect rect6 = tester.getRect(getItem(6));
// Item width is 1/8 of the viewport.
expect(rect6, const Rect.fromLTRB(700.0, 0.0, 800.0, 600.0));
expect(find.text('Item 4'), findsNothing);
expect(find.text('Item 7'), findsNothing);
});
testWidgets('The initialItem should be the first item with expanded size(max extent)', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
controller: CarouselController(initialItem: 5),
flexWeights: const <int>[1, 8, 1],
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
final Size viewportSize = MediaQuery.of(tester.element(find.byType(CarouselView))).size;
expect(viewportSize, const Size(800, 600));
// Item 5 should have be the expanded item.
expect(find.text('Item 5'), findsOneWidget);
final Rect rect5 = tester.getRect(getItem(5));
// Item width is 8/10 of the viewport.
expect(rect5, const Rect.fromLTRB(80.0, 0.0, 720.0, 600.0));
expect(find.text('Item 6'), findsOneWidget);
final Rect rect6 = tester.getRect(getItem(6));
// Item width is 1/10 of the viewport.
expect(rect6, const Rect.fromLTRB(720.0, 0.0, 800.0, 600.0));
expect(find.text('Item 4'), findsOneWidget);
final Rect rect4 = tester.getRect(getItem(4));
// Item width is 1/10 of the viewport.
expect(rect4, const Rect.fromLTRB(0.0, 0.0, 80.0, 600.0));
expect(find.text('Item 7'), findsNothing);
});
testWidgets('CarouselView respects itemSnapping', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
@ -279,6 +410,54 @@ void main() {
expect(getItem(3), findsOneWidget);
});
testWidgets('CarouselView.weighted respects itemSnapping', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
itemSnapping: true,
consumeMaxWeight: false,
flexWeights: const <int>[1, 7],
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
void checkOriginalExpectations() {
expect(getItem(0), findsOneWidget);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsNothing);
}
checkOriginalExpectations();
// Snap back to the original item.
await tester.drag(getItem(0), const Offset(-20, 0));
await tester.pumpAndSettle();
checkOriginalExpectations();
// Snap back to the original item.
await tester.drag(getItem(0), const Offset(50, 0));
await tester.pumpAndSettle();
checkOriginalExpectations();
// Snap to the next item.
await tester.drag(getItem(0), const Offset(-70, 0));
await tester.pumpAndSettle();
expect(getItem(0), findsNothing);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsNothing);
});
testWidgets('CarouselView respect itemSnapping when fling', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
@ -333,6 +512,69 @@ void main() {
expect(getItem(4), findsNothing);
});
testWidgets('CarouselView.weighted respect itemSnapping when fling', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
itemSnapping: true,
consumeMaxWeight: false,
flexWeights: const <int>[1, 8, 1],
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('$index'),
);
}),
),
),
)
);
await tester.pumpAndSettle();
Finder getItem(int index)
=> find.descendant(
of: find.byType(CarouselView),
matching: find.ancestor(
of: find.text('$index'),
matching: find.byType(Padding)
),
);
// Show item 0, 1, and 2.
expect(getItem(0), findsOneWidget);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsNothing);
// Should snap to item 2 because of a long drag(-100). Show item 2, 3 and 4.
await tester.fling(getItem(0), const Offset(-100, 0), 800);
await tester.pumpAndSettle();
expect(getItem(0), findsNothing);
expect(getItem(1), findsNothing);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsOneWidget);
expect(getItem(4), findsOneWidget);
// Fling to the next item (item 3). Show item 3, 4 and 5.
await tester.fling(getItem(2), const Offset(-50, 0), 800);
await tester.pumpAndSettle();
expect(getItem(2), findsNothing);
expect(getItem(3), findsOneWidget);
expect(getItem(4), findsOneWidget);
expect(getItem(5), findsOneWidget);
// Fling back to the previous item. Show item 2, 3 and 4.
await tester.fling(getItem(3), const Offset(50, 0), 800);
await tester.pumpAndSettle();
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsOneWidget);
expect(getItem(4), findsOneWidget);
expect(getItem(5), findsNothing);
});
testWidgets('CarouselView respects scrollingDirection: Axis.vertical', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
@ -367,6 +609,86 @@ void main() {
expect(getItem(3), findsOneWidget);
});
testWidgets('CarouselView.weighted respects scrollingDirection: Axis.vertical', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
flexWeights: const <int>[3, 2, 1],
padding: EdgeInsets.zero,
scrollDirection: Axis.vertical,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
await tester.pumpAndSettle();
expect(getItem(0), findsOneWidget);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsNothing);
final Rect rect0 = tester.getRect(getItem(0));
// Item width is 3/6 of the viewport.
expect(rect0, const Rect.fromLTRB(0.0, 0.0, 800.0, 300.0));
// Simulate a scroll up
await tester.drag(find.byType(CarouselView), const Offset(0, -300), kind: PointerDeviceKind.trackpad);
await tester.pumpAndSettle();
expect(getItem(0), findsNothing);
expect(getItem(3), findsOneWidget);
});
testWidgets('CarouselView.weighted respects scrollingDirection: Axis.vertical + itemSnapping: true', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
itemSnapping: true,
flexWeights: const <int>[3, 2, 1],
scrollDirection: Axis.vertical,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
await tester.pumpAndSettle();
expect(getItem(0), findsOneWidget);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsNothing);
final Rect rect0 = tester.getRect(getItem(0));
// Item width is 3/6 of the viewport.
expect(rect0, const Rect.fromLTRB(0.0, 0.0, 800.0, 300.0));
// Simulate a scroll up but less than half of the leading item, the leading
// item should go back to the original position because itemSnapping is set
// to true.
await tester.drag(find.byType(CarouselView), const Offset(0, -149), kind: PointerDeviceKind.trackpad);
await tester.pumpAndSettle();
expect(getItem(0), findsOneWidget);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsNothing);
// Simulate a scroll up more than half of the leading item, the leading
// item continue to scrolling and will disappear when animation ends because
// itemSnapping is set to true.
await tester.drag(find.byType(CarouselView), const Offset(0, -151), kind: PointerDeviceKind.trackpad);
await tester.pumpAndSettle();
expect(getItem(0), findsNothing);
expect(getItem(3), findsOneWidget);
});
testWidgets('CarouselView respects reverse', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
@ -374,7 +696,6 @@ void main() {
body: CarouselView(
itemExtent: 200,
reverse: true,
padding: EdgeInsets.zero,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
@ -407,6 +728,140 @@ void main() {
expect(rect3, const Rect.fromLTRB(0.0, 0.0, 200.0, 600.0));
});
testWidgets('CarouselView.weighted respects reverse', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
flexWeights: const <int>[4, 3, 2, 1],
reverse: true,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
await tester.pumpAndSettle();
expect(getItem(0), findsOneWidget);
final Rect rect0 = tester.getRect(getItem(0));
// Item 0 should be placed on the end of the screen.
const int item0Width = 80 * 4;
expect(rect0, const Rect.fromLTRB(800.0 - item0Width, 0.0, 800.0, 600.0));
expect(getItem(1), findsOneWidget);
final Rect rect1 = tester.getRect(getItem(1));
// Item 1 should be placed before item 0.
const int item1Width = 80 * 3;
expect(rect1, const Rect.fromLTRB(800.0 - item0Width - item1Width, 0.0, 800.0 - item0Width, 600.0));
expect(getItem(2), findsOneWidget);
final Rect rect2 = tester.getRect(getItem(2));
// Item 2 should be placed before item 1.
const int item2Width = 80 * 2;
expect(rect2, const Rect.fromLTRB(800.0 - item0Width - item1Width - item2Width, 0.0, 800.0 - item0Width - item1Width, 600.0));
expect(getItem(3), findsOneWidget);
final Rect rect3 = tester.getRect(getItem(3));
// Item 3 should be placed before item 2.
expect(rect3, const Rect.fromLTRB(0.0, 0.0, 800.0 - item0Width - item1Width - item2Width, 600.0));
});
testWidgets('CarouselView.weighted respects reverse + vertical scroll direction', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
reverse: true,
flexWeights: const <int>[4, 3, 2, 1],
scrollDirection: Axis.vertical,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
await tester.pumpAndSettle();
expect(getItem(0), findsOneWidget);
final Rect rect0 = tester.getRect(getItem(0));
// Item 0 should be placed on the end of the screen.
const int item0Height = 60 * 4;
expect(rect0, const Rect.fromLTRB(0.0, 600.0 - item0Height, 800.0, 600.0));
expect(getItem(1), findsOneWidget);
final Rect rect1 = tester.getRect(getItem(1));
// Item 1 should be placed before item 0.
const int item1Height = 60 * 3;
expect(rect1, const Rect.fromLTRB(0.0, 600.0 - item0Height - item1Height, 800.0, 600.0 - item0Height));
expect(getItem(2), findsOneWidget);
final Rect rect2 = tester.getRect(getItem(2));
// Item 2 should be placed before item 1.
const int item2Height = 60 * 2;
expect(rect2, const Rect.fromLTRB(0.0, 600.0 - item0Height - item1Height - item2Height, 800.0, 600.0 - item0Height - item1Height));
expect(getItem(3), findsOneWidget);
final Rect rect3 = tester.getRect(getItem(3));
// Item 3 should be placed before item 2.
expect(rect3, const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0 - item0Height - item1Height - item2Height));
});
testWidgets('CarouselView.weighted respects reverse + vertical scroll direction + itemSnapping', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
reverse: true,
flexWeights: const <int>[4, 3, 2, 1],
scrollDirection: Axis.vertical,
itemSnapping: true,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
await tester.pumpAndSettle();
expect(getItem(0), findsOneWidget);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsOneWidget);
expect(getItem(4), findsNothing);
final Rect rect0 = tester.getRect(getItem(0));
// Item height is 4/10 of the viewport.
expect(rect0, const Rect.fromLTRB(0.0, 360.0, 800.0, 600.0));
// Simulate a scroll down but less than half of the leading item, the leading
// item should go back to the original position because itemSnapping is set
// to true.
await tester.drag(find.byType(CarouselView), const Offset(0, 240 / 2 - 1), kind: PointerDeviceKind.trackpad);
await tester.pumpAndSettle();
expect(getItem(0), findsOneWidget);
expect(getItem(1), findsOneWidget);
expect(getItem(2), findsOneWidget);
expect(getItem(3), findsOneWidget);
expect(getItem(4), findsNothing);
// Simulate a scroll down more than half of the leading item, the leading
// item continue to scrolling and will disappear when animation ends because
// itemSnapping is set to true.
await tester.drag(find.byType(CarouselView), const Offset(0, 240 / 2 + 1), kind: PointerDeviceKind.trackpad);
await tester.pumpAndSettle();
expect(getItem(0), findsNothing);
expect(getItem(4), findsOneWidget);
});
testWidgets('CarouselView respects shrinkExtent', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
@ -445,6 +900,240 @@ void main() {
await tester.pump();
expect(tester.getRect(getItem(0)), const Rect.fromLTRB(-50, 0.0, 250, 600));
});
testWidgets('CarouselView.weighted respects consumeMaxWeight', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
flexWeights: const <int>[1, 2, 4, 2, 1],
itemSnapping: true,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
// The initial item is item 0. To make sure the layout stays the same, the
// first item should be placed at the middle of the screen and there are some
// white space as if there are two more shinked items before the first item.
final Rect rect0 = tester.getRect(getItem(0));
expect(rect0, const Rect.fromLTRB(240.0, 0.0, 560.0, 600.0));
for (int i = 0; i < 7; i++) {
await tester.drag(find.byType(CarouselView), const Offset(-80.0, 0.0));
await tester.pumpAndSettle();
}
// After scrolling the carousel 7 times, the last item(item 9) should be on
// the end of the screen.
expect(getItem(9), findsOneWidget);
expect(tester.getRect(getItem(9)), const Rect.fromLTRB(720.0, 0.0, 800.0, 600.0));
// Keep snapping twice. Item 9 should be fully expanded to the max size.
for (int i = 0; i < 2; i++) {
await tester.drag(find.byType(CarouselView), const Offset(-80.0, 0.0));
await tester.pumpAndSettle();
}
expect(getItem(9), findsOneWidget);
expect(tester.getRect(getItem(9)), const Rect.fromLTRB(240.0, 0.0, 560.0, 600.0));
});
testWidgets('The initialItem stays when the flexWeights is updated', (WidgetTester tester) async {
final CarouselController controller = CarouselController(initialItem: 3);
Widget buildCarousel(List<int> flexWeights) {
return MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
controller: controller,
flexWeights: flexWeights,
itemSnapping: true,
children: List<Widget>.generate(20, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
);
}
await tester.pumpWidget(buildCarousel(<int>[1, 1, 6, 1, 1]));
await tester.pumpAndSettle();
expect(find.text('Item 0'), findsNothing);
for (int i = 1; i <= 5; i++) {
expect(find.text('Item $i'), findsOneWidget);
}
Rect rect3 = tester.getRect(getItem(3));
expect(rect3.center.dx, 400.0);
expect(rect3.center.dy, 300.0);
expect(find.text('Item 6'), findsNothing);
await tester.pumpWidget(buildCarousel(<int>[7, 1]));
await tester.pumpAndSettle();
expect(find.text('Item 2'), findsNothing);
expect(find.text('Item 3'), findsOneWidget);
expect(find.text('Item 4'), findsOneWidget);
expect(find.text('Item 5'), findsNothing);
rect3 = tester.getRect(getItem(3));
expect(rect3, const Rect.fromLTRB(0.0, 0.0, 700.0, 600.0));
final Rect rect4 = tester.getRect(getItem(4));
expect(rect4, const Rect.fromLTRB(700.0, 0.0, 800.0, 600.0));
});
testWidgets('The item that currently occupies max weight stays when the flexWeights is updated', (WidgetTester tester) async {
final CarouselController controller = CarouselController(initialItem: 3);
Widget buildCarousel(List<int> flexWeights) {
return MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
controller: controller,
flexWeights: flexWeights,
itemSnapping: true,
children: List<Widget>.generate(20, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
);
}
await tester.pumpWidget(buildCarousel(<int>[1, 1, 6, 1, 1]));
await tester.pumpAndSettle();
// Item 3 is centered.
final Rect rect3 = tester.getRect(getItem(3));
expect(rect3.center.dx, 400.0);
expect(rect3.center.dy, 300.0);
// Simulate scroll to right and show item 4 to be the centered max item.
await tester.drag(find.byType(CarouselView), const Offset(-80.0, 0.0));
await tester.pumpAndSettle();
expect(find.text('Item 1'), findsNothing);
for (int i = 2; i <= 6; i++) {
expect(find.text('Item $i'), findsOneWidget);
}
Rect rect4 = tester.getRect(getItem(4));
expect(rect4.center.dx, 400.0);
expect(rect4.center.dy, 300.0);
await tester.pumpWidget(buildCarousel(<int>[7, 1]));
await tester.pumpAndSettle();
rect4 = tester.getRect(getItem(4));
expect(rect4, const Rect.fromLTRB(0.0, 0.0, 700.0, 600.0));
final Rect rect5 = tester.getRect(getItem(5));
expect(rect5, const Rect.fromLTRB(700.0, 0.0, 800.0, 600.0));
});
testWidgets('The initialItem stays when the itemExtent is updated', (WidgetTester tester) async {
final CarouselController controller = CarouselController(initialItem: 3);
Widget buildCarousel(double itemExtent) {
return MaterialApp(
home: Scaffold(
body: CarouselView(
controller: controller,
itemExtent: itemExtent,
itemSnapping: true,
children: List<Widget>.generate(20, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
);
}
await tester.pumpWidget(buildCarousel(234.0));
await tester.pumpAndSettle();
Offset rect3BottomRight = tester.getRect(getItem(3)).bottomRight;
expect(rect3BottomRight.dx, 234.0);
expect(rect3BottomRight.dy, 600.0);
await tester.pumpWidget(buildCarousel(400.0));
await tester.pumpAndSettle();
rect3BottomRight = tester.getRect(getItem(3)).bottomRight;
expect(rect3BottomRight.dx, 400.0);
expect(rect3BottomRight.dy, 600.0);
await tester.pumpWidget(buildCarousel(100.0));
await tester.pumpAndSettle();
rect3BottomRight = tester.getRect(getItem(3)).bottomRight;
expect(rect3BottomRight.dx, 100.0);
expect(rect3BottomRight.dy, 600.0);
});
testWidgets('While scrolling, one extra item will show at the end of the screen during items transition', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CarouselView.weighted(
flexWeights: const <int>[1, 2, 4, 2, 1],
consumeMaxWeight: false,
children: List<Widget>.generate(10, (int index) {
return Center(
child: Text('Item $index'),
);
}),
),
),
)
);
await tester.pumpAndSettle();
for (int i = 0; i < 5; i++) {
expect(getItem(i), findsOneWidget);
}
// Drag the first item to the middle. So the progress for the first item size change
// is 50%, original width is 80.
await tester.drag(getItem(0), const Offset(-40.0, 0.0), kind: PointerDeviceKind.trackpad);
await tester.pump();
expect(tester.getRect(getItem(0)).width, 40.0);
// The size of item 1 is changing to the size of item 0, so the size of item 1
// now should be item1.originalExtent - 50% * (item1.extent - item0.extent).
// Item1 originally should be 2/(1+2+4+2+1) * 800 = 160.0.
expect(tester.getRect(getItem(1)).width, 160 - 0.5 * (160 - 80));
// The extent of item 2 should be: item2.originalExtent - 50% * (item2.extent - item1.extent).
// the extent of item 2 originally should be 4/(1+2+4+2+1) * 800 = 320.0.
expect(tester.getRect(getItem(2)).width, 320 - 0.5 * (320 - 160));
// The extent of item 3 should be: item3.originalExtent + 50% * (item2.extent - item3.extent).
// the extent of item 3 originally should be 2/(1+2+4+2+1) * 800 = 160.0.
expect(tester.getRect(getItem(3)).width, 160 + 0.5 * (320 - 160));
// The extent of item 4 should be: item4.originalExtent + 50% * (item3.extent - item4.extent).
// the extent of item 4 originally should be 1/(1+2+4+2+1) * 800 = 80.0.
expect(tester.getRect(getItem(4)).width, 80 + 0.5 * (160 - 80));
// The sum of the first 5 items during transition is less than the screen width.
double sum = 0;
for (int i = 0; i < 5; i++) {
sum += tester.getRect(getItem(i)).width;
}
expect(sum, lessThan(MediaQuery.of(tester.element(find.byType(CarouselView))).size.width));
final double difference = MediaQuery.of(tester.element(find.byType(CarouselView))).size.width - sum;
// One more item should show on screen to fill the rest of the viewport.
expect(getItem(5), findsOneWidget);
expect(tester.getRect(getItem(5)).width, difference);
});
}
Finder getItem(int index) {