demo 通讯录 + 城市选择器 (字母索引左右联动 ListItemGroup+AlphabetIndexer)笔记

一、城市选择器实现笔记

1. 双层 for 循环渲染

数据结构

javascript 复制代码
interface BKCityContent {
  initial: string; // 字母索引
  cityNameList: string[]; // 城市列表
}

核心实现

javascript 复制代码
// 外层循环:字母分组 - 遍历城市数据,按字母分组显示
ForEach(this.cityContentList, (item: BKCityContent, index: number) => {
  // ListItemGroup:创建分组容器,header显示字母标题
  ListItemGroup({ header: this.ListItemGroupHeaderBuilder(item.initial) }) {
    // 内层循环:城市列表 - 遍历每个分组下的城市名称
    ForEach(item.cityNameList, (ele: string, index: number) => {
      ListItem() {
        Text(ele)  // 显示城市名称
          .width('100%')
          .padding({ left: 20 })
      }
      .width('100%')
      .height(50)  // 统一高度,保持列表整齐
      .backgroundColor(Color.White)
    })
  }
})

要点

  • 外层遍历字母分组,内层遍历城市
  • ListItemGroup 实现分组效果

2. 模态框左右联动

状态变量

javascript 复制代码
@State isShow: boolean = false    // 模态框显示
@State selected: number = 0       // 选中索引
scroller: Scroller = new Scroller() // 滚动器

联动代码

javascript 复制代码
// 模态框结构 - 使用Stack布局,右侧放置索引器
@Builder
ContentCoverBuilder() {
  Stack({ alignContent: Alignment.End }) {  // 内容右对齐,为索引器留出空间
    Column() {
      this.TopBuilder()    // 顶部搜索栏
      this.ListBuilder()   // 城市列表
    }
    .backgroundColor(Color.White)

    // 右侧索引器 - 提供快速定位功能
    AlphabetIndexer({arrayValue: this.alphabets, selected: this.selected})
      .usingPopup(true)  // 启用弹出提示,显示当前选中的字母
      .onSelect((index) => {
        this.scroller.scrollToIndex(index, true)  // 点击索引时滚动到对应位置
      })
  }
}

// 列表滚动监听 - 实现双向联动
List({scroller: this.scroller}) {  // 绑定滚动控制器
  // 列表内容
}
.onScrollIndex((index) => {
  this.selected = index  // 手动滚动时同步更新索引器的选中状态
})

联动机制

  • 点击索引 → onSelectscrollToIndex() 滚动
  • 手动滚动 → onScrollIndex → 更新 selected

3. 关键组件

AlphabetIndexer

javascript 复制代码
// 字母索引器 - 右侧快速定位组件
AlphabetIndexer({
  arrayValue: this.alphabets, // 索引数组:['#', '热', "A", "B", "C"...]
  selected: this.selected, // 当前选中的索引位置
})
  .usingPopup(true) // 启用弹出提示,显示当前选中的字母
  .onSelect((index) => {
    this.scroller.scrollToIndex(index, true); // 点击时滚动到对应位置
  });

ListItemGroup

javascript 复制代码
// 列表分组组件 - 按字母对城市进行分组显示
ListItemGroup({
  header: this.ListItemGroupHeaderBuilder(item.initial) // 自定义分组头部,显示字母
}) {
  // 分组内容 - 该字母下的所有城市
}
.padding({ bottom: 20 })  // 分组底部间距
.divider({ startMargin: 20, endMargin: 20, color: '#f3f3f3', strokeWidth: 2 }) // 分组间分割线

4. 数据组织

城市数据

javascript 复制代码
cityContentList: BKCityContent[] = [
  { initial: 'A', cityNameList: ['阿拉善', '鞍山', '安庆'] },
  { initial: 'B', cityNameList: ['北京', '保定', '包头'] },
  // ...
]

索引数组

javascript 复制代码
alphabets: string[] = ['#', '热', "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "W", "X", "Y", "Z"]

5. 交互流程

  1. 打开模态框:this.isShow = true
  2. 点击索引:onSelectscrollToIndex(index)
  3. 滚动列表:onScrollIndexthis.selected = index
  4. 关闭模态框:this.isShow = false

6. 关键代码

模态框绑定

javascript 复制代码
// 背景图片绑定模态框 - 点击图片显示城市选择器
Image($r("app.media.ic_BK_content"))
  .bindContentCover($$this.isShow, this.ContentCoverBuilder()) // 绑定模态框内容
  .onClick(() => {
    this.isShow = true; // 点击时显示模态框
  });

分组头部

javascript 复制代码
// 自定义分组头部构建器 - 显示字母标题
@Builder
ListItemGroupHeaderBuilder(title: string) {
  Text(title)  // 显示字母(如:A、B、C...)
    .padding({ left: 20, bottom: 15, top: 20 })  // 内边距
    .fontSize(14)  // 字体大小
    .fontColor(Color.Gray)  // 灰色字体
    .backgroundColor('#f8f8f8')  // 浅灰色背景
    .width('100%')  // 占满宽度
}

总结

  1. 双层循环:外层分组 + 内层列表
  2. 左右联动:索引器 + 滚动器双向同步
  3. 状态管理@State 控制显示,Scroller 控制滚动
  4. 数据驱动:接口规范数据结构

城市选择全部代码:

javascript 复制代码
interface BKCityContent {
  initial: string
  cityNameList: string[]
}

@Entry
@Component
struct Page10_Demo_BK {
  // 热门城市
  hotCitys: string[] = ['北京', '上海', '广州', '深圳', '天津', '杭州', '南京', '苏州', '成都', '武汉', '重庆', '西安', '香港', '澳门', '台北']
  // 历史城市
  historyCitys: string[] = ['北京', '上海', '广州', '深圳', '重庆']
  // 城市信息
  cityContentList: BKCityContent[] = [
    {
      initial: 'A',
      cityNameList: ['阿拉善', '鞍山', '安庆', '安阳', '阿坝', '安顺']
    },
    {
      initial: 'B',
      cityNameList: ['北京', '保定', '包头', '巴彦淖尔', '本溪', '白山']
    },
    {
      initial: 'C',
      cityNameList: ['成都', '重庆', '长春', '长沙', '承德', '沧州']
    },
    {
      initial: 'D',
      cityNameList: ['大连', '东莞', '大同', '丹东', '大庆', '大兴安岭']
    },
    {
      initial: 'E',
      cityNameList: ['鄂尔多斯', '鄂州', '恩施', '额尔古纳市', '二连浩特市', '恩施市']
    },
    {
      initial: 'F',
      cityNameList: ['福州', '佛山', '抚顺', '阜新', '阜阳', '抚州']
    },
    {
      initial: 'G',
      cityNameList: ['广州', '贵阳', '赣州', '桂林', '贵港', '广元']
    },
    {
      initial: 'H',
      cityNameList: ['杭州', '海口', '哈尔滨', '合肥', '呼和浩特', '邯郸']
    },
    {
      initial: 'J',
      cityNameList: ['济南', '晋城', '晋中', '锦州', '吉林', '鸡西']
    },
    {
      initial: 'K',
      cityNameList: ['昆明', '开封', '康定市', '昆山', '康保县', '宽城满族自治县']
    },
    {
      initial: 'L',
      cityNameList: ['兰州', '廊坊', '临汾', '吕梁', '辽阳', '辽源']
    },
    {
      initial: 'M',
      cityNameList: ['牡丹江', '马鞍山', '茂名', '梅州', '绵阳', '眉山']
    },
    {
      initial: 'N',
      cityNameList: ['南京', '宁波', '南昌', '南宁', '南通', '南平']
    },
    {
      initial: 'P',
      cityNameList: ['盘锦', '莆田', '萍乡', '平顶山', '濮阳', '攀枝花']
    },
    {
      initial: 'Q',
      cityNameList: ['青岛', '秦皇岛', '齐齐哈尔', '七台河', '衢州', '泉州']
    },
    {
      initial: 'R',
      cityNameList: ['日照', '日喀则', '饶阳县', '任丘市', '任泽区', '饶河县']
    },
    {
      initial: 'S',
      cityNameList: ['上海', '苏州', '深圳', '沈阳', '石家庄', '朔州']
    },
    {
      initial: 'T',
      cityNameList: ['天津', '太原', '唐山', '通辽', '铁岭', '通化']
    },
    {
      initial: 'W',
      cityNameList: ['无锡', '武汉', '乌海', '乌兰察布', '温州', '芜湖']
    },
    {
      initial: 'X',
      cityNameList: ['厦门', '西安', '西宁', '邢台', '忻州', '兴安盟']
    },
    {
      initial: 'Y',
      cityNameList: ['扬州', '阳泉', '运城', '营口', '延边', '伊春']
    },
    {
      initial: 'Z',
      cityNameList: ['郑州', '珠海', '张家口', '镇江', '舟山', '漳州']
    }
  ]
  // 右侧导航索引
  alphabets: string[] = ['#', '热', "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "W", "X", "Y", "Z"]
  //全模态的显示
  @State isShow:boolean=false
  //
  @State selected:number=0
 scroller:Scroller= new Scroller()
  build() {
    Column() {
      Image($r('app.media.ic_BK_content'))
        .width('100%')
        .bindContentCover($$this.isShow, this.ContentCoverBuilder())
       .onClick(()=>{
        this.isShow=true
        })
      // 全屏模态的内容
    }

    .width('100%')
    .height('100%')
    .backgroundColor('#f8f8f8')
  }

  @Builder
  ContentCoverBuilder() {
    Stack({ alignContent: Alignment.End }) {
      Column() {
        // 顶部
        this.TopBuilder();
        // 列表
        this.ListBuilder();
      }
      .backgroundColor(Color.White)

AlphabetIndexer({arrayValue:this.alphabets,selected:this.selected})
  .usingPopup(true)
  .onSelect((index)=>{
    this.scroller.scrollToIndex(index,true)
  })
    }
  }



  @Builder
  ListBuilder() {
    List({scroller:this.scroller}) {
      // 历史
      this.LocationListItemBuilder()
      // 热门
      this.HotListItemBuilder()

      // A-B的区域
      this.LetterListItemBuilder()
    }
    .divider({ startMargin: 20, endMargin: 20, color: '#f3f3f3', strokeWidth: 2 })
    .width('100%')
    .layoutWeight(1)

    .onScrollIndex((index)=>{
      this.selected=index
    })

  }

  @Builder
  LetterListItemBuilder() {
    // A-B的区域
    ForEach(this.cityContentList,(item:BKCityContent,index:number)=>{
      ListItemGroup({ header: this.ListItemGroupHeaderBuilder(item.initial) }) {
        ForEach(item.cityNameList,(ele:string,index:number)=>{
          ListItem() {
            Text(ele)
              .width('100%')
              .padding({ left: 20 })
          }
          .width('100%')
          .height(50)
          .backgroundColor(Color.White)
        })

      }
      .padding({ bottom: 20 })
      .divider({ startMargin: 20, endMargin: 20, color: '#f3f3f3', strokeWidth: 2 })
    })

  }

  @Builder
  ListItemGroupHeaderBuilder(title: string) {
    Text(title)
      .padding({ left: 20, bottom: 15, top: 20 })
      .fontSize(14)
      .fontColor(Color.Gray)
      .backgroundColor('#f8f8f8')
      .width('100%')
  }

  @Builder
  HotListItemBuilder() {
    // 热门
    ListItem() {
      Column({ space: 10 }) {
        Text('热门城市')
          .alignSelf(ItemAlign.Start)
          .fontColor(Color.Gray)
          .fontSize(14)
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.hotCitys,(item:string)=>{
            Text(item)
              .height(25)
              .backgroundColor(Color.White)
              .width('25%')
              .margin({ bottom: 10 })
          })

        }
        .padding({ left: 20, right: 20 })
      }
      .width('100%')
      .padding({ left: 20, right: 20, bottom: 10 })
    }
  }

  @Builder
  LocationListItemBuilder() {
    ListItem() {
      Column({ space: 15 }) {
        // 定位地址
        Row() {
          Text('北京')
          Text() {
            ImageSpan($r('app.media.ic_public_location_fill_blue'))
              .width(20)
            Span('开启定位')
          }
        }
        .width('100%')
        .padding({ top: 10, bottom: 10, right: 20, left: 20 })
        .justifyContent(FlexAlign.SpaceBetween)
        .backgroundColor(Color.White)

        // 历史
        Column({ space: 10 }) {
          Text('历史')
            .fontColor(Color.Gray)
            .alignSelf(ItemAlign.Start)
            .fontSize(14)
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.historyCitys,(item:string,indedx:number)=>{
              Text(item)
                .height(25)
                .backgroundColor(Color.White)
                .width('25%')
                .margin({ bottom: 10 })
            })

          }
          .padding({ left: 20, right: 20 })
        }
        .width('100%')
        .padding({ left: 20, right: 20 })
      }
    }
    .padding({ top: 20 })
  }

  @Builder
  TopBuilder() {
    Column() {
      // X + 输入框
      Row({ space: 20 }) {
        Image($r('app.media.ic_public_cancel'))
          .width(30)
          .fillColor(Color.Gray)
         .onClick(()=>{
          this.isShow=false
          })
        Row({ space: 5 }) {
          Image($r('app.media.ic_public_search'))
            .width(18)
          Text('请输入城市名称')
            .layoutWeight(1)
        }
        .height(50)
        .border({ width: .5, color: Color.Gray, radius: 5 })
        .padding({ left: 5 })
        .layoutWeight(1)
        .shadow({
          radius: 20,
          color: '#f6f6f7'
        })
      }
      .padding({
        left: 15,
        right: 15,
        top: 15
      })

      // 国内城市
      Column() {
        Text('国内城市')
          .fontSize(15)
          .fontWeight(800)
          .padding(5)
        Row()
          .width(20)
          .height(2)
          .backgroundColor('#0094ff')
          .borderRadius(2)
      }

    }
    .width('100%')
    .backgroundColor(Color.White)
    .height(100)
    .border({
      width: { bottom: 4 },
      color: '#f6f6f7',
    })
  }
}

效果展示:

二、通讯录字母索引左右联动笔记

1. 数据结构设计

javascript 复制代码
// 定义联系人数据结构
interface ContactData {
  initial: string    // 首字母
  nameList: string[] // 该字母下的联系人列表
}

// 数据组织:按字母分组存储
contacts: ContactData[] = [
  { initial: 'A', nameList: ['阿猫', '阿狗', ...] },
  { initial: 'B', nameList: ['白兔', '白鸽', ...] },
  // ... 26个字母分组
]

2. 核心变量定义

javascript 复制代码
// 滚动控制器 - 控制列表滚动
scroller: Scroller = new Scroller()

// 当前激活索引 - 用于左右联动
@State activeIndex: number = 0

// 字母索引数组 - 提供给AlphabetIndexer使用
alphabets: string[] = ['A', 'B', 'C', ..., 'Z']

3. 随机颜色功能

javascript 复制代码
getRandomColor(): ResourceColor {
  const r = Math.floor(Math.random() * 256);
  const g = Math.floor(Math.random() * 256);
  const b = Math.floor(Math.random() * 256);
  return `rgba(${r}, ${g}, ${b}, 0.5)`;
}

4. 左右联动实现流程

4.1 列表滚动 → 索引器高亮

javascript 复制代码
List({ scroller: this.scroller }) {
  // 列表内容...
}
.onScrollIndex((index) => {
  this.activeIndex = index  // 关键:滚动时更新激活索引
})

实现原理

  • 用户滚动列表时,onScrollIndex回调触发
  • 更新activeIndex,触发 UI 重新渲染
  • AlphabetIndexer 通过$$this.activeIndex自动高亮对应字母

4.2 索引器点击 → 列表跳转

javascript 复制代码
AlphabetIndexer({
  arrayValue: this.alphabets,
  selected: $$this.activeIndex, // 关键:双向绑定
}).onSelect((index) => {
  this.scroller.scrollToIndex(index); // 关键:点击时滚动到对应位置
});

实现原理

  • $$双向绑定:activeIndex变化时索引器自动更新
  • onSelect回调:点击字母时调用scrollToIndex跳转

5. 分组列表渲染流程

5.1 分组头部组件

javascript 复制代码
@Builder
itemHead(text: string) {
  Text(text)
    .fontSize(20)
    .backgroundColor('#fff1f3f5')
    .width('100%')
    .padding(5)
}

5.2 分组列表渲染

javascript 复制代码
ForEach(this.contacts, (item: ContactData, index: number) => {
  ListItemGroup({
    header: this.itemHead(item.initial),  // 设置分组头部
    space: 10
  }) {
    ForEach(item.nameList, (it: string, i: number) => {
      ListItem() {
        Row({ space: 10 }) {
          Image($r('app.media.ic_public_lianxiren'))
            .width(40)
            .fillColor(this.getRandomColor())  // 随机颜色
          Text(it)
        }
      }
    })
  }
  .divider({ startMargin: 60, strokeWidth: 1, color: '#ccc' })
})

渲染流程

  1. 外层遍历 26 个字母分组
  2. 每个分组用ListItemGroup包装
  3. 内层遍历该分组下的联系人
  4. 每个联系人显示头像+姓名
  5. 添加分割线

6. 弹窗功能实现

javascript 复制代码
AlphabetIndexer({ arrayValue: this.alphabets, selected: $$this.activeIndex })
  .usingPopup(true) // 启用弹窗
  .selectedColor(Color.Red) // 选中字母颜色
  .selectedBackgroundColor(Color.Green) // 选中背景色
  .popupColor(Color.Red) // 弹窗文字颜色
  .popupBackground(Color.Brown) // 弹窗背景色
  .popupTitleBackground(Color.Yellow); // 弹窗标题背景色

7. 关键样式设置

javascript 复制代码
List({ scroller: this.scroller })
  .sticky(StickyStyle.Header)  // 分组标题粘性显示
  .scrollBar(BarState.Off)     // 隐藏滚动条

AlphabetIndexer(...)
  .offset({ x: 0, y: -100 })   // 调整位置避免遮挡

8. 快速回到顶部

复制代码
Text("通讯录").onClick(() => {
  this.scroller.scrollToIndex(0, true); // 滚动到第一个位置
});

9. 完整交互流程

  1. 页面加载:显示所有联系人分组
  2. 滚动列表onScrollIndexactiveIndex更新 → 索引器高亮
  3. 点击字母onSelectscrollToIndex → 列表跳转
  4. 弹窗反馈:操作时显示当前选中字母
  5. 回到顶部 :点击标题 → scrollToIndex(0)

10. 关键技术点

  • 双向绑定$$this.activeIndex实现状态同步
  • 滚动控制Scroller对象精确控制滚动
  • 分组显示ListItemGroup实现分组列表
  • 粘性头部.sticky(StickyStyle.Header)提升体验
  • 随机颜色:动态生成增加视觉区分度

11. 常见问题

Q: 为什么用@State activeIndex?

A: 响应式状态,变化时 UI 自动更新,实现左右联动

Q: 如何实现平滑滚动?

A: scrollToIndex(index, true) 第二个参数 true

Q: 双向绑定怎么工作?

A: $$符号,activeIndex 变化时索引器自动更新,点击索引器时触发 onSelect

通讯录全部代码:

javascript 复制代码
// 定义联系人数据结构 - 每个字母分组包含首字母和对应的联系人列表
interface ContactData {
  initial: string    // 首字母,如 'A', 'B', 'C'
  nameList: string[] // 该字母下的联系人列表
}

@Entry
@Component
struct Page09_ContactAndAlpha {
  // 联系人数据 - 按字母A-Z分组存储,便于后续的字母索引和分组显示
  // 注意:这里不需要@State,因为数据不会动态变化,只是用来渲染
  contacts: ContactData[] = [
    { initial: 'A', nameList: ['阿猫', '阿狗', '阿虎', '阿龙', '阿鹰', '阿狼', '阿豹', '阿狮', '阿象', '阿鲸'] },
    { initial: 'B', nameList: ['白兔', '白鸽', '白鹤', '白鹭', '白狐', '白狼', '白虎', '白鹿', '白蛇', '白马'] },
    { initial: 'C', nameList: ['春花', '春风', '春雨', '春草', '春柳', '春燕', '春莺', '春蝶', '春蓝', '春绿'] },
    { initial: 'D', nameList: ['冬雪', '冬梅', '冬松', '冬竹', '冬云', '冬霜', '冬月', '冬夜', '冬青', '冬红'] },
    { initial: 'E', nameList: ['饿狼', '饿虎', '饿鹰', '饿豹', '饿熊', '饿蛇', '饿鱼', '饿虾', '饿蟹', '饿蚌'] },
    { initial: 'F', nameList: ['飞鸟', '飞鱼', '飞虫', '飞蜂', '飞蝶', '飞蛾', '飞蝉', '飞蝗', '飞鼠', '飞猫'] },
    { initial: 'G', nameList: ['孤狼', '孤鹰', '孤虎', '孤豹', '孤蛇', '孤鲨', '孤鲸', '孤鹿', '孤雁', '孤鸿'] },
    { initial: 'H', nameList: ['海鸥', '海龟', '海豚', '海星', '海马', '海葵', '海参', '海胆', '海螺', '海贝'] },
    { initial: 'I', nameList: ['火焰', '火球', '火箭', '火山', '火车', '火柴', '火把', '火鸟'] },
    { initial: 'J', nameList: ['金鱼', '金狮', '金刚', '金鹿', '金蛇', '金鹰', '金豹', '金虎', '金狐', '金猫'] },
    { initial: 'K', nameList: ['孔雀', '恐龙', '开心', '开怀', '开朗', '开拓', '开口', '开花', '开眼', '开天'] },
    { initial: 'L', nameList: ['老虎', '老鹰', '老鼠', '老狼', '老狗', '老猫', '老熊', '老鹿', '老龟', '老蛇'] },
    { initial: 'M', nameList: ['玫瑰', '牡丹', '梅花', '茉莉', '木兰', '棉花', '蜜蜂', '蚂蚁', '马蜂', '蟒蛇'] },
    { initial: 'N', nameList: ['南山', '南极', '南海', '南京', '南阳', '南风', '南瓜', '南竹', '南花', '南鸟'] },
    {
      initial: 'O',
      nameList: ['熊猫', '欧鹭', '欧洲', '欧阳', '欧文', '欧若拉', '欧米茄', '欧罗巴', '欧菲莉亚', '欧瑞斯']
    },
    { initial: 'P', nameList: ['苹果', '葡萄', '琵琶', '枇杷', '菩提', '瓢虫', '瓢泼', '飘零', '飘渺', '飘飘然'] },
    { initial: 'Q', nameList: ['七喜', '强风', '奇迹', '乾坤', '奇才', '晴天', '青竹', '秋水', '轻舞', '清泉'] },
    { initial: 'R', nameList: ['瑞雪', '瑞兽', '瑞光', '瑞云', '瑞彩', '瑞气', '瑞香', '瑞草', '瑞莲', '瑞竹'] },
    { initial: 'S', nameList: ['三羊', '三狗', '三猫', '三鱼', '三角', '三鹿', '三鹰', '三蛇', '三狐', '三豹'] },
    { initial: 'T', nameList: ['太阳', '天空', '田园', '太极', '太湖', '天鹅', '太空', '天使', '坦克', '甜橙'] },
    { initial: 'U', nameList: ['乌鸦', '乌鹊', '乌鱼', '乌龟', '乌云', '乌梅', '乌木', '乌金', '乌黑', '乌青'] },
    { initial: 'V', nameList: ['五虎', '五狼', '五鹰', '五豹', '五熊', '五蛇', '五鲨', '五鲸', '五鹿', '五马'] },
    { initial: 'W', nameList: ['悟空', '微笑', '温暖', '无畏', '温柔', '舞蹈', '问心', '悟道', '未来', '文学'] },
    { initial: 'X', nameList: ['西风', '西洋', '西子', '西施', '西岳', '西湖', '西柚', '西竹', '西花', '西鸟'] },
    { initial: 'Y', nameList: ['夜猫', '夜鹰', '夜莺', '夜空', '夜色', '夜月', '夜影', '夜翼', '夜狐', '夜狼'] },
    { initial: 'Z', nameList: ['珍珠', '紫薇', '紫霞', '紫竹', '紫云', '紫燕', '紫鸢', '紫藤', '紫荆', '紫罗兰'] },
  ]

  // 随机颜色生成函数 - 为每个联系人头像生成不同的背景色,增加视觉区分度
  getRandomColor(): ResourceColor {
    // 生成 0-255 的随机RGB值
    const r = Math.floor(Math.random() * 256);
    const g = Math.floor(Math.random() * 256);
    const b = Math.floor(Math.random() * 256);
    // 拼接成半透明的随机颜色并返回
    return `rgba(${r}, ${g}, ${b}, 0.5)`;
  }

  // 字母索引数组 - 提供给AlphabetIndexer组件使用的字母列表
  alphabets: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
    'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
  
  // 滚动控制器 - 用于控制列表的滚动行为
  scroller: Scroller = new Scroller()
  
  // 当前激活的索引位置 - 用于字母索引器的状态同步
  @State activeIndex: number = 0

  build() {
    Column() {
      // 顶部标题栏 - 包含标题和添加按钮
      Stack({ alignContent: Alignment.End }) {
        Text('通讯录')
          .width('100%')
          .textAlign(TextAlign.Center)
          .fontSize(20)
          .onClick(() => {
            // 点击标题回到顶部 - 滚动到第一个位置
            this.scroller.scrollToIndex(0, true)
          })
        Image($r('app.media.ic_public_add'))
          .width(20)
      }
      .width('100%')
      .padding(15)
      .backgroundColor('#fff1f3f5')

      // 搜索区域 - 模拟搜索框
      Row() {
        Row() {
          Image($r('app.media.ic_public_search'))
            .width(20)
            .fillColor(Color.Gray)
          Text('搜索')
            .fontColor(Color.Gray)
        }
        .backgroundColor(Color.White)
        .width('100%')
        .height(40)
        .borderRadius(5)
        .justifyContent(FlexAlign.Center)
      }
      .padding(10)
      .width('100%')
      .backgroundColor('#fff1f3f5')

      // 主要内容区域 - 使用Stack布局,列表和字母索引器重叠
      Stack({ alignContent: Alignment.End }) {
        // 联系人列表 - 核心显示区域
        List({ scroller: this.scroller }) {
          // 遍历所有字母分组
          ForEach(this.contacts, (item: ContactData, index: number) => {
            // 每个字母分组使用ListItemGroup包装
            ListItemGroup({ 
              header: this.itemHead(item.initial), // 设置分组头部
              space: 10 
            }) {
              // 遍历该分组下的所有联系人
              ForEach(item.nameList, (it: string, i: number) => {
                // 每个联系人的列表项
                ListItem() {
                  Row({ space: 10 }) {
                    // 联系人头像 - 使用随机颜色作为背景
                    Image($r('app.media.ic_public_lianxiren'))
                      .width(40)
                      .fillColor(this.getRandomColor())
                    // 联系人姓名
                    Text(it)
                  }
                }
              })
            }
            // 添加分割线美化界面
            .divider({
              startMargin: 60,  // 分割线左边距
              strokeWidth: 1,   // 分割线宽度
              color: '#ccc'     // 分割线颜色
            })
          })
        }
        .sticky(StickyStyle.Header)  // 分组标题粘性显示,滚动时粘在顶部
        .scrollBar(BarState.Off)     // 隐藏滚动条,提供更清爽的界面
        .onScrollIndex((index) => {
          // 滚动监听 - 当列表滚动时更新激活索引,实现左右联动
          this.activeIndex = index
        })

        // 字母索引器 - 右侧的字母快速定位工具
        AlphabetIndexer({ 
          arrayValue: this.alphabets,     // 字母数组
          selected: $$this.activeIndex    // 双向绑定当前选中索引
        })
          .offset({ x: 0, y: -100 })      // 调整位置,避免遮挡内容
          .usingPopup(true)               // 启用弹窗显示
          .selectedColor(Color.Red)       // 选中字母的颜色
          .selectedBackgroundColor(Color.Green) // 选中字母的背景色
          .popupColor(Color.Red)          // 弹窗内文字颜色
          .popupBackground(Color.Brown)   // 弹窗背景色
          .popupTitleBackground(Color.Yellow) // 弹窗标题背景色
          .onSelect((index) => {
            // 点击字母时的回调 - 滚动到对应的分组位置
            this.scroller.scrollToIndex(index)
          })
      }
    }
  }

  // 分组头部组件构建器 - 创建每个字母分组的标题显示组件
  @Builder
  itemHead(text: string) {
    Text(text)
      .fontSize(20)
      .backgroundColor('#fff1f3f5')
      .width('100%')
      .padding(5)
  }
}
相关推荐
LZQqqqqo9 小时前
C# 中 ArrayList动态数组、List<T>列表与 Dictionary<T Key, T Value>字典的深度对比
windows·c#·list
季春二九9 小时前
Windows 11 首次开机引导(OOBE 阶段)跳过登录微软账户,创建本地账户
windows·microsoft
芥子沫10 小时前
Jenkins常见问题及解决方法
windows·https·jenkins
cpsvps_net1 天前
美国服务器环境下Windows容器工作负载智能弹性伸缩
windows
甄超锋1 天前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
cpsvps1 天前
美国服务器环境下Windows容器工作负载基于指标的自动扩缩
windows
网硕互联的小客服1 天前
Apache 如何支持SHTML(SSI)的配置方法
运维·服务器·网络·windows·php
etcix1 天前
implement copy file content to clipboard on Windows
windows·stm32·单片机
许泽宇的技术分享1 天前
Windows MCP.Net:基于.NET的Windows桌面自动化MCP服务器深度解析
windows·自动化·.net