Android翻頁效果原理實現之曲線的實現
來源:程序員人生 發布時間:2015-01-21 08:29:31 閱讀次數:7191次
尊重原創轉載請注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!
炮兵
鎮樓
上1節我們通過引入折線實現了頁面的折疊翻轉效果,有了前面兩節的基礎呢其實曲線的實現可以變得非常簡單,為何這么說呢?由于曲線不過就是在折線的基礎上對Path加入了曲線的實現,進而只是影響了我們的Region區域,而其他的甚么事件啊、滑動計算啊之類的幾近都是不變的對吧,說白了就是對現有的折線View進行update改造,雖然是改造,但是我們該如何下手呢?首先我們來看看現實中翻頁的效果應當是怎樣的呢?如果大家身旁有書或本子乃至1張紙也行,嘗試以不同的方式去翻動它,你會發現除我們前面兩節曾提到過的1些限制外,還有1些special的現象:
1、翻起來的區域從側面來看是1個有弧度的區域,如圖所示側面圖:

而我們將依照第1節中的約定疏忽這部份弧度的表現,由于從正俯視的角度我們壓根看不到弧度的效果,So~我們強迫讓其與頁面平行:

2、根據拖拽點距離頁面高度的不同,我們可以得到不同的卷曲度:

而其在我們正俯視點的表現則是曲線的弧度不同:

一樣的,我們依照第1節的約定,為了簡化問題,我們將拖拽點距離頁面的高度視為1個定值使在我們正俯視點表現的曲線出發點從距離控件交點1/4處開始:

3、如上1節末所說,在曲折的區域圖象也會有相似的扭曲效果
OK,大致的1個分析就是這樣,我們根據分析結果可以得出下面的1個分析圖:

由上圖配合我們上面的分析我們可知:DB = 1/4OB,FA = 1/4OA,而點F和點D分別為兩條曲線(如無特殊聲明,我們所說的曲線均為貝賽爾曲線,下同)的出發點(固然你也能夠說是終點無所謂),這時候,我們以點A、B為曲線的控制點并以其為端點分別沿著x軸和y軸方向作線段AG、BC,另AG = AF、BC = BD,并令點G、C分別為曲線的終點,這樣,我們的這兩條2階貝塞爾曲線就非常非常的特殊,例如上圖中的曲線DC,它是由起始點D、C和控制點B構成,而BD = BC,也就是說3角形BDC是的等腰3角形,進1步地說就是曲線DC的兩條控制桿力臂相等,進1步地我們可以推斷出曲線DC的頂點J一定在直線DC的中垂線上,更進1步地我們可以根據《自定義控件其實很簡單5/12》所說的2階貝塞爾曲線公式得出當且僅當t
= 0.5時曲線的端點恰好會在頂點J上,由此我們可以非常非常簡單地得到曲線的頂點坐標。好了,YY歸YY我們還是要回歸到具體的操作中來,首先,我們要計算出點G、F、D、C的坐標值,這4點坐標也相當easy,就拿F點坐標來講,我們過點F分別作OM、AM的垂線:

由于FA = 1/4OA,那末我們可以得到F點的x坐標Fx = a + 3/4MA,y坐標Fy = b + 3/4OM,而G點的x坐標Gx = a + MA - 1/4x;其他兩點D、C就不多扯了,那末在代碼中如何體現呢?首先,為了便于視察效果,我們先注釋掉圖片的繪制:
/*
* 如果坐標點在原點(即還沒產生觸碰時)則繪制第1頁
*/
if (mPointX == 0 && mPointY == 0) {
// canvas.drawBitmap(mBitmaps.get(mBitmaps.size() - 1), 0, 0, null);
return;
}
// 省略大量代碼
//drawBitmaps(canvas);
并繪制線條:
canvas.drawPath(mPath, mPaint);
在上1節中我們在生成Path時將情況分為了兩種:
if (sizeLong > mViewHeight) {
//…………………………
} else {
//…………………………
}
一樣,我們也分開處理兩種情況,那末針對sizeLong > mViewHeight的時候此時控件頂部的曲線效果已是看不到了,我們只需斟酌底部的曲線效果:
// 計算曲線出發點
float startXBtm = btmX2 - CURVATURE * sizeShort;
float startYBtm = mViewHeight;
// 計算曲線終點
float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
float endYBtm = mPointY + (1 - CURVATURE) * mL;
// 計算曲線控制點
float controlXBtm = btmX2;
float controlYBtm = mViewHeight;
// 計算曲線頂點
float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;
/*
* 生成帶曲線的4邊形路徑
*/
mPath.moveTo(startXBtm, startYBtm);
mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPath.lineTo(mPointX, mPointY);
mPath.lineTo(topX1, 0);
mPath.lineTo(topX2, 0);
mPath.lineTo(bezierPeakXBtm, bezierPeakYBtm);
該部份的實際效果以下:

PS:為了便于大家對參數的理解,我對每個點的坐標都重新給予了1個援用其命名也淺顯易懂,實際進程可以省略這1步簡化代碼
而當sizeLong <= mViewHeight時這時候候不但底部有曲線效果,右邊也有:
/*
* 計算參數
*/
float leftY = mViewHeight - sizeLong;
float btmX = mViewWidth - sizeShort;
// 計算曲線出發點
float startXBtm = btmX - CURVATURE * sizeShort;
float startYBtm = mViewHeight;
float startXLeft = mViewWidth;
float startYLeft = leftY - CURVATURE * sizeLong;
/*
* 限制左邊曲線出發點
*/
if (startYLeft <= 0) {
startYLeft = 0;
}
/*
* 限制右邊曲線出發點
*/
if (startXBtm <= 0) {
startXBtm = 0;
}
// 計算曲線終點
float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
float endYBtm = mPointY + (1 - CURVATURE) * mL;
float endXLeft = mPointX + (1 - CURVATURE) * mK;
float endYLeft = mPointY - (1 - CURVATURE) * (sizeLong - mL);
// 計算曲線控制點
float controlXBtm = btmX;
float controlYBtm = mViewHeight;
float controlXLeft = mViewWidth;
float controlYLeft = leftY;
// 計算曲線頂點
float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;
float bezierPeakXLeft = 0.25F * startXLeft + 0.5F * controlXLeft + 0.25F * endXLeft;
float bezierPeakYLeft = 0.25F * startYLeft + 0.5F * controlYLeft + 0.25F * endYLeft;
/*
* 生成帶曲線的3角形路徑
*/
mPath.moveTo(startXBtm, startYBtm);
mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPath.lineTo(mPointX, mPointY);
mPath.lineTo(endXLeft, endYLeft);
mPath.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);
效果以下:

Path有了,我們就該斟酌如何將其轉換為Region,在這個進程中呢又1個問題,曲線路徑不像上1節的直線路徑我們可以輕易取得其范圍區域,由于我們的折疊區域其實應當是這樣的:

如圖所示紅色路徑區域,這部份區域則是我們折疊的區域,而事實上我們為了計算方便將整條2階貝賽爾曲線都繪制了出來,也就是說我們的Path除紅色線條部份還包括了藍色線條部份對吧,那末問題來了,如何將這兩部份“做掉”呢?其實方法很多,我們可以在計算的時候就只生成半條曲線,這是方法1我們利用純計算的方式,記得我在該系列文章開頭曾說過翻頁效果的實現可以有兩種方式,1種是純計算而另外一種則是利用圖形的組合思想,如何組合呢?這里對區域的計算我們就不用純計算的方式了,我們嘗試用圖形組合來試試。首先我們將Path轉為Region看看是甚么樣的:
Region region = computeRegion(mPath);
canvas.clipRegion(region);
canvas.drawColor(Color.RED);
// canvas.drawPath(mPath, mPaint);
效果以下:

可以看到我們沒有封閉的Path構成的Region效果,事實呢跟我們需要的區域差距有點大,首先上下兩個月半圓是過剩的,其次目測少了1塊對吧:

如上圖藍色的那塊,那末我們該如何把這塊“補”回來呢?利用圖形組合的思想,我們想法為該Region補1塊矩形:

然后差集掉兩個月半圓不就成了?這部份代碼改動較大,我先貼代碼再說吧:
if (sizeLong > mViewHeight) {
// 計算……額……按圖來AN邊~
float an = sizeLong - mViewHeight;
// 3角形AMN的MN邊
float largerTrianShortSize = an / (sizeLong - (mViewHeight - mPointY)) * (mViewWidth - mPointX);
// 3角形AQN的QN邊
float smallTrianShortSize = an / sizeLong * sizeShort;
/*
* 計算參數
*/
float topX1 = mViewWidth - largerTrianShortSize;
float topX2 = mViewWidth - smallTrianShortSize;
float btmX2 = mViewWidth - sizeShort;
// 計算曲線出發點
float startXBtm = btmX2 - CURVATURE * sizeShort;
float startYBtm = mViewHeight;
// 計算曲線終點
float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
float endYBtm = mPointY + (1 - CURVATURE) * mL;
// 計算曲線控制點
float controlXBtm = btmX2;
float controlYBtm = mViewHeight;
// 計算曲線頂點
float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;
/*
* 生成帶曲線的4邊形路徑
*/
mPath.moveTo(startXBtm, startYBtm);
mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPath.lineTo(mPointX, mPointY);
mPath.lineTo(topX1, 0);
mPath.lineTo(topX2, 0);
/*
* 替補區域Path
*/
mPathTrap.moveTo(startXBtm, startYBtm);
mPathTrap.lineTo(topX2, 0);
mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
mPathTrap.close();
/*
* 底部月半圓Path
*/
mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPathSemicircleBtm.close();
/*
* 生成包括折疊和下1頁的路徑
*/
//暫時沒用省略掉
// 計算月半圓區域
mRegionSemicircle = computeRegion(mPathSemicircleBtm);
} else {
/*
* 計算參數
*/
float leftY = mViewHeight - sizeLong;
float btmX = mViewWidth - sizeShort;
// 計算曲線出發點
float startXBtm = btmX - CURVATURE * sizeShort;
float startYBtm = mViewHeight;
float startXLeft = mViewWidth;
float startYLeft = leftY - CURVATURE * sizeLong;
// 計算曲線終點
float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
float endYBtm = mPointY + (1 - CURVATURE) * mL;
float endXLeft = mPointX + (1 - CURVATURE) * mK;
float endYLeft = mPointY - (1 - CURVATURE) * (sizeLong - mL);
// 計算曲線控制點
float controlXBtm = btmX;
float controlYBtm = mViewHeight;
float controlXLeft = mViewWidth;
float controlYLeft = leftY;
// 計算曲線頂點
float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;
float bezierPeakXLeft = 0.25F * startXLeft + 0.5F * controlXLeft + 0.25F * endXLeft;
float bezierPeakYLeft = 0.25F * startYLeft + 0.5F * controlYLeft + 0.25F * endYLeft;
/*
* 限制右邊曲線出發點
*/
if (startYLeft <= 0) {
startYLeft = 0;
}
/*
* 限制底部左邊曲線出發點
*/
if (startXBtm <= 0) {
startXBtm = 0;
}
/*
* 根據底部左邊限制點重新計算貝塞爾曲線頂點坐標
*/
float partOfShortLength = CURVATURE * sizeShort;
if (btmX >= -mValueAdded && btmX <= partOfShortLength - mValueAdded) {
float f = btmX / partOfShortLength;
float t = 0.5F * f;
float bezierPeakTemp = 1 - t;
float bezierPeakTemp1 = bezierPeakTemp * bezierPeakTemp;
float bezierPeakTemp2 = 2 * t * bezierPeakTemp;
float bezierPeakTemp3 = t * t;
bezierPeakXBtm = bezierPeakTemp1 * startXBtm + bezierPeakTemp2 * controlXBtm + bezierPeakTemp3 * endXBtm;
bezierPeakYBtm = bezierPeakTemp1 * startYBtm + bezierPeakTemp2 * controlYBtm + bezierPeakTemp3 * endYBtm;
}
/*
* 根據右邊限制點重新計算貝塞爾曲線頂點坐標
*/
float partOfLongLength = CURVATURE * sizeLong;
if (leftY >= -mValueAdded && leftY <= partOfLongLength - mValueAdded) {
float f = leftY / partOfLongLength;
float t = 0.5F * f;
float bezierPeakTemp = 1 - t;
float bezierPeakTemp1 = bezierPeakTemp * bezierPeakTemp;
float bezierPeakTemp2 = 2 * t * bezierPeakTemp;
float bezierPeakTemp3 = t * t;
bezierPeakXLeft = bezierPeakTemp1 * startXLeft + bezierPeakTemp2 * controlXLeft + bezierPeakTemp3 * endXLeft;
bezierPeakYLeft = bezierPeakTemp1 * startYLeft + bezierPeakTemp2 * controlYLeft + bezierPeakTemp3 * endYLeft;
}
/*
* 替補區域Path
*/
mPathTrap.moveTo(startXBtm, startYBtm);
mPathTrap.lineTo(startXLeft, startYLeft);
mPathTrap.lineTo(bezierPeakXLeft, bezierPeakYLeft);
mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
mPathTrap.close();
/*
* 生成帶曲線的3角形路徑
*/
mPath.moveTo(startXBtm, startYBtm);
mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPath.lineTo(mPointX, mPointY);
mPath.lineTo(endXLeft, endYLeft);
mPath.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);
/*
* 生成底部月半圓的Path
*/
mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPathSemicircleBtm.close();
/*
* 生成右邊月半圓的Path
*/
mPathSemicircleLeft.moveTo(endXLeft, endYLeft);
mPathSemicircleLeft.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);
mPathSemicircleLeft.close();
/*
* 生成包括折疊和下1頁的路徑
*/
//暫時沒用省略掉
/*
* 計算底部和右邊兩月半圓區域
*/
Region regionSemicircleBtm = computeRegion(mPathSemicircleBtm);
Region regionSemicircleLeft = computeRegion(mPathSemicircleLeft);
// 合并兩月半圓區域
mRegionSemicircle.op(regionSemicircleBtm, regionSemicircleLeft, Region.Op.UNION);
}
// 根據Path生成的折疊區域
Region regioFlod = computeRegion(mPath);
// 替補區域
Region regionTrap = computeRegion(mPathTrap);
// 令折疊區域與替補區域相加
regioFlod.op(regionTrap, Region.Op.UNION);
// 從相加后的區域中剔除掉月半圓的區域取得終究折疊區域
regioFlod.op(mRegionSemicircle, Region.Op.DIFFERENCE);
/*
* 根據裁剪區域填充畫布
*/
canvas.clipRegion(regioFlod);
canvas.drawColor(Color.RED);
200行的代碼我們就做了1件事就是正確計算Path,一樣我們還是依照之前的分了兩種情況來計算,第1種情況sizeLong > mViewHeight時,我們先計算替補的這塊區域:

如上代碼46⑷9行
/*
* 替補區域Path
*/
mPathTrap.moveTo(startXBtm, startYBtm);
mPathTrap.lineTo(topX2, 0);
mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
mPathTrap.close();
然后計算底部的月半圓Path:

對應代碼54⑸6行
/*
* 底部月半圓Path
*/
mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPathSemicircleBtm.close();
將當前折疊區域和替補區域相加再減去月半圓Path區域我們就能夠得到正確的折疊區域,對應代碼64行和192⑵01行:
// 計算月半圓區域
mRegionSemicircle = computeRegion(mPathSemicircleBtm);
// ………………中間省略巨量代碼………………
// 根據Path生成的折疊區域
Region regioFlod = computeRegion(mPath);
// 替補區域
Region regionTrap = computeRegion(mPathTrap);
// 令折疊區域與替補區域相加
regioFlod.op(regionTrap, Region.Op.UNION);
// 從相加后的區域中剔除掉月半圓的區域取得終究折疊區域
regioFlod.op(mRegionSemicircle, Region.Op.DIFFERENCE);
該情況下我們的折疊區域是醬紫的:

兩1種情況則略微復雜些,除要計算底部,我們還要計算右邊的月半圓Path區域,代碼165⑴74:
/*
* 生成底部月半圓的Path
*/
mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPathSemicircleBtm.close();
/*
* 生成右邊月半圓的Path
*/
mPathSemicircleLeft.moveTo(endXLeft, endYLeft);
mPathSemicircleLeft.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);
mPathSemicircleLeft.close();
替補區域的計算,147⑴51:
/*
* 替補區域Path
*/
mPathTrap.moveTo(startXBtm, startYBtm);
mPathTrap.lineTo(startXLeft, startYLeft);
mPathTrap.lineTo(bezierPeakXLeft, bezierPeakYLeft);
mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
mPathTrap.close();
區域的轉換,184⑴88:
/*
* 計算底部和右邊兩月半圓區域
*/
Region regionSemicircleBtm = computeRegion(mPathSemicircleBtm);
Region regionSemicircleLeft = computeRegion(mPathSemicircleLeft);
// 合并兩月半圓區域
mRegionSemicircle.op(regionSemicircleBtm, regionSemicircleLeft, Region.Op.UNION);
終究的計算跟上面第1種情況1樣,效果以下:

結合兩種情況,我們可以得到下面的效果:

然后,我們需要計算“下1頁”的區域,一樣,根據上1節我們的講授,我們先獲得折疊區域和下1頁區域之和再減去折疊區域就能夠得到下1頁的區域:
mRegionNext = computeRegion(mPathFoldAndNext);
mRegionNext.op(mRegionFold, Region.Op.DIFFERENCE);
繪制效果以下:

最后,我們結合上兩節,注入數據:
/**
* 繪制位圖數據
*
* @param canvas
* 畫布對象
*/
private void drawBitmaps(Canvas canvas) {
// 繪制位圖前重置isLastPage為false
isLastPage = false;
// 限制pageIndex的值范圍
mPageIndex = mPageIndex < 0 ? 0 : mPageIndex;
mPageIndex = mPageIndex > mBitmaps.size() ? mBitmaps.size() : mPageIndex;
// 計算數據起始位置
int start = mBitmaps.size() - 2 - mPageIndex;
int end = mBitmaps.size() - mPageIndex;
/*
* 如果數據出發點位置小于0則表示當前已到了最后1張圖片
*/
if (start < 0) {
// 此時設置isLastPage為true
isLastPage = true;
// 并顯示提示信息
showToast("This is fucking lastest page");
// 強迫重置起始位置
start = 0;
end = 1;
}
/*
* 計算當前頁的區域
*/
canvas.save();
canvas.clipRegion(mRegionCurrent);
canvas.drawBitmap(mBitmaps.get(end - 1), 0, 0, null);
canvas.restore();
/*
* 計算折疊頁的區域
*/
canvas.save();
canvas.clipRegion(mRegionFold);
canvas.translate(mPointX, mPointY);
/*
* 根據長短邊標識計算折疊區域圖象
*/
if (mRatio == Ratio.SHORT) {
canvas.rotate(90 - mDegrees);
canvas.translate(0, -mViewHeight);
canvas.scale(⑴, 1);
canvas.translate(-mViewWidth, 0);
} else {
canvas.rotate(-(90 - mDegrees));
canvas.translate(-mViewWidth, 0);
canvas.scale(1, ⑴);
canvas.translate(0, -mViewHeight);
}
canvas.drawBitmap(mBitmaps.get(end - 1), 0, 0, null);
canvas.restore();
/*
* 計算下1頁的區域
*/
canvas.save();
canvas.clipRegion(mRegionNext);
canvas.drawBitmap(mBitmaps.get(start), 0, 0, null);
canvas.restore();
}
終究效果以下:

該部份的代碼就不貼出了,大部份跟上1節相同,由于過兩天要去旅游時間略緊這節略講得粗糙,不過也沒甚么太大的改動,如果大家有不懂的地方可以留言或群里@哥,下1節我們將嘗試實現翻頁時圖象扭曲的效果。
源碼地址:傳送門
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈