1# 二级联动 2## 场景介绍 3列表的二级联动(Cascading List)是指根据一个列表(一级列表)的选择结果,来更新另一个列表(二级列表)的选项。这种联动可以使用户根据实际需求,快速定位到想要的选项,提高交互体验。例如,短视频中拍摄风格的选择、照片编辑时的场景的选择,本文即为大家介绍如何开发二级联动。 4## 效果呈现 5本例最终效果如下: 6 7 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根据实现思路,具体实现步骤如下: 201. 首先构建列表数据,在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 ``` 752. 数字标题的索引值判断,根据当前滚动数字的首项索引值计算出数字标题的索引,具体代码块如下: 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 ``` 1103. 点击数字标题,数字列表随之滑动:首先获取到点击数字标题的索引,通过该索引计算出下方对应数字的起始项索引,然后通过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 ``` 1224. 数字列表的滑动或点击导致数字标题的变动:通过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 142struct 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)