// Copyright 2020 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:gallery/l10n/gallery_localizations.dart'; const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; // BEGIN cardsDemo enum CardDemoType { standard, tappable, selectable, } class TravelDestination { const TravelDestination({ @required this.assetName, @required this.assetPackage, @required this.title, @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 assetPackage; final String title; final String description; final String city; final String location; final CardDemoType type; } List destinations(BuildContext context) => [ TravelDestination( assetName: 'places/india_thanjavur_market.png', assetPackage: _kGalleryAssetsPackage, title: GalleryLocalizations.of(context).cardsDemoTravelDestinationTitle1, description: GalleryLocalizations.of(context) .cardsDemoTravelDestinationDescription1, city: GalleryLocalizations.of(context).cardsDemoTravelDestinationCity1, location: GalleryLocalizations.of(context) .cardsDemoTravelDestinationLocation1, ), TravelDestination( assetName: 'places/india_chettinad_silk_maker.png', assetPackage: _kGalleryAssetsPackage, title: GalleryLocalizations.of(context).cardsDemoTravelDestinationTitle2, description: GalleryLocalizations.of(context) .cardsDemoTravelDestinationDescription2, city: GalleryLocalizations.of(context).cardsDemoTravelDestinationCity2, location: GalleryLocalizations.of(context) .cardsDemoTravelDestinationLocation2, type: CardDemoType.tappable, ), TravelDestination( assetName: 'places/india_tanjore_thanjavur_temple.png', assetPackage: _kGalleryAssetsPackage, title: GalleryLocalizations.of(context).cardsDemoTravelDestinationTitle3, description: GalleryLocalizations.of(context) .cardsDemoTravelDestinationDescription3, city: GalleryLocalizations.of(context).cardsDemoTravelDestinationCity1, location: GalleryLocalizations.of(context) .cardsDemoTravelDestinationLocation1, type: CardDemoType.selectable, ), ]; class TravelDestinationItem extends StatelessWidget { const TravelDestinationItem({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 height = 338.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), child: Column( children: [ SectionTitle( title: GalleryLocalizations.of(context).settingsTextScalingNormal), 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 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), child: Column( children: [ SectionTitle( title: GalleryLocalizations.of(context).cardsDemoTappable), 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'); }, // Generally, material cards use onSurface with 12% opacity for the pressed state. splashColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.12), // Generally, material cards do not have a highlight overlay. highlightColor: Colors.transparent, 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 { // This height will allow for all the Card's content to fit comfortably within the card. static const height = 298.0; var _isSelected = false; @override Widget build(BuildContext context) { final ColorScheme colorScheme = Theme.of(context).colorScheme; return SafeArea( top: false, bottom: false, child: Padding( padding: const EdgeInsets.all(8), child: Column( children: [ SectionTitle( title: GalleryLocalizations.of(context).cardsDemoSelectable), 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; }); }, // Generally, material cards use onSurface with 12% opacity for the pressed state. splashColor: colorScheme.onSurface.withOpacity(0.12), // Generally, material cards do not have a highlight overlay. highlightColor: Colors.transparent, child: Stack( children: [ Container( color: _isSelected // Generally, material cards use primary with 8% opacity for the selected state. // See: https://material.io/design/interaction/states.html#anatomy ? colorScheme.primary.withOpacity(0.08) : Colors.transparent, ), TravelDestinationContent(destination: widget.destination), Align( alignment: Alignment.topRight, child: Padding( padding: const EdgeInsets.all(8), child: Icon( Icons.check_circle, color: _isSelected ? colorScheme.primary : 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, 4, 4, 12), 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 Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final TextStyle titleStyle = theme.textTheme.headline.copyWith(color: Colors.white); final TextStyle descriptionStyle = theme.textTheme.subhead; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 184, child: Stack( children: [ Positioned.fill( // In order to have the ink splash appear above the image, you // 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, left: 16, right: 16, 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, 16, 16, 0), child: DefaultTextStyle( softWrap: false, overflow: TextOverflow.ellipsis, style: descriptionStyle, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // This array contains the three line description on each card // demo. Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( destination.description, style: descriptionStyle.copyWith(color: Colors.black54), ), ), Text(destination.city), Text(destination.location), ], ), ), ), if (destination.type == CardDemoType.standard) // share, explore buttons ButtonBar( alignment: MainAxisAlignment.start, children: [ FlatButton( child: Text(GalleryLocalizations.of(context).demoMenuShare, semanticsLabel: GalleryLocalizations.of(context) .cardsDemoShareSemantics(destination.title)), textColor: Colors.amber.shade500, onPressed: () { print('pressed'); }, ), FlatButton( child: Text(GalleryLocalizations.of(context).cardsDemoExplore, semanticsLabel: GalleryLocalizations.of(context) .cardsDemoExploreSemantics(destination.title)), textColor: Colors.amber.shade500, onPressed: () { print('pressed'); }, ), ], ), ], ); } } class CardsDemo extends StatefulWidget { @override _CardsDemoState createState() => _CardsDemoState(); } class _CardsDemoState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: Text(GalleryLocalizations.of(context).demoCardTitle), ), body: Scrollbar( child: ListView( padding: const EdgeInsets.only(top: 8, left: 8, right: 8), children: [ for (final destination in destinations(context)) Container( margin: const EdgeInsets.only(bottom: 8), child: (destination.type == CardDemoType.standard) ? TravelDestinationItem(destination: destination) : destination.type == CardDemoType.tappable ? TappableTravelDestinationItem( destination: destination) : SelectableTravelDestinationItem( destination: destination), ), ], ), ), ); } } // END