[Material] Update the card demo in the Gallery to demonstrate different uses of the Card widget (#27699)
Additionally, this adds a tappable Card example to the Card documentation.
This commit is contained in:
parent
f021ee27be
commit
c348be9739
@ -9,152 +9,338 @@ import '../../gallery/demo.dart';
|
|||||||
|
|
||||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||||
|
|
||||||
|
enum CardDemoType {
|
||||||
|
standard,
|
||||||
|
tappable,
|
||||||
|
selectable,
|
||||||
|
}
|
||||||
|
|
||||||
class TravelDestination {
|
class TravelDestination {
|
||||||
const TravelDestination({
|
const TravelDestination({
|
||||||
this.assetName,
|
@required this.assetName,
|
||||||
this.assetPackage,
|
@required this.assetPackage,
|
||||||
this.title,
|
@required this.title,
|
||||||
this.description,
|
@required this.description,
|
||||||
});
|
@required this.city,
|
||||||
|
@required this.location,
|
||||||
|
this.type = CardDemoType.standard,
|
||||||
|
}) : assert(assetName != null),
|
||||||
|
assert(assetPackage != null),
|
||||||
|
assert(title != null),
|
||||||
|
assert(description != null),
|
||||||
|
assert(city != null),
|
||||||
|
assert(location != null);
|
||||||
|
|
||||||
final String assetName;
|
final String assetName;
|
||||||
final String assetPackage;
|
final String assetPackage;
|
||||||
final String title;
|
final String title;
|
||||||
final List<String> description;
|
final String description;
|
||||||
|
final String city;
|
||||||
bool get isValid => assetName != null && title != null && description?.length == 3;
|
final String location;
|
||||||
|
final CardDemoType type;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<TravelDestination> destinations = <TravelDestination>[
|
const List<TravelDestination> destinations = <TravelDestination>[
|
||||||
const TravelDestination(
|
TravelDestination(
|
||||||
assetName: 'places/india_thanjavur_market.png',
|
assetName: 'places/india_thanjavur_market.png',
|
||||||
assetPackage: _kGalleryAssetsPackage,
|
assetPackage: _kGalleryAssetsPackage,
|
||||||
title: 'Top 10 Cities to Visit in Tamil Nadu',
|
title: 'Top 10 Cities to Visit in Tamil Nadu',
|
||||||
description: <String>[
|
description: 'Number 10',
|
||||||
'Number 10',
|
city: 'Thanjavur',
|
||||||
'Thanjavur',
|
location: 'Thanjavur, Tamil Nadu',
|
||||||
'Thanjavur, Tamil Nadu',
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const TravelDestination(
|
TravelDestination(
|
||||||
assetName: 'places/india_chettinad_silk_maker.png',
|
assetName: 'places/india_chettinad_silk_maker.png',
|
||||||
assetPackage: _kGalleryAssetsPackage,
|
assetPackage: _kGalleryAssetsPackage,
|
||||||
title: 'Artisans of Southern India',
|
title: 'Artisans of Southern India',
|
||||||
description: <String>[
|
description: 'Silk Spinners',
|
||||||
'Silk Spinners',
|
city: 'Chettinad',
|
||||||
'Chettinad',
|
location: 'Sivaganga, Tamil Nadu',
|
||||||
'Sivaganga, Tamil Nadu',
|
type: CardDemoType.tappable,
|
||||||
],
|
),
|
||||||
|
TravelDestination(
|
||||||
|
assetName: 'places/india_tanjore_thanjavur_temple.png',
|
||||||
|
assetPackage: _kGalleryAssetsPackage,
|
||||||
|
title: 'Brihadisvara Temple',
|
||||||
|
description: 'Temples',
|
||||||
|
city: 'Thanjavur',
|
||||||
|
location: 'Thanjavur, Tamil Nadu',
|
||||||
|
type: CardDemoType.selectable,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
class TravelDestinationItem extends StatelessWidget {
|
class TravelDestinationItem extends StatelessWidget {
|
||||||
TravelDestinationItem({ Key key, @required this.destination, this.shape })
|
const TravelDestinationItem({ Key key, @required this.destination, this.shape })
|
||||||
: assert(destination != null && destination.isValid),
|
: assert(destination != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
static const double height = 366.0;
|
// This height will allow for all the Card's content to fit comfortably within the card.
|
||||||
|
static const double height = 338.0;
|
||||||
final TravelDestination destination;
|
final TravelDestination destination;
|
||||||
final ShapeBorder shape;
|
final ShapeBorder shape;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SectionTitle(title: 'Normal'),
|
||||||
|
SizedBox(
|
||||||
|
height: height,
|
||||||
|
child: Card(
|
||||||
|
// This ensures that the Card's children are clipped correctly.
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: shape,
|
||||||
|
child: TravelDestinationContent(destination: destination),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TappableTravelDestinationItem extends StatelessWidget {
|
||||||
|
const TappableTravelDestinationItem({ Key key, @required this.destination, this.shape })
|
||||||
|
: assert(destination != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
// This height will allow for all the Card's content to fit comfortably within the card.
|
||||||
|
static const double height = 298.0;
|
||||||
|
final TravelDestination destination;
|
||||||
|
final ShapeBorder shape;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SectionTitle(title: 'Tappable'),
|
||||||
|
SizedBox(
|
||||||
|
height: height,
|
||||||
|
child: Card(
|
||||||
|
// This ensures that the Card's children (including the ink splash) are clipped correctly.
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: shape,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
print('Card was tapped');
|
||||||
|
},
|
||||||
|
splashColor: Theme.of(context).colorScheme.primary.withAlpha(30),
|
||||||
|
child: TravelDestinationContent(destination: destination),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectableTravelDestinationItem extends StatefulWidget {
|
||||||
|
const SelectableTravelDestinationItem({ Key key, @required this.destination, this.shape })
|
||||||
|
: assert(destination != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final TravelDestination destination;
|
||||||
|
final ShapeBorder shape;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SelectableTravelDestinationItemState createState() => _SelectableTravelDestinationItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectableTravelDestinationItemState extends State<SelectableTravelDestinationItem> {
|
||||||
|
|
||||||
|
// This height will allow for all the Card's content to fit comfortably within the card.
|
||||||
|
static const double height = 298.0;
|
||||||
|
bool _isSelected = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SectionTitle(title: 'Selectable (long press)'),
|
||||||
|
SizedBox(
|
||||||
|
height: height,
|
||||||
|
child: Card(
|
||||||
|
// This ensures that the Card's children (including the ink splash) are clipped correctly.
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: widget.shape,
|
||||||
|
child: InkWell(
|
||||||
|
onLongPress: () {
|
||||||
|
print('Selectable card state changed');
|
||||||
|
setState(() {
|
||||||
|
_isSelected = !_isSelected;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
splashColor: Theme.of(context).colorScheme.primary.withAlpha(30),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
color: _isSelected
|
||||||
|
? Theme.of(context).colorScheme.primary.withAlpha(41)
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
TravelDestinationContent(destination: widget.destination),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: _isSelected ? Colors.white : Colors.transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SectionTitle extends StatelessWidget {
|
||||||
|
const SectionTitle({
|
||||||
|
Key key,
|
||||||
|
this.title
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(4.0, 4.0, 4.0, 12.0),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(title, style: Theme.of(context).textTheme.subhead),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TravelDestinationContent extends StatelessWidget {
|
||||||
|
const TravelDestinationContent({ Key key, @required this.destination })
|
||||||
|
: assert(destination != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final TravelDestination destination;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
final TextStyle titleStyle = theme.textTheme.headline.copyWith(color: Colors.white);
|
final TextStyle titleStyle = theme.textTheme.headline.copyWith(color: Colors.white);
|
||||||
final TextStyle descriptionStyle = theme.textTheme.subhead;
|
final TextStyle descriptionStyle = theme.textTheme.subhead;
|
||||||
|
|
||||||
return SafeArea(
|
final List<Widget> children = <Widget>[
|
||||||
top: false,
|
// Photo and title.
|
||||||
bottom: false,
|
SizedBox(
|
||||||
child: Container(
|
height: 184.0,
|
||||||
padding: const EdgeInsets.all(8.0),
|
child: Stack(
|
||||||
height: height,
|
children: <Widget>[
|
||||||
child: Card(
|
Positioned.fill(
|
||||||
clipBehavior: Clip.antiAlias,
|
// In order to have the ink splash appear above the image, you
|
||||||
shape: shape,
|
// must use Ink.image. This allows the image to be painted as part
|
||||||
|
// of the Material and display ink effects above it. Using a
|
||||||
|
// standard Image will obscure the ink splash.
|
||||||
|
child: Ink.image(
|
||||||
|
image: AssetImage(destination.assetName, package: destination.assetPackage),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
child: Container(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 16.0,
|
||||||
|
left: 16.0,
|
||||||
|
right: 16.0,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
destination.title,
|
||||||
|
style: titleStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Description and share/explore buttons.
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
|
||||||
|
child: DefaultTextStyle(
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: descriptionStyle,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// photo and title
|
// three line description
|
||||||
SizedBox(
|
Padding(
|
||||||
height: 184.0,
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Stack(
|
child: Text(
|
||||||
children: <Widget>[
|
destination.description,
|
||||||
Positioned.fill(
|
style: descriptionStyle.copyWith(color: Colors.black54),
|
||||||
child: Image.asset(
|
|
||||||
destination.assetName,
|
|
||||||
package: destination.assetPackage,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 16.0,
|
|
||||||
left: 16.0,
|
|
||||||
right: 16.0,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(destination.title,
|
|
||||||
style: titleStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// description and share/explore buttons
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
|
|
||||||
child: DefaultTextStyle(
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: descriptionStyle,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
// three line description
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
|
||||||
child: Text(
|
|
||||||
destination.description[0],
|
|
||||||
style: descriptionStyle.copyWith(color: Colors.black54),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(destination.description[1]),
|
|
||||||
Text(destination.description[2]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// share, explore buttons
|
|
||||||
ButtonTheme.bar(
|
|
||||||
child: ButtonBar(
|
|
||||||
alignment: MainAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('SHARE', semanticsLabel: 'Share ${destination.title}'),
|
|
||||||
textColor: Colors.amber.shade500,
|
|
||||||
onPressed: () { /* do nothing */ },
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
child: Text('EXPLORE', semanticsLabel: 'Explore ${destination.title}'),
|
|
||||||
textColor: Colors.amber.shade500,
|
|
||||||
onPressed: () { /* do nothing */ },
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Text(destination.city),
|
||||||
|
Text(destination.location),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (destination.type == CardDemoType.standard) {
|
||||||
|
children.add(
|
||||||
|
// share, explore buttons
|
||||||
|
ButtonTheme.bar(
|
||||||
|
child: ButtonBar(
|
||||||
|
alignment: MainAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text('SHARE', semanticsLabel: 'Share ${destination.title}'),
|
||||||
|
textColor: Colors.amber.shade500,
|
||||||
|
onPressed: () { print('pressed'); },
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('EXPLORE', semanticsLabel: 'Explore ${destination.title}'),
|
||||||
|
textColor: Colors.amber.shade500,
|
||||||
|
onPressed: () { print('pressed'); },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CardsDemo extends StatefulWidget {
|
class CardsDemo extends StatefulWidget {
|
||||||
static const String routeName = '/material/cards';
|
static const String routeName = '/material/cards';
|
||||||
|
|
||||||
@ -169,7 +355,7 @@ class _CardsDemoState extends State<CardsDemo> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Travel stream'),
|
title: const Text('Cards'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
MaterialDemoDocumentationButton(CardsDemo.routeName),
|
MaterialDemoDocumentationButton(CardsDemo.routeName),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -193,15 +379,24 @@ class _CardsDemoState extends State<CardsDemo> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
itemExtent: TravelDestinationItem.height,
|
|
||||||
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
|
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
|
||||||
children: destinations.map<Widget>((TravelDestination destination) {
|
children: destinations.map<Widget>((TravelDestination destination) {
|
||||||
|
Widget child;
|
||||||
|
switch (destination.type) {
|
||||||
|
case CardDemoType.standard:
|
||||||
|
child = TravelDestinationItem(destination: destination, shape: _shape);
|
||||||
|
break;
|
||||||
|
case CardDemoType.tappable:
|
||||||
|
child = TappableTravelDestinationItem(destination: destination, shape: _shape);
|
||||||
|
break;
|
||||||
|
case CardDemoType.selectable:
|
||||||
|
child = SelectableTravelDestinationItem(destination: destination, shape: _shape);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8.0),
|
margin: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: TravelDestinationItem(
|
child: child,
|
||||||
destination: destination,
|
|
||||||
shape: _shape,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}).toList()
|
}).toList()
|
||||||
)
|
)
|
||||||
|
@ -56,6 +56,27 @@ import 'theme.dart';
|
|||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
///
|
///
|
||||||
|
/// Sometimes the primary action area of a card is the card itself. Cards can be
|
||||||
|
/// one large touch target that shows a detail screen when tapped.
|
||||||
|
///
|
||||||
|
/// {@tool snippet --template=stateless_widget}
|
||||||
|
///
|
||||||
|
/// This sample shows creation of a [Card] widget that can be tapped. When
|
||||||
|
/// tapped this [Card]'s [InkWell] displays an "ink splash" that fills the
|
||||||
|
/// entire card.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// Card(
|
||||||
|
/// child: InkWell(
|
||||||
|
/// splashColor: Colors.blue.withAlpha(30),
|
||||||
|
/// onTap: () { /* ... */ },
|
||||||
|
/// child: Text('A card that can be tapped'),
|
||||||
|
/// ),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [ListTile], to display icons and text in a card.
|
/// * [ListTile], to display icons and text in a card.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user