備注:最近工作需要凌埂,要用react實(shí)現(xiàn)旋轉(zhuǎn)木馬的輪播圖效果,在網(wǎng)上查了查沒有相似的案例诗芜,只有用react實(shí)現(xiàn)的簡單的輪播圖瞳抓,還有就是用jQuery實(shí)現(xiàn)的旋轉(zhuǎn)木馬輪播圖,在參考了這兩個(gè)實(shí)現(xiàn)方式的基礎(chǔ)上伏恐,我用react轉(zhuǎn)化了這種實(shí)現(xiàn)孩哑,過程真的很糾結(jié),但是還是做出來了翠桦,效果還可以横蜒。
效果圖:
實(shí)現(xiàn)思路分析
1.每張圖片(li節(jié)點(diǎn))的布局
<ul className={style['poster-list']} style={{width:width,height:height}}>
{
this.props.imgArray.map(function(item,index){
return <li ref={'items'+index} className={style['poster-item']} style={this.renderstyle(index)} key={index}><a href={this.props.linkArray[index]}><img width="100%" height="100%" src={item}/></a></li>;
}.bind(this))
}
</ul>
主要就是renderstyle函數(shù)在控制他們的排列:
renderstyle(index) {
const { number, width, imgWidth, scale, vertical, height } = this.props.lunboObject;
const middleIndex = Math.floor(number / 2);
const btnWidth = (width-imgWidth) / 2;
const gap = btnWidth/middleIndex;
let Imgleft;
let ImgTop;
let Imgscale;
let zIndex;
let opacity;
if(index <= middleIndex){
// 右側(cè)圖片
Imgscale = Math.pow(scale, (index));
Imgleft = width - (middleIndex-index)*gap - imgWidth*Imgscale;
zIndex=middleIndex+1 - index;
opacity=1/++index;
}else if(index > middleIndex){
// 左側(cè)圖片
Imgscale = Math.pow(scale, (number-index));
Imgleft = (index-(middleIndex+1))*gap;
zIndex = index-middleIndex;
opacity = 1 - middleIndex/index;
}
switch(vertical){
case 'bottom':
ImgTop = parseInt(height - height*Imgscale);
break;
case 'center':
ImgTop = parseInt((height - height*Imgscale)/2);
break;
default:
ImgTop = parseInt((height - height*Imgscale)/2);
}
return {
width: parseInt(imgWidth*Imgscale),
height: parseInt(height*Imgscale),
left:parseInt(Imgleft),
zIndex:zIndex,
opacity:opacity,
top:ImgTop
}
}
要想實(shí)現(xiàn)3D的效果,需要同時(shí)控制每張圖片的6個(gè)屬性來回變化销凑,下面分析他們的計(jì)算過程(先布局):
index關(guān)系:
實(shí)際上要分為左右兩部分實(shí)現(xiàn):
右側(cè):
Imgscale = Math.pow(scale, (index));
Imgleft = width - (middleIndex-index)*gap - imgWidth*Imgscale;
zIndex=middleIndex+1 - index;
opacity=1/++index;
圖片的Zindex和opacity需要認(rèn)真考慮一下如何設(shè)置丛晌,其他的都好說,就是數(shù)學(xué)關(guān)系斗幼。
zIndex實(shí)際上最中間的是3澎蛛,向右依次遞減;
opacity中間是1蜕窿,向右依次是1/2,1/3 ... 谋逻;
左側(cè):
// 左側(cè)圖片
Imgscale = Math.pow(scale, (number-index));
Imgleft = (index-(middleIndex+1))*gap;
zIndex = index-middleIndex;
opacity = 1 - middleIndex/index;
左側(cè)唯一麻煩的是,opacity的設(shè)置桐经,我是需找的數(shù)學(xué)規(guī)律毁兆,只要讓左側(cè)的opacity呈現(xiàn)1,1/2,1/3 ... 即可。
2.點(diǎn)擊箭頭讓他動(dòng)起來
如果是一般的輪播圖就好辦了阴挣,直接控制ul的left值就可以气堕,不用直接操作dom,而這種輪播圖是不斷控制每一個(gè)li,讓他的狀態(tài)變化到上一個(gè)或者下一個(gè)li的狀態(tài)送巡,用state控制變量的方式實(shí)在是不會(huì),所以還是操作的dom盒卸。
首先骗爆,組件掛載后,獲取dom對(duì)象蔽介,組成數(shù)組:
componentDidMount() {
for(let i=0;i<this.props.lunboObject.number;i++){
this.itemsArr.push(findDOMNode(this.refs['items'+(i)]));
};
this.autoPlay();
}
然后摘投,比如點(diǎn)擊左側(cè)按鈕的時(shí)候,遍歷dom數(shù)組虹蓄,讓當(dāng)前的li的狀態(tài)變?yōu)樗纳弦粋€(gè)(prev)li的狀態(tài)犀呼,處理一下臨界的問題。
this.itemsArr.forEach((item, index) => {
let self = item;
let next = this.itemsArr[index+1];
if(index == (len-1)){
next = this.itemsArr[0];
}
this.rotateStyle(self, next);
})
rotateStyle這個(gè)函數(shù)是控制他運(yùn)動(dòng)的薇组,
rotateStyle(self, next) {
const { left, top, width, height, zIndex, opacity } = next.style;
this.animate(self, {left:left,width:width,height:height,zIndex:zIndex,opacity: opacity,top:top}, this.props.lunboObject.tweenString, () => {
++this.LOOPNUM ;
});
}
animate是封裝的緩動(dòng)函數(shù)外臂,這個(gè)不重要,就不詳細(xì)講了律胀。
點(diǎn)擊右側(cè)按鈕的時(shí)候原理類似宋光,就不再贅述。
3.自己動(dòng)起來
這個(gè)就沒啥可說的了炭菌,就是設(shè)置定時(shí)器不斷觸發(fā)右擊箭頭函數(shù)罪佳,鼠標(biāo)移入清除定時(shí)器,鼠標(biāo)移出黑低,開啟定時(shí)器即可赘艳。
4.小圓點(diǎn)跟著點(diǎn)亮
維護(hù)一個(gè)全局的state變量,activeIndex克握,每次dom運(yùn)動(dòng)的話就會(huì)變化這個(gè)值蕾管,然后控制點(diǎn)是否點(diǎn)亮。
5點(diǎn)擊某個(gè)小圓點(diǎn)菩暗,讓他運(yùn)動(dòng)到當(dāng)前位置
這是難點(diǎn)娇掏!
代碼如下:
// 點(diǎn)擊小圓點(diǎn)動(dòng)
gotoDotView() {
if(this.state.dotsIndex == this.state.activeIndex){
return ;
}else{
let len = this.itemsArr.length;
// 運(yùn)動(dòng)到小圓點(diǎn)指示的位置
if(this.state.dotsIndex - this.state.activeIndex > 0){
// 如果點(diǎn)擊在右側(cè) 向左運(yùn)動(dòng)
const dotsDiff = this.state.dotsIndex - this.state.activeIndex;
this.setState({
activeIndex: this.state.activeIndex + dotsDiff
})
this.itemsArr.forEach((item, index) => {
let self = item;
let nextIndex = Number.parseInt(index-dotsDiff);
if(nextIndex < 0){
nextIndex = nextIndex+len;
}
let next = this.itemsArr[nextIndex];
this.rotateStyle(self, next);
})
}else{
// 如果點(diǎn)擊在左側(cè)
const dotsDiff = this.state.activeIndex - this.state.dotsIndex;
this.setState({
activeIndex: this.state.activeIndex - dotsDiff
})
this.itemsArr.forEach((item, index) => {
let self = item;
let prevIndex = Number.parseInt(index+dotsDiff);
if(prevIndex >= len){
prevIndex = prevIndex-len;
}
let prev = this.itemsArr[prevIndex];
this.rotateStyle(self, prev);
})
}
}
}
這里要分為兩種情況,點(diǎn)擊點(diǎn)在當(dāng)前活動(dòng)點(diǎn)的右側(cè)勋眯,或者左側(cè)婴梧。然后記錄當(dāng)前兩個(gè)點(diǎn)之間的差值,這個(gè)時(shí)候客蹋,遍歷每個(gè)dom塞蹭,當(dāng)前的item要變?yōu)橛?jì)算完差值后的item的狀態(tài),并且考慮臨界值的處理讶坯,我也說不清楚番电,具體還是看代碼吧。
6.關(guān)于緩動(dòng)函數(shù)
看代碼:
/*
* animate函數(shù)是動(dòng)畫封裝函數(shù)
* @para0 elem參數(shù)就是運(yùn)動(dòng)的對(duì)象
* @para1 targetJSON參數(shù)就是運(yùn)動(dòng)的終點(diǎn)狀態(tài),可以寫px漱办,也可以不寫px
* @para2 time是運(yùn)動(dòng)總時(shí)間这刷,毫秒為單位
* @para3 tweenString緩沖描述詞,比如"Linear"
* @para4 callback是回調(diào)函數(shù)娩井,可選
*/
animate(elem , targetJSON , tweenString , callback){
// 緩沖描述詞集合
const Tween = {
Linear: (t, b, c, d) => {
return c * t / d + b;
},
//二次的
QuadEaseIn: (t, b, c, d) => {
return c * (t /= d) * t + b;
},
QuadEaseOut: (t, b, c, d) => {
return -c * (t /= d) * (t - 2) + b;
},
QuadEaseInOut: (t, b, c, d) => {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
},
//三次的
CubicEaseIn: (t, b, c, d) => {
return c * (t /= d) * t * t + b;
},
CubicEaseOut: (t, b, c, d) => {
return c * ((t = t / d - 1) * t * t + 1) + b;
},
CubicEaseInOut: (t, b, c, d) => {
if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
return c / 2 * ((t -= 2) * t * t + 2) + b;
},
//四次的
QuartEaseIn: (t, b, c, d) => {
return c * (t /= d) * t * t * t + b;
},
QuartEaseOut: (t, b, c, d) => {
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
},
QuartEaseInOut: (t, b, c, d) => {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
}
};
let interval = 15;
let time = 300;
//初始狀態(tài)暇屋,放在origninalJSON里面
let originalJSON = {};
//變化的多少,放在deltaJSON里面
let deltaJSON = {};
for(let k in targetJSON){
originalJSON[k] = parseFloat(elem.style[k]);
//把每個(gè)targetJSON中的值都去掉px
targetJSON[k] = parseFloat(targetJSON[k]);
//變化量JSON
deltaJSON[k] = targetJSON[k] - originalJSON[k];
}
//總執(zhí)行函數(shù)次數(shù):
let maxFrameNumber = time / interval;
//當(dāng)前幀編號(hào)
let frameNumber = 0;
//這是一個(gè)臨時(shí)變量一會(huì)兒用
let tween;
//定時(shí)器
let timer = setInterval(() => {
//要讓所有的屬性發(fā)生變化
for(let k in originalJSON){
// tween就表示這一幀應(yīng)該在的位置:
tween = Tween[tweenString](frameNumber , originalJSON[k] , deltaJSON[k] , maxFrameNumber);
//根據(jù)是不是opacity來設(shè)置單位
if(k != "opacity"){
elem.style[k] = tween + "px";
}else{
elem.style[k] = tween;
}
}
//計(jì)數(shù)器
frameNumber++;
if(frameNumber == maxFrameNumber){
for(let k in targetJSON){
if(k == "opacity" || k == "zIndex"){
elem.style[k] = targetJSON[k];
}else{
elem.style[k] = targetJSON[k] + "px";
}
}
clearInterval(timer);
//拿掉是否在動(dòng)屬性洞辣,設(shè)為false
callback && callback();
}
},interval);
}
實(shí)際上這個(gè)封裝也不難咐刨,主要在Tween的理解上,每個(gè)緩動(dòng)函數(shù)接收四個(gè)參數(shù)扬霜,分別為:當(dāng)前幀編號(hào)定鸟,初始值,結(jié)束值著瓶,結(jié)束幀編號(hào)联予。
在一個(gè)就是注意opacity和zIndex要淡出處理一下就可以了。
最后說一下材原,這個(gè)輪播是可以根據(jù)實(shí)際情況進(jìn)行個(gè)化配置躯泰,
lunboObject: {
"width":995,//幻燈片的寬度
"height":335,//幻燈片的高度
"imgWidth":690,//幻燈片第一幀的寬度
"interval": 2000,//幻燈片滾動(dòng)的間隔時(shí)間
"scale":0.85, //記錄顯示比例關(guān)系
"number":5,
"autoPlay":true,
"vertical":"top", // center或者bottom,居中對(duì)齊或底部對(duì)齊
"tweenString":"QuadEaseIn" // 運(yùn)動(dòng)方式,緩沖曲線
}