1. 程式人生 > >Android翻頁效果原理實現之引入折線

Android翻頁效果原理實現之引入折線

               

尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!

炮兵鎮樓

PS:寫得太嗨忘了說明一點,下面文章中提到的“長邊”(也就是程式碼部分中出現的sizeLong)指的是摺疊區域直角三角形中與控制元件右邊相連的邊,而“短邊”(也就是程式碼部分中出現的sizeShort)則指的是摺疊區域直角三角形中與控制元件底邊相連的邊。兩者術語並非指的是較長的邊和較短的邊,這點要注意。其命名來源於My參考圖…………囧……

上一節中我們講了翻頁的原理實現,說白了就是Canvas中clip方法的使用,而現實生活中的翻頁必然不是像我們上節demo那樣左右切換的,我們總是會在看書翻頁的時候掀起紙張的一角拉向書的另一側實現翻頁,翻頁的過程對紙張來說是一個曲度和形狀改變的過程,這一節我們先不講曲度的實現,我們先假設翻頁的過程是一個摺頁的過程,類似下圖:

先以摺頁的方式對翻頁過程進行一個細緻的分析,然後再在下一節將折線變為曲線。摺頁的實現可分為兩種方式,一種是純計算,我們利用已知的條件根據各類公式定理計算出未知的值,第二種呢則是通過圖形的組合巧妙地去獲取圖形的交併集來實現,第二種方式需要很好的空間想象力這裡就先不說了,而第一種純計算的方式呢又可以分為使用高等數學和解三角形兩種方法,前者對於數學不好的童鞋來說不易理解,這裡我們選擇後者使用解三角形來計算,首先我們先來搞個簡單的輔助圖:

圖很簡單,一看就懂,大家可以拿個本子或者書嘗試摺頁,不管你如何折,摺疊區域AOB和下一頁顯示的區域APB必定是完全相等的對吧,那麼我們就可以得到一個驚人的事實:角AOB恆為直角,這時我們來新增一些輔助線便於理解:

我們設摺疊後的三角形AOB的短邊長度為x而長邊長度為y,由圖可以得出以下運算:

我們可以使用相同的方法去解得y的值,這裡我使用的是等面積法,由圖可知梯形MOBP的面積是三角形MOA、AOB、APB面積之和:

這樣我們可以根據任意一點得出兩邊邊長,我們來程式碼中實踐一下看看是不是這樣的呢?為了便於理解,這裡我重新使用了一個新的FoldView:

public class FoldView extends View public FoldView(Context context, AttributeSet attrs) {  super(context, attrs); }}
那麼嘗試根據我們以上分析的原理來繪製這麼一個摺頁的效果,獲取事件點、獲取控制元件寬高就不說了,我們重點來看看onDraw中的計算:
@Override
protected void onDraw(Canvas canvas) // 重繪時重置路徑 mPath.reset(); // 繪製底色 canvas.drawColor(Color.WHITE); /*  * 如果座標點在右下角則不執行繪製  */ if (pointX == 0 && pointY == 0) {  return; } /*  * 額,這個該怎麼註釋好呢……根據圖來  */ float mK = mViewWidth - pointX; float mL = mViewHeight - pointY; // 需要重複使用的引數存值避免重複計算 float temp = (float) (Math.pow(mL, 2) + Math.pow(mK, 2)); /*  * 計算短邊長邊長度  */ float sizeShort = temp / (2F * mK); float sizeLong = temp / (2F * mL); /*  * 生成路徑  */ mPath.moveTo(pointX, pointY); mPath.lineTo(mViewWidth, mViewHeight - sizeLong); mPath.lineTo(mViewWidth - sizeShort, mViewHeight); mPath.close(); // 繪製路徑 canvas.drawPath(mPath, mPaint);}
每次繪製的時候我們需要重置Path不然上一次的Path就會跟這一次疊加在一起,效果如下:

效果是大致出來了,但是我們發現有一處不對的地方,當我們非常靠左或非常靠下地摺疊時:

如果再往下折

如果再往左折

此時我們的Path就會消失掉,其實這跟我們我們現實中的摺頁是一樣的,摺頁的過程是有限制的,如下圖:

右下角點P因為受裝訂線的制約,其半徑最大隻能為紙張的寬度,如果我們始終以該寬度為半徑摺頁,那麼點P的軌跡就可以形成曲線Q,圖中半透明紅色區域為一個半圓形,也就是說,我們的點P只能在該範圍內才應當有效對吧,那麼該如何做限制呢?很簡單,我們只需在計算長短邊長之前判斷觸控點是否在該區域即可:

/** * 計算短邊的有效區域 */private void computeShortSizeRegion() // 短邊圓形路徑物件 Path pathShortSize = new Path(); // 用來裝載Path邊界值的RectF物件 RectF rectShortSize = new RectF(); // 新增圓形到Path pathShortSize.addCircle(0, mViewHeight, mViewWidth, Path.Direction.CCW); // 計算邊界 pathShortSize.computeBounds(rectShortSize, true); // 將Path轉化為Region mRegionShortSize.setPath(pathShortSize, new Region((int) rectShortSize.left, (int) rectShortSize.top, (int) rectShortSize.right, (int) rectShortSize.bottom));}
同樣計算有效區域這個過程是在onSizeChanged中進行,我們說過儘量不要在一些重複呼叫的方法內執行沒必要的計算,在onDraw裡我們只需在繪製錢判斷下當前觸控點是否在該區域內,如果不在,那麼我們通過座標x軸重新計算座標y軸:
/* * 判斷觸控點是否在短邊的有效區域內 */if (!mRegionShortSize.contains((int) mPointX, (int) mPointY)) { // 如果不在則通過x座標強行重算y座標 mPointY = (float) (Math.sqrt((Math.pow(mViewWidth, 2) - Math.pow(mPointX, 2))) - mViewHeight); // 精度附加值避免精度損失 mPointY = Math.abs(mPointY) + mValueAdded;}
那麼如何來計算y座標呢?很簡單,我們只需根據圓的方程求解即可,因為P的軌跡是個圓~在得到新的y座標mPointY後我們還應該為其加上一點點的精度值mValueAdded來挽回因浮點計算而損失的精度。而對於過分往下折出現的問題我們使用限制下折最大值的方法來避免:
/* * 緩衝區域判斷 */float area = mViewHeight - mBuffArea;if (mPointY >= area) { mPointY = area;}
在控制元件下方接近底部的地方我們設定一個緩衝區域,觸控點永遠不能到達該區域,因為沒有必要也沒有意義,再往下就要劃出控制元件了,別浪費多餘的計算,執行效果大致如下:

大致的效果出來了,我們還需要做一些補充工作,當觸控點在右下角某個區域時如果我們擡起手指,那麼就讓“紙張”自動滑下去,同理當觸控點在左邊某個區域時我們讓“紙張”自動翻過去,這裡我們約定這兩個區域分別是控制元件右下角寬高四分之一的區域和控制元件左側八分之一的區域(當然你可以約定你自己的控制元件行為,這裡我就哪簡單往哪走了~):

那麼在上一節中我們也有類似的效果,這裡我們依葫蘆畫瓢,當手指擡起時判斷當前事件點是否位於右下角自滑區域內,如果在那麼以當前事件點為座標點A右下角為座標點B根據兩點式我們可以獲得一條直線方程:

此後根據不斷自加遞增的x座標不斷計算對應的y座標直至點滑至右下角為止,既然涉及到事件,So我們在onTouchEvent處理:

case MotionEvent.ACTION_UP:// 手指擡起時候 /*  * 獲取當前事件點  */ float x = event.getX(); float y = event.getY(); /*  * 如果當前事件點位於右下自滑區域  */ if (x > mAutoAreaRight && y > mAutoAreaButtom) {  // 獲取當前點為直線方程座標之一  float startX = x, startY = y;  /*   * 當x座標小於控制元件寬度時   */  while (x < mViewWidth) {   // 不斷讓x自加   x++;   // 重置當前點的值   mPointX = x;   mPointY = startY + ((x - startX) * (mViewHeight - startY)) / (mViewWidth - startX);   // 重繪檢視   invalidate();  } } break;
OK,我們來看看效果:

大家看到當手指彈起時如果觸控點在右下角的自滑區域內的話就會自動“滑動”到右下角去,可是大家細心的話會發現效果好像不太對啊!怎麼一下子就到右下角了?說好的“滑動”呢?好像毫無滑動效果啊!!!!為什麼會這樣?其實如果你細心就會發現上一節我們在講圖片左右兩側自滑的時候也是一樣的效果!根本就沒有什麼滑動!為什麼?難道在我們的while迴圈中沒有執行invalidate嗎?大家可以嘗試在View中重寫invalidate()方法Log一些資訊看看invalidate()是否沒有沒執行。這裡鑑於篇幅我就直接簡單地說一下了,具體的我們會在《自定義控制元件其實很簡單》系列文章講到View繪製流程的時候詳細闡述。這裡我先可以告訴大家的是invalidate()方法即便你呼叫了也不會馬上執行,invalidate()的作用更準確地說是將我們的View標記為無效,當View被標記為無效後Android就會嘗試去呼叫onDraw()對其重繪,如果大家曾翻閱過API 文件就會看到在invalidate()方法中Google給出了這麼一句話:

我們知道UI的重新整理需要在UI Thread也就是主執行緒中進行,這裡會涉及到一個叫做message和message queue的東西,message你可以見文知意地稱其為訊息而message queue則為訊息佇列,我們將一個message壓入message queue後UI Thread會處理它,而我們重新整理UI也需要有message作為載體去告訴UI Thread誒需要更新UI了哦,而當我們在UI Thread中去做一個loop不斷地往message queue中壓入訊息時,我們的UI Thread是不會去處理這些message的,直到loop結束為止,這就是為什麼我們在while中不斷呼叫invalidate()的時候你只會看到最後的結果而不會得到中間過程的變化。這裡我只闡述了一個很淺顯能懂的原因,更深入的原因涉及到View中各種標識位的運算如上所說篇幅過長就不多說了。那麼知道了原因該如何去處理呢?message和message queue如果大家對Handler有一定的瞭解一定不陌生,沒錯,這裡我們也將使用Handler來實現我們的滑動,首先,在我們的View中建立一個內部類,該內部類是Handler的一個子類,我們將使用它來更新View實現滑動效果:

/** * 處理滑動的Handler */@SuppressLint("HandlerLeak")private class SlideHandler extends Handler @Override public void handleMessage(Message msg) {  // 迴圈呼叫滑動計算  FoldView.this.slide();  // 重繪檢視  FoldView.this.invalidate(); } /**  * 延遲向Handler傳送訊息實現時間間隔  *   * @param delayMillis  *            間隔時間  */ public void sleep(long delayMillis) {  this.removeMessages(0);  sendMessageDelayed(obtainMessage(0), delayMillis); }}
我們額外提供一個slide()方法來對引數值進行更新:
/** * 計算滑動引數變化 */private void slide() /*  * 如果x座標恆小於控制元件寬度  */ if (isSlide && mPointX < mViewWidth) {  // 則讓x座標自加  mPointX++;  // 並根據x座標的值重新計算y座標的值  mPointY = mStart_BR_Y + ((mPointX - mStart_BR_X) * (mViewHeight - mStart_BR_Y)) / (mViewWidth - mStart_BR_X);  // 讓SlideHandler處理重繪  mSlideHandler.sleep(1); }}
而在onTouchEvent中我們則不在處理引數的計算和重繪,僅需簡單呼叫slide()方法即可:
case MotionEvent.ACTION_UP:// 手指擡起時候 /*  * 獲取當前事件點  */ float x = event.getX(); float y = event.getY(); /*  * 如果當前事件點位於右下自滑區域  */ if (x > mAutoAreaRight && y > mAutoAreaButtom) {  // 獲取並設定直線方程的起點  mStart_BR_X = x;  mStart_BR_Y = y;  // OK要開始滑動了哦~  isSlide = true;  // 滑動  slide(); } break;
注:mStart_BR_X和mStart_BR_Y為直線方程的一點,這裡我單獨使用兩個引用存值便於大家理解,如果各位數學基礎好完全可以將其併入到slide()方法中一併計算並省去這兩個引用的宣告。

這裡我們定義了一個boolean型別的isSlide標識值,目的是方便控制動畫,我們對外提供一個slideStop方法便於其他元件對動畫的控制(當然你可以提供更多方法來控制slide,這裡就不多說了),例如當Activity的onDestroy被呼叫時讓動畫停止:

/** * 為isSlide提供對外的停止方法便於必要時釋放滑動動畫 */public void slideStop() { isSlide = false;}
在這一過程中,我們在事件手指擡起時判斷點的所在,如果在滑動區域我們則觸發slide()方法的執行,在slide()方法中我們重新計算座標值並呼叫SlideHandler的sleep(long delayMillis)方法,sleep(long delayMillis)的處理邏輯也很簡單,根據delayMillis延時向SlideHandler傳送obtainMessage,在SlideHandler的handleMessage方法中再次呼叫slide()方法重新計算引數值並重新整理介面。看到這裡你可能會問為什麼slide()和sleep()沒有死迴圈而?Don't worry!我們來細緻分析一下我們到底幹了什麼,首先我們建立了一個Handler的子類SlideHandler與當前Thread繫結在一起由此我們才可以直接給Thread傳送並處理message。因為Handler對message的處理都是非同步的,所以在我們自定的SlideHandler中sleep()方法也是個非同步方法,所以slide()和sleep()之間的相互呼叫才沒有構成死迴圈。

好了,分析歸分析,我們還是要看實際效果的對吧,執行一下看看:

是不是有“滑動”的效果了?之前我們在處理MOVE的時候在onDraw中定義了一個底部的緩衝區:

float area = mViewHeight - mBuffArea;if (mPointY >= area) { mPointY = area;}
而在自滑的時候我們是不需要去判斷它的,So~我們改改:
/* * 緩衝區域判斷 */float area = mViewHeight - mBuffArea;if (!isSlide && mPointY >= area) { mPointY = area;}
只有當沒有產生滑動動畫時才去判斷緩衝區~

至此,從《自定義控制元件其實很簡單》系列開始我們已經學會三種對View進行重新整理的方式:第一種是在剛開始講《自定義控制元件其實很簡單1/12》讓View作為Runnable的實現類,在run方法中更新,另一種是我們後來用的比較多的直接在onDraw方法中invalidate(),最後一種呢則是上面我們講的Handler來處理繪製邏輯,這三種方法雖說本質一樣但是實現方式各不相同且應用場景也不盡相同~第一種更傾向於多種狀態進行同時重繪,第二種侷限性很大雖說常見但能實現的功能很弱,第三種可以應用到絕大多數的重繪情景且不受不同狀態的影響自由度更大。

好了,我們繼續修改下程式碼讓左側也實現自滑的功能:

如圖所示,當我們的觸控點x座標落於控制元件左側1/8處彈起手指時,我們讓該點與“上一頁”的左下角相連構成一條直線,讓點沿著該直線不斷下滑直至“上一頁”的左下角,鑑於我們要分開對左下和右下滑動進行處理,這裡我定義一個列舉內部類:

/** * 列舉類定義滑動方向 */private enum Slide { LEFT_BOTTOM, RIGHT_BOTTOM}
對應地我們就需要一個成員變數來存值咯:
private Slide mSlide;// 定義當前滑動是往左下滑還是右下滑
重新整理onTouchEvent處理邏輯:
case MotionEvent.ACTION_UP:// 手指擡起時候 /*  * 獲取當前事件點  */ float x = event.getX(); float y = event.getY(); /*  * 如果當前事件點位於右下自滑區域  */ if (x > mAutoAreaRight && y > mAutoAreaButtom) {  // 當前為往右下滑  mSlide = Slide.RIGHT_BOTTOM;  // 摩擦吧騷年!  justSlide(x, y); } /*  * 如果當前事件點位於左側自滑區域  */ if (x < mAutoAreaLeft) {  // 當前為往左下滑  mSlide = Slide.LEFT_BOTTOM;  // 摩擦吧騷年!  justSlide(x, y); } break;
我們將一些相同的方法封裝在justSlide中:
/** * 在這光滑的地板上~ *  * @param x *            當前觸控點x * @param y *            當前觸控點y */private void justSlide(float x, float y) // 獲取並設定直線方程的起點 mStart_X = x; mStart_Y = y; // OK要開始滑動了哦~ isSlide = true// 滑動 slide();}
slide()中的處理則會根據滑動方向來計算引數值:
/** * 計算滑動引數變化 */private void slide() /*  * 如果滑動標識值為false則返回  */ if (!isSlide) {  return; } /*  * 如果當前滑動標識為向右下滑動x座標恆小於控制元件寬度  */ if (mSlide == Slide.RIGHT_BOTTOM && mPointX < mViewWidth) {  // 則讓x座標自加  mPointX += 10;  // 並根據x座標的值重新計算y座標的值  mPointY = mStart_Y + ((mPointX - mStart_X) * (mViewHeight - mStart_Y)) / (mViewWidth - mStart_X);  // 讓SlideHandler處理重繪  mSlideHandler.sleep(25); } /*  * 如果當前滑動標識為向左下滑動x座標恆大於控制元件寬度的負值  */ if (mSlide == Slide.LEFT_BOTTOM && mPointX > -mViewWidth) {  // 則讓x座標自減  mPointX -= 20;  // 並根據x座標的值重新計算y座標的值  mPointY = mStart_Y + ((mPointX - mStart_X) * (mViewHeight - mStart_Y)) / (-mViewWidth - mStart_X);  // 讓SlideHandler處理重繪  mSlideHandler.sleep(25); }}
看看效果:

好像一切都很完美,但是我們發現當Path繪製到快結束時卡了兩下,同時我們的LogCat也出現瞭如下警告:

說是我們的Path太大了已經超出了texture的渲染範圍,什麼是texture這要涉及到GL等Android底層對圖形繪製的過程,我們不需要理解,但是要知道的是,從API 11開始Android開始支援HW硬體加速繪製檢視,硬體加速對texture是有限制的,這個限制值因機而異,如上面我們在警告資訊的最後看到的max = 16384 x 16384,如何解決呢?最簡單的方法當然是直接關閉硬體加速咯,關於關閉硬體加速的兩種方法在《自定義控制元件其實很簡單》中我們已經說過:

setLayerType(LAYER_TYPE_SOFTWARE, null);
這樣我們再次執行:

誒~Good,很順暢也沒出現警告了對吧,但是Path的大小依然是沒有改變的,依然是灰常灰常大,超出控制元件上方的部分依舊被繪製而且很大很大,其實這部分繪製是完全沒必要的,而且為此我們還關閉了HW更可怕的是還將APP的最低支援版本升到了11……我們可不可以通過其他方式來避免呢?答案是肯定的,方法很多,但是最簡明扼要的還是Calculation~~~~~我們嘗試去判斷摺疊後長邊的的長度,如果長邊的長度大於控制元件的高度則我們摺疊的分部就不是一個三角形而是一個四邊形了:

如上圖中的四邊形OBQM,那麼如何來生成這個四邊形的區域呢?我們曾約定在手指MOVE的過程中控制元件下方有一個“緩衝區域”,也就是說我們的觸控點Y座標永遠不可能在MOVE的過程中與控制元件底部重合,這個約定給我們在計算四邊形區域的時候帶來一個好處:如上圖所示OA邊總會與PN的延長線有交點,那樣計算就非常簡單了,我們過點O作一條垂直於PN邊的垂線與PN邊相交於點D:

那麼我麼就會有:

由此很容易得出

MN = largerTrianShortSize = an / (sizeLong - (mViewHeight - mPointY)) * (mViewWidth - mPointX);QN = smallTrianShortSize = an / sizeLong * sizeShort;
在onDraw中我們在計算出sizeLong和sizeShort後加一步判斷:
/* * 計算短邊長邊長度 */float sizeShort = temp / (2F * mK);float sizeLong = temp / (2F * mL);// 移動路徑起點至觸控點mPath.moveTo(mPointX, mPointY);if (sizeLong > mViewHeight) { // 計算……額……按圖來AN邊~ float an = sizeLong - mViewHeight; // 三角形AMN的MN邊 float largerTrianShortSize = an / (sizeLong - (mViewHeight - mPointY)) * (mViewWidth - mPointX); // 三角形AQN的QN邊 float smallTrianShortSize = an / sizeLong * sizeShort; /*  * 生成四邊形路徑  */ mPath.lineTo(mViewWidth - largerTrianShortSize, 0); mPath.lineTo(mViewWidth - smallTrianShortSize, 0); mPath.lineTo(mViewWidth - sizeShort, mViewHeight); mPath.close();} else/*  * 生成三角形路徑  */ mPath.lineTo(mViewWidth, mViewHeight - sizeLong); mPath.lineTo(mViewWidth - sizeShort, mViewHeight); mPath.close();}// 繪製路徑canvas.drawPath(mPath, mPaint);
來看看具體效果:

挺不錯的感覺,好,繼續!

折線是OK了,剩下的問題是如何將我們的圖片顯示,也就是上一節的內容融合進來呢?在此之前,我們要知道的是摺疊區域、當前頁和下一頁這三部分顯示的是不同的內容,如文章開頭的示圖:

那麼如何顯示不同的圖片就必須要先將這三部分表達出來對吧,我們之前曾學過Region區域物件,在這裡就可以派上用場。首先,我們可以考慮將整個控制元件分為三個區域:顯示當前頁的區域、顯示摺疊的區域(也就是當前頁的背面)、顯示下一頁的區域。與上圖類似,我們可以利用圖層的功能使用三個圖層來模擬。第一步我們宣告一個Region型別的引用來定義當前頁的區域:

private Region mRegionCurrent;// 當前頁區域,其實就是控制元件的大小
因為當前頁的區域其實就是控制元件大小(置於最底層無所謂了~),我們直接就可以在onSizeChanged中完成物件的生成:
// 計算當前頁區域mRegionCurrent.set(0, 0, mViewWidth, mViewHeight);
爾後我們需要計算摺疊區域和下一頁的路徑:

如圖紅色線條所示路徑,我們需要這部分割槽域來計算下一頁的區域,即:下一頁區域=線條部分割槽域-摺疊區域對吧,同樣我們宣告一個Path型別的引用來定義該部分Path:

private Path mPathFoldAndNext;// 一個包含摺疊和下一頁區域的Path
在onDraw中在計算摺疊區域的同時計算該部分割槽域:
/* * 計算短邊長邊長度 */float sizeShort = temp / (2F * mK);float sizeLong = temp / (2F * mL);// 移動路徑起點至觸控點mPath.moveTo(mPointX, mPointY);mPathFoldAndNext.moveTo(mPointX, mPointY);if (sizeLong > mViewHeight) { // 計算……額……按圖來AN邊~ float an = sizeLong - mViewHeight; // 三角形AMN的MN邊 float largerTrianShortSize = an / (sizeLong - (mViewHeight - mPointY)) * (mViewWidth - mPointX); // 三角形AQN的QN邊 float smallTrianShortSize = an / sizeLong * sizeShort; /*  * 計算引數  */ float topX1 = mViewWidth - largerTrianShortSize; float topX2 = mViewWidth - smallTrianShortSize; float btmX2 = mViewWidth - sizeShort; /*  * 生成四邊形路徑  */ mPath.lineTo(topX1, 0); mPath.lineTo(topX2, 0); mPath.lineTo(btmX2, mViewHeight); mPath.close(); /*  * 生成包含摺疊和下一頁的路徑  */ mPathFoldAndNext.lineTo(topX1, 0); mPathFoldAndNext.lineTo(mViewWidth, 0); mPathFoldAndNext.lineTo(mViewWidth, mViewHeight); mPathFoldAndNext.lineTo(btmX2, mViewHeight); mPathFoldAndNext.close();} else/*  * 計算引數  */ float leftY = mViewHeight - sizeLong; float btmX = mViewWidth - sizeShort; /*  * 生成三角形路徑  */ mPath.lineTo(mViewWidth, leftY); mPath.lineTo(btmX, mViewHeight); mPath.close(); /*  * 生成包含摺疊和下一頁的路徑  */ mPathFoldAndNext.lineTo(mViewWidth, leftY); mPathFoldAndNext.lineTo(mViewWidth, mViewHeight); mPathFoldAndNext.lineTo(btmX, mViewHeight); mPathFoldAndNext.close();}
將路徑轉換為Region我們獨立一個方法來呼叫:
/** * 通過路徑計算區域 *  * @param path *            路徑物件 * @return 路徑的Region */private Region computeRegion(Path path) { Region region = new Region(); RectF f = new RectF(); path.computeBounds(f, true); region.setPath(path, new Region((int) f.left, (int) f.top, (int) f.right, (int) f.bottom)); return region;}
之後我們就可以計算並繪製這三部分割槽域:
/* * 定義區域 */Region regionFold = null;Region regionNext = null;/* * 通過路徑成成區域 */regionFold = computeRegion(mPath);regionNext = computeRegion(mPathFoldAndNext);/* * 計算當前頁的區域 */canvas.save();canvas.clipRegion(mRegionCurrent);canvas.clipRegion(regionNext, Region.Op.DIFFERENCE);canvas.drawColor(0xFFF4D8B7);canvas.restore();/* * 計算摺疊頁的區域 */canvas.save();canvas.clipRegion(regionFold);canvas.drawColor(0xFF663C21);canvas.restore();/* * 計算下一頁的區域 */canvas.save();canvas.clipRegion(regionNext);canvas.clipRegion(regionFold, Region.Op.DIFFERENCE);canvas.drawColor(0xFF9596C4);canvas.restore();
我們看看效果是否與我們期待的一致:

差不多對吧,只是有點小問題,我們在沒觸控之前顯示的是空白這很好解決,下面我們結合上一節的內容把圖片的效果也整合進來,該部分全部的程式碼如下,我就直接貼出來了:

package com.aigestudio.pagecurl.views;import java.util.ArrayList;import java.util.List;import android.annotation.SuppressLint;import android.content.Context;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.graphics.RectF;import android.graphics.Region;import android.os.Handler;import android.os.Message;import android.text.TextPaint;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.Toast;/** * 摺疊View *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @version 1.0.0 * @since 2014/12/27 */public class FoldView extends View private static final float VALUE_ADDED = 1 / 500F;// 精度附加值佔比 private static final float BUFF_AREA = 1 / 50F;// 底部緩衝區域佔比 private static final float AUTO_AREA_BUTTOM_RIGHT = 3 / 4F, AUTO_AREA_BUTTOM_LEFT = 1 / 8F;// 右下角和左側自滑區域佔比 private static final float AUTO_SLIDE_BL_V = 1 / 25F, AUTO_SLIDE_BR_V = 1 / 100F;// 滑動速度佔比 private static final float TEXT_SIZE_NORMAL = 1 / 40F, TEXT_SIZE_LARGER = 1 / 20F;// 標準文字尺寸和大號文字尺寸的佔比 private List<Bitmap> mBitmaps;// 點陣圖資料列表 private SlideHandler mSlideHandler;// 滑動處理Handler private Paint mPaint;// 畫筆 private TextPaint mTextPaint;// 文字畫筆 private Context mContext;// 上下文環境引用 private Path mPath;// 摺疊路徑 private Path mPathFoldAndNext;// 一個包含摺疊和下一頁區域的Path private Region mRegionShortSize;// 短邊的有效區域 private Region mRegionCurrent;// 當前頁區域,其實就是控制元件的大小 private int mViewWidth, mViewHeight;// 控制元件寬高 private int mPageIndex;// 當前顯示mBitmaps資料的下標 private float mPointX, mPointY;// 手指觸控點的座標 private float mValueAdded;// 精度附減值 private float mBuffArea;// 底部緩衝區域 private float mAutoAreaButtom, mAutoAreaRight, mAutoAreaLeft;// 右下角和左側自滑區域 private float mStart_X, mStart_Y;// 直線起點座標 private float mAutoSlideV_BL, mAutoSlideV_BR;// 滑動速度 private float mTextSizeNormal, mTextSizeLarger;// 標準文字尺寸和大號文字尺寸 private float mDegrees;// 當前Y邊長與Y軸的夾角 private boolean isSlide, isLastPage, isNextPage;// 是否執行滑動、是否已到最後一頁、是否可顯示下一頁的標識值 private Slide mSlide;// 定義當前滑動是往左下滑還是右下滑 /**  * 列舉類定義滑動方向  */ private enum Slide {  LEFT_BOTTOM, RIGHT_BOTTOM } private Ratio mRatio;// 定義當前摺疊邊長 /**  * 列舉類定義長邊短邊  */ private enum Ratio {  LONG, SHORT } public FoldView(Context context, AttributeSet attrs) {  super(context, attrs);  mContext = context;  /*   * 例項化文字畫筆並設定引數   */  mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);  mTextPaint.setTextAlign(Paint.Align.CENTER);  /*   * 例項化畫筆物件並設定引數   */  mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  mPaint.setStyle(Paint.Style.STROKE);  mPaint.setStrokeWidth(2);  /*   * 例項化路徑物件   */  mPath = new Path();  mPathFoldAndNext = new Path();  /*   * 例項化區域物件   */  mRegionShortSize = new Region();  mRegionCurrent = new Region();  // 例項化滑動Handler處理器  mSlideHandler = new SlideHandler(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {  /*   * 獲取控制元件寬高   */  mViewWidth = w;  mViewHeight = h;  // 初始化點陣圖資料  if (null != mBitmaps) {   initBitmaps();  }  // 計算文字尺寸  mTextSizeNormal = TEXT_SIZE_NORMAL * mViewHeight;  mTextSizeLarger = TEXT_SIZE_LARGER * mViewHeight;  // 計算精度附加值  mValueAdded = mViewHeight * VALUE_ADDED;  // 計算底部緩衝區域  mBuffArea = mViewHeight * BUFF_AREA;  /*   * 計算自滑位置   */  mAutoAreaButtom = mViewHeight * AUTO_AREA_BUTTOM_RIGHT;  mAutoAreaRight = mViewWidth * AUTO_AREA_BUTTOM_RIGHT;  mAutoAreaLeft = mViewWidth * AUTO_AREA_BUTTOM_LEFT;  // 計算短邊的有效區域  computeShortSizeRegion();  /*   * 計算滑動速度   */  mAutoSlideV_BL = mViewWidth * AUTO_SLIDE_BL_V;  mAutoSlideV_BR = mViewWidth * AUTO_SLIDE_BR_V;  // 計算當前頁區域  mRegionCurrent.set(0, 0, mViewWidth, mViewHeight); } /**  * 初始化點陣圖資料  * 縮放點陣圖尺寸與螢幕匹配  */ private void initBitmaps() {  List<Bitmap> temp = new ArrayList<Bitmap>();  for (int i = mBitmaps.size() - 1; i >= 0; i--) {   Bitmap bitmap = Bitmap.createScaledBitmap(mBitmaps.get(i), mViewWidth, mViewHeight, true);   temp.add(bitmap);  }  mBitmaps = temp; } /**  * 計算短邊的有效區域  */ private void computeShortSizeRegion() {  // 短邊圓形路徑物件  Path pathShortSize = new Path();  // 用來裝載Path邊界值的RectF物件  RectF rectShortSize = new RectF();  // 新增圓形到Path  pathShortSize.addCircle(0, mViewHeight, mViewWidth, Path.Direction.CCW);  // 計算邊界  pathShortSize.computeBounds(rectShortSize, true);  // 將Path轉化為Region  mRegionShortSize.setPath(pathShortSize, new Region((int) rectShortSize.left, (int) rectShortSize.top, (int) rectShortSize.right, (int) rectShortSize.bottom)); } @Override protected void onDraw(Canvas canvas) {  /*   * 如果資料為空則顯示預設提示文字   */  if (null == mBitmaps || mBitmaps.size() == 0) {   defaultDisplay(canvas);   return;  }  // 重繪時重置路徑  mPath.reset();  mPathFoldAndNext.reset();  // 繪製底色  canvas.drawColor(Color.WHITE);  /*   * 如果座標點在原點(即還沒發生觸碰時)則繪製第一頁   */  if (mPointX == 0 && mPointY == 0) {   canvas.drawBitmap(mBitmaps.get(mBitmaps.size() - 1), 0, 0, null);   return;  }  /*   * 判斷觸控點是否在短邊的有效區域內   */  if (!mRegionShortSize.contains((int) mPointX, (int) mPointY)) {   // 如果不在則通過x座標強行重算y座標   mPointY = (float) (Math.sqrt((Math.pow(mViewWidth, 2) - Math.pow(mPointX, 2))) - mViewHeight);   // 精度附加值避免精度損失   mPointY = Math.abs(mPointY) + mValueAdded;  }  /*   * 緩衝區域判斷   */  float area = mViewHeight - mBuffArea;  if (!isSlide && mPointY >= area) {   mPointY = area;  }  /*   * 額,這個該怎麼註釋好呢……根據圖來   */  float mK = mViewWidth - mPointX;  float mL = mViewHeight - mPointY;  // 需要重複使用的引數存值避免重複計算  float temp = (float) (Math.pow(mL, 2) + Math.pow(mK, 2));  /*   * 計算短邊長邊長度   */  float sizeShort = temp / (2F * mK);  float sizeLong = temp / (2F * mL);  /*   * 根據長短邊邊長計算旋轉角度並確定mRatio的值   */  if (sizeShort < sizeLong) {   mRatio = Ratio.SHORT;   float sin = (mK - sizeShort) / sizeShort;   mDegrees = (float) (Math.asin(sin) / Math.PI * 180);  } else {   mRatio = Ratio.LONG;   float cos = mK / sizeLong;   mDegrees = (float) (Math.acos(cos) / Math.PI * 180);  }  // 移動路徑起點至觸控點  mPath.moveTo(mPointX, mPointY);  mPathFoldAndNext.moveTo(mPointX, mPointY);  if (sizeLong > mViewHeight) {   // 計算……額……按圖來AN邊~   float an = sizeLong - mViewHeight;   // 三角形AMN的MN邊   float largerTrianShortSize = an / (sizeLong - (mViewHeight - mPointY)) * (mViewWidth - mPointX);   // 三角形AQN的QN邊   float smallTrianShortSize = an / sizeLong * sizeShort;   /*    * 計算引數    */   float topX1 = mViewWidth - largerTrianShortSize;   float topX2 = mViewWidth - smallTrianShortSize;   float btmX2 = mViewWidth - sizeShort;   /*    * 生成四邊形路徑    */   mPath.lineTo(topX1, 0);   mPath.lineTo(topX2, 0);   mPath.lineTo(btmX2, mViewHeight);   mPath.close();   /*    * 生成包含摺疊和下一頁的路徑    */   mPathFoldAndNext.lineTo(topX1, 0);   mPathFoldAndNext.lineTo(mViewWidth, 0);   mPathFoldAndNext.lineTo(mViewWidth, mViewHeight);   mPathFoldAndNext.lineTo(btmX2, mViewHeight);   mPathFoldAndNext.close();  } else {   /*    * 計算引數    */   float leftY = mViewHeight - sizeLong;   float btmX = mViewWidth - sizeShort;   /*    * 生成三角形路徑    */   mPath.lineTo(mViewWidth, leftY);   mPath.lineTo(btmX, mViewHeight);   mPath.close();   /*    * 生成包含摺疊和下一頁的路徑    */   mPathFoldAndNext.lineTo(mViewWidth, leftY);   mPathFoldAndNext.lineTo(mViewWidth, mViewHeight);   mPathFoldAndNext.lineTo(btmX, mViewHeight);   mPathFoldAndNext.close();  }  drawBitmaps(canvas); } /**  * 繪製點陣圖資料  *   * @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則表示當前已經到了最後一張圖片   */  if (start < 0) {   // 此時設定isLastPage為true   isLastPage = true;   // 並顯示提示資訊   showToast("This is fucking lastest page");   // 強制重置起始位置   start = 0;   end = 1;  }  /*   * 定義區域   */  Region regionFold = null;  Region regionNext = null;  /*   * 通過路徑成成區域   */  regionFold = computeRegion(mPath);  regionNext = computeRegion(mPathFoldAndNext);  /*   * 計算當前頁的區域   */  canvas.save();  canvas.clipRegion(mRegionCurrent);  canvas.clipRegion(regionNext, Region.Op.DIFFERENCE);  canvas.drawBitmap(mBitmaps.get(end - 1), 0, 0, null);  canvas.restore();  /*   * 計算摺疊頁的區域   */  canvas.save();  canvas.clipRegion(regionFold);  canvas.translate(mPointX, mPointY);  /*   * 根據長短邊標識計算摺疊區域影象   */  if (mRatio == Ratio.SHORT) {   canvas.rotate(90 - mDegrees);   canvas.translate(0, -mViewHeight);   canvas.scale(-1, 1);   canvas.translate(-mViewWidth, 0);  } else {   canvas.rotate(-(90 - mDegrees));   canvas.translate(-mViewWidth, 0);   canvas.scale(1, -1);   canvas.translate(0, -mViewHeight);  }  canvas.drawBitmap(mBitmaps.get(end - 1), 0, 0, null);  canvas.restore();  /*   * 計算下一頁的區域   */  canvas.save();  canvas.clipRegion(regionNext);  canvas.clipRegion(regionFold, Region.Op.DIFFERENCE);  canvas.drawBitmap(mBitmaps.get(start), 0