Preface:

This is the 20th day of my participation in the August More Text Challenge. In preparation for the Nuggets’ August challenge, I’m going to pick 31 components this month that I haven’t covered before for a full analysis and attribute presentation. These articles will serve as important material for future compilation of Flutter components. I hope I can stick to it, your support will be my biggest motivation ~

This series Component articles The list of
1.NotificationListener 2.Dismissible 3.Switch
4.Scrollbar 5.ClipPath 6.CupertinoActivityIndicator
7.Opacity 8.FadeTransition 9. AnimatedOpacity
10. FadeInImage 11. Offstage 12. TickerMode
13. Visibility 14. Padding 15. AnimatedContainer
16.CircleAvatar 17.PhysicalShape 18.Divider
Flexible, Expanded, and Spacer 20.Card [this article]

1. Know the Card component

As a member of Material Design, the Card component of Flutter is naturally required. The source code notes describe it as a panel with slightly rounded corners and facade shadows. This article will look at the composition of Card components from the source point of view, and tell about the use of Card in some small points of attention.


1. Basic Card information

Below is the definition and constructor of the Card component class, which you can see inherits from the StatelessWidget. No parameters must be passed in. You can configure colors, shadow colors, shapes, margins, and other properties.


2. Simple use of Card

Return the Container component as content with buildContent, as shown below. After the upper layer is wrapped with the Card component, there will be a small rounded corner + shadow effect, where the color attribute is the color of the panel.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Card(
          color: const Color(0xffB3FE65),
          child: buildContent(),
        ),
      ),
    );
  }
  
  Widget buildContent() {
    return Container(
        width: 200,
        height: 0.618 * 200,
        padding: const EdgeInsets.all(10),
        child: Text(-Penny: What's your Card?, style: TextStyle(fontSize: 20))); }}Copy the code

2. ShadowColor and Elevation attributes

The color of the shadow can be set by shadowColor, and the depth of the shadow can be set by elevation.

Card(
  color: Color(0xffB3FE65),
  shadowColor: Colors.blueAccent,
  elevation: 8,
  child: buildContent(),
)
Copy the code

3. The margin properties

If you can’t see the margins with a single Card, you can use two auxiliary boxes. As you can see below, Card has margins by default. The property that regulates margins is margin.

Using the following code, you can make the left margin 20 and the right margin 30.

Card(
  margin: EdgeInsets.only(left: 20,right: 30),
  color: Color(0xffB3FE65),
  child: buildContent(),
)
Copy the code

4. ClipBehavior

Clip is an enumeration class with four forms, as follows:

enum Clip {
  none, / / no
  hardEdge, / / hard edges
  antiAlias, / / anti-aliasing
  antiAliasWithSaveLayer, // Save the anti-aliasing layer
}
Copy the code

In the left image below, use image decoration in the content container and you will be wondering why there are no rounded corners. Because the default clipping behavior of Card is clip.None. You need to round the corners by specifying the clipBehavior, which is a little detail, and if you don’t know, you’re probably going to feel bad about the Card component.

Clip.none Clip.antiAlias
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child:
            Card(
              clipBehavior: Clip.antiAlias, //<-- clipping behavior
              color: const Color(0xffB3FE65),
              child: buildContent(),
            ),
      ),
    );
  }
  Widget buildContent() {
    return Container(
        width: 200,
        height: 0.618 * 200,
        padding: const EdgeInsets.all(10),
        decoration: const BoxDecoration( //<-- add a decorative image
            image: DecorationImage(
                fit: BoxFit.cover,
                image: AssetImage('assets/images/anim_draw.webp')
            )
        ),
        child: Text(-Penny: What's your Card?, style: TextStyle(fontSize: 20,color: Colors.white))); }}Copy the code

5. Shape attribute

It’s just a simple property configuration, but your Card is more powerful than that. If you think the default rounded corners are a little too small and you want to make them bigger, or if you don’t like rounded corners, you need to get creative first. Shape will open the door for you. All you need is a ShapeBorder object, and since it is an abstract class, you need to find a subclass of it. The framework provides the following subclass. As for the adaptation of shape attribute, it was introduced in detail in “Path in Hand, World I have” before, and will not be repeated here.

To add a rounded rectangle, for example, use the round Rectangleborder shape.

Card(
  clipBehavior: Clip.antiAlias,
  color: const Color(0xffB3FE65),
  shape: const RoundedRectangleBorder(
      side: BorderSide.none,
      borderRadius: BorderRadius.all(Radius.circular(10))),
  elevation: 3,
  shadowColor: Colors.blueAccent,
  child: buildContent(),
),
Copy the code

In addition to the built-in Shape, we can also define our own Shape, such as taking the path of a polystar via nStarPath and returning the path in the StarShapeBorder#getOuterPath inherited from ShapeBorder, Then you can tailor it to that path. For convenience, the polygonal star is written down, and the outer container should be 100 wide and high.

class StarShapeBorder extends ShapeBorder {
  @override
  EdgeInsetsGeometry get dimensions => null;

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) {
    return null;
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection textDirection}) =>
      nStarPath(9.50.40, dx: 50, dy: 50);

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
    
  }

  @override
  ShapeBorder scale(double t) {
    return null;
  }

  Path nStarPath(int num.double R, double r, {dx = 0, dy = 0}) {
    Path _path = Path();
    _path.reset(); // Reset the path
    double perRad = 2 * pi / num; // Each Angle
    double radA = perRad / 2 / 2; / / a Angle
    double radB = 2 * pi / (num - 1) / 2 - radA / 2 + radA; // Start Angle B
    _path.moveTo(cos(radA) * R + dx, -sin(radA) * R + dy); // Move to the starting point
    for (int i = 0; i < num; i++) { // Cycle generation point, path to
      _path.lineTo(
          cos(radA + perRad * i) * R + dx, -sin(radA + perRad * i) * R + dy);
      _path.lineTo(
          cos(radB + perRad * i) * r + dx, -sin(radB + perRad * i) * r + dy);
    }
    _path.close();
    return_path; }}Copy the code

5. BorderOnForeground properties

This attribute, which no one cares about, determines whether the ShapeBorder is drawn in the foreground. As you can see above, there is a paint method in StarShapeBorder that provides the paint operation. Here, simply draw a small circle in the upper left corner of the region. The default borderOnForeground is true, and the drawn decoration will appear in the foreground, as shown below.

@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
  canvas.drawCircle(Offset.zero, 50, Paint().. color=Colors.blueAccent); }Copy the code

If borderOnForeground is set to false, the drawn content does not appear in the foreground.


2. Water ripple of Card

1. Wrong use

If you place InkWell above the Center, its ripples will be covered by the foreground. As shown in the figure below, the point on the upper margin shows water ripples.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child:  InkWell(
        onTap: (){
        },
        child:Card(
        clipBehavior: Clip.antiAlias,
        color: const Color(0xffB3FE65),
        elevation: 3,
        shadowColor: Colors.blueAccent,
        child: buildContent()),
      ),
    ),
  );
}
Copy the code

2. Use it correctly

The correct way to use it is to nest InkWell on the Child component.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child:  Card(
        clipBehavior: Clip.antiAlias,
        color: const Color(0xffB3FE65),
        elevation: 3,
        shadowColor: Colors.blueAccent,
        child: InkWell(
            splashColor: Colors.blue.withAlpha(30),
            onTap: (){
            },
            child:buildContent()),
      ),
    ),
  );
}
Copy the code

3. Solutions that cannot trigger water ripples

In some cases, such as using Image, or setting the color for the Container, or loading, the water ripple will not trigger.

The Ink component can be used instead of a Container or Image.

Widget buildContent() {
  return Ink(
      width: 200,
      height: 0.618 * 200,
      decoration: const BoxDecoration(
          image: DecorationImage(
              fit: BoxFit.cover,
              image: AssetImage('assets/images/anim_draw.webp'))),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text("Card: a Card",
            style: TextStyle(fontSize: 20,color: Colors.white)),
      ));
}
Copy the code

Third, Card source code analysis

Well, it’s time for the last look at the source code. As a StatelessWidget, the Card component must be “wasted” for other component functions. The core code is as follows: It is a combination of Container + Material components.

So why would a simple object have to separate a Card component? Clearly, semantic clarity, simplicity, simplicity is king. The other thing is that you can uniformly set the CardTheme to determine the default behavior of the Card. If you do not have a Card component, you can use the Material component to achieve the effect, but each time you need to set a lot of objects, and you cannot set the theme, use encapsulation for better use.

@override
Widget build(BuildContext context) {
  final ThemeData theme = Theme.of(context);
  final CardTheme cardTheme = CardTheme.of(context);
  return Semantics(
    container: semanticContainer,
    child: Container(
      margin: margin ?? cardTheme.margin ?? const EdgeInsets.all(4.0),
      child: Material(
        type: MaterialType.card,
        shadowColor: shadowColor ?? cardTheme.shadowColor ?? theme.shadowColor,
        color: color ?? cardTheme.color ?? theme.cardColor,
        elevation: elevation ?? cardTheme.elevation ?? _defaultElevation,
        shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder(
          borderRadius: BorderRadius.all(Radius.circular(4.0)), ), borderOnForeground: borderOnForeground, clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? Clip.none, child: Semantics( explicitChildNodes: ! semanticContainer, child: child, ), ), ), ); }Copy the code

As you can see from the components of the StatelessWidget, the main purpose of this type of component is to facilitate the use of users. Its internal implementation is dependent on other components. When you look at the internal implementation of StatelessWidget, you can connect many components together. A lot of questions that we’ve had, we’ll be able to solve. Understand the internal implementation, in use, will also be more confident. That’s the end of this article. Thanks for watching and see you tomorrow