鸿蒙Next实现通讯录索引条AlphabetIndexer

2025-06-27 22:56:52
108次阅读
0个评论

当我们需要列表展示通讯录、城市名时,通常会使用到右侧的索引条,可以帮助用户快速定位到某一类的头部。本文介绍一下使用List+ListItemGroup+AlphabetIndexer实现2种常见模式的通讯录。看一下实现效果: 演示.gif

实现过程: 1.以通讯录为例,联系人一般我们以首字母分类,所以索引列表就是名字的首字母A-Z,由于会有一些特殊符号,或者数字开头等不是汉字或字母开头的,我们都归类为#,这样我们就定义好了,索引列表数据

private value: 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','#'];

2.列表中,我们需要将不同首字母的名字展示到一组,如果想要实现吸顶效果,这时就需要用到ListItemGroup,使用scrollToItemInGroup,可以快速定位到某一组的头部。因此我们需要将联系人数据分组,如果接口数据已经时分组好的,那就不需要我们处理了,如果是没有分组好的,我们需要借用三方工具,将数据做一下分组,并且以字母顺序排好,这里我直接举一个分组好的例子。

contactGroup: ContactGroup[] = [
    {
      letter: 'A',
      name: ['艾先生', '艾女士', '安迪', '爱迪生']
    },
    {
      letter: 'B',
      name: ['爸爸', '宝宝', '白色', '白鹿']
    },
    {
      letter: 'C',
      name: ['曹操', '曹丕', '曹植', '崔崔']
    },
    {
      letter: 'D',
      name: ['邓小', '邓超']
    },
    {
      letter: 'F',
      name: ['冯小刚', '方形']
    },
    {
      letter: 'Z',
      name: ['赵丽颖', '张雨绮','张韶涵']
    },
    {
      letter: '#',
      name: ['12345', '110', '120', '119', '114']
    }
  ];

3.分组联系人数据展示,需要定义一个ListItemGroup头部组件,头部只需要展示A-Z#,ListItem展示联系人具体信息。 4.添加索引组件,详细属性说明在源码中添加了注释 5.索引选中,关联List滑动。当索引选中时,会回调onSelect,返回选中的index。例如选中索引A,返回index=0, 然后获取分组联系人letter=A的index,调用ListScroller的scrollToItemInGroup方法,可以定位到分组A联系人。其中scrollToItemInGroup需要传2个参数,分别是分组A的index,组内元素的scrollToItemInGroup,这个在二级索引时会用到,这里我们传0就可以。 6.当滑动List时,关联索引选中值。监听List的onScrollIndex方法,该方法返回了当前list可见的ListItemGroup,然后通过letter,定位到AlphabetIndexer选中索引。 7.如果需要实现二级索引,需要监听AlphabetIndexer的onRequestPopupData函数,该方法设置提示弹窗二级索引项内容事件,回调参数为当前选中项索引,回调返回值为提示弹窗需显示的二级索引项内容,这里我们提前写死了2级索引数组。

//ABCDFZ一级索引选中时 返回的二级索引 仿手机通讯录,去重姓分组
  private arrayA: string[] = ['艾', '安', '爱'];
  private arrayB: string[] = ['爸', '宝', '白'];
  private arrayC: string[] = ['曹', '崔'];
  private arrayD: string[] = ['邓'];
  private arrayF: string[] = ['冯', '方'];
  private arrayZ: string[] = ['赵', '张'];
//onRequestPopupData 函数回调
onRequestPopupData((index: number) => {
  //一级索引选中时,返回二级索引的数据
  if (this.value[index] == 'A') {
    this.popupData=[]
    this.popupData=[...this.arrayA]
    return this.arrayA;
  } else if (this.value[index] == 'B') {
    this.popupData=[]
    this.popupData=[...this.arrayB]
    return this.arrayB;
  } else if (this.value[index] == 'C') {
    this.popupData=[]
    this.popupData=[...this.arrayC]
    return this.arrayC;
  } else if (this.value[index] == 'D') {
    this.popupData=[]
    this.popupData=[...this.arrayD]
    return this.arrayD;
  } else if (this.value[index] == 'F') {
    this.popupData=[]
    this.popupData=[...this.arrayF]
    return this.arrayF;
  } else if (this.value[index] == 'Z') {
    this.popupData=[]
    this.popupData=[...this.arrayZ]
    return this.arrayZ;
  } else {
    this.popupData=[]
    return [];
  }
})

8.二级索引点击时,跳转到对应的联系人位置,通过监听onPopupSelect,弹窗二级索引选中事件,回调参数为当前选中二级索引项索引,然后去匹配一级索引对应的分组中联系人的姓在group中的index,然后使用第5步中提到的方法,定位到分组中的联系人。

源码

export interface ContactGroup {
  letter: string;
  name: string[];
}

@Entry
@ComponentV2
struct AlphabetIndexerSample {
  private scroller: ListScroller = new ListScroller();
  private arrayA: string[] = ['艾', '安', '爱'];
  private arrayB: string[] = ['爸', '宝', '白'];
  private arrayC: string[] = ['曹', '崔'];
  private arrayD: string[] = ['邓'];
  private arrayF: string[] = ['冯', '方'];
  private arrayZ: string[] = ['赵', '张'];
  private value: 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','#'];
  contactGroup: ContactGroup[] = [
    {
      letter: 'A',
      name: ['艾先生', '艾女士', '安迪', '爱迪生']
    },
    {
      letter: 'B',
      name: ['爸爸', '宝宝', '白色', '白鹿']
    },
    {
      letter: 'C',
      name: ['曹操', '曹丕', '曹植', '崔崔']
    },
    {
      letter: 'D',
      name: ['邓小', '邓超']
    },
    {
      letter: 'F',
      name: ['冯小刚', '方形']
    },
    {
      letter: 'Z',
      name: ['赵丽颖', '张雨绮','张韶涵']
    },
    {
      letter: '#',
      name: ['12345', '110', '120', '119', '114']
    }
  ];
  //使用groupheader时,如果吸顶模式关闭,定位到groupitem时,默认显示的是group的第一个元素,不显示头
  @Local  stick:StickyStyle =StickyStyle.None
  @Local  groupIndex:number = -1;
  @Local  popupData:string[] =[]
  @Local  popupModel:boolean =true
  @Local  usingPopup:boolean =true
  @Local  selected:number =0
  @Builder
  itemHead(text: string) {
    Text(text)
      .fontSize(20)
      .width('100%')
      .height(40)
      .padding(10)
      .backgroundColor(Color.White)
  }

  build() {
    Column(){
      Row(){
        Button('通讯录模式索引定位').onClick(()=>{
          this.popupModel =true
          this.usingPopup =true
        })
        Button('联系人无二级索引吸顶').onClick(()=>{
          this.popupModel =false
          this.usingPopup =false
          this.stick=StickyStyle.Header
        })
      }

      RelativeContainer() {
        List({ space: 10, initialIndex: 0 , scroller: this.scroller }) {
          ForEach(this.contactGroup, (item: ContactGroup) => {
            ListItemGroup({ header: this.itemHead(item.letter) }) {
              ForEach(item.name, (name: string) => {
                ListItem() {
                  Row({space:20}) {
                    Text(name.slice(-1))
                      .fontColor(Color.White)
                      .fontSize(20)
                      .textAlign(TextAlign.Center)
                      .backgroundColor('#CCCCCC')
                      .borderRadius(25)
                      .height(50)
                      .width(50)

                    Text(name)
                      .width('100%')
                      .height(80)
                      .fontSize(20)
                      .textAlign(TextAlign.Start)
                  }
                }
              }, (item: string) => item)
            }
            .divider({ startMargin: 60, strokeWidth: 0.5, color: '#CCCCCC' }) // 每行之间的分界线
          })
        }
        .sticky(this.stick)
        .padding({left:20,right:30})
        .scrollBar(BarState.Off)
        .width('100%')
        .height('100%')
        .onScrollStart(()=>{
          this.usingPopup?this.stick =StickyStyle.None:this.stick=StickyStyle.Header
        })
        .onDidScroll(()=>{
          // console.info('onDidScroll');
        })
        .onScrollIndex((start: number, end: number, center: number) => {
          console.debug('start:'+start+'end:'+end+'center:'+center);
          let  arrayValue = this.contactGroup[start].letter
          this.selected= this.value.findIndex((value:string)=>value==arrayValue)
          this.usingPopup =false
        })


        AlphabetIndexer({ arrayValue: this.value, selected: this.selected })
          .color(0x99182431)//未选中项文本颜色
          .selectedColor('#FFFFFF')//选中项文本颜色 ABCD...
          .selectedBackgroundColor(0xCCCCCC)// 选中项背景颜色
          .autoCollapse(false)// 关闭自适应折叠模式
          .enableHapticFeedback(false)// 关闭触控反馈
          .popupColor('#3F56EA')// 提示弹窗一级索引文本颜色
          .popupBackground('#FFFFFF')// 提示弹窗背景颜色
          .usingPopup(this.usingPopup)// 索引项被选中时显示提示弹窗
          .selectedFont({ size: 16, weight: FontWeight.Normal })// 选中项文本样式
          .popupFont({ size: 30, weight: FontWeight.Bolder })// 提示弹窗一级索引的文本样式
          .itemSize(24)// 索引项的尺寸大小
          .alignStyle(IndexerAlign.Right)// 提示弹窗在索引条右侧弹出
          .popupItemBorderRadius(24)// 设置提示弹窗索引项背板圆角半径
          .itemBorderRadius(14)// 设置索引项背板圆角半径
          .popupBackgroundBlurStyle(BlurStyle.NONE)// 设置提示弹窗的背景模糊材质
          .popupTitleBackground('#CCCCCC')// 设置提示弹窗一级索引项背景颜色 ABC背景
          .popupSelectedColor(Color.Black)// 弹窗二级索引 选中项  文本颜色
          .popupUnselectedColor(Color.Black)// 提示弹窗二级索引 未选中项 文本颜色
          .popupItemFont({ size: 30, style: FontStyle.Normal })// 提示弹窗二级索引项文本样式
          .popupItemBackgroundColor(Color.Transparent)// 提示弹窗二级索引项背景颜色
          .height('60%')
          .onSelect((index: number) => {
            this.stick =StickyStyle.Header
            this.groupIndex =  this.contactGroup.findIndex((item:ContactGroup)=>item.letter==this.value[index])
            console.info(this.value[index] + ' Selected!');
            console.info('GroupIndex:'+this.groupIndex);
            this.scroller.scrollToItemInGroup(this.groupIndex,0)
          })
          .onRequestPopupData((index: number) => {
            //一级索引选中时,返回二级索引的数据
            if (this.value[index] == 'A') {
              this.popupData=[]
              this.popupData=[...this.arrayA]
              return this.arrayA;
            } else if (this.value[index] == 'B') {
              this.popupData=[]
              this.popupData=[...this.arrayB]
              return this.arrayB;
            } else if (this.value[index] == 'C') {
              this.popupData=[]
              this.popupData=[...this.arrayC]
              return this.arrayC;
            } else if (this.value[index] == 'D') {
              this.popupData=[]
              this.popupData=[...this.arrayD]
              return this.arrayD;
            } else if (this.value[index] == 'F') {
              this.popupData=[]
              this.popupData=[...this.arrayF]
              return this.arrayF;
            } else if (this.value[index] == 'Z') {
              this.popupData=[]
              this.popupData=[...this.arrayZ]
              return this.arrayZ;
            } else {
              this.popupData=[]
              return [];
            }
          })
          .onPopupSelect((index: number) => {
            if (this.popupData.length==0)return
            this.stick =StickyStyle.None
            let indexInGroup =   this.contactGroup[this.groupIndex].name.findIndex((item:string)=>item[0]==this.popupData[index])
            this.scroller.scrollToItemInGroup(this.groupIndex,indexInGroup)
            console.info('onPopupSelect:' + indexInGroup);
          })
          .alignRules(AlignRules.alignParentRightCenter)
      }
    }
  }
}

如果有用,请点点关注,有问题,请留言或私信。

收藏00

登录 后评论。没有帐号? 注册 一个。