HarmonyOS6 - Tabs组件实现仿微信页面实战

1. 效果图如下

202403072043815

2. 页面介绍

wechat 是微信的主界面,也是用户登录成功后看到的核心页面。这个页面采用了经典的底部 Tab 导航设计,包含四个标签页:

  1. 微信 - 聊天消息列表
  2. 通讯录 - 联系人列表
  3. 发现 - 发现功能入口
  4. - 个人中心

本文将采用实战方式为大家演示 Tabs 架构的搭建方法,以及每个 Tab 页面的具体实现。

3. 编码

1. 微信首页

/**
 * 【微信】聊天列表页面
 */

import { UserModel } from './UserModel'

@Entry
@Component
export struct WeiXinChat01 {
  userArray: Array<UserModel> = [
    new UserModel($r('app.media.boy1'), '张三', '今天去哪玩了啊?'),
    new UserModel($r('app.media.dog'), 'Tom', '吃了吗?'),
    new UserModel($r('app.media.boy2'), '王五', '不要在卷啦,出来玩一会吧,下午回...'),
    new UserModel($r('app.media.1003'), '李四', '出来打篮球啊'),
    new UserModel($r('app.media.boy3'), '王天霸', '走,逛街去吧'),
    new UserModel($r('app.media.touxiang'), '李莉莉', '你女朋友来找你了'),

    new UserModel($r('app.media.boy1'), '张三', '今天去哪玩了啊?'),
    new UserModel($r('app.media.dog'), 'Tom', '吃了吗?'),
    new UserModel($r('app.media.boy2'), '王五', '不要在卷啦'),
    new UserModel($r('app.media.1003'), '李四', '出来打篮球啊'),
    new UserModel($r('app.media.boy3'), '王天霸', '走,逛街去吧'),
    new UserModel($r('app.media.touxiang'), '李莉莉', '你女朋友来找你了'),
  ]

  build() {

    Column() {

      //通讯录
      Row() {
        Column() {
          Text("通讯录").fontSize(20)
        }
        .width('57%')
        .alignItems(HorizontalAlign.End)

        Column() {
          Image($r('app.media.addPerson'))
            .width(25)
            .height(25)
            .margin({ right: 40 })
        }
        .alignItems(HorizontalAlign.End)
        .width('50%')
      }
      .margin({ top: 30, bottom: 2 })
      .width('100%')

      //分割线
      Divider().strokeWidth(10).color('#ededed')

      //搜索框
      Row() {
        TextInput({ placeholder: "搜索" })
          .backgroundColor('#ffffffff')
          .width('95%')
          .margin({ bottom: 1 })
          .textAlign(TextAlign.Center)
      }

      //用户列表
      List() {
        ForEach(this.userArray, (user: UserModel) => {
          ListItem() {
            Row() {
              Column() {
                Image(user.icon)
                  .width(55)
                  .height(55)
                  .borderRadius(10)
              }

              Column() {
                Row() {
                  Text(user.nickName)
                    .fontSize(20)
                    .margin({ top: 7, left: 10 })
                }
                .width('100%') //这里需要加上100%

                Row() {
                  Text(user.msg)
                    .fontSize(16)
                    .fontColor('#C3C3C3')
                    .margin({ top: 6, left: 10 })
                }
                .width('100%') //这里需要加上100%

                Row() {
                  Divider()
                    .color('#fff3f1f1')
                    .strokeWidth(1)
                }
                .margin({ top: 5 })
              }
            }
            .margin({ left: 5, top: 10 })
            .width('100%')

          }.padding({ left: 8 })

        })
      }
      .backgroundColor('#ffffffff') //白色
      .height('85%')

    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ededed') //背景色:非白色

  }
}

UserModel代码如下:

/**
 * 微信首页聊天列表,每一行用户对象信息
 */
export class UserModel {
  icon: Resource;
  nickName: string
  msg: string

  constructor(icon: Resource, nickName: string, msg: string) {
    this.icon = icon;
    this.nickName = nickName;
    this.msg = msg;
  }
}

2. 通讯录页面

/**
 * 【通讯录】页面
 */
import { ItemModel } from './ItemModel'
import { PersonModel } from './PersonModel'

@Entry
@Component
export struct WeiXinAddressBook02 {
  itemArray1: Array<ItemModel> = [
    new ItemModel("新的朋友", $r('app.media.newFriend')),
    new ItemModel("仅聊天的朋友", $r('app.media.onlyChat')),
    new ItemModel("群聊", $r('app.media.qunliao')),
    new ItemModel("标签", $r('app.media.tag')),
    new ItemModel("公众号", $r('app.media.gongzhonghao')),
    new ItemModel("企业微信联系人", $r('app.media.qiyeweixin')),
  ]
  itemArrayA: Array<ItemModel> = [
    new ItemModel("阿狗", $r('app.media.dog')),
    new ItemModel("A-妞妞18120120212", $r('app.media.touxiang')),
    new ItemModel("阿杰", $r('app.media.boy3')),
    new ItemModel("阿hong", $r('app.media.1003')),
    new ItemModel("阿闷", $r('app.media.boy1')),
  ]
  itemArrayB: Array<ItemModel> = [
    new ItemModel("阿狗", $r('app.media.dog')),
    new ItemModel("A-妞妞18120120212", $r('app.media.touxiang')),
    new ItemModel("阿杰", $r('app.media.boy3')),
    new ItemModel("阿hong", $r('app.media.1003')),
    new ItemModel("阿闷", $r('app.media.boy1')),
  ]
  itemArrayC: Array<ItemModel> = [
    new ItemModel("阿狗", $r('app.media.dog')),
    new ItemModel("A-妞妞18120120212", $r('app.media.touxiang')),
    new ItemModel("阿杰", $r('app.media.boy3')),
    new ItemModel("阿hong", $r('app.media.1003')),
    new ItemModel("阿闷", $r('app.media.boy1')),
  ]
  itemArrayBottom: Array<ItemModel> = [
    new ItemModel("阿狗", $r('app.media.dog')),
    new ItemModel("A-妞妞18120120212", $r('app.media.touxiang')),
    new ItemModel("阿杰", $r('app.media.boy3')),
    new ItemModel("阿hong", $r('app.media.1003')),
    new ItemModel("阿闷", $r('app.media.boy1')),
  ]
  @State personList: Array<PersonModel> = [
    new PersonModel("", this.itemArray1),
    new PersonModel("A", this.itemArrayA),
    new PersonModel("B", this.itemArrayB),
    new PersonModel("C", this.itemArrayC),
  ]

  @Builder itemHead(text: string) {
    // 列表分组的头部组件,对应联系人分组A、B等位置的组件
    Text(text)
      .fontSize(20)
      .backgroundColor('#fff1f3f5')
      .width('100%')
      .padding(5)
  }

  build() {

    Column() {

      //通讯录
      Row() {
        Column() {
          Text("通讯录").fontSize(20)
        }
        .width('57%')
        .alignItems(HorizontalAlign.End)

        Column() {
          Image($r('app.media.addUser'))
            .width(25)
            .height(25)
            .margin({ right: 40 })
        }
        .alignItems(HorizontalAlign.End)
        .width('50%')
      }
      .margin({ top: 30, bottom: 2 })
      .width('100%')

      //分割线
      Divider().strokeWidth(10).color('#ededed')

      //搜索框
      Row() {
        TextInput({ placeholder: "搜索" })
          .backgroundColor('#ffffffff')
          .width('95%')
          .margin({ bottom: 1 })
          .textAlign(TextAlign.Center)
      }

      //List组件
      List() {
        ForEach(this.personList, (personModel) => {
          ListItemGroup({ header: this.itemHead(personModel.alphabet) }) {
            // 循环渲染ListItem
            ForEach(personModel.items, item => {
              ListItem() {
                //好友列表
                Column() {
                  Row() {
                    Image(item.icon)
                      .width(30)
                      .height(30)
                      .borderRadius(7) //圆角处理
                      .objectFit(ImageFit.Cover)

                    Text(item.title).fontSize(18).margin({ left: 10 })
                  }.width('100%').backgroundColor(Color.White)
                  .padding(10)

                  Divider()
                    .strokeWidth(1)
                    .color("#eee")
                    .padding({ left: 45, right: 0 })
                }
              }.padding({ left: 8 })
            })
          }
        })
      }
      .backgroundColor('#ffffffff') //白色
      .height('85%')

    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ededed') //背景色:非白色

  }
}

ItemModel代码如下:

export class ItemModel {
  title: string;
  icon: Resource;

  constructor(title: string, icon: Resource) {
    this.title = title;
    this.icon = icon;
  }
}

PersonModel代码如下:

import { ItemModel } from './ItemModel';

export class PersonModel {
  alphabet: string;
  items: Array<ItemModel>

  constructor(alphabet: string, items: Array<ItemModel>) {
    this.alphabet = alphabet;
    this.items = items;
  }
}

3. 发现页面

/**
 * 【发现】页面
 */

@Entry
@Component
export struct WeiXinFind03 {
  build() {
    Column() {

      //发现
      Row() {
        Text("发现").fontSize(20)
      }.margin({ top: 30, bottom: 10 })

      Divider() //分隔器组件
        .strokeWidth(10) //分割线宽度,默认值1
        .color('#ededed') //分割线颜色

      Column() {
        Row() {
          Row() {
            Image($r('app.media.pengyouquan'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover) //设置图片的填充效果,保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界
            Text("朋友圈").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)

        }.width('100%').backgroundColor('#ffffff')
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween) //水平方向方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐
      }.backgroundColor(Color.White)


      Divider()
        .strokeWidth(10)
        .color('#ededed')

      Column() {
        Row() {
          Row() {
            Image($r('app.media.shipinhao'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("视频号").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)

      Column() {
        Row() {
          Row() {
            Image($r('app.media.zhibo'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("直播").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)

      Divider()
        .strokeWidth(10)
        .color('#ededed')

      Column() {
        Row() {
          Row() {
            Image($r('app.media.saoyisao'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("扫一扫").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)

      Column() {
        Row() {
          Row() {
            Image($r('app.media.tingyiting'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("听一听").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)

      Divider()
        .strokeWidth(10)
        .color('#ededed')

      Column() {
        Row() {
          Row() {
            Image($r('app.media.kanyikan'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("看一看").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)

      Column() {
        Row() {
          Row() {
            Image($r('app.media.souyisou'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("搜一搜").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)


      Divider()
        .strokeWidth(10)
        .color('#ededed')

      Column() {
        Row() {
          Row() {
            Image($r('app.media.xiaochengxu'))
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("小程序").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 20, right: 0 })
      }.backgroundColor(Color.White)

    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xeeeeee)
  }
}

4. 我的页面

/**
 * 【我】页面
 */
@Entry
@Component
export struct WeiXinMe04 {
  build() {
    Column() {
      Row() {
        Column() {
          Row() {
            Image('https://img1.baidu.com/it/u=357360635,2692794844&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500')
              .width(80)
              .height(80)
              .borderRadius(10) //圆角效果
              .margin({ left: 27 }) //左边距
          }
        }

        Column() {
          Row() {
            Text("波波老师").fontSize(25).fontWeight(90).margin({ left: 10 })
          }

          Row() {
            Text("微信号:wjb1134135987")
              .fontSize(14)
              .fontColor('#737373')
              .margin({ left: 10 })
          }
          .margin({ top: 8 })
        }
        .alignItems(HorizontalAlign.Start) //子元素在水平方向左对齐
        .margin({ left: 12 })
      }
      .width('100%')
      .height('180vp')
      .backgroundColor("#ffffff")

      Divider() //分隔器组件
        .strokeWidth(10) //分割线宽度,默认值1
        .color('#ededed') //分割线颜色

      Column() {
        Row() {
          Row() {
            Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/01.jpg")
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover) //设置图片的填充效果,保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界
            Text("服务").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)

        }.width('100%').backgroundColor('#ffffff')
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween) //水平方向方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐
      }.backgroundColor(Color.White)


      Divider()
        .strokeWidth(10)
        .color('#ededed')

      Column() {
        Row() {
          Row() {
            Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/02.jpg")
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("收藏").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)

      Column() {
        Row() {
          Row() {
            Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/03.jpg")
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("朋友圈").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)


      Column() {
        Row() {
          Row() {
            Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/04.jpg")
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("视频号").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)


      Column() {
        Row() {
          Row() {
            Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/05.jpg")
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("卡包").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 50, right: 0 })
      }.backgroundColor(Color.White)


      Column() {
        Row() {
          Row() {
            Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/06.jpg")
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("表情").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 20, right: 0 })
      }.backgroundColor(Color.White)


      Divider()
        .strokeWidth(10)
        .color('#ededed')

      Column() {
        Row() {
          Row() {
            Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/07.jpg")
              .width(28)
              .height(28)
              .objectFit(ImageFit.Cover)
            Text("设置").fontSize(17).margin({ left: 10 })
          }

          Image("https://photo-tupige.oss-cn-beijing.aliyuncs.com/wechat/arrow_forward.jpg")
            .width(28)
            .height(28)
        }.width(`100%`).backgroundColor(Color.White)
        .padding(10)
        .justifyContent(FlexAlign.SpaceBetween)

        Divider()
          .strokeWidth(1)
          .color("#eee")
          .padding({ left: 20, right: 0 })
      }.backgroundColor(Color.White)

    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xeeeeee)
  }
}

5. 菜单页

以上是微信的四个单页面,现在需要做底部的四个tab菜单页面,新建一个页面,代码如下:

/**
 * 微信首页
 */

//引入其他组件
import { WeiXinChat01 } from './WeiXinChat01';
import { WeiXinAddressBook02 } from './WeiXinAddressBook02';
import { WeiXinFind03 } from './WeiXinFind03';
import { WeiXinMe04 } from './WeiXinMe04';

@Entry
@Component
struct Weixin_Home {
  @State currentIndex: number = 0;
  private tabController: TabsController = new TabsController();

  @Builder tabBuilder(title: string, targetIndex, selectedImg: Resource, normalImg: Resource) {
    Column() {
      Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
        .size({ width: 20, height: 20 })
      Text(title)
        .fontColor(this.currentIndex === targetIndex ? Color.Green : Color.Black)
    }
    .width('100%')
    .height(50)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.currentIndex = targetIndex;
      this.tabController.changeIndex(targetIndex);
    })
    .backgroundColor(this.currentIndex === targetIndex ? 0xeeeeee : Color.White)
  }

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.End, controller: this.tabController }) {

        //微信
        TabContent() {
          WeiXinChat01()
        }.tabBar(this.tabBuilder('微信', 0, $r('app.media.message_chosse'), $r('app.media.message')))

        //通讯录
        TabContent() {
          WeiXinAddressBook02()
        }.tabBar(this.tabBuilder('通讯录', 1, $r('app.media.book_chose'), $r('app.media.book')))

        //发现
        TabContent() {
          WeiXinFind03()
        }.tabBar(this.tabBuilder('发现', 2, $r('app.media.find_chose'), $r('app.media.find')))

        //我
        TabContent() {
          WeiXinMe04()
        }.tabBar(this.tabBuilder('我', 3, $r('app.media.my_chose'), $r('app.media.my')))

      }
    }
  }
}

4. 代码解读

以上案例代码中的图标,大家可以根据自己的需求进行修改,有些图片我用的是网络图片,你也可以改成本地图片。

以上案例中使用到了Tabs,它是通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。它仅支持子组件TabContent,以及渲染控制类型if/else和ForEach,不建议自定义组件作为子组件。

所以在案例中使用了TabContent作为子组件去存放每一个页面

Tab 切换动画原理:

当用户点击某个 Tab:

  1. 触发 onClick 事件
  2. 执行 this.currentIndex = targetIndex;
  3. @State 变量改变,触发 UI 刷新
  4. this.tabController.changeIndex(targetIndex) 显示对应页面
  5. tabBuilder中使用三目运算符控制图标和颜色同时更新
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐