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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user