博客 / 詳情

返回

HarmonyOS ArkTS 組件進階 - AlphabetIndexer 自學指南

1. AlphabetIndexer 是什麼?

AlphabetIndexer 是 ArkUI 信息展示類組件中的 索引條組件,典型場景是:

  • 通訊錄按 A~Z 快速定位聯繫人;
  • 城市選擇列表按拼音首字母定位;
  • 歌曲/視頻列表按首字母快速跳轉;
  • 任意「長列表 + 字母索引」的導航場景。

特點簡單總結一下:

  • 只能聯動另一側的容器組件(常見是 List / Grid);
  • 支持彈窗 展示一級/二級索引(如:A →「安、艾、奧」等列表);
  • 支持 自動摺疊模式(索引項很多時自動壓縮呈現);
  • 支持 背景模糊、圓角、觸控振動反饋 等 UI 細節。
支持:從 API 7 起引入,API 11、12、18 逐步增強(元服務、多級索引、自動摺疊等能力)。

2. 核心接口概覽

2.1 組件創建

AlphabetIndexer(options: AlphabetIndexerOptions)

AlphabetIndexerOptions 常用字段(簡化版):

字段名 類型 必填 説明
arrayValue Array<string> 索引條顯示的字符串數組,每個元素一個索引項,比如 ['#','A','B',...,'Z']
selected number 初始選中的索引下標,支持 $$ 雙向綁定
⚠️ 注意:arrayValue 的順序要與你的業務列表邏輯保持一致,否則跳轉會「錯位」。

2.2 樣式相關常用屬性

下面列的是日常開發最常用的一批屬性,方便你查表式使用:

AlphabetIndexer({ arrayValue, selected })
  // 文本顏色 & 字體
  .color(value: ResourceColor)                  // 未選中項文字顏色
  .selectedColor(value: ResourceColor)          // 選中項文字顏色
  .popupColor(value: ResourceColor)             // 彈窗一級索引文字顏色
  .font(value: Font)                            // 未選中項字體
  .selectedFont(value: Font)                    // 選中項字體
  .popupFont(value: Font)                       // 彈窗一級索引字體

  // 尺寸 & 對齊
  .itemSize(value: string | number)             // 單個索引項大小(正方形邊長,vp)
  .alignStyle(value: IndexerAlign, offset?)     // 彈窗相對索引條左右對齊 + 間距
  .popupPosition(value: Position)               // 彈窗位置(相對索引條上邊框中點)

  // 背景 & 圓角
  .selectedBackgroundColor(value: ResourceColor)     // 選中項背景色
  .popupBackground(value: ResourceColor)             // 彈窗背景色
  .popupItemBackgroundColor(value: ResourceColor)    // 彈窗二級索引項背景色
  .itemBorderRadius(value: number)                   // 索引條每一格圓角
  .popupItemBorderRadius(value: number)              // 彈窗裏每一格圓角
  .popupBackgroundBlurStyle(value: BlurStyle)        // 彈窗背景模糊材質
  .popupTitleBackground(value: ResourceColor)        // 彈窗一級索引背景

  // 行為控制
  .usingPopup(value: boolean)                   // 是否展示彈窗
  .autoCollapse(value: boolean)                 // 是否開啓自適應摺疊模式
  .enableHapticFeedback(value: boolean)         // 是否啓用觸控振動反饋

提示:

  • width="auto" 時索引條寬度會隨 最長索引項寬度 自適應;
  • padding 默認是 4vp
  • 字體縮放 maxFontScale/minFontScale 強制為 1,不跟隨系統字體大小變化。

2.3 事件與回調

// 常用事件
.onSelect((index: number) => void)                         // 索引項選中
.onRequestPopupData((index: number) => Array<string>)      // 請求二級索引內容
.onPopupSelect((index: number) => void)                    // 彈窗二級索引被選中

三個類型別名(API 18+):

type OnAlphabetIndexerSelectCallback = (index: number) => void
type OnAlphabetIndexerPopupSelectCallback = (index: number) => void
type OnAlphabetIndexerRequestPopupDataCallback = (index: number) => Array<string>
usingPopup(true) 時,onRequestPopupData 會在索引項被選中時觸發,返回的字符串數組會 豎排顯示在彈窗中,最多顯示 5 條,超過可上下滑動。

2.4 對齊方式枚舉 IndexerAlign

enum IndexerAlign {
  Left,     // 彈窗在索引條一側
  Right,    // 彈窗在索引條另一側
  START,    // 跟隨 LTR/RTL 方向的開始側
  END       // 跟隨 LTR/RTL 方向的結束側
}
在國際化場景(LTR/RTL)下,用 START / END 可以避免你手動切換 Left/Right。

3. 最小可用示例:先能跑起來

下面先給一個最小可跑版本(不帶二級索引、不帶各種炫酷效果),你可以先在 demo 工程裏試一把。

// xxx.ets
@Entry
@Component
struct SimpleAlphabetIndexerSample {
  private indexes: 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'];

  @State currentIndex: number = 0;

  build() {
    Row() {
      // 左邊可以是 List / Grid,這裏先用簡單的佔位
      Column() {
        Text(`當前索引:${this.indexes[this.currentIndex]}`)
          .fontSize(24)
          .margin(10)
      }
      .width('70%')

      // 右側是 AlphabetIndexer
      AlphabetIndexer({ arrayValue: this.indexes, selected: this.currentIndex })
        .usingPopup(false)
        .itemSize(24)
        .selectedColor(0xFF007DFF)
        .selectedBackgroundColor(0x1A007DFF)
        .onSelect((index: number) => {
          this.currentIndex = index;
          console.info(`Selected index: ${this.indexes[index]}`);
        })
    }
    .width('100%')
    .height('100%')
  }
}

這個最小例子主要讓你熟悉:

  • 如何傳入 arrayValue
  • 如何用 selected + onSelect 做一個最基本的「選中反饋」。

接下來,我們用完整例子演示 聯動 List,彈窗展示二級索引,自動摺疊 和 模糊材質


4. 示例一:聯動 List + 自定義彈窗內容

這個例子主要展示:

  • 左邊 List 展示聯繫人姓氏;
  • 右邊 AlphabetIndexer 做 A~Z 索引;
  • onRequestPopupData 根據當前字母動態返回二級索引列表(如「安、卜、白…」)。
// xxx.ets
@Entry
@Component
struct AlphabetIndexerSample1 {
  private arrayA: string[] = ['安'];
  private arrayB: string[] = ['卜', '白', '包', '畢', '丙'];
  private arrayC: string[] = ['曹', '成', '陳', '催'];
  private arrayL: 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'];

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Row() {
        // 左側 List:模擬按首字母分組的聯繫人列表
        List({ space: 20, initialIndex: 0 }) {
          ForEach(this.arrayA, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayB, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayC, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayL, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)
        }
        .width('50%')
        .height('100%')

        // 右側 AlphabetIndexer:開啓彈窗 & 自定義樣式
        AlphabetIndexer({ arrayValue: this.value, selected: 0 })
          .autoCollapse(false)                        // 關閉自適應摺疊模式
          .enableHapticFeedback(false)                // 關閉觸控振動
          .selectedColor(0xFFFFFF)                    // 選中項文本顏色
          .popupColor(0xFFFAF0)                       // 彈窗一級索引文本顏色
          .selectedBackgroundColor(0xCCCCCC)          // 選中項背景色
          .popupBackground(0xD2B48C)                  // 彈窗背景色
          .usingPopup(true)                           // 選中時顯示彈窗
          .selectedFont({ size: 16, weight: FontWeight.Bolder })
          .popupFont({ size: 30, weight: FontWeight.Bolder })
          .itemSize(28)                               // 索引項尺寸
          .alignStyle(IndexerAlign.Left)              // 彈窗在索引條一側
          .popupItemBorderRadius(24)                  // 彈窗項圓角
          .itemBorderRadius(14)                       // 索引項圓角
          .popupBackgroundBlurStyle(BlurStyle.NONE)   // 關閉背景模糊
          .popupTitleBackground(0xCCCCCC)             // 彈窗一級索引背景
          .popupSelectedColor(0x00FF00)               // 彈窗二級索引選中文本顏色
          .popupUnselectedColor(0x0000FF)             // 彈窗二級索引未選中文本顏色
          .popupItemFont({ size: 30, style: FontStyle.Normal })
          .popupItemBackgroundColor(0xCCCCCC)
          .onSelect((index: number) => {
            console.info(this.value[index] + ' Selected!');
            // 一般這裏會配合 List 滾動到對應分組
          })
          .onRequestPopupData((index: number) => {
            // 字母 → 二級索引內容的映射
            if (this.value[index] == 'A') {
              return this.arrayA;
            } else if (this.value[index] == 'B') {
              return this.arrayB;
            } else if (this.value[index] == 'C') {
              return this.arrayC;
            } else if (this.value[index] == 'L') {
              return this.arrayL;
            } else {
              // 其它字母只顯示一級索引
              return [];
            }
          })
          .onPopupSelect((index: number) => {
            console.info('onPopupSelected:' + index);
            // 可在這裏根據二級索引定位到更具體的位置
          })
      }
      .width('100%')
      .height('100%')
    }
  }
}

使用要點小結:

  • usingPopup(true) + onRequestPopupData 是做「二級索引」的關鍵;
  • 當返回空數組時,彈窗只顯示一級索引(如僅一個「A」)。

5. 示例二:開啓自適應摺疊模式

當索引項很多時(比如 26 個字母 + #),在手機上全顯示會比較擠。
autoCollapse(true) 可以讓系統根據 索引數量 + 高度 自動選擇:

  • 全顯示;
  • 短折疊;
  • 長摺疊。

下面這個示例支持「切換摺疊模式」以及「動態調整索引條高度」:

// xxx.ets
@Entry
@Component
struct AlphabetIndexerSample2 {
  private arrayA: string[] = ['安'];
  private arrayB: string[] = ['卜', '白', '包', '畢', '丙'];
  private arrayC: string[] = ['曹', '成', '陳', '催'];
  private arrayJ: 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'];

  @State isNeedAutoCollapse: boolean = false;
  @State indexerHeight: string = '75%';

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Row() {
        // 左側 List:模擬數據
        List({ space: 20, initialIndex: 0 }) {
          ForEach(this.arrayA, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayB, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayC, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayJ, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)
        }
        .width('50%')
        .height('100%')

        Column() {
          // 上半部分:索引條本體
          Column() {
            AlphabetIndexer({ arrayValue: this.value, selected: 0 })
              .autoCollapse(this.isNeedAutoCollapse)  // 是否開啓摺疊
              .height(this.indexerHeight)             // 動態控制索引條高度
              .enableHapticFeedback(false)
              .selectedColor(0xFFFFFF)
              .popupColor(0xFFFAF0)
              .selectedBackgroundColor(0xCCCCCC)
              .popupBackground(0xD2B48C)
              .usingPopup(true)
              .selectedFont({ size: 16, weight: FontWeight.Bolder })
              .popupFont({ size: 30, weight: FontWeight.Bolder })
              .itemSize(28)
              .alignStyle(IndexerAlign.Right)
              .popupTitleBackground("#D2B48C")
              .popupSelectedColor(0x00FF00)
              .popupUnselectedColor(0x0000FF)
              .popupItemFont({ size: 30, style: FontStyle.Normal })
              .popupItemBackgroundColor(0xCCCCCC)
              .onSelect((index: number) => {
                console.info(this.value[index] + ' Selected!');
              })
              .onRequestPopupData((index: number) => {
                if (this.value[index] == 'A') {
                  return this.arrayA;
                } else if (this.value[index] == 'B') {
                  return this.arrayB;
                } else if (this.value[index] == 'C') {
                  return this.arrayC;
                } else if (this.value[index] == 'J') {
                  return this.arrayJ;
                } else {
                  return [];
                }
              })
              .onPopupSelect((index: number) => {
                console.info('onPopupSelected:' + index);
              })
          }
          .height('80%')
          .justifyContent(FlexAlign.Center)

          // 下半部分:控制按鈕
          Column() {
            Button('切換成摺疊模式')
              .margin('5vp')
              .onClick(() => {
                this.isNeedAutoCollapse = true;
              })
            Button('切換索引條高度到30%')
              .margin('5vp')
              .onClick(() => {
                this.indexerHeight = '30%';
              })
            Button('切換索引條高度到70%')
              .margin('5vp')
              .onClick(() => {
                this.indexerHeight = '70%';
              })
          }
          .height('20%')
        }
        .width('50%')
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height(720)
    }
  }
}

關於 autoCollapse 的摺疊規則要點(邏輯簡化版本):

  • 如果首項是 "#":判斷時會 先去掉首項 再看數量;
  • 9 個以內:全顯示;
  • 9~13 個:根據高度自適應選擇全顯示或「短折疊」;
  • 13 個以上:根據高度在「短折疊 / 長摺疊」中自適應。

6. 示例三:彈窗背景模糊材質

在更偏「設計感」的頁面上,通常會需要 毛玻璃彈窗效果
popupBackgroundBlurStyle 就是用來控制彈窗的背景模糊材質的。

下面這個示例:

  • 用按鈕切換兩種模糊材質;
  • 背景是一張圖片(記得換成自己的資源)。
// xxx.ets
@Entry
@Component
struct AlphabetIndexerSample3 {
  private arrayA: string[] = ['安'];
  private arrayB: string[] = ['卜', '白', '包', '畢', '丙'];
  private arrayC: string[] = ['曹', '成', '陳', '催'];
  private arrayL: 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'];

  @State customBlurStyle: BlurStyle = BlurStyle.NONE;

  build() {
    Stack({ alignContent: Alignment.Start }) {
      Row() {
        // 左側 List:依舊是一些示例數據
        List({ space: 20, initialIndex: 0 }) {
          ForEach(this.arrayA, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayB, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayC, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)

          ForEach(this.arrayL, (item: string) => {
            ListItem() {
              Text(item)
                .width('80%')
                .height('5%')
                .fontSize(30)
                .textAlign(TextAlign.Center)
            }
          }, (item: string) => item)
        }
        .width('30%')
        .height('100%')

        Column() {
          // 上半部分:切換模糊材質的按鈕
          Column() {
            Text('切換模糊材質: ')
              .fontSize(24)
              .fontColor(0xcccccc)
              .width('100%')
            Button('COMPONENT_REGULAR')
              .margin('5vp')
              .width(200)
              .onClick(() => {
                this.customBlurStyle = BlurStyle.COMPONENT_REGULAR;
              })
            Button('BACKGROUND_THIN')
              .margin('5vp')
              .width(200)
              .onClick(() => {
                this.customBlurStyle = BlurStyle.BACKGROUND_THIN;
              })
          }
          .height('20%')

          // 下半部分:索引條 + 模糊彈窗
          Column() {
            AlphabetIndexer({ arrayValue: this.value, selected: 0 })
              .usingPopup(true)
              .alignStyle(IndexerAlign.Left)
              .popupItemBorderRadius(24)
              .itemBorderRadius(14)
              .popupBackgroundBlurStyle(this.customBlurStyle) // 核心點
              .popupTitleBackground(0xCCCCCC)
              .onSelect((index: number) => {
                console.info(this.value[index] + ' Selected!');
              })
              .onRequestPopupData((index: number) => {
                if (this.value[index] == 'A') {
                  return this.arrayA;
                } else if (this.value[index] == 'B') {
                  return this.arrayB;
                } else if (this.value[index] == 'C') {
                  return this.arrayC;
                } else if (this.value[index] == 'L') {
                  return this.arrayL;
                } else {
                  return [];
                }
              })
              .onPopupSelect((index: number) => {
                console.info('onPopupSelected:' + index);
              })
          }
          .height('80%')
        }
        .width('70%')
      }
      .width('100%')
      .height('100%')
      // 注意替換為你工程中的圖片資源
      .backgroundImage($r('app.media.image'))
    }
  }
}

小 Tips:

  • 模糊效果會疊加在 popupBackground 上,所以顏色看起來會和你寫的不完全一樣;
  • 如果不想要毛玻璃效果,可以設為 BlurStyle.NONE

7. 實戰開發中的常見坑 & 小技巧

  1. 索引項太多 vs 高度不夠

    • itemSize 是索引項區域的正方形邊長;
    • 實際大小會被組件寬高和 padding 限制;
    • 當高度不夠時,建議開啓 autoCollapse(true),否則界面會很擠。
  2. 二級索引內容過多

    • onRequestPopupData 返回的字符串數組 最多顯示 5 行,超出可以滑動,但不宜塞太多;
    • 建議二級列表只放「常用/命中率高」的條目,避免彈窗太長影響體驗。
  3. 觸控反饋別忘了權限

    • enableHapticFeedback(true) 時,需要在 module.json5 裏配置振動權限:

      "requestPermissions": [
        { "name": "ohos.permission.VIBRATE" }
      ]
    • 否則有的機型上會沒有振動效果或直接報權限問題。
  4. 國際化 & RTL 支持

    • 如果你的應用要支持 RTL 語言(如阿拉伯語),對齊方式儘量用 START / END

      .alignStyle(IndexerAlign.START)
    • 這樣在 LTR/RTL 場景下會自動切換索引條左/右側。
  5. 聯動 List 記得加「滾動定位」

    • onSelect 裏除了打印日誌,一般會調用 ListscrollToIndexposition 綁定;
    • 做到「按字母 → 左側列表跳到對應分組」才是完整體驗。
  6. 寬度自適應的使用

    • width('auto') 時,寬度會跟隨最長索引文本寬度變化;
    • 如果你用的是多字母組合(比如「熱門」、「最近」),注意可能導致索引條變寬,對佈局有影響。

如果你後面打算寫 通訊錄、城市選擇、音樂/視頻列表 之類的實戰 demo,可以直接在上面的三個示例基礎上改數據結構,把 List 的滾動聯動補齊,就已經是一份很完整的 ArkUI 索引條實戰工程了。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.