1 # 二级联动
2 ## 场景介绍
3 列表的二级联动(Cascading List)是指根据一个列表(一级列表)的选择结果,来更新另一个列表(二级列表)的选项。这种联动可以使用户根据实际需求,快速定位到想要的选项,提高交互体验。例如,短视频中拍摄风格的选择、照片编辑时的场景的选择,本文即为大家介绍如何开发二级联动。
4 ## 效果呈现
5 本例最终效果如下:
6 
7 ![](figures/secondarylinkage.gif)
8 
9 ## 运行环境
10 本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发:
11 - IDE: DevEco Studio 3.1 Beta2
12 - SDK: Ohos_sdk_public 3.2.11.9 (API Version 9 Release)
13 ## 实现思路
14 - 数字标题(titles)以及下方的数字列表(contents)分组展示:通过两个List组件分别承载数字标题和数字项。
15 - 滚动数字列表,上方数字标题也随之变动:通过List组件的onScrollIndex事件获取到当前滚动数字的索引,根据该索引计算出对应标题数字的索引,然后通过Scroller的scrollToIndex方法跳转到对应的数字标题,且通过Line组件为选中的标题添加下划线。
16 - 点击数字标题,下方的数字列表也随之变化:首先获取到点击数字标题的索引,通过该索引计算出下方对应数字的起始项索引,然后通过scroller的scrollToIndex方法跳转到对应索引的数字项。
17 
18 ## 开发步骤
19 根据实现思路,具体实现步骤如下:
20 1. 首先构建列表数据,在records中记录数字列表中各个数字的首项索引值,具体代码块如下:
21     ```ts
22     ...
23     @State typeIndex: number = 0
24     private tmp: number = 0
25     private titles: Array<string> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
26     private contents: Array<string> = ["1", "1", "1", "1", "1", "1", "1", "1", "1", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "3"
27         , "3", "3", "3", "3", "4", "4", "4", "5", "5", "5", "5", "5", "6", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7",
28         "8", "8", "8", "8", "8", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9"]
29     private records: Array<number> = [0, 9, 21, 26, 29, 34, 35, 47, 52, 63]
30     private classifyScroller: Scroller = new Scroller();
31     private scroller: Scroller = new Scroller();
32     ...
33     ```
34     数字标题列表:具体代码块如下:
35     ```ts
36     ...
37     build() {
38       Column({ space: 0 }) {
39         List  ({ space: 50, scroller: this.classifyScroller, initialIndex: 0 }) {
40           ForEach(this.titles, (item, index) => {
41             ListItem() {
42               Column() {
43                 Text(item)
44                   .fontSize(14)
45                 ...
46               }
47             }
48           }
49           ...
50         }
51         .listDirection(Axis.Horizontal)
52         .height(50)
53       }
54     }
55     ```
56     数字列表,具体代码块如下:
57     ```ts
58     List({ space: 20, scroller: this.scroller }) {
59       ForEach(this.contents, (item, index) => {
60         ListItem() {
61           Column({ space: 5 }) {
62             Image($r("app.media.app_icon"))
63               .width(40)
64               .height(40)
65             Text(item)
66               .fontSize(12)
67           }
68           ...
69         }
70       }
71     }
72     .listDirection(Axis.Horizontal) //列表排列方向水平
73     .edgeEffect(EdgeEffect.None) //不支持滑动效果
74     ```
75 2. 数字标题的索引值判断,根据当前滚动数字的首项索引值计算出数字标题的索引,具体代码块如下:
76     ```ts
77     ...
78     findClassIndex(ind: number) { // 当前界面最左边图的索引值ind
79       let ans = 0
80       // 定义一个i 并进行遍历 this.records.length = 10
81       for (let i = 0; i < this.records.length; i++) {
82         // 判断ind在this.records中那两个临近索引值之间
83         if (ind >= this.records[i] && ind < this.records[i + 1]) {
84           ans = i
85           break
86         }
87       }
88       return ans
89     }
90     findItemIndex(ind: number) {
91       // 将ind重新赋值给类型标题列表的索引值
92       return this.records[ind]
93     }
94     ...
95     ```
96     通过Line组件构成标题下滑线,具体代码块如下:
97     ```ts
98     ...
99     if (this.typeIndex == index) {
100       Line()
101         //根据长短判断下划线
102         .width(item.length === 2 ? 25 : item.length === 3 ? 35 : 50)
103         .height(3)
104         .strokeWidth(20)
105         .strokeLineCap(LineCapStyle.Round)
106         .backgroundColor('#ffcf9861')
107     }
108     ...
109     ```
110 3. 点击数字标题,数字列表随之滑动:首先获取到点击数字标题的索引,通过该索引计算出下方对应数字的起始项索引,然后通过scroller的scrollToIndex方法跳转到对应索引的数字项,具体代码块如下:
111     ```ts
112     ...
113     .onClick(() => {
114       this.typeIndex = index
115       this.classifyScroller.scrollToIndex(index)
116       let itemIndex = this.findItemIndex(index)
117       console.log("移动元素:" + itemIndex)
118       this.scroller.scrollToIndex(itemIndex)
119     })
120     ...
121     ```
122 4. 数字列表的滑动或点击导致数字标题的变动:通过List组件中onScrollIndex事件获取的到屏幕中最左边数字的索引值start,然后通过该索引值计算出对应的数字标题的索引currentClassIndex,然后通过scrollToIndex控制数字标题跳转到对应索引处,具体代码块如下:
123     ```ts
124     ...
125     .onScrollIndex((start) => {
126       let currentClassIndex = this.findClassIndex(start)
127       console.log("找到的类索引为: " + currentClassIndex)
128       if (currentClassIndex != this.tmp) {
129         this.tmp = currentClassIndex
130         console.log("类别移动到索引: " + currentClassIndex)
131         this.typeIndex = currentClassIndex
132         this.classifyScroller.scrollToIndex(currentClassIndex)
133       }
134     })
135     ...
136     ```
137 ## 完整代码
138 完整示例代码如下:
139 ```ts
140 @Entry
141 @Component
142 struct TwoLevelLink {
143   @State typeIndex: number = 0
144   private tmp: number = 0
145   private titles: Array<string> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
146   private contents: Array<string> = ["1", "1", "1", "1", "1", "1", "1", "1", "1", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "2", "3"
147     , "3", "3", "3", "3", "4", "4", "4", "5", "5", "5", "5", "5", "6", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7", "7",
148     "8", "8", "8", "8", "8", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9"]
149   private colors: Array<string> = ["#18183C", "#E8A027", "#D4C3B3", "#A4AE77", "#A55D51", "#1F3B54", "#002EA6", "#FFE78F", "#FF770F"]
150   private records: Array<number> = [0, 9, 21, 26, 29, 34, 35, 47, 52, 63]
151   private classifyScroller: Scroller = new Scroller();
152   private scroller: Scroller = new Scroller();
153   // 根据数字列表索引计算对应数字标题的索引
154   findClassIndex(ind: number) {
155     let ans = 0
156     for (let i = 0; i < this.records.length; i++) {
157       if (ind >= this.records[i] && ind < this.records[i + 1]) {
158         ans = i
159         break
160       }
161     }
162     return ans
163   }
164   // 根据数字标题索引计算对应数字列表的索引
165   findItemIndex(ind: number) {
166     return this.records[ind]
167   }
168   build() {
169     Column({ space: 0 }) {
170       List  ({ space: 50, scroller: this.classifyScroller, initialIndex: 0 }) {
171         ForEach(this.titles, (item, index) => {
172           ListItem() {
173             Column() {
174               Text(item)
175                 .fontSize(24)
176               if (this.typeIndex == index) {
177                 Line()
178                   .width(item.length === 2 ? 25 : item.length === 3 ? 35 : 50)
179                   .height(3)
180                   .strokeWidth(20)
181                   .strokeLineCap(LineCapStyle.Round)
182                   .backgroundColor('#ffcf9861')
183               }
184             }
185             .onClick(() => {
186               this.typeIndex = index
187               this.classifyScroller.scrollToIndex(index)
188               let itemIndex = this.findItemIndex(index)
189               console.log("移动元素:" + itemIndex)
190               this.scroller.scrollToIndex(itemIndex)
191             })
192           }
193         })
194       }
195       .listDirection(Axis.Horizontal)
196       .height(50)
197       List({ space: 20, scroller: this.scroller }) {
198         ForEach(this.contents, (item, index) => {
199           ListItem() {
200             Column({ space: 5 }) {
201               Text(item)
202                 .fontSize(30)
203                 .fontColor(Color.White)
204             }
205             .width(60)
206             .height(60)
207             .backgroundColor(this.colors[item-1])
208             .justifyContent(FlexAlign.Center)
209             .onClick(() => {
210               this.scroller.scrollToIndex(index)
211             })
212           }
213         })
214       }
215       .listDirection(Axis.Horizontal) //列表排列方向水平
216       .edgeEffect(EdgeEffect.None) //不支持滑动效果
217       .onScrollIndex((start) => {
218         let currentClassIndex = this.findClassIndex(start)
219         console.log("找到的类索引为: " + currentClassIndex)
220         if (currentClassIndex != this.tmp) {
221           this.tmp = currentClassIndex
222           console.log("类别移动到索引: " + currentClassIndex)
223           this.typeIndex = currentClassIndex
224           this.classifyScroller.scrollToIndex(currentClassIndex)
225         }
226       })
227     }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
228   }
229 }
230 ```
231 ## 参考
232 [List](../application-dev/reference/apis-arkui/arkui-ts/ts-container-list.md)
233 
234 [Line](../application-dev/reference/apis-arkui/arkui-ts/ts-drawing-components-line.md)
235 
236 [Scroll](../application-dev/reference/apis-arkui/arkui-ts/ts-container-scroll.md)