Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

用Flutter构建漂亮的UI界面 - 基础组件篇 #12

Open
SmallStoneSK opened this issue Jul 6, 2019 · 7 comments
Open

用Flutter构建漂亮的UI界面 - 基础组件篇 #12

SmallStoneSK opened this issue Jul 6, 2019 · 7 comments
Labels
Flutter Flutter学习

Comments

@SmallStoneSK
Copy link
Owner

SmallStoneSK commented Jul 6, 2019

1. 前言

Flutter作为时下最流行的技术之一,凭借其出色的性能以及抹平多端的差异优势,早已引起大批技术爱好者的关注,甚至一些闲鱼美团腾讯等大公司均已开始使用。虽然目前其生态还没有完全成熟,但身靠背后的Google加持,其发展速度已经足够惊人,可以预见将来对Flutter开发人员的需求也会随之增长。

无论是为了现在的技术尝鲜还是将来的潮流趋势,都9102年了,作为一个前端开发者,似乎没有理由不去尝试它。正是带着这样的心理,笔者也开始学习Flutter,同时建了一个用于练习的仓库,后续所有代码都会托管在上面,欢迎star,一起学习。

今天分享的是Flutter中最常用到的一些基础组件,它们是构成UI界面的基础元素:容器绝对定位布局文本图片图标等。

cover.png

2. 基础组件

2.1 Container(容器组件)

Container组件是最常用的布局组件之一,可以认为它是web开发中的div,rn开发中的View。其往往可以用来控制大小、背景颜色、边框、阴影、内外边距和内容排列方式等。我们先来看下其构造函数:

Container({
  Key key,
  double width,
  double height,
  this.margin,
  this.padding,
  Color color,
  this.alignment,
  BoxConstraints constraints,
  Decoration decoration,
  this.foregroundDecoration,
  this.transform,
  this.child,
})

2.1.1 widthheightmarginpadding

这些属性的含义和我们已经熟知的并没有区别。唯一需要注意的是,marginpadding的赋值不是一个简单的数字,因为其有left, top, right, bottom四个方向的值需要设置。Flutter提供了EdgeInsets这个类,帮助我们方便地生成四个方向的值。通常情况下,我们可能会用到EdgeInsets的4种构造方法:

  • EdgeInsets.all(value): 用于设置4个方向一样的值;
  • EdgeInsets.only(left: val1, top: val2, right: val3, bottom: val4): 可以单独设置某个方向的值;
  • EdgeInsets.symmetric(horizontal: val1, vertical: val2): 用于设置水平/垂直方向上的值;
  • EdgeInsets.fromLTRB(left, top, right, bottom): 按照左上右下的顺序设置4个方向的值。

2.1.2 color

该属性的含义是背景颜色,等同于web/rn中的backgroundColor。需要注意的是Flutter中有一个专门表示颜色的Color类,而非我们常用的字符串。不过我们可以非常轻松地进行转换,举个栗子:

在web/rn中我们会用'#FF0000''red'来表示红色,而在Flutter中,我们可以用Color(0xFFFF0000)Colors.red来表示。

2.1.3 alignment

该属性是用来决定Container组件的子组件将以何种方式进行排列(PS:再也不用为怎么居中操心了)。其可选值通常会用到:

  • Alignment.topLeft: 左上
  • Alignment.topCenter: 上中
  • Alignment.topRight: 右上
  • Alignment.centerLeft: 左中
  • Alignment.center: 居中
  • Alignment.centerRight: 右中
  • Alignment.bottomLeft: 左下
  • Alignment.bottomCenter: 下中
  • Alignment.bottomRight: 右下

2.1.4 constraints

在web/rn中我们通常会用minWidth/maxWidth/minHeight/maxHeight等属性来限制容器的宽高。在Flutter中,你需要使用BoxConstraints(盒约束)来实现该功能。

// 容器的大小将被限制在[100*100 ~ 200*200]内
BoxConstraints(
  minWidth: 100,
  maxWidth: 200,
  minHeight: 100,
  maxHeight: 200,
)

2.1.5 decoration

该属性非常强大,字面意思是装饰,因为通过它你可以设置边框阴影渐变圆角等常用属性。BoxDecoration继承自Decoration类,因此我们通常会生成一个BoxDecoration实例来设置这些属性。

1) 边框

可以用Border.all构造函数直接生成4条边框,也可以用Border构造函数单独设置不同方向上的边框。不过令人惊讶的是官方提供的边框竟然不支持虚线issue在这里)。

// 同时设置4条边框:1px粗细的黑色实线边框
BoxDecoration(
  border: Border.all(color: Colors.black, width: 1, style: BorderStyle.solid)
)

// 设置单边框:上边框为1px粗细的黑色实线边框,右边框为1px粗细的红色实线边框
BoxDecoration(
  border: Border(
    top: BorderSide(color: Colors.black, width: 1, style: BorderStyle.solid),
    right: BorderSide(color: Colors.red, width: 1, style: BorderStyle.solid),
  ),
)

2) 阴影

阴影属性和web中的boxShadow几乎没有区别,可以指定xyblurspreadcolor等属性。

BoxDecoration(
  boxShadow: [
    BoxShadow(
      offset: Offset(0, 0),
      blurRadius: 6,
      spreadRadius: 10,
      color: Color.fromARGB(20, 0, 0, 0),
    ),
  ],
)

3) 渐变

如果你不想容器的背景颜色是单调的,可以尝试用gradient属性。Flutter同时支持线性渐变径向渐变

// 从左到右,红色到蓝色的线性渐变
BoxDecoration(
  gradient: LinearGradient(
    begin: Alignment.centerLeft,
    end: Alignment.centerRight,
    colors: [Colors.red, Colors.blue],
  ),
)

// 从中心向四周扩散,红色到蓝色的径向渐变
BoxDecoration(
  gradient: RadialGradient(
    center: Alignment.center,
    colors: [Colors.red, Colors.blue],
  ),
)

4) 圆角

通常情况下,你可能会用到BorderRadius.circular构造函数来同时设置4个角的圆角,或是BorderRadius.only构造函数来单独设置某几个角的圆角:

// 同时设置4个角的圆角为5
BoxDecoration(
  borderRadius: BorderRadius.circular(5),
)

// 设置单圆角:左上角的圆角为5,右上角的圆角为10
BoxDecoration(
  borderRadius: BorderRadius.only(
    topLeft: Radius.circular(5),
    topRight: Radius.circular(10),
  ),
)

2.1.6 transform

transform属性和我们在web/rn中经常用到的基本也没有差别,主要包括:平移缩放旋转倾斜。在Flutter中,封装了矩阵变换类Matrix4帮助我们进行变换:

  • translationValues(x, y, z): 平移x, y, z;
  • rotationX(radians): x轴旋转radians弧度;
  • rotationY(radians): y轴旋转radians弧度;
  • rotationZ(radians): z轴旋转radians弧度;
  • skew(alpha, beta): x轴倾斜alpha度,y轴倾斜beta度;
  • skewX(alpha): x轴倾斜alpha度;
  • skewY(beta): y轴倾斜beta度;

2.1.7 小结

Container组件的属性很丰富,虽然有些用法上和web/rn有些许差异,但基本上大同小异,所以过渡起来也不会有什么障碍。另外,由于Container组件是单子节点组件,也就是只允许子节点有一个。所以在布局上,很多时候我们会用RowColumn组件进行/布局。

2.2 Row/Column(行/列组件)

RowColumn组件其实和web/rn中的Flex布局(弹性盒子)特别相似,或者我们可以就这么理解。使用Flex布局的同学对主轴次轴的概念肯定都已经十分熟悉,Row组件的主轴就是横向,Column组件的主轴就是纵向。且它们的构造函数十分相似(已省略不常用属性):

Row({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  List<Widget> children = const <Widget>[],
})

Column({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  List<Widget> children = const <Widget>[],
})

2.2.1 mainAxisAlignment

该属性的含义是主轴排列方式,根据上述构造函数可以知道RowColumn组件在主轴方向上默认都是从start开始,也就是说Row组件默认从左到右开始排列子组件,Column组件默认从上到下开始排列子组件。

当然,你还可以使用其他的可选值:

  • MainAxisAlignment.start
  • MainAxisAlignment.end
  • MainAxisAlignment.center
  • MainAxisAlignment.spaceBetween
  • MainAxisAlignment.spaceAround
  • MainAxisAlignment.spaceEvenly

2.2.2 crossAxisAlignment

该属性的含义是次轴排列方式,根据上述构造函数可以知道RowColumn组件在次轴方向上默认都是居中。

这里有一点需要特别注意:由于Column组件次轴方向上(即水平)默认是居中对齐,所以水平方向上不会撑满其父容器,此时需要指定CrossAxisAlignment.stretch才可以。

另外,crossAxisAlignment其他的可选值有:

  • crossAxisAlignment.start
  • crossAxisAlignment.end
  • crossAxisAlignment.center
  • crossAxisAlignment.stretch
  • crossAxisAlignment.baseline

2.2.3 mainAxisSize

字面意思上来说,该属性指的是在主轴上的尺寸。其实就是指在主轴方向上,是包裹其内容,还是撑满其父容器。它的可选值有MainAxisSize.minMainAxisSize.max。由于其默认值都是MainAxisSize.max,所以主轴方向上默认大小都是尽可能撑满父容器的。

2.2.4 小结

由于Row/Column组件和我们熟悉的Flex布局非常相似,所以上手起来非常容易,几乎零学习成本。

2.3 Stack/Positoned(绝对定位布局组件)

绝对定位布局在web/rn开发中也是使用频率较高的一种布局方式,Flutter也提供了相应的组件实现,需要将StackPositioned组件搭配在一起使用。比如下方的这个例子就是创建了一个黄色的盒子,并且在其四个角落放置了4个红色的小正方形。Stack组件就是绝对定位的容器,Positioned组件通过lefttop rightbottom四个方向上的属性值来决定其在父容器中的位置。

Container(
  height: 100,
  color: Colors.yellow,
  child: Stack(
    children: <Widget>[
      Positioned(
        left: 10,
        top: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
      Positioned(
        right: 10,
        top: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
      Positioned(
        left: 10,
        bottom: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
      Positioned(
        right: 10,
        bottom: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
    ],
  ),
)

2.4 Text(文本组件)

Text组件也是日常开发中最常用的基础组件之一,我们通常用它来展示文本信息。来看下其构造函数(已省略不常用属性):

const Text(
  this.data, {
  Key key,
  this.style,
  this.textAlign,
  this.softWrap,
  this.overflow,
  this.maxLines,
})
  • data: 显示的文本信息;
  • style: 文本样式,Flutter提供了一个TextStyle类,最常用的fontSizefontWeightcolorbackgroundColorshadows等属性都是通过它设置的;
  • textAlign: 文字对齐方式,常用可选值有TextAlignleftrightcenterjustify
  • softWrap: 文字是否换行;
  • overflow: 当文字溢出的时候,以何种方式处理(默认直接截断)。可选值有TextOverflowclipfadeellipsisvisible
  • maxLines: 当文字超过最大行数还没显示完的时候,就会根据overflow属性决定如何截断处理。

FlutterText组件足够灵活,提供了各种属性让我们定制,不过一般情况下,我们更多地只需下方几行代码就足够了:

Text(
  '这是测试文本',
  style: TextStyle(
    fontSize: 13,
    fontWeight: FontWeight.bold,
    color: Color(0xFF999999),
  ),
)

除了上述的应用场景外,有时我们还会遇到富文本的需求(即一段文本中,可能需要不同的字体样式)。比如在一些UI设计中经常会遇到表示价格的时候,符号比金额的字号小点。对于此类需求,我们可以用Flutter提供的Text.rich构造函数来创建相应的文本组件:

Text.rich(TextSpan(
  children: [
    TextSpan(
      '¥',
      style: TextStyle(
        fontSize: 12,
        color: Color(0xFFFF7528),
      ),
    ),
    TextSpan(
      '258',
      style: TextStyle(
        fontSize: 15,
        color: Color(0xFFFF7528),
      ),
    ),
  ]
))

2.5 Image(图片组件)

Image图片组件作为丰富内容的基础组件之一,日常开发中的使用频率也非常高。看下其构造函数(已省略不常用属性):

Image({
  Key key,
  @required this.image,
  this.width,
  this.height,
  this.color,
  this.fit,
  this.repeat = ImageRepeat.noRepeat,
})
  • image: 图片源,最常用到主要有两种(AssetImageNetworkImage)。使用AssetImage之前,需要在pubspec.yaml文件中声明好图片资源,然后才能使用;而NextworkImage指定图片的网络地址即可,主要是在加载一些网络图片时会用到;
  • width: 图片宽度;
  • height: 图片高度;
  • color: 图片的背景颜色,当网络图片未加载完毕之前,会显示该背景颜色;
  • fit: 当我们希望图片根据容器大小进行适配而不是指定固定的宽高值时,可以通过该属性来实现。其可选值有BoxFitfillcontaincoverfitWidthfitHeightnonescaleDown
  • repeat: 决定当图片实际大小不足指定大小时是否使用重复效果。

另外,Flutter还提供了Image.networkImage.asset构造函数,其实是语法糖。比如下方的两段代码结果是完全一样的:

Image(
  image: NetworkImage('https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg'),
  width: 100,
  height: 100,
)

Image.network(
  'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg',
  width: 100,
  height: 100,
)

2.6 Icon(图标组件)

Icon图标组件相比于图片有着放大不会失真的优势,在日常开发中也是经常会被用到。Flutter更是直接内置了一套Material风格的图标(你可以在这里预览所有的图标类型)。看下构造函数:

const Icon(
  this.icon, {
  Key key,
  this.size,
  this.color,
})
  • icon: 图标类型;
  • size: 图标大小;
  • color: 图标颜色。

3. 布局实战

通过上一节的介绍,我们对ContainerRowColumnStackPositionedTextImageIcon组件有了初步的认识。接下来,就让我们通过一个实际的例子来加深理解和记忆。

3.1 准备工作 - 数据类型

根据上述卡片中的内容,我们可以定义一些字段。为了规范开发流程,我们先给卡片定义一个数据类型的类,这样在后续的开发过程中也能更好地对数据进行Mock和管理:

class PetCardViewModel {
  /// 封面地址
  final String coverUrl;

  /// 用户头像地址
  final String userImgUrl;

  /// 用户名
  final String userName;

  /// 用户描述
  final String description;

  /// 话题
  final String topic;

  /// 发布时间
  final String publishTime;

  /// 发布内容
  final String publishContent;

  /// 回复数量
  final int replies;

  /// 喜欢数量
  final int likes;

  /// 分享数量
  final int shares;

  const PetCardViewModel({
    this.coverUrl,
    this.userImgUrl,
    this.userName,
    this.description,
    this.topic,
    this.publishTime,
    this.publishContent,
    this.replies,
    this.likes,
    this.shares,
  });
}

3.2 搭建骨架,布局拆分

根据给的视觉图,我们可以将整体进行拆分,一共划分成4个部分:CoverUserInfoPublishContentInteractionArea。为此,我们可以搭起代码的基本骨架:

class PetCard extends StatelessWidget {
  final PetCardViewModel data;

  const PetCard({
    Key key,
    this.data,
  }) : super(key: key);

  Widget renderCover() {
    
  }

  Widget renderUserInfo() {
    
  }

  Widget renderPublishContent() {
  
  }

  Widget renderInteractionArea() {
   
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        boxShadow: [
          BoxShadow(
            blurRadius: 6,
            spreadRadius: 4,
            color: Color.fromARGB(20, 0, 0, 0),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          this.renderCover(),
          this.renderUserInfo(),
          this.renderPublishContent(),
          this.renderInteractionArea(),
        ],
      ),
    );
  }
}

3.3 封面区域

为了更好的凸现图片的效果,这里加了一个蒙层,所以此处刚好可以用得上Stack/Positioned布局和LinearGradient渐变,Dom结构如下:

cover

Widget renderCover() {
  return Stack(
    fit: StackFit.passthrough,
    children: <Widget>[
      ClipRRect(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(8),
          topRight: Radius.circular(8),
        ),
        child: Image.network(
          data.coverUrl,
          height: 200,
          fit: BoxFit.fitWidth,
        ),
      ),
      Positioned(
        left: 0,
        top: 100,
        right: 0,
        bottom: 0,
        child: Container(
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color.fromARGB(0, 0, 0, 0),
                Color.fromARGB(80, 0, 0, 0),
              ],
            ),
          ),
        ),
      ),
    ],
  );
}

3.4 用户信息区域

用户信息区域就非常适合使用RowColumn组件来进行布局,Dom结构如下:

user-info

Widget renderUserInfo() {
  return Container(
    margin: EdgeInsets.only(top: 16),
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Row(
          children: <Widget>[
            CircleAvatar(
              radius: 20,
              backgroundColor: Color(0xFFCCCCCC),
              backgroundImage: NetworkImage(data.userImgUrl),
            ),
            Padding(padding: EdgeInsets.only(left: 8)),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  data.userName,
                  style: TextStyle(
                    fontSize: 15,
                    fontWeight: FontWeight.bold,
                    color: Color(0xFF333333),
                  ),
                ),
                Padding(padding: EdgeInsets.only(top: 2)),
                Text(
                  data.description,
                  style: TextStyle(
                    fontSize: 12,
                    color: Color(0xFF999999),
                  ),
                ),
              ],
            ),
          ],
        ),
        Text(
          data.publishTime,
          style: TextStyle(
            fontSize: 13,
            color: Color(0xFF999999),
          ),
        ),
      ],
    ),
  );
}

3.5 发布内容区域

通过这块区域的UI练习,我们可以实践Container组件设置不同的borderRadius,以及Text组件文本内容超出时的截断处理,Dom结构如下:

publish-content

Widget renderPublishContent() {
  return Container(
    margin: EdgeInsets.only(top: 16),
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Container(
          margin: EdgeInsets.only(bottom: 14),
          padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
          decoration: BoxDecoration(
            color: Color(0xFFFFC600),
            borderRadius: BorderRadius.only(
              topRight: Radius.circular(8),
              bottomLeft: Radius.circular(8),
              bottomRight: Radius.circular(8),
            ),
          ),
          child: Text(
            '# ${data.topic}',
            style: TextStyle(
              fontSize: 12,
              color: Colors.white,
            ),
          ),
        ),
        Text(
          data.publishContent,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
          style: TextStyle(
            fontSize: 15,
            fontWeight: FontWeight.bold,
            color: Color(0xFF333333),
          ),
        ),
      ],
    ),
  );
}

3.6 互动区域

在这个模块,我们会用到Icon图标组件,可以控制其大小和颜色等属性,Dom结构如下:

interaction-area

Widget renderInteractionArea() {
  return Container(
    margin: EdgeInsets.symmetric(vertical: 16),
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Row(
          children: <Widget>[
            Icon(
              Icons.message,
              size: 16,
              color: Color(0xFF999999),
            ),
            Padding(padding: EdgeInsets.only(left: 6)),
            Text(
              data.replies.toString(),
              style: TextStyle(
                fontSize: 15,
                color: Color(0xFF999999),
              ),
            ),
          ],
        ),
        Row(
          children: <Widget>[
            Icon(
              Icons.favorite,
              size: 16,
              color: Color(0xFFFFC600),
            ),
            Padding(padding: EdgeInsets.only(left: 6)),
            Text(
              data.likes.toString(),
              style: TextStyle(
                fontSize: 15,
                color: Color(0xFF999999),
              ),
            ),
          ],
        ),
        Row(
          children: <Widget>[
            Icon(
              Icons.share,
              size: 16,
              color: Color(0xFF999999),
            ),
            Padding(padding: EdgeInsets.only(left: 6)),
            Text(
              data.shares.toString(),
              style: TextStyle(
                fontSize: 15,
                color: Color(0xFF999999),
              ),
            ),
          ],
        ),
      ],
    ),
  );
}

3.7 小结

通过上面的一个例子,我们成功地把一个看起来复杂的UI界面一步步拆解,将之前提到的组件都用了个遍,并且最终得到了不错的效果。其实,日常开发中90%以上的需求都离不开上述提到的基础组件。因此,只要稍加练习,熟悉了Flutter中的基础组件用法,就已经算是迈出了一大步哦~

这里还有银行卡朋友圈的UI练习例子,由于篇幅原因就不贴代码了,可以去github仓库看。

4. 总结

本文首先介绍了Flutter中构建UI界面最常用的基础组件(容器绝对定位布局文本图片图标)用法。接着,介绍了一个较复杂的UI实战例子。通过对Dom结构的层层拆解,前文提到过的组件得到一个综合运用,也算是巩固了前面所学的概念知识。

不过最后不得不吐槽一句:Flutter的嵌套真的很难受。。。如果不对UI布局进行模块拆分,那绝对是噩梦般的体验。而且不像web/rn开发样式可以单独抽离,Flutter这种将样式当做属性的处理方式,一眼看去真的很难理清dom结构,对于新接手代码的开发人员而言,需要费点时间理解。。。

@tubu-long
Copy link

请问issue怎么下载啊,为什么不用仓库管理啊

@SmallStoneSK
Copy link
Owner Author

请问issue怎么下载啊,为什么不用仓库管理啊

下载什么?没懂什么意思。。。

@ChenZhengRunHi
Copy link

sta已点,真厉害,期待更新文章,跟着学习

@w4mxl
Copy link

w4mxl commented Aug 6, 2019

文章写的真好,基础 widget 的介绍基本面面俱到,👍。

关于 Stack 想补充说一下它的 fit 属性,因为自己学习的时候有着重理解了一下。

fit:用于定义没有定位的(non-positioned)子 widget 如何去适应 Stack 的大小,默认为 loose

其中 StackFit 有如下几种:

loose:子节点宽松的取值,可以从min到max的尺寸;
expand:子节点尽可能的占用空间,取max尺寸;
passthrough:不改变子节点的约束条件;

具体效果可以在 @SmallStoneSK 提供的例子中改变属性值看看区别,也可以自己写个 Stack 的 demo。

另外这篇文章Stack 的属性都有示例效果,可以看下。

@SmallStoneSK
Copy link
Owner Author

@w4mxl 哈哈,多谢补充~

@boybian
Copy link

boybian commented Mar 19, 2020

前端高手,请教一个问题,如何在同一个页面里上下放2个LISTVIEW哈?

@SmallStoneSK
Copy link
Owner Author

前端高手,请教一个问题,如何在同一个页面里上下放2个LISTVIEW哈?

@boybian 可以通过CustomScrollView配合SliverList使用,使用方式可以参考这篇文章

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Flutter Flutter学习
Projects
None yet
Development

No branches or pull requests

5 participants