Android實(shí)現(xiàn)炫酷的ViewPager3D組件
不知道還有沒有人記得風(fēng)靡于一時(shí)的gallery3d組件?
類似于這樣的效果:

為什么忽然會(huì)提到gallery3d?是因?yàn)閯偤糜行枨笠鲱愃菩Ч捻?yè)面。
剛把視覺拿到手的時(shí)候,首先想到的便是gallery3d組件,將gallery3d組件挪過來后發(fā)現(xiàn)并不是很合意。原因嘛,一個(gè)是效果差別較大,另外就是gallery這個(gè)組件也早被ViewPager代替而過時(shí)棄用了。于是就打算用ViewPager自己手寫一個(gè)。
做需求之前當(dāng)然要先分析設(shè)計(jì)一波。
我們先來拆分一下它的特性:
1、中間的Page保持大小不變兩邊逐層遞減。
2、中間的Page保持角度不變兩邊以Y軸向中間方向內(nèi)旋,角度遞增
3、每一頁(yè)P(yáng)age都有一個(gè)倒影,倒影半透明,透明度從中間往兩邊逐層遞減
4、每一頁(yè)都深入部分到前一頁(yè)(以中間為第一頁(yè)),但重疊區(qū)域只顯示上層Page
1、逐層遞減
利用View的scaleX、scaleY的特性,對(duì)Page進(jìn)行縮放。
設(shè)定縮減因子為scaleFactor,
那么每一層的遞減公式為:
1-scaleFactor+scaleFactor*(1 - Math.abs(pagePosition))2、逐層旋轉(zhuǎn)
利用View的setRotationY特性,對(duì)Page進(jìn)行內(nèi)旋。
設(shè)定旋轉(zhuǎn)因子為rotationFactor,
那么每一層的內(nèi)旋角度公式為:
float rotationY = - position * rotation//控制一下最大旋轉(zhuǎn)角if( rotationY > MAX_ROTATION ) {rotationY = MAX_ROTATION ;} else if ( rotationY < - MAX_ROTATION ) {rotationY = - MAX_ROTATION ;}return rotationY;
3、倒影
利用View的buildDrawingCache方法生成View的cache圖片,然后對(duì)圖片處理為倒影,顯示到對(duì)應(yīng)的View的下方。透明度的變化,直接使用View.setAlpha()方法,倒影生成方式大概是這樣:
//生成cache圖片view.buildDrawingCache();Bitmap bm = paramView.getDrawingCache();Bitmap tmp = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), Config.ARGB_4444);new Canvas(tmp).drawBitmap(bm, 0, 0, new Paint());view.destroyDrawingCache();view.setDrawingCacheEnabled(false);//返回倒影圖片return createReflectedImage(tmp,0.5);
4、裁切重疊區(qū)域
要讓Page之間重疊,我們利用View的translationX屬性,讓View移動(dòng)到上一頁(yè)的區(qū)域內(nèi)。
裁切則利用canvas的clipRect方法,對(duì)View的顯示區(qū)域進(jìn)行裁切。但是前提是要能準(zhǔn)確計(jì)算重疊的部分。
要準(zhǔn)確計(jì)算重疊部分~,我們先看一張圖吧。

上圖中的灰色半透明部分(0)為中間的Page,紅色半透明部分(1)為右邊第一頁(yè),依次類推,黑色線框是在第一頁(yè)執(zhí)行完 scale、translationX后的位置、紅色半透明以及重疊的部分則是執(zhí)行完 scale、translationX后再執(zhí)行rotationY后的最終位置。
由圖我們知道,想要準(zhǔn)確的裁切掉(0)和(1)的重疊區(qū)域,必須要準(zhǔn)確計(jì)算出非重疊區(qū)域/(1)的大小的比例。
計(jì)算View旋轉(zhuǎn)后的寬度
怎么計(jì)算呢?
這要用到立體幾何的知識(shí),下圖是View內(nèi)旋后的中心線上的平面圖,至于怎么畫出來的,自己腦補(bǔ)吧,囧~。

其中:
Ao 是視距距離;
θ 是旋轉(zhuǎn)角度;
ao 是原始寬度;
a’o 是旋轉(zhuǎn)后映射到平面坐標(biāo)系上的寬度。
A點(diǎn)是眼睛的視角位置;
ab是View的旋轉(zhuǎn)中心線,也是View的初始寬度;
穿過ab線的藍(lán)色線條是View的繪制平面;
xy是ab沿o點(diǎn)進(jìn)行Y軸旋轉(zhuǎn)后的位置;
紅色線條是眼睛看向旋轉(zhuǎn)后x、y點(diǎn)經(jīng)過或延長(zhǎng)經(jīng)過繪制平面的最大邊界;
也就是說,經(jīng)過o點(diǎn)的θ角度旋轉(zhuǎn)后,View在繪制面板上看到的大小將由ab位置變成a'b'的位置。
我們解題的目標(biāo)是,求出a'o,以及b'o的長(zhǎng)度,這樣給定我們?cè)紝挾纫约靶D(zhuǎn)中心點(diǎn),我們就能計(jì)算出最終的寬度。
求a'o的步驟
ab是原始坐標(biāo),o是原點(diǎn)(0,0)
0、ao=xo,bo=yo
∵ cosθ = xx' ÷ xo
∴
1、xx' = cosθ ? xo
2、x’o = sinθ ? xo
∵ ∠Axx' = ∠Aa'o
∴ Ao / a'o = Ax' / xx'
∴ ( Ao + x'o ) ÷ xx' = Ao / a'o
a’o = (Ao * xx') / (Ao + x'o)
根據(jù)0、1、2得
a’o = (Ao * cosθ * ao) / ( Ao + sinθ * ao )
同時(shí)可以逆推出:
Ao = a'o*sinθ *ao / (a'o-cosθ * ao);
ao = (a'o * Ao / (Ao *cosθ - a'o * sinθ));
查了半天沒查到Android View的初始視距是多少,所以程序中使用的Ao的大小其實(shí)是通過給定 θ 以及 ao、a'o計(jì)算出來的,至于這3個(gè)數(shù)據(jù)的采集方式,可以去參考sample內(nèi)的AoTestActivity類。
求b'o的步驟
yy’ / b’o = Ay’ /Ao
b’o = yy’ * Ao / Ay’
Ay’ = Ao - y’o
yy’ = sin ( 90 - θ ) * yo
y’o = cos ( 90 - θ ) * yo
b’o = sin ( 90 - θ ) * yo * Ao / ( Ao - cos ( 90 - θ ) * yo)
根據(jù)公式計(jì)算第(1)頁(yè)的最終大小(偽代碼):
float ao = page.getWidth()*scale;float width = (Ao * cosθ * ao) / ( Ao + sinθ * ao )
計(jì)算出(1)-b的寬度(偽代碼)
float keepWidth = page.getWidth*(1 - (scaleFactor - (1 -translationXFactor) ) / scaleFactor))根據(jù)keepWidth反求出旋轉(zhuǎn)之前的寬度keepWidth = keepWidth * Ao / (Ao * cosθ - keepWidth * sinθ )clipStart = 0;//clip操作要計(jì)算View的最初始的大小,所以這里要用page.getWidth()clipRight = page.getWidth() - keepWidth;//TODO 上面的只是第一頁(yè)的算法,其他頁(yè)數(shù)會(huì)有不同,都寫下來就太多了,所以,大家有興趣直接去翻源碼吧。最難的是中間的兩頁(yè)。
至此重疊區(qū)域的計(jì)算方式有了,然后根據(jù)上面的步驟以及計(jì)算方式最終搗鼓出這么一個(gè)效果的組件:



當(dāng)然不僅僅是這么簡(jiǎn)單?。?br style="box-sizing: border-box;color: rgb(64, 64, 64);font-family: -apple-system, BlinkMacSystemFont, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;font-size: 16px;text-align: start;white-space: normal;background-color: rgb(255, 255, 255);">這個(gè)組件內(nèi)置了translation、scale、rotation、alpha4個(gè)轉(zhuǎn)換器,可以自定義這4個(gè)轉(zhuǎn)換器實(shí)現(xiàn)各種不同效果的翻頁(yè)。圖1就是自定義后做出的新效果。
具體如何使用以及源碼請(qǐng)移步:
https://github.com/bambootang/ViewPager3D
到這里就結(jié)束啦。
