1. 程式人生 > >Android翻頁效果原理實現之翻頁的嘗試

Android翻頁效果原理實現之翻頁的嘗試

               

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

炮兵鎮樓

在《自定義控制元件其實很簡單》系列的前半部分中我們用了整整六節近兩萬字兩百多張配圖講了Android圖形的繪製,雖然篇幅很巨大但仍然只是圖形繪製的冰山一角,旨在領大家入門,至於修行成果就看各位的了……那麼這個些列主要是通過前面學習到的一些方法來嘗試完成一個翻頁的效果。

對於我個人來說,我是不太建議大家在沒自己去嘗試前看本文的,因為你看了別人的思路就會有個慣性思維朝著別人的思路去靠,實際上如果你自己嘗試去分析去做的話不見得做不出來,甚至可能方法更簡捷效率更高。分析這個效果的時候我還找妹子要了個新的筆記本翻了兩三個小時,後來又在PS裡模擬了一下然後通過繪圖計算最終才有了一個簡短的思路。

翻頁雖然幾乎每個人都試過,除非你沒看過書……沒碰過本子……甚至沒碰過紙……一個看似簡單的動作其實隱藏了巨量的資訊,如果我們要把翻頁的過程模擬得真實,涉及面會相當廣,這裡我假定我們所做的翻頁效果類似於課本翻頁,從右下角掀開紙張翻至左邊,而我們的控制元件就假定為課本的右邊部分而左邊部分呢在我們控制元件左側外看不到,那麼控制元件的左端即可看成我們課本的裝訂線,這樣的假定我們可以簡化問題,如之前我所說,控制元件必定都是不完美的,如果存在完美的控制元件就不需要我們Custom了~那麼這個控制元件實現的是一個怎麼樣的效果呢?效果很簡單,往控制元件中傳入多張圖片,我們以翻頁的形式依次展示這些圖片。整個翻頁的原理都是想通的,雖然這個效果我模擬得很簡單,但是你完全可以照我的思路定義ViewGroup或者ValueAnimation等等……

為了進一步簡化問題,我們將整個翻頁效果的實現分為四部分,第一部分為翻頁的嘗試實現,第二部分呢則是折線翻頁的實現,第三部分我們嘗試引入曲線翻頁,第四部分則為一些後續效果的處理以及效率的優化,如果有必要還會增加一些章節繼續完善效果。這樣我們的流程就很清晰了,這一節我們首先來嘗試實現翻頁,如果大家能哪個本子或者書來跟著我的思路走就更好了,本來是打算拍些Photo作為展示的,但是我發現現實的翻頁不好控制,算了,一些理論上的東西只好靠各位自行動手+腦補了。

首先,我們要進行一些約定,上面說到我們模擬的翻頁效果是以控制元件左側為紙張裝訂線使其能夠實現從右下角翻開的效果,這是約定之一,其次,我們規定忽略掀起部分相對於本頁的弧線使之始終與本頁平行,再次規定視點與光源方向均位於紙張正上方並且光源為單光源聚光燈光錐剛好罩住紙張,這些約定不理解不要緊,在涉及到的時候我會具體說明。

我們知道View並非像ViewGroup那樣是個容器可以容納其他的控制元件,那麼要將一些圖片依次“放入”View中並依次呈現該如何辦呢?通過前面對Canvas的學習,我們可以嘗試使用Canvas的“圖層”功能來實現這一效果,將Bitmap依次至於不同的“圖層”再通過Canvas的clipXXX方法裁剪呈現不同Bitmap,理論上是可行的,那實際如何呢?我們來試試,新建一個View的子類PageCurlView:

public class PageTurnView extends View private List<Bitmap> mBitmaps;// 點陣圖資料列表 public PageTurnView(Context context, AttributeSet attrs) {  super(context, attrs); }}
同樣,PageCurlView是與資料有關的,我們對外提供一個方法來為PageCurlView設定資料:
/** * 設定點陣圖資料 *  * @param mBitmaps *            點陣圖資料列表 */public synchronized void setBitmaps(List<Bitmap> mBitmaps) /*  * 如果資料為空則丟擲異常  */ if (null == mBitmaps || mBitmaps.size() == 0)  throw new IllegalArgumentException("no bitmap to display"); /*  * 如果資料長度小於2則GG思密達  */ if (mBitmaps.size() < 2)  throw new IllegalArgumentException("fuck you and fuck to use imageview"); this.mBitmaps = mBitmaps; invalidate();}
這裡要注意,如果圖片小於兩張,那就沒必要去做翻頁效果了,當然你也可以將其繪製出來然後在使用者實行“翻頁”的時候提示“已是最後一頁”也可以,這裡我就直接不允許圖片張數小於2張了。

在《自定義控制元件其實很簡單5/12》中我們自定義了一個折線檢視,在該例中我們為PolylineView設定了一個初始化資料,即當用戶沒有設定資料時預設顯示了一組隨機值資料。那在這裡呢我不再做初始化資料而是當繪製時如果資料為空那麼我們就顯示一組文字資訊提示使用者設定資料:

/** * 預設顯示 *  * @param canvas *            Canvas物件 */private void defaultDisplay(Canvas canvas) // 繪製底色 canvas.drawColor(Color.WHITE); // 繪製標題文字 mTextPaint.setTextSize(mTextSizeLarger); mTextPaint.setColor(Color.RED); canvas.drawText("FBI WARNING", mViewWidth / 2, mViewHeight / 4, mTextPaint); // 繪製提示文字 mTextPaint.setTextSize(mTextSizeNormal); mTextPaint.setColor(Color.BLACK); canvas.drawText("Please set data use setBitmaps method", mViewWidth / 2, mViewHeight / 3, mTextPaint);}

如果沒有設定資料,那麼PageCurlView的預設顯示效果如下:

如果有資料,那麼我們在繪製這些點陣圖之前要對其大小進行調整,這裡我就直接將點陣圖的大小矯正與控制元件一致,當然實際應用當中你可以根據比例來縮放圖片使其保持寬高比,這裡我就直接捨棄寬高比了:

/** * 初始化點陣圖資料 * 縮放點陣圖尺寸與螢幕匹配 */private void initBitmaps() { List<Bitmap> temp = new ArrayList<Bitmap>(); for (int i = 0; i < mBitmaps.size(); i++) {  Bitmap bitmap = Bitmap.createScaledBitmap(mBitmaps.get(i), mViewWidth, mViewHeight, true);  temp.add(bitmap); } mBitmaps = temp;}
那麼資料有了,我們嘗試將其繪製出來看看:
/** * 繪製點陣圖 *  * @param canvas *            Canvas物件 */private void drawBtimaps(Canvas canvas) for (int i = 0; i < mBitmaps.size(); i++) {  canvas.save();  canvas.drawBitmap(mBitmaps.get(i), 0, 0, null);  canvas.restore(); }}
如上程式碼所示,每一次繪製點陣圖我們都鎖定還原Canvas使每一個Bitmap在繪製時都獨立開來,方便我們操作:

非常壯觀的建築物~雖然我們是把Bitmap繪製出來了,但是細心的朋友會發現,繪製順序是顛倒的,位於列表末端的Bitmap被繪製在了最頂層,很簡單,我們在initBitmaps的時候掉個頭不就是了麼:

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;}
這時候執行就會顯示第一張圖片了:

古典歐式建築~~

很多細心的朋友可能會有這樣的疑問問什麼不在drawBtimaps裡面翻轉順序呢?原因有二,其一是既然是初始化資料那麼我們希望在initBitmaps之後拿到的資料是直接能用的而不是在draw的時候還要執行沒必要計算影響效率,其二是我們會在drawBtimaps執行一些計算,需要的一些引數包括迴圈的一些引數,如果我們的迴圈還要自行計算必定會增加邏輯的複雜度。

圖片是畫出來了,但是要如何去“遮住”上一張同時顯示下一張圖片呢?我們可以利用《自定義控制元件其實很簡單5/12》中講到的clipXXX裁剪方法去做,通過控制clipRect的right座標來顯示圖片,好我們修改一下drawBtimaps方法加入clip:

/** * 繪製點陣圖 *  * @param canvas *            Canvas物件 */private void drawBtimaps(Canvas canvas) for (int i = 0; i < mBitmaps.size(); i++) {  canvas.save();  canvas.clipRect(0, 0, mClipX, mViewHeight);  canvas.drawBitmap(mBitmaps.get(i), 0, 0, null);  canvas.restore(); }}
mClipX為裁剪區域右端的座標值,我們在onSizeChanged中為其賦值使其等於控制元件寬度:
@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) // 省去一些程式碼…… // 初始化裁剪右端點座標 mClipX = mViewWidth;}
並且重寫View的onTouchEvent方法獲取事件,將當前觸控點的X座標賦予mClipX並重繪檢視:
@Overridepublic boolean onTouchEvent(MotionEvent event) // 獲取觸控點的x座標 mClipX = event.getX();  invalidate(); return true;}
此時執行效果如下:

大家可以看到雖然我們可以通過手指的滑動來clip裁剪圖片,但目測並沒有達到我們理想的效果,clip裁剪了所有的圖片而我們其實只想裁剪第一張並使第二張顯示出來……OK,那我們再改下drawBtimaps方法:

/** * 繪製點陣圖 *  * @param canvas *            Canvas物件 */private void drawBtimaps(Canvas canvas) for (int i = 0; i < mBitmaps.size(); i++) {  canvas.save();  /*   * 僅裁剪位於最頂層的畫布區域   */  if (i == mBitmaps.size() - 1) {   canvas.clipRect(0, 0, mClipX, mViewHeight);  }  canvas.drawBitmap(mBitmaps.get(i), 0, 0, null);  canvas.restore(); }}
我們只針對位於最頂層的畫布區域進行裁剪而其他的則保持不變,這樣我們就可以得到一個“翻”的效果:

現在想想每次我們去滑動都要從至右滑到至左對吧,可是我們的手指是有寬度的,想精確地一次性從至右滑到至左太麻煩,我們可以在左右兩端設定一個區域,噹噹前觸控點在該區域時讓我們的圖片自動滑至或者說吸附到至左或至右:

@Overridepublic boolean onTouchEvent(MotionEvent event) switch (event.getAction() & MotionEvent.ACTION_MASK) { default:  // 獲取觸控點的x座標  mClipX = event.getX();  invalidate();  breakcase MotionEvent.ACTION_UP:// 觸點擡起時  // 判斷是否需要自動滑動  judgeSlideAuto();  break; } return true;}
那麼這個事件我們在手指擡起時觸發,手指擡起後判斷當前點的位置:
/** * 判斷是否需要自動滑動 * 根據引數的當前值判斷繪製 */private void judgeSlideAuto() /*  * 如果裁剪的右端點座標在控制元件左端五分之一的區域內,那麼我們直接讓其自動滑到控制元件左端  */ if (mClipX < mViewWidth * 1 / 5F) {  while (mClipX > 0) {   mClipX--;   invalidate();  } } /*  * 如果裁剪的右端點座標在控制元件右端五分之一的區域內,那麼我們直接讓其自動滑到控制元件右端  */ if (mClipX > mViewWidth * 4 / 5F) {  while (mClipX < mViewWidth) {   mClipX++;   invalidate();  } }}
如圖所示,當我們的觸控點在距控制元件左端1/5的區域內時擡起手指後圖片自動吸附到了左端,同樣當我們的觸控點在距控制元件右端4/5-5/5的區域內時擡起手指後圖片自動吸附到了右端:

OK,好像沒什麼問題是麼?哈哈哈哈哈啊哈哈哈哈哈哈哈哈如果你真要這麼認為你就上當了,大家可以認真地看看是不是真沒問題,這裡其實我給大家挖了個坑,看似沒啥問題,其實涉及到一個很重要的資訊,下一節我們會講。這裡要注意,因為我們會不斷地觸發觸控事件,也就是說onTouchEvent會不斷地被呼叫,而在onTouchEvent中我們會不斷重複地去計算mViewWidth * 1 / 5F和 mViewWidth * 4 / 5F的值,這對我們控制元件的效率來說是相當不利的,我們考慮將其封裝成成員變數並在onSizeChanged中賦予初始值:

@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) // 省去一些程式碼…… // 計算控制元件左側和右側自動吸附的區域 autoAreaLeft = mViewWidth * 1 / 5F; autoAreaRight = mViewWidth * 4 / 5F;}
再在judgeSlideAuto中呼叫:
private void judgeSlideAuto() /*  * 如果裁剪的右端點座標在控制元件左端五分之一的區域內,那麼我們直接讓其自動滑到控制元件左端  */ if (mClipX < autoAreaLeft) {  while (mClipX > 0) {   mClipX--;   invalidate();  } } /*  * 如果裁剪的右端點座標在控制元件右端五分之一的區域內,那麼我們直接讓其自動滑到控制元件右端  */ if (mClipX > autoAreaRight) {  while (mClipX < mViewWidth) {   mClipX++;   invalidate();  } }}
物件和引數的複用一定要運用得爐火純青,特別是在像onDraw、onTouchEvent這類有可能被不斷重複呼叫的方法中,儘量避免不必要的計算(特別是浮點值的計算)和物件生成,這樣對提高View執行效率有著舉足輕重的意義!

到目前為之我們成功翻起了第一張圖片,但是如何能夠連續不斷地翻起剩下的圖片呢?先別急,在這之前我們先來看一個浪費資源影響效率的東西。我在setBitmaps的時候傳了五張圖片進來,也就是說mBitmaps資料列表的size長度為5,而在drawBtimaps中呢我們直接通過迴圈將五張圖片依次繪製在了Canvas中:

for (int i = 0; i < mBitmaps.size(); i++) { canvas.save(); // 省略一些程式碼…… canvas.drawBitmap(mBitmaps.get(i), 0, 0, null); canvas.restore();}
現在想想看我們是否有這樣的必要呢?這裡是五張圖片,如果是10張、100張、1000張、10000張……呢?這樣直接一次性地全部draw絕逼要崩~而事實上我們也沒有必要去一次性繪製這麼多圖片,因為我們每次最多隻會顯示兩張:上一張翻和下一張顯示,也就是說我們僅需顯示當前最頂層的兩張圖片即可~這樣在有大量圖片的時候可以大大提高我們繪圖的效率,然後通過一些引數的判斷來不斷地在滑動過程中繪製往後的兩張圖片直至最後一張圖片繪製完成,so~我們更改一下drawBtimaps:
/** * 繪製點陣圖 *  * @param canvas *            Canvas物件 */private void drawBtimaps(Canvas canvas) // 繪製點陣圖前重置isLastPage為false isLastPage = false// 限制pageIndex的值範圍 pageIndex = pageIndex < 0 ? 0 : pageIndex; pageIndex = pageIndex > mBitmaps.size() ? mBitmaps.size() : pageIndex; // 計算資料起始位置 int start = mBitmaps.size() - 2 - pageIndex; int end = mBitmaps.size() - pageIndex; /*  * 如果資料起點位置小於0則表示當前已經到了最後一張圖片  */ if (start < 0) {  // 此時設定isLastPage為true  isLastPage = true;  // 並顯示提示資訊  showToast("This is fucking lastest page");  // 強制重置起始位置  start = 0;  end = 1; } for (int i = start; i < end; i++) {  canvas.save();  /*   * 僅裁剪位於最頂層的畫布區域   * 如果到了末頁則不在執行裁剪   */  if (!isLastPage && i == end - 1) {   canvas.clipRect(0, 0, mClipX, mViewHeight);  }  canvas.drawBitmap(mBitmaps.get(i), 0, 0, null);  canvas.restore(); }}
我們增加了一個int型別的成員變數pageIndex,用來作為計算讀取資料列表的參考值。在這裡我們約定控制元件左側小於autoAreaLeft的區域為“回滾區域”,什麼意思呢?如果我們的手指觸控點在該區域,那麼我們就認為使用者的操作為“返回上一頁”(當然你也可以去計算滑動起始點之差的正負來判斷使用者的行為,事件不是本系列重點就不講了)。pageIndex的作用可以用簡單用下圖表示:

五種顏色代表五張圖片,左上角的序號表示其在列表中的下標位置,當pageIndex為0時start為3而end為5,那麼意味著列表最後的圖片會被繪製在最上層,接著繪製倒數第二張,如果pageIndex為1時start為2而end為4,那麼意味著列表倒數第二張的圖片會被繪製在最上層,接著繪製倒數第三張……以此類推,那麼我們該如何控制pageIndex的值呢?由上可知,pageIndex++表示顯示下一頁而pageIndex--則表示上一頁,那麼故事就很簡單了,當我們的mClipX值為0時意味著圖片已被裁剪完了,那麼這時候我們就可以使pageIndex++,而當用戶的手指觸碰回滾區域的時候則讓pageIndex--顯示回上一頁,既然需要判斷事件,那我們只好修改onTouchEvent咯:

@Overridepublic boolean onTouchEvent(MotionEvent event) // 每次觸發TouchEvent重置isNextPage為true isNextPage = true/*  * 判斷當前事件型別  */ switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN:// 觸控式螢幕幕時  // 獲取當前事件點x座標  mCurPointX = event.getX();  /*   * 如果事件點位於回滾區域   */  if (mCurPointX < mAutoAreaLeft) {   // 那就不翻下一頁了而是上一頁   isNextPage = false;   pageIndex--;   mClipX = mCurPointX;   invalidate();  }  breakcase MotionEvent.ACTION_MOVE:// 滑動時  float SlideDis = mCurPointX - event.getX();  if (Math.abs(SlideDis) > mMoveValid) {   // 獲取觸控點的x座標   mClipX = event.getX();   invalidate();  }  breakcase MotionEvent.ACTION_UP:// 觸點擡起時  // 判斷是否需要自動滑動  judgeSlideAuto();  /*   * 如果當前頁不是最後一頁   * 如果是需要翻下一頁   * 並且上一頁已被clip掉   */  if (!isLastPage && isNextPage && mClipX <= 0) {   pageIndex++;   mClipX = mViewWidth;   invalidate();  }  break; } return true;}
在ACTION_MOVE事件中我們重新定義了事件的執行標準,如果MOVE的距離小於mMoveValid = mViewWidth * 1 / 100F即控制元件寬度的百分之一則無效,上面程式碼的註釋和上面的分析過程一致就不多扯了,具體效果如下:

以下是這一部分PageTurnView的全部程式碼,下一節我們將嘗試引入折線,將單純的由右至左切換變成一個摺頁翻轉的效果

public class PageTurnView extends View private static final float TEXT_SIZE_NORMAL = 1 / 40F, TEXT_SIZE_LARGER = 1 / 20F;// 標準文字尺寸和大號文字尺寸的佔比 private TextPaint mTextPaint;// 文字畫筆 private Context mContext;// 上下文環境引用 private List<Bitmap> mBitmaps;// 點陣圖資料列表 private int pageIndex;// 當前顯示mBitmaps資料的下標 private int mViewWidth, mViewHeight;// 控制元件寬高 private float mTextSizeNormal, mTextSizeLarger;// 標準文字尺寸和大號文字尺寸 private float mClipX;// 裁剪右端點座標 private float mAutoAreaLeft, mAutoAreaRight;// 控制元件左側和右側自動吸附的區域 private float mCurPointX;// 指尖觸碰螢幕時點X的座標值 private float mMoveValid;// 移動事件的有效距離 private boolean isNextPage, isLastPage;// 是否該顯示下一頁、是否最後一頁的標識值 public PageTurnView(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); } @Override public boolean onTouchEvent(MotionEvent event) {  // 每次觸發TouchEvent重置isNextPage為true  isNextPage = true;  /*   * 判斷當前事件型別   */  switch (event.getAction() & MotionEvent.ACTION_MASK) {  case MotionEvent.ACTION_DOWN:// 觸控式螢幕幕時   // 獲取當前事件點x座標   mCurPointX = event.getX();   /*    * 如果事件點位於回滾區域    */   if (mCurPointX < mAutoAreaLeft) {    // 那就不翻下一頁了而是上一頁    isNextPage = false;    pageIndex--;    mClipX = mCurPointX;    invalidate();   }   break;  case MotionEvent.ACTION_MOVE:// 滑動時   float SlideDis = mCurPointX - event.getX();   if (Math.abs(SlideDis) > mMoveValid) {    // 獲取觸控點的x座標    mClipX = event.getX();    invalidate();   }   break;  case MotionEvent.ACTION_UP:// 觸點擡起時   // 判斷是否需要自動滑動   judgeSlideAuto();   /*    * 如果當前頁不是最後一頁    * 如果是需要翻下一頁    * 並且上一頁已被clip掉    */   if (!isLastPage && isNextPage && mClipX <= 0) {    pageIndex++;    mClipX = mViewWidth;    invalidate();   }   break;  }  return true; } /**  * 判斷是否需要自動滑動  * 根據引數的當前值判斷自動滑動  */ private void judgeSlideAuto() {  /*   * 如果裁剪的右端點座標在控制元件左端十分之一的區域內,那麼我們直接讓其自動滑到控制元件左端   */  if (mClipX < mAutoAreaLeft) {   while (mClipX > 0) {    mClipX--;    invalidate();   }  }  /*   * 如果裁剪的右端點座標在控制元件右端十分之一的區域內,那麼我們直接讓其自動滑到控制元件右端   */  if (mClipX > mAutoAreaRight) {   while (mClipX < mViewWidth) {    mClipX++;    invalidate();   }  } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {  // 獲取控制元件寬高  mViewWidth = w;  mViewHeight = h;  // 初始化點陣圖資料  initBitmaps();  // 計算文字尺寸  mTextSizeNormal = TEXT_SIZE_NORMAL * mViewHeight;  mTextSizeLarger = TEXT_SIZE_LARGER * mViewHeight;  // 初始化裁剪右端點座標  mClipX = mViewWidth;  // 計算控制元件左側和右側自動吸附的區域  mAutoAreaLeft = mViewWidth * 1 / 5F;  mAutoAreaRight = mViewWidth * 4 / 5F;  // 計算一度的有效距離  mMoveValid = mViewWidth * 1 / 100F; } /**  * 初始化點陣圖資料  * 縮放點陣圖尺寸與螢幕匹配  */ 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; } @Override protected void onDraw(Canvas canvas) {  /*   * 如果資料為空則顯示預設提示文字   */  if (null == mBitmaps || mBitmaps.size() == 0) {   defaultDisplay(canvas);   return;  }  // 繪製點陣圖  drawBtimaps(canvas); } /**  * 預設顯示  *   * @param canvas  *            Canvas物件  */ private void defaultDisplay(Canvas canvas) {  // 繪製底色  canvas.drawColor(Color.WHITE);  // 繪製標題文字  mTextPaint.setTextSize(mTextSizeLarger);  mTextPaint.setColor(Color.RED);  canvas.drawText("FBI WARNING", mViewWidth / 2, mViewHeight / 4, mTextPaint);  // 繪製提示文字  mTextPaint.setTextSize(mTextSizeNormal);  mTextPaint.setColor(Color.BLACK);  canvas.drawText("Please set data use setBitmaps method", mViewWidth / 2, mViewHeight / 3, mTextPaint); } /**  * 繪製點陣圖  *   * @param canvas  *            Canvas物件  */ private void drawBtimaps(Canvas canvas) {  // 繪製點陣圖前重置isLastPage為false  isLastPage = false;  // 限制pageIndex的值範圍  pageIndex = pageIndex < 0 ? 0 : pageIndex;  pageIndex = pageIndex > mBitmaps.size() ? mBitmaps.size() : pageIndex;  // 計算資料起始位置  int start = mBitmaps.size() - 2 - pageIndex;  int end = mBitmaps.size() - pageIndex;  /*   * 如果資料起點位置小於0則表示當前已經到了最後一張圖片   */  if (start < 0) {   // 此時設定isLastPage為true   isLastPage = true;   // 並顯示提示資訊   showToast("This is fucking lastest page");   // 強制重置起始位置   start = 0;   end = 1;  }  for (int i = start; i < end; i++) {   canvas.save();   /*    * 僅裁剪位於最頂層的畫布區域    * 如果到了末頁則不在執行裁剪    */   if (!isLastPage && i == end - 1) {    canvas.clipRect(0, 0, mClipX, mViewHeight);   }   canvas.drawBitmap(mBitmaps.get(i), 0, 0, null);   canvas.restore();  } } /**  * 設定點陣圖資料  *   * @param bitmaps  *            點陣圖資料列表  */ public synchronized void setBitmaps(List<Bitmap> bitmaps) {  /*   * 如果資料為空則丟擲異常   */  if (null == bitmaps || bitmaps.size() == 0)   throw new IllegalArgumentException("no bitmap to display");  /*   * 如果資料長度小於2則GG思密達   */  if (bitmaps.size() < 2)   throw new IllegalArgumentException("fuck you and fuck to use imageview");  mBitmaps = bitmaps;  invalidate(); } /**  * Toast顯示  *   * @param msg  *            Toast顯示文字  */ private void showToast(Object msg) {  Toast.makeText(mContext, msg.toString(), Toast.LENGTH_SHORT).show(); }}
該部分原始碼下載:傳送門