阅读 1815

Flutter从静态界面到抽取封装

今天将用Flutter的组件来实际布局演练一下,在此之前你需要熟悉Flex布局


1、微信条目的静态布局

这个平时非常常见,而且相对简单,所以是个练手的不错人选

简单分析一下:一共三块,用Row布局,左右分别处于头尾,中间自延伸
头像使用Image,小红点用ClipOval对Container裁剪,堆叠在一起,用Stack布局  
中间的文字是两行的Column,右边的也是两行的Column,比较简单,剩下的就是边距了。
复制代码

1.1:左侧头像

用一个ClipRRect来进行图片的圆角操作,Container来限制大小,
通过Stack布局将小红点放到图片左上角,小红点通过ClipOval对Container裁剪

var left = Container(
  child: ClipRRect(
    borderRadius: BorderRadius.circular(5),
    child: Image.asset(
      //头像
      "images/娜美.jpg",
      fit: BoxFit.cover,
    ),
  ),
  width: 50,
  height: 50,
);
var leftWrap = Stack(
  alignment: Alignment(1.2, -1.2),
  children: <Widget>[
    left,
    ClipOval(child: Container(width: 10, height: 10, color: Colors.red,),)
  ],);
复制代码

2.文字和边距的处理

想让两头的固定,中间填满,在Flex布局中可以用Expanded将其包裹

var center = Column(
  mainAxisSize: MainAxisSize.min,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Text("心如止水", style: nameTextStyle),
    Text(
      "在吗?小哥哥",
      style: infoTextStyle,
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
    )
  ],
);
var right = Column(
  mainAxisAlignment: MainAxisAlignment.center,
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Text("06:45", style: infoTextStyle),
    Icon(
      Icons.visibility_off,
      size: 18,
      color: Color(0xff999999),
    )
  ],
);
var body = Row(
  children: <Widget>[leftWrap, Expanded(child: center,), right],//使用Expanded
);
复制代码

边距根据需求自己加一下,可以用Padding,也可以用Container


2、微信条目的封装

封装一个组件,首先要看它是否有状态,判断的标准很简单:
看它的界面是否有需要因响应而改变的部分,有则将该字段当做状态值。
这个条目组件有个小红点,是会随着状态的不同而显隐的,所以写成有状态的组件
封装成组件的好处在于复用起来非常方便,如下就不用再重新写一遍了。


2.1:创建信息描述类和组建类
import 'package:flutter/material.dart';

class ItemChart extends StatefulWidget {
  ItemChart({Key key,this.chartBean,}) : super(key: key);
  final ChartBean chartBean;
  @override
  _ItemChartState createState() => _ItemChartState();
}

class _ItemChartState extends State<ItemChart> {
  bool _checked;//是否已查看
  ChartBean _chartBean;//条目描述类
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

class ChartBean{
  Image image;//头像
  String name;//名字
  String sentence;//句子
  String time;//时间
  bool shield;//是否屏蔽

  ChartBean({this.image, this.name, this.sentence, this.time,
      this.shield=false});
}
复制代码

2.2:使用信息描述类替换写死字段
class _ItemChartState extends State<ItemChart> {
  var nameTextStyle = TextStyle(color: Colors.black, fontSize: 16);
  var infoTextStyle = TextStyle(color: Color(0xff999999), fontSize: 12);
  bool _checked=false;//是否已查看

  @override
  Widget build(BuildContext context) {

    var left = Container(
      child: ClipRRect(
        borderRadius: BorderRadius.circular(5),
        child: widget.chartBean.image,
      ),
      width: 50,
      height: 50,
    );

    var leftWrap = Stack(
      alignment: Alignment(1.2, -1.2),
      children: <Widget>[
        left,
        ClipOval(child: Container(width: 10, height: 10, color: Colors.red,),)
      ],);

    var center = Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(widget.chartBean.name, style: nameTextStyle),
        Padding(padding: EdgeInsets.only(top:4),child: Text(
          widget.chartBean.sentence,
          style: infoTextStyle,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),)
      ],
    );

    var right = Column(
      mainAxisAlignment: MainAxisAlignment.center,
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Text(widget.chartBean.time, style: infoTextStyle),
        widget.chartBean.shield?Icon(
          Icons.visibility_off,
          size: 18,
          color: Color(0xff999999),
        ):Container(height: 18,)
      ],
    );
    var body = Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: <Widget>[Padding(padding: EdgeInsets.only(right: 10),child: _checked?left:leftWrap,), Expanded(child: center,), right],
    );
    return Container(child: body, padding: EdgeInsets.all(10),);
  }
}
复制代码

2.3:添加点击事件与状态改变

定义一个回调函数类型,在最外层用一个InkWell涟漪事件响应组件来触发监听
在其中将_checked字段进行改变,再用setState重新渲染。

class ItemChart extends StatefulWidget {
  ItemChart({Key key,this.chartBean,this.onTap}) : super(key: key);
  final ChartBean chartBean;
  final TapCallback onTap;
  @override
  _ItemChartState createState() => _ItemChartState();
}

typedef TapCallback = void Function(ChartBean bean);

---->[build方法中添加监听以及回调及状态改变]----
return InkWell(
  child: Container(child: body, padding: EdgeInsets.all(10)),
  onTap: () {
    _checked = true;
    if (widget.onTap != null) {
      widget.onTap(widget.chartBean);
    }
    setState(() {});
  },
);

---->[使用]----
show = ItemChart(
  onTap: (bean){
    print(bean.name);
  },
  chartBean: ChartBean(
    name: "张风捷特烈",
      sentence: "我是要成为编程之王的男人。你要不要成为编程之王的女人?",
      time: "08:30",
      shield: false,
      image: Image.asset(
    "images/icon_head.png",
  )),
);
复制代码

这样,当拿到json数据,解析,填充到ListView中就非常方便了。


3、掘金简介的静态界面

个人觉得掘金的简介还是挺好看的,就来看看这个如何布局:


3.1:布局分析

最外层做个Card,其中主要三部分,可以用Row来包,
左边头像,可以用Image ,加圆形裁剪。
中间是一个三行的Column ,水平方向靠左,并且自延伸
右边是两行的Column,上下左右居中
复制代码

3.2: 左侧

使用ClipOval将一个Image裁成圆形

var left = ClipOval(
    child: Image.asset(
  //头像
  "images/icon_head.png",
  width: 50,
  height: 50,
));
复制代码

3.3:中间

这里是关于文字的操作,有一点要注意的Flex中的textBaseline属性对文字中的作用
使用Expanded可以让Row尽可能延展,文字到头也会自动换行,当横屏是也会适应。

var titleTextStyle=TextStyle(color: Colors.black, fontSize: 20);
var infoTextStyle=TextStyle(color: Color(0xff72777B), fontSize: 14);

var user = Row(
  children: <Widget>[
    Text(
      "张风捷特烈",
      style: titleTextStyle,
    ),
    ClipRRect(
      borderRadius: BorderRadius.circular(5),
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 4),
        color:Color(0xff34D19B) ,
        child: Text("Lv4",
          style: TextStyle(
            fontWeight: FontWeight.w900,
            color: Colors.white,fontSize: 10,
          ),
        )),)
  ],
);
var info = Row(
  children: <Widget>[
    Container(
      margin: EdgeInsets.only(right: 5),
      child: Icon(Icons.next_week, size: 15),
    ),
    Text(
      "创世神 | 无",
      style: infoTextStyle,
    )
  ],
);
var say = Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  textBaseline: TextBaseline.ideographic,
  children: <Widget>[
    Container(
      margin: EdgeInsets.only(right: 5),
      child: Icon(Icons.keyboard, size: 15),
    ),
    Expanded(
        child: Text("海的彼岸有我未曾见证的风采",
            style:infoTextStyle))
  ],
);

复制代码

3.4:右边

也比较简单,通过一个Column将两块拼出来。

var right = Column(
  crossAxisAlignment: CrossAxisAlignment.center,
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Row(
      children: <Widget>[
        Icon(
          Icons.language,
          size: 18,
        ),
        Padding(
            padding: EdgeInsets.symmetric(horizontal: 10),
            child: Icon(Icons.local_pharmacy, size: 18)),
        Icon(Icons.person_pin_circle, size: 18)
      ],
    ),
    OutlineButton(
      onPressed: () {},
      child: Text(
        "编辑资料",
        style: TextStyle(fontSize: 11),
      ),
      textTheme: ButtonTextTheme.primary,
      borderSide: BorderSide(color: Color(0xff6F80F7), width: 1),
    ),
  ],
);
复制代码

3.5:将几部分拼合

var result = Card(
    child: Row(
  children: <Widget>[
    Container(
      margin: EdgeInsets.only(left: 10, right: 10),
      child: left,
    ),
    Expanded(child: center),
    Container(
      margin: EdgeInsets.only(left: 10, right: 10),
      child: right,
    ),
  ],
  mainAxisAlignment: MainAxisAlignment.spaceAround,
));
复制代码

4.对静态组件的封装

上面写的只是一个静态的布局,也只是个玩偶而已,复用和可维护性是很低的。
如何让它成为一个能随意更改内容的有灵魂的组件呢?如下,可以很容易复用
将可以抽离的写死字段抽离出来,自定义一个描述类作为入参,这是基本的思路


4.1:创建描述类

将页面上的字段进行抽取,形成一个类

class User {
  String name;//名字
  int lever;//等级
  Image image;//头像
  String position;//职务
  String company;//公司
  String proverbs;//箴言
  User({this.name, this.lever, this.image, this.position, this.company,
    this.proverbs});
}
复制代码

4.2:创建一个UserPanel组件

继承自StatelessWidget的无状态组件,传入一个User对象

class UserPanel extends StatelessWidget {
  final User user;
  UserPanel({this.user});
  
  @override
  Widget build(BuildContext context) {
    return null;
  }
}
复制代码

4.3:结合

将上面的代码中写死的部分用User对象的属性替换即可,然后再build方法里返回出去
比如在名字的等级,其他的就不贴了,换一下就行了。在使用时传入User对象即可

var user = Row(
  children: <Widget>[
    Text(
      this.user.name,
      style: titleTextStyle,
    ),
    ClipRRect(
      borderRadius: BorderRadius.circular(5),
      child: Container(
          padding: EdgeInsets.symmetric(horizontal: 4),
          color: Color(0xff34D19B),
          child: Text(
            "Lv${this.user.lever}",
            style: TextStyle(
              fontWeight: FontWeight.w900,
              color: Colors.white,
              fontSize: 10,
            ),
          )),
    )
  ],
);

---->[使用方法]----
 UserPanel(
   user: User(
       name: "Toly",
       lever: 4,
       company: "捷特王国",
       position: "编程之王",
       proverbs: "心之既在,无问东西。",
       image: Image.asset(
         //头像
         "images/head_me.jpg",
       )),
 ),
复制代码

4.4:添加点击事件监听

既然封装了,就完善一些,将图像和编辑资料的点击回调传递出去
如果有其他的需要点击,也可以类似的回调出去

//定义两个回调函数类型
typedef HeadTapCallback = void Function();
typedef EditTapCallback = void Function(User user);

//声明成员变量
final HeadTapCallback onHeadTap;
final EditTapCallback onEditTap;
UserPanel({
    this.user,this.onHeadTap,this.onEditTap});

//头像点击回调
var left = InkWell(
    child: ClipOval(
        child: Container(
          width: 50,
          height: 50,
          child: this.user.image,
        )),
    onTap:onHeadTap,
);

//编辑按钮点击回调
OutlineButton(
  onPressed: (){
    if (onEditTap!=null) {
      onEditTap(this.user);
    }
  },
  child: Text(
    "编辑资料",
    style: TextStyle(fontSize: 11),
  ),
  textTheme: ButtonTextTheme.primary,
  borderSide: BorderSide(color: Color(0xff6F80F7), width: 1),
),

---->[使用]----
UserPanel(
  onHeadTap: (){
    print("----------------");
  },
  onEditTap: (user){
    print("----------------user:${user.name}");
  },
  user: User(
      name: "Toly",
      lever: 4,
      company: "捷特王国",
      position: "编程之王",
      proverbs: "心之既在,无问东西。",
      image: Image.asset(
        //头像
        "images/head_me.jpg",
      )),
),
复制代码

这样该组件就可以独立出来,从一个写死的静态界面变成了可复用的组件
它会根据你传入的User对象进行不同的表现,也就是它是"活的",
User便是他的灵魂,回调监听便是他的行为。而不像静态界面,只是人偶而已。
今天从有状态和无状态两种组件看了一下如何对组件进行简单的封装,希望你有所收获。


5.仿淘宝商品item

就不写静态界面了,直接上。布局和上面大同小异,只要能够划分好结构,都好办
这里要提一点的是下面的价格通过TextSpan处理了一下,你可以好好看看。


5.1、组件使用
var show = Goods(
        onTap:(goods){
            print(goods.title);
        },
        width: 200,
        goods: GoodBean(
          price: 21.89,
          saleCount: 99,
          title: "得力笔记本文具商务复古25K/A5记事本PU软皮面日记本子定制可印logo简约工作笔记本会议记录本小清新大学生用",
          caverUrl: "https://img.alicdn.com/imgextra/i3/108452043/O1CN01IMPSxR1QxjhmdZLXA_!!0-saturn_solar.jpg_220x220.jpg_.webp"),));
  }
复制代码

5.2:组件封装
import 'package:flutter/material.dart';

class Goods extends StatelessWidget {
  Goods({Key key, this.goods, this.width,this.onTap}) : super(key: key);

  final GoodBean goods;
  final double width;
  final TapCallback onTap;
  @override
  Widget build(BuildContext context) {
    var top = Image.network(goods.caverUrl);

    var center = Container(
      child: Text(
        goods.title,
        maxLines: 2,
      ),
    );
    var bottom = Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Padding(
          padding: EdgeInsets.only(right: 5),
          child: _getPriceWidget(goods.price),
        ),
        Expanded(
          child: Text("${goods.saleCount}人付款",style: TextStyle(fontSize:13,color: Colors.black38),),
        ),
        Icon(Icons.more_horiz,color: Colors.black38)
      ],
    );

    var result = Column(
        mainAxisAlignment: MainAxisAlignment.center,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          top,
          Padding(
            padding: EdgeInsets.all(10),
            child: center,
          ),
          Padding(
            padding: EdgeInsets.fromLTRB(10,0,10,10),
            child: bottom,
          )
        ]);
    return Card(
      elevation: 5,
      child: InkWell(child: Container(
        width: width,
        child: result,
      ),onTap: (){
        if(onTap!=null){
          onTap(goods);
        }
      },),
    );
  }

  Widget _getPriceWidget(double price) {
    var prices = price.toString().split(".");
    var span = TextSpan(
      text: '¥',
      style: TextStyle(fontSize: 12, color: Colors.deepOrangeAccent),
      children: <TextSpan>[
        TextSpan(
            text: "${prices[0]}.",
            style: TextStyle(fontSize: 16, color: Colors.deepOrangeAccent)),
        TextSpan(
            text: "${prices[1]}",
            style: TextStyle(fontSize: 12, color: Colors.deepOrangeAccent)),
      ],
    );
    return RichText(text: span);
  }
}

typedef TapCallback = void Function(GoodBean bean);

class GoodBean {
  String caverUrl; //封面图链接
  String title; //链接
  double price; //价格
  int saleCount;
  GoodBean({this.caverUrl, this.title, this.price, this.saleCount}); //销售数
}
复制代码
结语

本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328,期待与你的交流与切磋。

关注下面的标签,发现更多相似文章
评论