4、Flutter项目之我的&通讯录

一、我的界面搭建

1.1 开始搭建

  • 首先将组件按照模块划分开来.创建相应的文件夹,拖入其中.
  • 其次导航栏颜色和标题字体颜色与MaterialApp下设置的ThemeData有关.
    • 如果primarySwatch颜色设置的深色体,那么导航栏上的标题颜色就为白色
    • 如果primarySwatch颜色设置的浅色体,那么导航栏上的标题颜色就为黑色.
  • 布局思路:

1、根据微信我的界面UI、导航栏上没有标题.因此删掉Scaffold中设置的AppBar部分.

2、由UI效果,选用Stack层叠式布局

3、整体划分为相机+头部视图和下边的Cell视图.

4、然后先画出一个相机,接着按照Stack布局包含Header+Cell; Cell按照发现界面的设置逻辑,布局出来.

    • 头部相机部分的实现,因为采用层叠式布局,那么相机在Stack最后的位置.
Container(
  //1.设置相机Container的间距
  margin: EdgeInsets.only(top: 50,right: 10),
  height: 30,
  child: Row(
    //2.设置主轴,让相机图片靠右展示.
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
      Image(image: AssetImage("images/相机.png"),width: 30,height: 30,),
    ],
  ),
)
    • 接下来布局ListView:它包含头视图和列表上的Cell
ListView(
  children: [
    Container(
      color: Colors.white,
      height: 200,
    ),
    //5.灰色分割线
    SizedBox(height: 10,),
    //6.设置支付Cell
    DiscoverCell(title: "支付", imageName: "images/微信 支付.png"),
    //8.余下部分根据发现界面的布局逻辑,填充
    ...
    ],
    ),)
    • 灰色分割线
    • 设置支付Cell
    • 余下部分根据发现界面的布局逻辑,填充
  • 布局过程中发现顶部因为刘海屏的特性,会空出一部分刘海,Flutter官方给出适配方案.通过MediaQuery.removePadding移除刘海边距
    • 那么这部分的Container内容为
Container(
  //4.设置主题色
  color: Color.fromRGBO(220, 220, 220, 1),
  //7.顶部因为刘海屏,导致空出多余的刘海部分,Flutter官方给出适配方案.通过MediaQuery.removePadding移除刘海边距.
  child: MediaQuery.removePadding(
      removeTop: true,
      context: context,
      child: ListView(...),)
),
  • 综上,布局效果为

1.2 我的界面顶部头视图补充

  • 1. 接下里布局我们的头视图部分.整个个人信息部分可以作为一整个Container.所以可以完全抽离出来,作为一个头视图组件.
    • 此时ListView的布局为
ListView(
  children: [
    //9.抽离出来的组件
    headWidget(),
    //5.灰色分割线
    SizedBox(height: 10,),
    //6.设置支付Cell
    DiscoverCell(title: "支付", imageName: "images/微信 支付.png"),
    ],),
 )
    • 抽离出来的组件样式先设置为
//10. 头视图组件
Widget headWidget(){
  return Container(
    height: 200,
    color: Colors.white,
    child: Container(
      //10.2 内部距离顶部100
      margin: EdgeInsets.only(top: 100),
      //10.3 四周边距设置为10
      padding: EdgeInsets.all(10),
      //10.1 设置内部包含一个Container
      child: Container(
          color: Colors.red
      ),
    ),
  );
}

2.接下来分析头部视图中的组成.

  • 可以分为左右结构.左边为头像整体,右边为名称+微信号的上下布局.
    • 也就是 Row包含Column,Column包含Row.
  • 因此: 先画出头像视图的部分
//10.1 设置内部包含一个Container
child: Container(
  color: Colors.red,
  //11.1 头像向左偏移20,设置整体偏移
  margin: EdgeInsets.only(left: 20),
  //11.2 整体内边距设置为5
  padding: EdgeInsets.all(5),
  //11. 外层的Row布局: headImage+selfInformation
  child: Row(
    children: [
      //11.3 头像
      Container(
        //11.4 设置头像圆角,因为装饰器只会对Container起作用,
        // 不会对Image起作用,所以为了看出圆角效果,我们将头像Image放入装饰器中.
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(16.0),
          image: DecorationImage(image: AssetImage('images/fly_in_wind.png'),
              //11.6如果设置的图片宽高不是正方形,那么需要设置图片填充模式来满足切圆角后的适配.
              fit: BoxFit.cover
          ),
        ),
        //11.5 添加头像宽高,没有宽高,装饰器设置看不见图片.
        width: 70,
        height: 70,
      ),
      //12 右边个人信息部分
      Container()
    ],
  ),
),
    • 11. 外层的Row布局: headImage+selfInformation
      • 11.1 头像向左偏移20,设置整体偏移
      • 11.2 整体内边距设置为5
      • 11.3 头像Container
      • 11.4 设置头像圆角,因为装饰器只会对Container起作用,不会对Image起作用,所以为了看出圆角效果,我们将头像Image放入装饰器中.
      • 11.5 添加头像宽高,没有宽高,装饰器设置看不见图片.
      • 11.6如果设置的图片宽高不是正方形,那么需要设置图片填充模式来满足切圆角后的适配

 3. 接下来布局头视图右边部分.

  • 右边个人信息部分,拿到设备屏幕宽度,设置剩余的宽度: 屏幕的宽度-10-5-20-70
Container(
  //12.1 拿到设备屏幕宽度.设置剩余的宽度
  width: MediaQuery.of(context).size.width-115,
),
  • 也可以整体通过Expanded填充,不去考虑计算问题,布局如下
//12.2 也可以整体通过Expanded填充,不去考虑计算问题
Expanded(child: Container(
   //12.3.6 设置边距
   padding: EdgeInsets.only(left: 10),
   //12.3 设置列
   child: Column(
     //12.3.3 设置主轴方向居中,则会靠中线对齐.
     mainAxisAlignment: MainAxisAlignment.center,
     children: [
      //12.3.1 微信昵称
      Container(
        //12.3.7 设置对齐方式
        alignment: Alignment.centerLeft,
        height: 35,
        child: Text('随风',style: TextStyle(fontSize: 25),),
      ),
      //12.3.2 微信号
      Container(
        height: 35,
        child: Row(
          children: [
            Text("微信号: 123456",style: TextStyle(
              fontSize: 17,
              color: Colors.grey
            ),),
            //12.3.4 设置箭头
            Image(image: AssetImage('images/icon_right.png'),width: 15,)
          ],
          //12.3.5 设置主轴方向,让内部视图左右分开
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
        ),
      )
    ],
  ),
))
    • 12.3 设置列: Expanded中的Column
    • 12.3.1 微信昵称:Column中的Container
    • 12.3.2 微信号:Column中的Container
    • 12.3.3 设置Column主轴方向居中,则会靠中线对齐.
    • 12.3.4 设置微信号同级的箭头
    • 12.3.5 设置微信号Container主轴方向,让内部视图左右分开
    • 12.3.6 设置Expanded个人信息整体边距局左10
    • 12.3.7 设置微信昵称Container中的对齐方式:居左

4.综上: 我的界面布局完成.下边列表上的Cell会跳转界面.

二、通讯录界面搭建

2.1 导航栏上的UI实现

1、根据微信UI效果,我们在这里需要添加模型数据.定义一个friends_data文件,

    • 定义一个Friends模型,不写继承关系的话,默认继承Object
    • 根据UI得知,需要头像,昵称,昵称对应的英文大写字母
class Friends {
  final String imageUrl;
  final String name;
  final String? indexLetter;
  //构造函数
  Friends(this.imageUrl, this.name, this.indexLetter);
}

2、来到friends_page中,需要修改导航栏的背景颜色和标题颜色和一个添加通讯录人员的图标.在这个时候,我们发现主题背景颜色经常使用,所以我们这个时候封装一个const常量文件,将其放入其中,统一调用.

import 'package:flutter/material.dart';
final WeChatThemeColor = Color.fromRGBO(220, 220, 220, 1.0);
    • 接下来全局修改这个主题灰色,必要时导入const.dart头文件

3、导航栏上添加图片.采用AppBar的actions列表,右因为有响应事件,所以采用 GestureDetector包装的方式

  • 通讯录根视图这里设置如下:
Scaffold(
  appBar: AppBar(
    backgroundColor: WeChatThemeColor,
    //去除导航条上的默认黑线
    elevation: 0.0,
    //默认标题居中, 设置标题颜色为黑色
    centerTitle: true,
    title: const Text("通讯录",style: TextStyle(fontSize: 16,color: Colors.black),),
    actions: [
      //右上角加号按钮及响应.
      GestureDetector(
        child: Container(
          margin: EdgeInsets.only(right: 10),
          child: Image(image: AssetImage('images/icon_friends_add.png'),width: 25,),
        ),
        onTap: (){
          Navigator.of(context).push(
            MaterialPageRoute(builder: (BuildContext context) => DiscoverChildPage(title: "通讯录"))
          );
        },
      )
    ],
  ),
  body: const Center(child:
      Text("通讯录界面"),
  ),
);

2.2 列表上的内容填充

  • 因为通讯录上有固定的内容 ['新的朋友',‘群聊’,‘标签’,‘公众号’],所以这些可以作为本地数据
  • 创建一个friend_page_cell.dart文件.在其中定义我们要使用的字段.
    • 因为imageUrl、indexLetter、imageAssets都可能为空、所以设置为?类型
import 'package:flutter/material.dart';
class FriendsPageCell extends StatelessWidget {
  final String? imageUrl;
  final String name;
  final String? indexLetter;
  final String? imageAssets;
  const FriendsPageCell({super.key, this.imageUrl, required this.name, this.indexLetter, this.imageAssets});
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

2.3 网络模型数据添加

  • 将已知网络数据添加到friends_data.dart文件中,并且添加上message和time字段.

2.4 数据加载

回到friends_page中,列表数据加载分为网络数据和本地数据

  • 本地数据: 那么在_FriendsPageState中,我们定义一个头部视图固定数据
final List _headerData = [
  Friends(imageUrl: 'images/新的朋友.png', name: '新的朋友'),
  Friends(imageUrl: 'images/群聊.png', name: '群聊'),
  Friends(imageUrl: 'images/标签.png', name: '标签'),
  Friends(imageUrl: 'images/公众号.png', name: '公众号'),
];
  • 网络数据加载
    • 创建一个_itemForRow(BuildContext,int index)方法,实现内部布局逻辑.
//3. 设置cellForRow布局
Widget _itemForRow(BuildContext context, int index) {
  //如果小于4个,就返回本地图片
  if (index < _headerData.length){
    return FriendsPageCell(imageAssets: _headerData[index].imageUrl, name: _headerData[index].name,);
  }
  //否则加载网络图片
  return FriendsPageCell(
    imageUrl: datas[index - 4].imageUrl,
    name: datas[index-4].name,
  );
}
  • 其中FriendsPageCell中的实现为
    • 根据imageUrl是否为null,选择加载网络图片或者本地图片
    • 名称的显示
    • 其中,在设置三目运算符时AssetImage类型推断报错,无法转换为ImageProvider类型,查看AssetImage的继承链,发现父类中就是来自ImageProvider,所以加上as强转
Container(
    height: 54,
    color: Colors.white,
    child: Row(
      children: [
        Container( //图片
        width: 34,height:34,
        margin: EdgeInsets.all(10),
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(6.0),
            image: DecorationImage(image: imageUrl != null ? NetworkImage(imageUrl!) : AssetImage(imageAssets!) as ImageProvider)),
        ), //名称
        Container(child: Text(name,style: TextStyle(fontSize: 18),),)
      ],
    ),
);

2.5 数据调用及赋值

  • 在_FriendsPageState中,Scaffold的body实现部分为
Container(
  child: ListView.builder(
    //2. 长度= 固定长度+网络数据数组长度
    itemCount: _headerData.length+datas.length,
    itemBuilder: _itemForRow,
  ),
)
  • 通过ListView.build加载网络及本地List中的数据.itemBuilder对应的封装为 _itemForRow.
  • itemCount长度= 本地固定长度+网络数据长度

2.6 分割线处理

  • 现在我们就可以看到布局的效果了,但是cell直接没有分割线.所以需要将这个细节完善.
  • 因为不设置宽度,线条不会显示,所以想使用屏幕宽度余量来设置cell上线条的宽度.那么我们在const.dart文件中封装下屏幕宽高.
//屏幕宽高
double ScreenWidth(BuildContext context)  => MediaQuery.of(context).size.width;
double ScreenHeight(BuildContext context) => MediaQuery.of(context).size.height;
  • 在Cell中,发现分割线与名称是相对应的宽度,所以我们在名称的Container中添加一个Column结构,名称在上,线条在下.因此布局如下:
Container(//名称的Container
  child: Column(
    children: [
    Container(//名称
      height: 44,
      width: ScreenWidth(context)-54,//宽度填充后,对齐方式才会生效,否则没有宽度会默认居中.
      alignment: Alignment.centerLeft,//对齐方式
      child: Text(name,style: TextStyle(fontSize: 18),),
    ),
      Container(//线条
        height: 0.5,
        color: WeChatThemeColor,
        width: ScreenWidth(context)-54,)
  ],),
)
  • 当前效果为:

2.7 显示分组Cell的Header

2.7.1 cell上添加属性

  • 让有些Cell显示大写字母标题,有些隐藏.那么我们在Cell上添加groupTitle;
    • _itemForRow上添加属性调用.
//3. 设置cellForRow布局
Widget _itemForRow(BuildContext context, int index) {
  //如果小于4个,就返回本地图片
  if (index < _headerData.length){
    return FriendsPageCell(imageAssets: _headerData[index].imageUrl, name: _headerData[index].name,);
  }
  //否则加载网络图片
  return FriendsPageCell(
    imageUrl: datas[index - 4].imageUrl,
    name: datas[index-4].name,
    groupTitle: datas[index-4].indexLetter,
  );
}
  • 在FriendsPageCell原先的基础上,需要添加一个组头,那么需要将原来的Container改为Column布局方式.
@override
Widget build(BuildContext context) {
  return Column(
    children: [
        Container(color: Colors.grey, height: 30,),
        Container(...),
    ],);
}

2.7.2 cell上分割线处理

  • 这个时候全部都加上了一个组头,但是分割线线条显示异常.此时我们将与之同级的Text的Container高度改为53.5,此时线条就达到了最底部.

  • 将friends_page的Scaffold的body中Container的color设置为灰色主题色.然后将friends_page_cell 中Column内的头视图完善.
Container(
  alignment: Alignment.centerLeft,
  padding: EdgeInsets.only(left: 10),
  //如果当前分组值不为空,就显示分组头
  height: groupTitle != null ? 30 : 0.0,
  color: Color.fromRGBO(1, 1, 1, 0.0),
  child: groupTitle != null ? Text(groupTitle!) : null,
)

2.7.3 数据构造

  • 这个时候发现数据量有些少,那么我们手动创建一些重复的网络数据即可.
    • 重写initState初始化方法
    • .. 表示级联操作
final List<Friends> _listDatas = [];
@override
void initState() {
  super.initState();
  _listDatas..addAll(datas)..addAll(datas);
}
  • 将datas的调用替换为 _listDatas.

2.7.4 数据排序

  • 通过数组的sort方法进行排序
void initState() {
  super.initState();
  _listDatas..addAll(datas)..addAll(datas);
  _listDatas.sort((Friends a , Friends b){
    return (a.indexLetter != null && b.indexLetter != null) ? a.indexLetter!.compareTo(b.indexLetter!) : 0;
  });
}

2.7.5 组头过滤

  • 如果当前Cell和上一个Cell的IndexLetter一样,就不显示组头.
//3. 设置cellForRow布局
Widget _itemForRow(BuildContext context, int index) {
  //如果小于4个,就返回本地图片
  if (index < _headerData.length){
    return FriendsPageCell(imageAssets: _headerData[index].imageUrl, name: _headerData[index].name,);
  }
  //如果当前Cell和上一个Cell的IndexLetter一样,就不显示组头.
  if (index > 4 && _listDatas[index - 4].indexLetter == _listDatas[index - 5].indexLetter) {
    return FriendsPageCell(imageUrl: _listDatas[index - 4].imageUrl,name: _listDatas[index - 4].name);
  }
  //否则加载网络图片
  return FriendsPageCell(
    imageUrl: _listDatas[index - 4].imageUrl,
    name: _listDatas[index-4].name,
    groupTitle: _listDatas[index-4].indexLetter,
  );
}

综上: Cell的分组显示完成,效果图为

猜你喜欢

转载自blog.csdn.net/SharkToping/article/details/130514501