Android翻頁效果原理實現之模擬扭曲

jopen 9年前發布 | 15K 次閱讀 Android Android開發 移動開發

上一節我們實現了翻頁的曲線效果,但是效果有點小瑕疵不知道大家發現沒有:

如圖,我們發現折疊區域怪怪的,并沒有實現我們之前的“彎曲”效果,為什么呢?是計算錯了么?其實不是的,我們之前測試的時候使用的將canvas 填色,但是這里我們用到的是一張位圖,雖然我們的Path是曲線、Region有曲線區域,但是我們的Bitmap是個規規矩矩的矩形啊,怎么彎曲~怎么辦呢?說起扭曲,我們首先想到的是drawBitmapMesh方法,它是我們現在了解的也是唯一的一個能對圖像進行扭曲的API,而使用 drawBitmapMesh方法呢我們也可以有多種思路,最簡單的就是最大化恒定細分值,將圖像分割成一定的網格區域,然后判斷離曲線起點和頂點最近的細分線獲取該區域內的細分線交點按指定方向百分比遞減移動起點和頂點的距離值即可,這種方法簡單粗暴,但扭曲不是很精確,正確地說精確度取決于細分,細分也大越精確當然也越耗性能,而第二種方法呢是根據曲線的起點和頂點動態生成細分值,我們可以確保在起點和頂點處都有一條細分線,這樣就可以很準確地計算扭曲范圍,但是我們就需要動態地去不斷計算細分值相當麻煩,用哪種呢?這里鑒于時間關系還是嘗試用第一種去做,首先定義寬高的細分值:

private static final int SUB_WIDTH = 19, SUB_HEIGHT = 19;// 細分值橫豎各19個網格

19個網格將控件分割為20×20的網格細分線條區域,爾后我們就不需要使用drawBitmap繪制折疊區域了而是改用 drawBitmapMesh。之前在講1/6的時候有盆友多次小窗過我離屏緩沖是個什么意思需要注意什么,這里我權當演示,在繪制扭曲圖像的時候使用一個單獨的Bitmap并將其裝載進一個額外的Canvas中:

private Canvas mCanvasFoldCache;// 執行繪制離屏緩沖的Canvas
private Bitmap mBitmapFoldCache;// 存儲繪制離屏緩沖數據的Bitmap

在構造方法中我們實例化mCanvasFoldCache:

/*

  • 實例化Canvas / mCanvasFoldCache = new Canvas();</pre>

    在onSizeChanged中我們生成mBitmapFoldCache:

    /
  • 生成緩沖位圖并注入Canvas / mBitmapFoldCache = Bitmap.createBitmap(mViewWidth + 100, mViewHeight + 100, Bitmap.Config.ARGB_8888); mCanvasFoldCache.setBitmap(mBitmapFoldCache);</pre>

    這里+100的目的是讓Bitmap有多余的空間繪制扭曲的那部分圖像,我們之前說過Canvas的大小實際取決于內部裝載的Bitmap,如果這里我們不+100,那么mBitmapFoldCache的大小就剛好和我們的控件一樣大,但是我們實現扭曲的那一部分圖像是超出該范圍外的:

    如上圖透明紅色的范圍是我們mBitmapFoldCache的大小,但是底部和右側的扭曲沒有被包含進來,為了彌補這部分的損失我將mBitmapFoldCache的寬高各+100,當然你也可以計算出具體的值,這里只做演示。

    而在繪制時,我們先將所有的數據繪制到mBitmapFoldCache上再將該Bitmap繪制到我們的canvas中:

    mCanvasFoldCache.drawBitmapMesh(mBitmaps.get(end - 1), SUB_WIDTH, SUB_HEIGHT, mVerts, 0, null, 0, null);
    canvas.drawBitmap(mBitmapFoldCache, 0, 0, null);

    這里要注意的是,我們的mBitmapFoldCache在onSizeChanged方法中生成,每次我們繪制的時候都不再重新生成,也就是說,每次繪制其實都是疊加在上一次的繪制數據上,那么這就會給我們帶來一個問題,雖然顯示結果有可能不會出錯但是每次繪制都要不斷計算前面的像素次數一多必定會大大影響性能,這時候我們考慮在繪制每一次結果前清空掉mBitmapFoldCache中的內容:

    mCanvasFoldCache.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    mCanvasFoldCache.drawBitmapMesh(mBitmaps.get(end - 1), SUB_WIDTH, SUB_HEIGHT, mVerts, 0, null, 0, null);
    canvas.drawBitmap(mBitmapFoldCache, 0, 0, null);

    題外話到此為止,實際上我們不需要緩沖繪制,直接使用drawBitmapMesh即可:

    canvas.drawBitmapMesh(mBitmaps.get(end - 1), SUB_WIDTH, SUB_HEIGHT, mVerts, 0, null, 0, null);

    而重點則是我們的這些扭曲點怎么生成,在構造方法中我們實例化坐標數組:

    // 實例化數組并初始化默認數組數據
    mVerts = new float[(SUB_WIDTH + 1)  (SUB_HEIGHT + 1)  2];</pre> 

    在計算了曲線各個點坐標之后我們生成扭曲坐標:

    if (sizeLong > mViewHeight) {
     // 省略大量代碼……
    } else {
     // 省略巨量代碼……
     /

    • 生成折疊區域的扭曲坐標 / int index = 0; for (int y = 0; y <= SUB_HEIGHT; y++) { float fy = mViewHeight y / SUB_HEIGHT; for (int x = 0; x <= SUB_WIDTH; x++) {

       float fx = mViewWidth * x / SUB_WIDTH;
      
       mVerts[index * 2 + 0] = fx;
       mVerts[index * 2 + 1] = fy;
      
       index += 1;
      

      } } }</pre>

      雖然上面我們生成了坐標數組,但是并沒有扭曲圖像,在進行下一步操作前我們先來分析一下如何進行扭曲呢,當我們在折疊區域以drawBitmapMesh的方式繪制Bitmap時這時候的圖像實質上是被網格分割了的:

      我們的方法其實很簡單,只需要把從短邊長度減短邊長度乘以1/4的位置開始到短邊長度位置的點按遞增向下拽即可對吧:

      如上圖所示的兩個藍點分別代表短邊長度減短邊長度乘以1/4的位置和短邊長度位置,因為我們的網格是不變的,但是位置在不斷改變,我們應當獲取離當前位置最近的網格點,比如上圖中的兩個藍點此時我們應該獲取到網格中的對應位置是:

      如圖中綠色的藍點,考慮到更好的容差值,我們令起點往后挪一個點而終點往前挪一個點,最終我們的取舍點如下:

      同樣,我們右側的也一樣:

      那在代碼中的實現也很簡單:

      // 計算底部扭曲的起始細分下標
      mSubWidthStart = Math.round((btmX / mSubMinWidth)) - 1;
      mSubWidthEnd = Math.round(((btmX + CURVATURE * sizeShort) / mSubMinWidth)) + 1;

// 計算右側扭曲的起始細分下標 mSubHeightStart = (int) (leftY / mSubMinHeight) - 1; mSubHeightEnd = (int) (leftY + CURVATURE * sizeLong / mSubMinHeight) + 1;</pre>

我們只需要將mSubWidthStart到mSubWidthEnd之間的點往下“拽”,mSubHeightStart到mSubHeightEnd的點往右“拽”即可實現初步的“扭曲”效果對吧,但是這個拽是有講究的,首先,拽的距離是倍增的,如圖:

每一個點的偏移值相對于上一個點來說是倍增的,倍增多少呢?是基于最大的偏移值來說的,這里為了簡化一定的問題,我就不去計算了,而是給定一個固定的起始值和倍增率:

// 長邊偏移
float offsetLong = CURVATURE / 2F * sizeLong;

// 長邊偏移倍增 float mulOffsetLong = 1.0F;

// 短邊偏移 float offsetShort = CURVATURE / 2F * sizeShort;

// 短邊偏移倍增 float mulOffsetShort = 1.0F;</pre>

這時候我們可以考慮開始計算扭曲坐標:

// 計算底部扭曲的起始細分下標
mSubWidthStart = Math.round((btmX / mSubMinWidth)) - 1;
mSubWidthEnd = Math.round(((btmX + CURVATURE * sizeShort) / mSubMinWidth)) + 1;

// 計算右側扭曲的起始細分下標 mSubHeightStart = (int) (leftY / mSubMinHeight) - 1; mSubHeightEnd = (int) (leftY + CURVATURE * sizeLong / mSubMinHeight) + 1;

/*

  • 生成折疊區域的扭曲坐標 */ int index = 0;

// 長邊偏移 float offsetLong = CURVATURE / 2F * sizeLong;

// 長邊偏移倍增 float mulOffsetLong = 1.0F;

// 短邊偏移 float offsetShort = CURVATURE / 2F * sizeShort;

// 短邊偏移倍增 float mulOffsetShort = 1.0F; for (int y = 0; y <= SUB_HEIGHT; y++) { float fy = mViewHeight y / SUB_HEIGHT; for (int x = 0; x <= SUB_WIDTH; x++) { float fx = mViewWidth x / SUB_WIDTH;

    /*
     * 右側扭曲
     */
    if (x == SUB_WIDTH) {
        if (y >= mSubHeightStart && y <= mSubHeightEnd) {
            fx = mViewWidth * x / SUB_WIDTH + offsetLong * mulOffsetLong;
            mulOffsetLong = mulOffsetLong / 1.5F;
        }
    }

    /*
     * 底部扭曲
     */
    if (y == SUB_HEIGHT) {
        if (x >= mSubWidthStart && x <= mSubWidthEnd) {
            fy = mViewHeight * y / SUB_HEIGHT + offsetShort * mulOffsetShort;
            mulOffsetShort = mulOffsetShort / 1.5F;
        }
    }

    mVerts[index * 2 + 0] = fx;
    mVerts[index * 2 + 1] = fy;

    index += 1;
}

}</pre>

效果如下:

上面的圖因為上傳大小的限制我壓縮過可能大家看不清楚,如果大家DL我想項目運行可以看到在我們翻動的過程中扭曲的部分會有一點小跳動,原因很簡單,我們的扭曲只針對了底部最后的一行點y == SUB_HEIGHT和右側最右的一列點x == SUB_WIDTH,而事實上扭曲是個拉扯聯動的效果,扭曲不僅僅會影響最后一行/列同時也會影響倒數第二、三、四行等,只不過這個影響效力是遞減的,這部分就留給大家自己去做了,原理我講的很清楚了。

這一節到此為止,下一節我們將完善最終效果結束本例所有的Study~

源碼下載:傳送門

 來源:AigeStudio

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!