1# 创建轮播 (Swiper) 2 3 4[Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md)组件提供滑动轮播显示的能力。Swiper本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。 5 6针对复杂页面场景,可以使用 Swiper 组件的预加载机制,利用主线程的空闲时间来提前构建和布局绘制组件,优化滑动体验。<!--Del-->详细指导见[Swiper高性能开发指导](../performance/swiper_optimization.md)。<!--DelEnd--> 7 8 9## 布局与约束 10 11Swiper作为一个容器组件,如果设置了自身尺寸属性,则在轮播显示过程中均以该尺寸生效。如果自身尺寸属性未被设置,则分两种情况:如果设置了prevMargin或者nextMargin属性,则Swiper自身尺寸会跟随其父组件;如果未设置prevMargin或者nextMargin属性,则会自动根据子组件的大小设置自身的尺寸。 12 13 14## 循环播放 15 16通过loop属性控制是否循环播放,该属性默认值为true。 17 18当loop为true时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果loop为false,则在第一页或最后一页时,无法继续向前或者向后切换页面。 19 20- loop为true 21 22```ts 23Swiper() { 24 Text('0') 25 .width('90%') 26 .height('100%') 27 .backgroundColor(Color.Gray) 28 .textAlign(TextAlign.Center) 29 .fontSize(30) 30 31 Text('1') 32 .width('90%') 33 .height('100%') 34 .backgroundColor(Color.Green) 35 .textAlign(TextAlign.Center) 36 .fontSize(30) 37 38 Text('2') 39 .width('90%') 40 .height('100%') 41 .backgroundColor(Color.Pink) 42 .textAlign(TextAlign.Center) 43 .fontSize(30) 44} 45.loop(true) 46``` 47 48 49 50- loop为false 51 52```ts 53Swiper() { 54 // ... 55} 56.loop(false) 57``` 58 59 60 61 62## 自动轮播 63 64Swiper通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。 65 66autoPlay为true时,会自动切换播放子组件,子组件与子组件之间的播放间隔通过interval属性设置。interval属性默认值为3000,单位毫秒。 67 68```ts 69Swiper() { 70 // ... 71} 72.loop(true) 73.autoPlay(true) 74.interval(1000) 75``` 76 77 78 79 80## 导航点样式 81 82Swiper提供了默认的导航点样式和导航点箭头样式,导航点默认显示在Swiper下方居中位置,开发者也可以通过indicator属性自定义导航点的位置和样式,导航点箭头默认不显示。 83 84通过indicator属性,开发者可以设置导航点相对于Swiper组件上下左右四个方位的位置,同时也可以设置每个导航点的尺寸、颜色、蒙层和被选中导航点的颜色。 85 86- 导航点使用默认样式 87 88```ts 89Swiper() { 90 Text('0') 91 .width('90%') 92 .height('100%') 93 .backgroundColor(Color.Gray) 94 .textAlign(TextAlign.Center) 95 .fontSize(30) 96 97 Text('1') 98 .width('90%') 99 .height('100%') 100 .backgroundColor(Color.Green) 101 .textAlign(TextAlign.Center) 102 .fontSize(30) 103 104 Text('2') 105 .width('90%') 106 .height('100%') 107 .backgroundColor(Color.Pink) 108 .textAlign(TextAlign.Center) 109 .fontSize(30) 110} 111``` 112 113 114 115- 自定义导航点样式 116 117导航点直径设为30vp,左边距为0,导航点颜色设为红色。 118 119```ts 120Swiper() { 121 // ... 122} 123.indicator( 124 Indicator.dot() 125 .left(0) 126 .itemWidth(15) 127 .itemHeight(15) 128 .selectedItemWidth(30) 129 .selectedItemHeight(15) 130 .color(Color.Red) 131 .selectedColor(Color.Blue) 132) 133``` 134 135 136 137Swiper通过设置[displayArrow](../reference/apis-arkui/arkui-ts/ts-container-swiper.md#displayarrow10)属性,可以控制导航点箭头的大小、位置、颜色,底板的大小及颜色,以及鼠标悬停时是否显示箭头。 138 139- 箭头使用默认样式 140 141```ts 142Swiper() { 143 // ... 144} 145.displayArrow(true, false) 146``` 147 148 149 150- 自定义箭头样式 151 152箭头显示在组件两侧,大小为18vp,导航点箭头颜色设为蓝色。 153 154```ts 155Swiper() { 156 // ... 157} 158.displayArrow({ 159 showBackground: true, 160 isSidebarMiddle: true, 161 backgroundSize: 24, 162 backgroundColor: Color.White, 163 arrowSize: 18, 164 arrowColor: Color.Blue 165 }, false) 166``` 167 168 169 170## 页面切换方式 171 172Swiper支持手指滑动、点击导航点和通过控制器三种方式切换页面,以下示例展示通过控制器切换页面的方法。 173 174```ts 175@Entry 176@Component 177struct SwiperDemo { 178 private swiperBackgroundColors: Color[] = [Color.Blue, Color.Brown, Color.Gray, Color.Green, Color.Orange, 179 Color.Pink, Color.Red, Color.Yellow]; 180 private swiperAnimationMode: (SwiperAnimationMode | boolean | undefined)[] = [undefined, true, false, 181 SwiperAnimationMode.NO_ANIMATION, SwiperAnimationMode.DEFAULT_ANIMATION, SwiperAnimationMode.FAST_ANIMATION]; 182 private swiperController: SwiperController = new SwiperController(); 183 private animationModeIndex: number = 0; 184 private animationMode: (SwiperAnimationMode | boolean | undefined) = undefined; 185 @State animationModeStr: string = 'undefined'; 186 @State targetIndex: number = 0; 187 188 aboutToAppear(): void { 189 this.toSwiperAnimationModeStr(); 190 } 191 192 build() { 193 Column({ space: 5 }) { 194 Swiper(this.swiperController) { 195 ForEach(this.swiperBackgroundColors, (backgroundColor: Color, index: number) => { 196 Text(index.toString()) 197 .width(250) 198 .height(250) 199 .backgroundColor(backgroundColor) 200 .textAlign(TextAlign.Center) 201 .fontSize(30) 202 }) 203 } 204 .indicator(true) 205 206 Row({ space: 12 }) { 207 Button('showNext') 208 .onClick(() => { 209 this.swiperController.showNext(); // 通过controller切换到后一页 210 }) 211 Button('showPrevious') 212 .onClick(() => { 213 this.swiperController.showPrevious(); // 通过controller切换到前一页 214 }) 215 }.margin(5) 216 217 Row({ space: 12 }) { 218 Text('Index:') 219 Button(this.targetIndex.toString()) 220 .onClick(() => { 221 this.targetIndex = (this.targetIndex + 1) % this.swiperBackgroundColors.length; 222 }) 223 }.margin(5) 224 Row({ space: 12 }) { 225 Text('AnimationMode:') 226 Button(this.animationModeStr) 227 .onClick(() => { 228 this.animationModeIndex = (this.animationModeIndex + 1) % this.swiperAnimationMode.length; 229 this.toSwiperAnimationModeStr(); 230 }) 231 }.margin(5) 232 233 Row({ space: 12 }) { 234 Button('changeIndex(' + this.targetIndex + ', ' + this.animationModeStr + ')') 235 .onClick(() => { 236 this.swiperController.changeIndex(this.targetIndex, this.animationMode); // 通过controller切换到指定页 237 }) 238 }.margin(5) 239 }.width('100%') 240 .margin({ top: 5 }) 241 } 242 243 private toSwiperAnimationModeStr() { 244 this.animationMode = this.swiperAnimationMode[this.animationModeIndex]; 245 if ((this.animationMode === true) || (this.animationMode === false)) { 246 this.animationModeStr = '' + this.animationMode; 247 } else if ((this.animationMode === SwiperAnimationMode.NO_ANIMATION) || 248 (this.animationMode === SwiperAnimationMode.DEFAULT_ANIMATION) || 249 (this.animationMode === SwiperAnimationMode.FAST_ANIMATION)) { 250 this.animationModeStr = SwiperAnimationMode[this.animationMode]; 251 } else { 252 this.animationModeStr = 'undefined'; 253 } 254 } 255} 256``` 257 258 259 260 261## 轮播方向 262 263Swiper支持水平和垂直方向上进行轮播,主要通过vertical属性控制。 264 265当vertical为true时,表示在垂直方向上进行轮播;为false时,表示在水平方向上进行轮播。vertical默认值为false。 266 267 268- 设置水平方向上轮播。 269 270```ts 271Swiper() { 272 // ... 273} 274.indicator(true) 275.vertical(false) 276``` 277 278 279 280 281 282- 设置垂直方向轮播。 283 284```ts 285Swiper() { 286 // ... 287} 288.indicator(true) 289.vertical(true) 290``` 291 292 293 294 295 296## 每页显示多个子页面 297 298Swiper支持在一个页面内同时显示多个子组件,通过[displayCount](../reference/apis-arkui/arkui-ts/ts-container-swiper.md#displaycount8)属性设置。 299 300```ts 301Swiper() { 302 Text('0') 303 .width(250) 304 .height(250) 305 .backgroundColor(Color.Gray) 306 .textAlign(TextAlign.Center) 307 .fontSize(30) 308 Text('1') 309 .width(250) 310 .height(250) 311 .backgroundColor(Color.Green) 312 .textAlign(TextAlign.Center) 313 .fontSize(30) 314 Text('2') 315 .width(250) 316 .height(250) 317 .backgroundColor(Color.Pink) 318 .textAlign(TextAlign.Center) 319 .fontSize(30) 320 Text('3') 321 .width(250) 322 .height(250) 323 .backgroundColor(Color.Blue) 324 .textAlign(TextAlign.Center) 325 .fontSize(30) 326} 327.indicator(true) 328.displayCount(2) 329``` 330 331 332 333## 自定义切换动画 334 335Swiper支持通过[customContentTransition](../reference/apis-arkui/arkui-ts/ts-container-swiper.md#customcontenttransition12)设置自定义切换动画,可以在回调中对视窗内所有页面逐帧设置透明度、缩放比例、位移、渲染层级等属性实现自定义切换动画。 336 337```ts 338@Entry 339@Component 340struct SwiperCustomAnimationExample { 341 private DISPLAY_COUNT: number = 2 342 private MIN_SCALE: number = 0.75 343 344 @State backgroundColors: Color[] = [Color.Green, Color.Blue, Color.Yellow, Color.Pink, Color.Gray, Color.Orange] 345 @State opacityList: number[] = [] 346 @State scaleList: number[] = [] 347 @State translateList: number[] = [] 348 @State zIndexList: number[] = [] 349 350 aboutToAppear(): void { 351 for (let i = 0; i < this.backgroundColors.length; i++) { 352 this.opacityList.push(1.0) 353 this.scaleList.push(1.0) 354 this.translateList.push(0.0) 355 this.zIndexList.push(0) 356 } 357 } 358 359 build() { 360 Column() { 361 Swiper() { 362 ForEach(this.backgroundColors, (backgroundColor: Color, index: number) => { 363 Text(index.toString()).width('100%').height('100%').fontSize(50).textAlign(TextAlign.Center) 364 .backgroundColor(backgroundColor) 365 .opacity(this.opacityList[index]) 366 .scale({ x: this.scaleList[index], y: this.scaleList[index] }) 367 .translate({ x: this.translateList[index] }) 368 .zIndex(this.zIndexList[index]) 369 }) 370 } 371 .height(300) 372 .indicator(false) 373 .displayCount(this.DISPLAY_COUNT, true) 374 .customContentTransition({ 375 timeout: 1000, 376 transition: (proxy: SwiperContentTransitionProxy) => { 377 if (proxy.position <= proxy.index % this.DISPLAY_COUNT || proxy.position >= this.DISPLAY_COUNT + proxy.index % this.DISPLAY_COUNT) { 378 // 同组页面完全滑出视窗外时,重置属性值 379 this.opacityList[proxy.index] = 1.0 380 this.scaleList[proxy.index] = 1.0 381 this.translateList[proxy.index] = 0.0 382 this.zIndexList[proxy.index] = 0 383 } else { 384 // 同组页面未滑出视窗外时,对同组中左右两个页面,逐帧根据position修改属性值 385 if (proxy.index % this.DISPLAY_COUNT === 0) { 386 this.opacityList[proxy.index] = 1 - proxy.position / this.DISPLAY_COUNT 387 this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - proxy.position / this.DISPLAY_COUNT) 388 this.translateList[proxy.index] = - proxy.position * proxy.mainAxisLength + (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0 389 } else { 390 this.opacityList[proxy.index] = 1 - (proxy.position - 1) / this.DISPLAY_COUNT 391 this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - (proxy.position - 1) / this.DISPLAY_COUNT) 392 this.translateList[proxy.index] = - (proxy.position - 1) * proxy.mainAxisLength - (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0 393 } 394 this.zIndexList[proxy.index] = -1 395 } 396 } 397 }) 398 }.width('100%') 399 } 400} 401``` 402 403 404 405## Swiper与Tabs联动 406 407Swiper选中的元素改变时,会通过onSelected回调事件,将元素的索引值index返回。通过调用tabsController.changeIndex(index)方法来实现Tabs页签的切换。 408 409```ts 410// xxx.ets 411class MyDataSource implements IDataSource { 412 private list: number[] = [] 413 414 constructor(list: number[]) { 415 this.list = list 416 } 417 418 totalCount(): number { 419 return this.list.length 420 } 421 422 getData(index: number): number { 423 return this.list[index] 424 } 425 426 registerDataChangeListener(listener: DataChangeListener): void { 427 } 428 429 unregisterDataChangeListener() { 430 } 431} 432 433@Entry 434@Component 435struct TabsSwiperExample { 436 @State fontColor: string = '#182431' 437 @State selectedFontColor: string = '#007DFF' 438 @State currentIndex: number = 0 439 private list: number[] = [] 440 private tabsController: TabsController = new TabsController() 441 private swiperController: SwiperController = new SwiperController() 442 private swiperData: MyDataSource = new MyDataSource([]) 443 444 aboutToAppear(): void { 445 for (let i = 0; i <= 9; i++) { 446 this.list.push(i); 447 } 448 this.swiperData = new MyDataSource(this.list) 449 } 450 451 @Builder tabBuilder(index: number, name: string) { 452 Column() { 453 Text(name) 454 .fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor) 455 .fontSize(16) 456 .fontWeight(this.currentIndex === index ? 500 : 400) 457 .lineHeight(22) 458 .margin({ top: 17, bottom: 7 }) 459 Divider() 460 .strokeWidth(2) 461 .color('#007DFF') 462 .opacity(this.currentIndex === index ? 1 : 0) 463 }.width('20%') 464 } 465 466 build() { 467 Column() { 468 Tabs({ barPosition: BarPosition.Start, controller: this.tabsController }) { 469 ForEach(this.list, (index: number) =>{ 470 TabContent().tabBar(this.tabBuilder(index, '页签 ' + this.list[index])) 471 }) 472 } 473 .onTabBarClick((index: number) => { 474 this.currentIndex = index 475 this.swiperController.changeIndex(index, true) 476 }) 477 .barMode(BarMode.Scrollable) 478 .backgroundColor('#F1F3F5') 479 .height(56) 480 .width('100%') 481 482 Swiper(this.swiperController) { 483 LazyForEach(this.swiperData, (item: string) => { 484 Text(item.toString()) 485 .onAppear(()=>{ 486 console.info('onAppear ' + item.toString()) 487 }) 488 .onDisAppear(()=>{ 489 console.info('onDisAppear ' + item.toString()) 490 }) 491 .width('100%') 492 .height('40%') 493 .backgroundColor(0xAFEEEE) 494 .textAlign(TextAlign.Center) 495 .fontSize(30) 496 }, (item: string) => item) 497 } 498 .loop(false) 499 .onSelected((index: number) => { 500 console.info("onSelected:" + index) 501 this.currentIndex = index; 502 this.tabsController.changeIndex(index) 503 }) 504 } 505 } 506} 507``` 508 509 510## 设置圆点导航点间距 511 512针对圆点导航点,可以通过DotIndicator的space属性来设置圆点导航点的间距。 513 514```ts 515Swiper() { 516 // ... 517} 518.indicator( 519 new DotIndicator() 520 .space(LengthMetrics.vp(3)) 521) 522``` 523 524## 导航点忽略组件大小 525 526当导航点的bottom设为0之后,导航点的底部与Swiper的底部还会有一定间距。如果希望消除该间距,可通过调用bottom(bottom, ignoreSize)属性来进行设置。将ignoreSize 设置为true,即可忽略导航点组件大小,达到消除该间距的目的。 527 528- 圆点导航点忽略组件大小。 529 530```ts 531Swiper() { 532 // ... 533} 534.indicator( 535 new DotIndicator() 536 .bottom(LengthMetrics.vp(0), true) 537) 538``` 539 540- 数字导航点忽略组件大小。 541 542```ts 543Swiper() { 544 // ... 545} 546.indicator( 547 new DigitIndicator() 548 .bottom(LengthMetrics.vp(0), true) 549) 550``` 551 552圆点导航点设置间距及忽略组件大小完整示列代码如下: 553 554```ts 555import { LengthMetrics } from '@kit.ArkUI' 556 557// MyDataSource.ets 558class MyDataSource implements IDataSource { 559 private list: number[] = [] 560 561 constructor(list: number[]) { 562 this.list = list 563 } 564 565 totalCount(): number { 566 return this.list.length 567 } 568 569 getData(index: number): number { 570 return this.list[index] 571 } 572 573 registerDataChangeListener(listener: DataChangeListener): void { 574 } 575 576 unregisterDataChangeListener() { 577 } 578} 579 580// SwiperExample.ets 581@Entry 582@Component 583struct SwiperExample { 584 585 @State space: LengthMetrics = LengthMetrics.vp(0) 586 @State spacePool: LengthMetrics[] = [LengthMetrics.vp(0), LengthMetrics.px(3), LengthMetrics.vp(10)] 587 @State spaceIndex: number = 0 588 589 @State ignoreSize: boolean = false 590 @State ignoreSizePool: boolean[] = [false, true] 591 @State ignoreSizeIndex: number = 0 592 593 private swiperController1: SwiperController = new SwiperController() 594 private data1: MyDataSource = new MyDataSource([]) 595 596 aboutToAppear(): void { 597 let list1: number[] = [] 598 for (let i = 1; i <= 10; i++) { 599 list1.push(i); 600 } 601 this.data1 = new MyDataSource(list1) 602 } 603 604 build() { 605 Scroll() { 606 Column({ space: 20 }) { 607 Swiper(this.swiperController1) { 608 LazyForEach(this.data1, (item: string) => { 609 Text(item.toString()) 610 .width('90%') 611 .height(120) 612 .backgroundColor(0xAFEEEE) 613 .textAlign(TextAlign.Center) 614 .fontSize(30) 615 }, (item: string) => item) 616 } 617 .indicator(new DotIndicator() 618 .space(this.space) 619 .bottom(LengthMetrics.vp(0), this.ignoreSize) 620 .itemWidth(15) 621 .itemHeight(15) 622 .selectedItemWidth(15) 623 .selectedItemHeight(15) 624 .color(Color.Gray) 625 .selectedColor(Color.Blue)) 626 .displayArrow({ 627 showBackground: true, 628 isSidebarMiddle: true, 629 backgroundSize: 24, 630 backgroundColor: Color.White, 631 arrowSize: 18, 632 arrowColor: Color.Blue 633 }, false) 634 635 Column({ space: 4 }) { 636 Button('spaceIndex:' + this.spaceIndex).onClick(() => { 637 this.spaceIndex = (this.spaceIndex + 1) % this.spacePool.length; 638 this.space = this.spacePool[this.spaceIndex]; 639 }).margin(10) 640 641 Button('ignoreSizeIndex:' + this.ignoreSizeIndex).onClick(() => { 642 this.ignoreSizeIndex = (this.ignoreSizeIndex + 1) % this.ignoreSizePool.length; 643 this.ignoreSize = this.ignoreSizePool[this.ignoreSizeIndex]; 644 }).margin(10) 645 }.margin(2) 646 }.width('100%') 647 } 648 } 649} 650``` 651 652 653 654## 相关实例 655 656针对Swiper组件开发,有以下相关实例可供参考: 657 658- [电子相册(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/ElectronicAlbum) 659 660- [Swiper的使用(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/SwiperArkTS) 661<!--RP1--><!--RP1End-->