cards_demo.dart 12.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hans Muller's avatar
Hans Muller committed
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

7 8
import '../../gallery/demo.dart';

9 10
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';

11 12 13 14 15 16
enum CardDemoType {
  standard,
  tappable,
  selectable,
}

Hans Muller's avatar
Hans Muller committed
17
class TravelDestination {
18
  const TravelDestination({
19 20 21 22 23 24
    required this.assetName,
    required this.assetPackage,
    required this.title,
    required this.description,
    required this.city,
    required this.location,
25
    this.type = CardDemoType.standard,
26
  });
Hans Muller's avatar
Hans Muller committed
27 28

  final String assetName;
29
  final String assetPackage;
Hans Muller's avatar
Hans Muller committed
30
  final String title;
31 32 33 34
  final String description;
  final String city;
  final String location;
  final CardDemoType type;
Hans Muller's avatar
Hans Muller committed
35 36
}

37 38
const List<TravelDestination> destinations = <TravelDestination>[
  TravelDestination(
39
    assetName: 'places/india_thanjavur_market.png',
40
    assetPackage: _kGalleryAssetsPackage,
41
    title: 'Top 10 Cities to Visit in Tamil Nadu',
42 43 44
    description: 'Number 10',
    city: 'Thanjavur',
    location: 'Thanjavur, Tamil Nadu',
Hans Muller's avatar
Hans Muller committed
45
  ),
46
  TravelDestination(
47
    assetName: 'places/india_chettinad_silk_maker.png',
48
    assetPackage: _kGalleryAssetsPackage,
49
    title: 'Artisans of Southern India',
50 51 52 53 54 55 56 57 58 59 60 61 62
    description: 'Silk Spinners',
    city: 'Chettinad',
    location: '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,
63
  ),
Hans Muller's avatar
Hans Muller committed
64 65
];

66
class TravelDestinationItem extends StatelessWidget {
67
  const TravelDestinationItem({ super.key, required this.destination, this.shape });
Hans Muller's avatar
Hans Muller committed
68

69 70
  // This height will allow for all the Card's content to fit comfortably within the card.
  static const double height = 338.0;
Hans Muller's avatar
Hans Muller committed
71
  final TravelDestination destination;
72
  final ShapeBorder? shape;
Hans Muller's avatar
Hans Muller committed
73

74
  @override
Hans Muller's avatar
Hans Muller committed
75
  Widget build(BuildContext context) {
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    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),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
Hans Muller's avatar
Hans Muller committed
99

100
class TappableTravelDestinationItem extends StatelessWidget {
101
  const TappableTravelDestinationItem({ super.key, required this.destination, this.shape });
102 103 104 105

  // 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;
106
  final ShapeBorder? shape;
107 108 109

  @override
  Widget build(BuildContext context) {
110
    return SafeArea(
111 112
      top: false,
      bottom: false,
113
      child: Padding(
114
        padding: const EdgeInsets.all(8.0),
115 116 117 118 119 120 121 122 123 124 125 126 127
        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');
                  },
128 129 130 131
                  // 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,
132 133 134 135 136 137 138 139 140 141 142 143
                  child: TravelDestinationContent(destination: destination),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SelectableTravelDestinationItem extends StatefulWidget {
144
  const SelectableTravelDestinationItem({ super.key, required this.destination, this.shape });
145 146

  final TravelDestination destination;
147
  final ShapeBorder? shape;
148 149

  @override
150
  State<SelectableTravelDestinationItem> createState() => _SelectableTravelDestinationItemState();
151 152 153 154 155 156 157 158 159 160
}

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) {
161 162
    final ColorScheme colorScheme = Theme.of(context).colorScheme;

163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    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;
                    });
                  },
184 185 186 187
                  // 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,
188 189 190 191
                  child: Stack(
                    children: <Widget>[
                      Container(
                        color: _isSelected
192 193 194
                          // 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)
195
                          : Colors.transparent,
196
                      ),
197 198 199 200
                      TravelDestinationContent(destination: widget.destination),
                      Align(
                        alignment: Alignment.topRight,
                        child: Padding(
201
                          padding: const EdgeInsets.all(8.0),
202 203
                          child: Icon(
                            Icons.check_circle,
204
                            color: _isSelected ? colorScheme.primary : Colors.transparent,
205
                          ),
206
                        ),
207
                      ),
208
                    ],
209
                  ),
210
                ),
211
              ),
212 213 214 215 216 217 218 219 220 221
            ),
          ],
        ),
      ),
    );
  }
}

class SectionTitle extends StatelessWidget {
  const SectionTitle({
222
    super.key,
223
    this.title,
224
  });
225

226
  final String? title;
227 228 229 230 231 232 233

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(4.0, 4.0, 4.0, 12.0),
      child: Align(
        alignment: Alignment.centerLeft,
234
        child: Text(title!, style: Theme.of(context).textTheme.subtitle1),
235 236 237 238 239 240
      ),
    );
  }
}

class TravelDestinationContent extends StatelessWidget {
241
  const TravelDestinationContent({ super.key, required this.destination });
242 243 244 245 246 247

  final TravelDestination destination;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
248 249
    final TextStyle titleStyle = theme.textTheme.headline5!.copyWith(color: Colors.white);
    final TextStyle descriptionStyle = theme.textTheme.subtitle1!;
250
    final ButtonStyle textButtonStyle = TextButton.styleFrom(foregroundColor: Colors.amber.shade500);
251

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        // Photo and title.
        SizedBox(
          height: 184.0,
          child: Stack(
            children: <Widget>[
              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(),
269 270
                ),
              ),
271 272 273 274 275 276 277 278 279 280 281
              Positioned(
                bottom: 16.0,
                left: 16.0,
                right: 16.0,
                child: FittedBox(
                  fit: BoxFit.scaleDown,
                  alignment: Alignment.centerLeft,
                  child: Text(
                    destination.title,
                    style: titleStyle,
                  ),
282
                ),
283
              ),
284 285
            ],
          ),
286
        ),
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
        // 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(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                // three line description
                Padding(
                  padding: const EdgeInsets.only(bottom: 8.0),
                  child: Text(
                    destination.description,
                    style: descriptionStyle.copyWith(color: Colors.black54),
                  ),
                ),
                Text(destination.city),
                Text(destination.location),
              ],
308
            ),
309
          ),
310
        ),
311 312
        if (destination.type == CardDemoType.standard)
          // share, explore buttons
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
          Padding(
            padding: const EdgeInsetsDirectional.only(start: 8, top: 8),
            child: OverflowBar(
              alignment: MainAxisAlignment.start,
              spacing: 8,
              children: <Widget>[
                TextButton(
                  style: textButtonStyle,
                  onPressed: () { print('pressed'); },
                  child: Text('SHARE', semanticsLabel: 'Share ${destination.title}'),
                ),
                TextButton(
                  style: textButtonStyle,
                  onPressed: () { print('pressed'); },
                  child: Text('EXPLORE', semanticsLabel: 'Explore ${destination.title}'),
                ),
              ],
            ),
331 332
          ),
      ],
Hans Muller's avatar
Hans Muller committed
333 334 335 336
    );
  }
}

Hans Muller's avatar
Hans Muller committed
337
class CardsDemo extends StatefulWidget {
338
  const CardsDemo({super.key});
339

340
  static const String routeName = '/material/cards';
341

Hans Muller's avatar
Hans Muller committed
342
  @override
343
  State<CardsDemo> createState() => _CardsDemoState();
Hans Muller's avatar
Hans Muller committed
344 345 346
}

class _CardsDemoState extends State<CardsDemo> {
347
  ShapeBorder? _shape;
Hans Muller's avatar
Hans Muller committed
348

349
  @override
Hans Muller's avatar
Hans Muller committed
350
  Widget build(BuildContext context) {
351 352
    return Scaffold(
      appBar: AppBar(
353
        title: const Text('Cards'),
Hans Muller's avatar
Hans Muller committed
354
        actions: <Widget>[
355
          MaterialDemoDocumentationButton(CardsDemo.routeName),
356
          IconButton(
357 358 359 360
            icon: const Icon(
              Icons.sentiment_very_satisfied,
              semanticLabel: 'update shape',
            ),
Hans Muller's avatar
Hans Muller committed
361 362 363
            onPressed: () {
              setState(() {
                _shape = _shape != null ? null : const RoundedRectangleBorder(
364 365 366 367 368
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(16.0),
                    topRight: Radius.circular(16.0),
                    bottomLeft: Radius.circular(2.0),
                    bottomRight: Radius.circular(2.0),
Hans Muller's avatar
Hans Muller committed
369 370 371 372 373 374
                  ),
                );
              });
            },
          ),
        ],
Hans Muller's avatar
Hans Muller committed
375
      ),
376 377
      body: Scrollbar(
        child: ListView(
378
          primary: true,
379 380
          padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
          children: destinations.map<Widget>((TravelDestination destination) {
381
            Widget? child;
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
            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(
              margin: const EdgeInsets.only(bottom: 8.0),
              child: child,
            );
          }).toList(),
        ),
400
      ),
Hans Muller's avatar
Hans Muller committed
401 402 403
    );
  }
}