基于angular實(shí)現(xiàn)模擬微信小程序swiper組件
這段時(shí)間的主業(yè)是完成一個(gè)家政類小程序,終于是過審核發(fā)布了。不得不說微信的這個(gè)小程序生態(tài)還是頗有想法的,拋開他現(xiàn)有的一些問題不說,其提供的組件系統(tǒng)乍一看還是蠻酷的。比如其提供的一個(gè)叫swiper的視圖組件,就可以在寫界面的時(shí)候省不少時(shí)間和代碼,輪播圖片跟可滑動(dòng)列表都可以用。導(dǎo)致現(xiàn)在回來寫angular項(xiàng)目時(shí)也想整一個(gè)這樣的組件出來,本文就將使用angular的組件能力和服務(wù)能力完成這么一個(gè)比較通用,耦合度較低的swiper出來。
首先要選擇使用的技術(shù),要實(shí)現(xiàn)的是與界面打交道的東西,自然是實(shí)現(xiàn)成一個(gè)組件,最終要實(shí)現(xiàn)的效果是寫下這樣的代碼就可以完成一個(gè)可以滑動(dòng)的視圖來:
<swipers> <swiper>視圖1</swiper> <swiper>視圖2</swiper> </swipers>
然后要把最基本的組件定義寫出來,顯然這里要定義兩個(gè)組件。第一個(gè)是父級(jí)組件,選擇器名字就叫ytm-swipers,目前做的事情僅僅是做一個(gè)外殼定義基本樣式,使用時(shí)的子標(biāo)簽都會(huì)插入在ng-content標(biāo)簽中。
@Component({
selector: 'ytm-swipers',
template: `
<div class="view-body">
<ng-content></ng-content>
</div>
`,
styles: [`
.view-body{height: 100%;width: 100%;overflow: hidden;position: relative;}
`]
})
第二個(gè)就是子視圖了,在父級(jí)組件下,每個(gè)子組件都會(huì)沾滿父級(jí)組件,只有當(dāng)前的子組件會(huì)顯示,當(dāng)切換視圖時(shí)實(shí)際做的就是更改這些子組件的顯示方式,說的最簡單的話,這個(gè)子組件還是僅僅用來加一個(gè)子外殼,給外殼添加基本樣式,實(shí)際的頁面內(nèi)容原封不動(dòng)放在ng-content標(biāo)簽中。
@Component({
selector: 'swiper',
template: `
<div class="view-child" *ngIf="swiper.displayList.indexOf(childId) >= 0"
[ngClass]="{'active': swiper.displayList[0] === childId,
'prev': swiper.displayList[2] === childId, 'next': swiper.displayList[1] === childId}">
<ng-content></ng-content>
</div>
`,
styles: [`
.view-child{
height: 100%;width: 100%;position: absolute;top: 0;
transition: 0.5s linear;background: #fff;
overflow-x: hidden;
}
.view-child.active{left: 0;z-index: 9;}
.view-child.next{left: 100%;z-index: 7;}
.view-child.prev{left: -100%;z-index: 8;}
`]
})
下一步是要讓這兩個(gè)父子組件完成心靈的溝通,講道理其實(shí)可以直接使用ElementRef強(qiáng)行取到DOM來操作,不過這里使用的是組件內(nèi)服務(wù)。和普通的服務(wù)使用上沒差別,不過其provider是聲明在某個(gè)組件里的,所以此服務(wù)只有在此組件以及子組件中可以注入使用。
@Injectable()
class SwiperService {
public swiperList: number[];
public displayList: number[]; // 0為當(dāng)前 1為下一個(gè) 2為上一個(gè)
public current: number;
private changing: boolean;
constructor() {
this.changing = false;
this.swiperList = [];
this.displayList = [];
this.current = 0;
}
public Add(id: number) {
this.swiperList.push(id);
switch (this.swiperList.length) {
case 1:
this.displayList[0] = id;
return;
case 2:
this.displayList[1] = id;
return;
default:
this.displayList[2] = id;
return;
}
}
public Next(): Promise<any> {
if (this.changing) {
return new Promise<any>((resolve, reject) => {
return reject('on changing');
});
}
this.changing = true;
let c = this.swiperList.indexOf(this.displayList[0]);
let n = this.swiperList.indexOf(this.displayList[1]);
let p = this.swiperList.indexOf(this.displayList[2]);
p = c;
c = n;
n = (c + 1) % this.swiperList.length;
this.displayList[0] = this.swiperList[c];
this.displayList[2] = this.swiperList[p];
this.displayList[1] = -1;
setTimeout(() => {
this.displayList[1] = this.swiperList[n];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
}
public Prev(): Promise<any> {
if (this.changing) {
return new Promise<any>((resolve, reject) => {
return reject('on changing');
});
}
this.changing = true;
let c = this.swiperList.indexOf(this.displayList[0]);
let n = this.swiperList.indexOf(this.displayList[1]);
let p = this.swiperList.indexOf(this.displayList[2]);
n = c;
c = p;
p = p - 1 < 0 ? this.swiperList.length - 1 : p - 1;
this.displayList[0] = this.swiperList[c];
this.displayList[1] = this.swiperList[n];
this.displayList[2] = -1;
setTimeout(() => {
this.displayList[2] = this.swiperList[p];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
}
public Skip(index: number): Promise<any> {
let c = this.swiperList.indexOf(this.displayList[0]);
if (this.changing || c === index) {
return new Promise<any>((resolve, reject) => {
reject('on changing or no change');
});
}
this.changing = true;
let n = (index + 1) % this.swiperList.length;
let p = index - 1 < 0 ? this.swiperList.length - 1 : index - 1;
this.displayList[0] = this.swiperList[index];
if (index > c) {
this.displayList[2] = this.swiperList[p];
this.displayList[1] = -1;
setTimeout(() => {
this.displayList[1] = this.swiperList[n];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
} else {
this.displayList[1] = this.swiperList[n];
this.displayList[2] = -1;
setTimeout(() => {
this.displayList[2] = this.swiperList[p];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
}
}
}
用到的變量包括: changing變量保證同時(shí)只能進(jìn)行一個(gè)切換,保證切換完成才能進(jìn)行下一個(gè)切換;swiperList裝填所有的視圖的id,這個(gè)id在視圖初始化的時(shí)候生成;displayList數(shù)組只會(huì)有三個(gè)成員,裝填的依次是當(dāng)前視圖在swiperList中的索引,下一個(gè)視圖的索引,上一個(gè)視圖的索引;current變量用戶指示當(dāng)前顯示的視圖的id。實(shí)際視圖中的顯示的控制就是使用ngClass指令來根據(jù)displayList和視圖id附加相應(yīng)的類,當(dāng)前視圖會(huì)正好顯示,前一視圖會(huì)在左邊剛好遮擋,后一視圖會(huì)在右邊剛好遮擋。
同時(shí)服務(wù)還要提供幾個(gè)方法:Add用于添加制定id的視圖,Next用于切換到下一個(gè)視圖(左滑時(shí)調(diào)用),Prev用于切換到前一個(gè)視圖(右滑時(shí)調(diào)用),再來一個(gè)Skip用于直接切換到指定id的視圖。
在子視圖中注入此服務(wù),需要在子視圖初始化時(shí)生成一個(gè)id并Add到視圖列表中:
export class YTMSwiperViewComponent {
public childId: number;
constructor(@Optional() @Host() public swiper: SwiperService) {
this.childId = this.swip
@Injectable()
class SwiperService {
public swiperList: number[];
public displayList: number[]; // 0為當(dāng)前 1為下一個(gè) 2為上一個(gè)
public current: number;
private changing: boolean;
constructor() {
this.changing = false;
this.swiperList = [];
this.displayList = [];
this.current = 0;
}
public Add(id: number) {
this.swiperList.push(id);
switch (this.swiperList.length) {
case 1:
this.displayList[0] = id;
return;
case 2:
this.displayList[1] = id;
return;
default:
this.displayList[2] = id;
return;
}
}
public Next(): Promise<any> {
if (this.changing) {
return new Promise<any>((resolve, reject) => {
return reject('on changing');
});
}
this.changing = true;
let c = this.swiperList.indexOf(this.displayList[0]);
let n = this.swiperList.indexOf(this.displayList[1]);
let p = this.swiperList.indexOf(this.displayList[2]);
p = c;
c = n;
n = (c + 1) % this.swiperList.length;
this.displayList[0] = this.swiperList[c];
this.displayList[2] = this.swiperList[p];
this.displayList[1] = -1;
setTimeout(() => {
this.displayList[1] = this.swiperList[n];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
}
public Prev(): Promise<any> {
if (this.changing) {
return new Promise<any>((resolve, reject) => {
return reject('on changing');
});
}
this.changing = true;
let c = this.swiperList.indexOf(this.displayList[0]);
let n = this.swiperList.indexOf(this.displayList[1]);
let p = this.swiperList.indexOf(this.displayList[2]);
n = c;
c = p;
p = p - 1 < 0 ? this.swiperList.length - 1 : p - 1;
this.displayList[0] = this.swiperList[c];
this.displayList[1] = this.swiperList[n];
this.displayList[2] = -1;
setTimeout(() => {
this.displayList[2] = this.swiperList[p];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
}
public Skip(index: number): Promise<any> {
let c = this.swiperList.indexOf(this.displayList[0]);
if (this.changing || c === index) {
return new Promise<any>((resolve, reject) => {
reject('on changing or no change');
});
}
this.changing = true;
let n = (index + 1) % this.swiperList.length;
let p = index - 1 < 0 ? this.swiperList.length - 1 : index - 1;
this.displayList[0] = this.swiperList[index];
if (index > c) {
this.displayList[2] = this.swiperList[p];
this.displayList[1] = -1;
setTimeout(() => {
this.displayList[1] = this.swiperList[n];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
} else {
this.displayList[1] = this.swiperList[n];
this.displayList[2] = -1;
setTimeout(() => {
this.displayList[2] = this.swiperList[p];
this.changing = false;
}, 500);
return new Promise<any>((resolve, reject) => {
return resolve(this.displayList[0]);
});
}
}
}
er.swiperList.length;
this.swiper.Add(this.swiper.swiperList.length);
}
}
這個(gè)id其實(shí)就是已有列表的索引累加,且一旦有新視圖被初始化,都會(huì)添加到列表中(支持動(dòng)態(tài)加入很酷,雖然不知道會(huì)有什么隱藏問題發(fā)生)。
父組件中首先必須要配置一個(gè)provider聲明服務(wù):
@Component({
selector: 'ytm-swipers',
template: `
<div class="view-body">
<ng-content></ng-content>
</div>
`,
styles: [`
.view-body{height: 100%;width: 100%;overflow: hidden;position: relative;}
`],
providers: [SwiperService]
})
然后就是要監(jiān)聽手勢滑動(dòng)事件,做出相應(yīng)的切換。以及傳入一個(gè)current變量,每當(dāng)此變量更新時(shí)都要切換到對(duì)應(yīng)id的視圖去,實(shí)際使用效果就是:
<ytm-swipers [current]="1">...</ytm-swipers>可以將視圖切換到id喂1的視圖也就是第二個(gè)視圖。
export class YTMSwiperComponent implements OnChanges {
@Input() public current: number;
@Output() public onSwiped = new EventEmitter<Object>();
private touchStartX;
private touchStartY;
constructor(private swiper: SwiperService) {
this.current = 0;
}
public ngOnChanges(sc: SimpleChanges) {
if (sc.current && sc.current.previousValue !== undefined &&
sc.current.previousValue !== sc.current.currentValue) {
this.swiper.Skip(sc.current.currentValue).then((id) => {
console.log(id);
this.onSwiped.emit({current: id, bySwipe: false});
}).catch((err) => {
console.log(err);
});
}
}
@HostListener('touchstart', ['$event']) public onTouchStart(e) {
this.touchStartX = e.changedTouches[0].clientX;
this.touchStartY = e.changedTouches[0].clientY;
}
@HostListener('touchend', ['$event']) public onTouchEnd(e) {
let moveX = e.changedTouches[0].clientX - this.touchStartX;
let moveY = e.changedTouches[0].clientY - this.touchStartY;
if (Math.abs(moveY) < Math.abs(moveX)) {
/**
* Y軸移動(dòng)小于X軸 判定為橫向滑動(dòng)
*/
if (moveX > 50) {
this.swiper.Prev().then((id) => {
// this.current = id;
this.onSwiped.emit({current: id, bySwipe: true});
}).catch((err) => {
console.log(err);
});
} else if (moveX < -50) {
this.swiper.Next().then((id) => {
// this.current = id;
this.onSwiped.emit({current: id, bySwipe: true});
}).catch((err) => {
console.log(err);
});
}
}
this.touchStartX = this.touchStartY = -1;
}
}
此外代碼中還添加了一個(gè)回調(diào)函數(shù),可以再視圖完成切換時(shí)執(zhí)行傳入的回調(diào),這個(gè)使用的是angular的EventEmitter能力。
以上就是全部實(shí)現(xiàn)了,實(shí)際的使用示例像這樣:
<ytm-swipers [current]="0" (onSwiped)="切換回調(diào)($event)">
<swiper>
視圖1
</swiper>
<swiper>
視圖2
</swiper>
<swiper>
視圖3
</swiper>
</ytm-swipers>
視圖的切換有了兩種方式,一是手勢滑動(dòng),不過沒有寫實(shí)時(shí)拖動(dòng),僅僅是判斷左右滑做出反應(yīng)罷了,二是更新[current]節(jié)點(diǎn)的值。
整個(gè)組件的實(shí)現(xiàn)沒有使用到angular一些比較底層的能力,僅僅是玩弄css樣式以及組件嵌套并通過服務(wù)交互,以及Input、Output與外界交互。相比之下ionic的那些組件就厲害深?yuàn)W多了,筆者還有很長的路要走。
以上所述是小編給大家介紹的基于angular實(shí)現(xiàn)模擬微信小程序swiper組件,希望對(duì)大家有所幫助,如果大家有任何疑問歡迎給我留言小編會(huì)及時(shí)回復(fù)大家的!
相關(guān)文章
AngularJS使用$http配置對(duì)象方式與服務(wù)端交互方法
今天小編就為大家分享一篇AngularJS使用$http配置對(duì)象方式與服務(wù)端交互方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08
詳解angular分頁插件tm.pagination二次觸發(fā)問題解決方案
這篇文章主要介紹了詳解angular分頁插件tm.pagination二次觸發(fā)問題解決方案,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07
AngularJS自定義指令之復(fù)制指令實(shí)現(xiàn)方法
這篇文章主要介紹了AngularJS自定義指令之復(fù)制指令實(shí)現(xiàn)方法,結(jié)合完整實(shí)例形式分析了AngularJS自定義指令實(shí)現(xiàn)復(fù)制功能的相關(guān)操作技巧,需要的朋友可以參考下2017-05-05
Angular4實(shí)現(xiàn)鼠標(biāo)懸停3d傾斜效果
這篇文章主要介紹了Angular4實(shí)現(xiàn)鼠標(biāo)懸停3d傾斜效果,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-10-10
Angular2中constructor和ngOninit的使用講解
這篇文章主要介紹了Angular2中constructor和ngOninit的使用講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
AngularJS基礎(chǔ) ng-init 指令簡單示例
本文主要介紹AngularJS ng-init 指令,這里幫大家整理了關(guān)于ng-init 指令的基本知識(shí)資料,并附有簡單的代碼示例,有需要學(xué)習(xí)的小伙伴可以參考下2016-08-08
angular 實(shí)時(shí)監(jiān)聽input框value值的變化觸發(fā)函數(shù)方法
今天小編就為大家分享一篇angular 實(shí)時(shí)監(jiān)聽input框value值的變化觸發(fā)函數(shù)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08
AngularJS基礎(chǔ) ng-click 指令示例代碼
本文介紹AngularJS ng-click 指令,這里整理了ng-click指令的基礎(chǔ)知識(shí)并且附有簡單示例代碼和實(shí)現(xiàn)效果圖,有需要的小伙伴參考下2016-08-08
angularjs實(shí)現(xiàn)文字上下無縫滾動(dòng)特效代碼
這篇文章主要介紹了angularjs實(shí)現(xiàn)文字上下無縫滾動(dòng)特效代碼的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09

