这段时间的主业是完成一个家政类小程序,终于是过审核发布了。不得不说微信的这个小程序生态还是颇有想法的,抛开他现有的一些问题不说,其提供的组件系统乍一看还是蛮酷的。比如其提供的一个叫swiper的视图组件,就可以在写界面的时候省不少时间和代码,轮播图片跟可滑动列表都可以用。导致现在回来写angular项目时也想整一个这样的组件出来,本文就将使用angular的组件能力和服务能力完成这么一个比较通用,耦合度较低的swiper出来。
首先要选择使用的技术,要实现的是与界面打交道的东西,自然是实现成一个组件,最终要实现的效果是写下这样的代码就可以完成一个可以滑动的视图来:
<swipers>
<swiper>视图1</swiper>
<swiper>视图2</swiper>
</swipers>
然后要把最基本的组件定义写出来,显然这里要定义两个组件。第一个是父级组件,选择器名字就叫ytm-swipers,目前做的事情仅仅是做一个外壳定义基本样式,使用时的子标签都会插入在ng-content标签中。
1 @Component({ 2 selector: 'ytm-swipers', 3 template: ` 4 <div class="view-body"> 5 <ng-content></ng-content> 6 </div> 7 `, 8 styles: [` 9 .view-body{height: 100%; 100%;overflow: hidden;position: relative;} 10 `] 11 })
第二个就是子视图了,在父级组件下,每个子组件都会沾满父级组件,只有当前的子组件会显示,当切换视图时实际做的就是更改这些子组件的显示方式,说的最简单的话,这个子组件还是仅仅用来加一个子外壳,给外壳添加基本样式,实际的页面内容原封不动放在ng-content标签中。
1 @Component({ 2 selector: 'swiper', 3 template: ` 4 <div class="view-child" *ngIf="swiper.displayList.indexOf(childId) >= 0" 5 [ngClass]="{'active': swiper.displayList[0] === childId, 6 'prev': swiper.displayList[2] === childId, 'next': swiper.displayList[1] === childId}"> 7 <ng-content></ng-content> 8 </div> 9 `, 10 styles: [` 11 .view-child{ 12 height: 100%; 100%;position: absolute;top: 0; 13 transition: 0.5s linear;background: #fff; 14 overflow-x: hidden; 15 } 16 .view-child.active{left: 0;z-index: 9;} 17 .view-child.next{left: 100%;z-index: 7;} 18 .view-child.prev{left: -100%;z-index: 8;} 19 `] 20 })
下一步是要让这两个父子组件完成心灵的沟通,讲道理其实可以直接使用ElementRef强行取到DOM来操作,不过这里使用的是组件内服务。和普通的服务使用上没差别,不过其provider是声明在某个组件里的,所以此服务只有在此组件以及子组件中可以注入使用。
1 @Injectable() 2 class SwiperService { 3 public swiperList: number[]; 4 public displayList: number[]; // 0为当前 1为下一个 2为上一个 5 public current: number; 6 private changing: boolean; 7 constructor() { 8 this.changing = false; 9 this.swiperList = []; 10 this.displayList = []; 11 this.current = 0; 12 } 13 public Add(id: number) { 14 this.swiperList.push(id); 15 switch (this.swiperList.length) { 16 case 1: 17 this.displayList[0] = id; 18 return; 19 case 2: 20 this.displayList[1] = id; 21 return; 22 default: 23 this.displayList[2] = id; 24 return; 25 } 26 } 27 public Next(): Promise<any> { 28 if (this.changing) { 29 return new Promise<any>((resolve, reject) => { 30 return reject('on changing'); 31 }); 32 } 33 this.changing = true; 34 let c = this.swiperList.indexOf(this.displayList[0]); 35 let n = this.swiperList.indexOf(this.displayList[1]); 36 let p = this.swiperList.indexOf(this.displayList[2]); 37 p = c; 38 c = n; 39 n = (c + 1) % this.swiperList.length; 40 this.displayList[0] = this.swiperList[c]; 41 this.displayList[2] = this.swiperList[p]; 42 this.displayList[1] = -1; 43 setTimeout(() => { 44 this.displayList[1] = this.swiperList[n]; 45 this.changing = false; 46 }, 500); 47 return new Promise<any>((resolve, reject) => { 48 return resolve(this.displayList[0]); 49 }); 50 } 51 public Prev(): Promise<any> { 52 if (this.changing) { 53 return new Promise<any>((resolve, reject) => { 54 return reject('on changing'); 55 }); 56 } 57 this.changing = true; 58 let c = this.swiperList.indexOf(this.displayList[0]); 59 let n = this.swiperList.indexOf(this.displayList[1]); 60 let p = this.swiperList.indexOf(this.displayList[2]); 61 n = c; 62 c = p; 63 p = p - 1 < 0 ? this.swiperList.length - 1 : p - 1; 64 this.displayList[0] = this.swiperList[c]; 65 this.displayList[1] = this.swiperList[n]; 66 this.displayList[2] = -1; 67 setTimeout(() => { 68 this.displayList[2] = this.swiperList[p]; 69 this.changing = false; 70 }, 500); 71 return new Promise<any>((resolve, reject) => { 72 return resolve(this.displayList[0]); 73 }); 74 } 75 public Skip(index: number): Promise<any> { 76 let c = this.swiperList.indexOf(this.displayList[0]); 77 if (this.changing || c === index) { 78 return new Promise<any>((resolve, reject) => { 79 reject('on changing or no change'); 80 }); 81 } 82 this.changing = true; 83 let n = (index + 1) % this.swiperList.length; 84 let p = index - 1 < 0 ? this.swiperList.length - 1 : index - 1; 85 this.displayList[0] = this.swiperList[index]; 86 if (index > c) { 87 this.displayList[2] = this.swiperList[p]; 88 this.displayList[1] = -1; 89 setTimeout(() => { 90 this.displayList[1] = this.swiperList[n]; 91 this.changing = false; 92 }, 500); 93 return new Promise<any>((resolve, reject) => { 94 return resolve(this.displayList[0]); 95 }); 96 } else { 97 this.displayList[1] = this.swiperList[n]; 98 this.displayList[2] = -1; 99 setTimeout(() => { 100 this.displayList[2] = this.swiperList[p]; 101 this.changing = false; 102 }, 500); 103 return new Promise<any>((resolve, reject) => { 104 return resolve(this.displayList[0]); 105 }); 106 } 107 } 108 }
用到的变量包括: changing变量保证同时只能进行一个切换,保证切换完成才能进行下一个切换;swiperList装填所有的视图的id,这个id在视图初始化的时候生成;displayList数组只会有三个成员,装填的依次是当前视图在swiperList中的索引,下一个视图的索引,上一个视图的索引;current变量用户指示当前显示的视图的id。实际视图中的显示的控制就是使用ngClass指令来根据displayList和视图id附加相应的类,当前视图会正好显示,前一视图会在左边刚好遮挡,后一视图会在右边刚好遮挡。
同时服务还要提供几个方法:Add用于添加制定id的视图,Next用于切换到下一个视图(左滑时调用),Prev用于切换到前一个视图(右滑时调用),再来一个Skip用于直接切换到指定id的视图。
在子视图中注入此服务,需要在子视图初始化时生成一个id并Add到视图列表中:
1 export class YTMSwiperViewComponent { 2 public childId: number; 3 constructor(@Optional() @Host() public swiper: SwiperService) { 4 this.childId = this.swiper.swiperList.length; 5 this.swiper.Add(this.swiper.swiperList.length); 6 } 7 }
这个id其实就是已有列表的索引累加,且一旦有新视图被初始化,都会添加到列表中(支持动态加入很酷,虽然不知道会有什么隐藏问题发生)。
父组件中首先必须要配置一个provider声明服务:
1 @Component({ 2 selector: 'ytm-swipers', 3 template: ` 4 <div class="view-body"> 5 <ng-content></ng-content> 6 </div> 7 `, 8 styles: [` 9 .view-body{height: 100%; 100%;overflow: hidden;position: relative;} 10 `], 11 providers: [SwiperService] 12 })
然后就是要监听手势滑动事件,做出相应的切换。以及传入一个current变量,每当此变量更新时都要切换到对应id的视图去,实际使用效果就是:
<ytm-swipers [current]="1">...</ytm-swipers>可以将视图切换到id为1的视图也就是第二个视图。
1 export class YTMSwiperComponent implements OnChanges { 2 @Input() public current: number; 3 @Output() public onSwiped = new EventEmitter<Object>(); 4 private touchStartX; 5 private touchStartY; 6 constructor(private swiper: SwiperService) { 7 this.current = 0; 8 } 9 public ngOnChanges(sc: SimpleChanges) { 10 if (sc.current && sc.current.previousValue !== undefined && 11 sc.current.previousValue !== sc.current.currentValue) { 12 this.swiper.Skip(sc.current.currentValue).then((id) => { 13 console.log(id); 14 this.onSwiped.emit({current: id, bySwipe: false}); 15 }).catch((err) => { 16 console.log(err); 17 }); 18 } 19 } 20 @HostListener('touchstart', ['$event']) public onTouchStart(e) { 21 this.touchStartX = e.changedTouches[0].clientX; 22 this.touchStartY = e.changedTouches[0].clientY; 23 } 24 @HostListener('touchend', ['$event']) public onTouchEnd(e) { 25 let moveX = e.changedTouches[0].clientX - this.touchStartX; 26 let moveY = e.changedTouches[0].clientY - this.touchStartY; 27 if (Math.abs(moveY) < Math.abs(moveX)) { 28 /** 29 * Y轴移动小于X轴 判定为横向滑动 30 */ 31 if (moveX > 50) { 32 this.swiper.Prev().then((id) => { 33 // this.current = id; 34 this.onSwiped.emit({current: id, bySwipe: true}); 35 }).catch((err) => { 36 console.log(err); 37 }); 38 } else if (moveX < -50) { 39 this.swiper.Next().then((id) => { 40 // this.current = id; 41 this.onSwiped.emit({current: id, bySwipe: true}); 42 }).catch((err) => { 43 console.log(err); 44 }); 45 } 46 } 47 this.touchStartX = this.touchStartY = -1; 48 } 49 }
此外代码中还添加了一个回调函数,可以再视图完成切换时执行传入的回调,这个使用的是angular的EventEmitter能力。
以上就是全部实现了,实际的使用示例像这样:
1 <ytm-swipers [current]="0" (onSwiped)="切换回调($event)"> 2 <swiper> 3 视图1 4 </swiper> 5 <swiper> 6 视图2 7 </swiper> 8 <swiper> 9 视图3 10 </swiper> 11 </ytm-swipers>
视图的切换有了两种方式,一是手势滑动,不过没有写实时拖动,仅仅是判断左右滑做出反应罢了,二是更新[current]节点的值。
整个组件的实现没有使用到angular一些比较底层的能力,仅仅是玩弄css样式以及组件嵌套并通过服务交互,以及Input、Output与外界交互。相比之下ionic的那些组件就厉害深奥多了,笔者还有很长的路要走。
很久未写博客,原因之一是临毕业事情有点多,赚了波奖学金不亏,之二是为了写游戏一边在玩游戏一边在学DX编程,之三是懒。