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