Android一種翻板式互動效果

AiLo發表於2018-06-18

原創文章,轉載請聯絡作者。個人部落格

梧桐落,又還秋色,又還寂寞。

效果圖,檔案比較大,稍稍等一下 (●゚ω゚●):

Android一種翻板式互動效果

前言

首先,首先!Demo只是對FliBoard的立體感直板翻頁式互動效果作了模仿,只是效果只是效果。那種翻頁元件挺麻煩的,以後可能會抽時間做一下( ̄▽ ̄)"
立體感是一種模仿,在二維平面上,合理地利用光影、透視(遠小近大)等方式,塑造一種近似現實三維世界的感jio。為什麼會產生立體感? 是因為人的視網膜接受到的,全是三維世界的投影。是你的大腦以及經驗,腦補出了三維世界
舉栗子,下面這張圖片,你會把它看作一個彎曲三角嗎

Android一種翻板式互動效果

同理,動畫也無非是利用了人眼的視覺暫留而已。某種程度,它和魔術擁有相同的本質————欺騙

效果解析

解析效果前,先提一下會用到的知識點

1、用到的知識點

  • graphics.Camera,圖形包下用來處理3D旋轉的類
  • canvas、Matrix

2、效果拆解

直板式的翻頁,效果其實並不複雜。手機螢幕之後,是一個三維座標系。想象一下有張板子(Bitmap)放在XY座標系,要達到翻頁效果,讓其繞著X軸旋轉即可。正常情況下,板子(Bitmap)是作為整體旋轉。我們將板子中心點移到X軸上,那麼繞著Z軸旋轉時,上下兩部分運動的方向肯定是相反的。就像這樣:

Android一種翻板式互動效果

上圖為繞著X軸旋轉45度,縮放0.5f效果

如上圖所示,為達到效果,必須將上下兩部分分開繪製。你可以採用將Bitmap分割的方式,也可以分割Canvas。Demo裡,我採取的是分割Canvas。使用方法canvas.clipRect(left, top, right, bottom)

3、手勢拆解

翻頁共有三種狀態,靜態、下翻以及上翻。靜態不必贅述,下面會分析一下上翻和下翻繪製。

3.1 向下翻頁繪製解析

向下翻頁,就是翻過當前頁回到上一頁。在效果拆解那部分,我們已經知道,45度時,上半部分會偏向螢幕後。所以要讓上半部分向下翻轉。旋轉角度得是負數。也就是,在一個完整的下翻週期內,角度的變化為0到-180度
其中0到-90度內,當前頁正在下翻,頁面變動在上半區域,此時可以看到的介面有:下翻ing的當前頁上半部分當前頁產生的陰影上一頁的上半部分(保持不動)。而在-90到-180度階段,此時下翻的動作接近完成,頁面變動在下半區域,此時可以看到的介面有:即將翻過的上一頁的下半部分上一頁翻轉產生的陰影當前頁的下半部分

Android一種翻板式互動效果

3.2 向上翻頁繪製解析

向上翻頁,就是翻過當前頁去下一頁。和下翻邏輯相反,這是一個0到180度的週期活動。0到90度為正在上翻,頁面變動在下半區域。而90到180度,上翻動作接近完成,頁面變動在上班區域,很快會看到完整的下一頁。

具體實現

用自定義View來實現,這裡只貼出主要程式碼,部分邏輯會用虛擬碼表述,完整程式碼文末提供。

1、繪製

因為只是仿寫效果,所以全部邏輯放在了一個自定義View內部。先看一些主要的成員變數。

    //向下翻旋轉角度,0~-180f
    private var rotateF 

    //向上翻旋轉角度,0~180f
    private var rotateS

    //翻動狀態,0為鬆手,1為向下翻,-1為向上翻
    private var statusFlip = 0

    //當前頁
    private var curPage

	 //用於3D旋轉的Camera類
    private val camera

	 //繪製Bitmap的Matrix
    private val drawMatrix

	 //中心點X座標
    private val centerX

	 //中心點Y座標
    private val centerY

    //當前Bitmap
    private var curBitmap: Bitmap

    //上一張Bitmap
    private var lastBitmap: Bitmap
    
    //下一張Bitmap
    private var nextBitmap: Bitmap
複製程式碼

我維護了兩個變數用來分別控制下翻和上翻的角度變化。與此同時,也分了兩個方法,來分別繪製上半部分和下半部分。

//上半部分繪製
fun drawFirstHalf(canvas: Canvas?, bitmap: Bitmap?, rotate: Float) {
    canvas?.save()
    //將canvas上半部分切割
    canvas?.clipRect(0, 0, width, height / 2)
    camera.save()
    //camera繞著X軸旋轉
    camera.rotateX(角度變化小於-90度,不再處理)
    camera.getMatrix(drawMatrix)
    camera.restore()
    //隨著旋轉角度變化的縮放值,只縮放Y軸
    drawMatrix.preScale(1.0f, 縮放比)
    //將圖片移到中心點
    drawMatrix.preTranslate(-centerX, -centerY)
    drawMatrix.postTranslate(centerX, centerY)
    canvas?.drawBitmap(this, drawMatrix, null)
    canvas?.restore()
    }
複製程式碼
fun drawSecondHalf(canvas: Canvas?, bitmap: Bitmap?, rotate: Float) {  
    canvas?.save()
    camera.save()
    //切割下半部分canvas
    canvas?.clipRect(0, height / 2, width, height)
    camera.rotateX(繞著X軸旋轉角度,大於90度後只不再處理變化)
    camera.getMatrix(drawMatrix)
    camera.restore()
    drawMatrix.preScale(1.0f, 縮放比隨著角度變化)
    drawMatrix.preTranslate(-centerX, -centerY)
    drawMatrix.postTranslate(centerX, centerY)
    canvas?.drawBitmap(this, drawMatrix, null)
    canvas?.restore()     
    }
複製程式碼

2、 手勢處理

手勢處理較為簡單,只需要在MOVE的時候,判斷此時的狀態是上翻還是下翻。然後在抬手UP的時候,根據此時的距離,來判斷是否下翻成功或是上翻成功。倘若距離不夠標準閾值,那麼一切歸於原位。

  • 其中startX、startY為手指落點
MotionEvent.ACTION_MOVE -> {
                    val x = this.x
                    val y = this.y
                    //當y運動距離大於x的1.5倍時,才判斷為垂直翻動
                    val disY = y - startY
                    if (Math.abs(disY) > 1f && Math.abs(disY) >= Math.abs(x - startX) * 1.5f) {
                        if (statusFlip == 0) {
                            //滑動間距為正並且不是第一頁判斷為向下翻,滑動間距為負並且不是最後一頁判斷為向上翻
                            statusFlip = if (disY > 0 && curPage != 0) DOWN_FLIP
                            else if (disY < 0 && curPage != girls.lastIndex) UP_FLIP else 0
                        }
                        val ratio = Math.abs(disY) / centerY
                        if (statusFlip == DOWN_FLIP) {
                            //向下翻並且當前頁不等於0
                            rotateF = ratio * -180f
                            Log.d("cece", ": rotateF : " + rotateF);
                            invalidate()
                        } else if (statusFlip == UP_FLIP) {
                            //向上翻,並且不是最後一頁
                            if (curPage != girls.lastIndex) {
                                rotateS = ratio * 180f
                                Log.d("cece", ": rotateS : " + rotateS);
                                invalidate()
                            }
                        }
                    }
                }
複製程式碼
  • 當手指抬起時,首先判斷此時的狀態,然後再判斷移動過的距離是否滿足閾值。不滿足的迴歸當前頁,滿足閾值的,繼續執行未完成的狀態。
if (statusFlip != 0) {
            drawMatrix.reset()
            //放手的時候,有動畫發生
            if (Math.abs(event.y - startY) <= centerY / 2) {
                //滑動距離小於1/4螢幕高,判定仍停留在當前頁
                rotateF = 0f
                rotateS = 0f
                statusFlip = 0
                invalidate()
            } else {
                //滑動距離超過臨界值,判定為跳過當前頁
                if (statusFlip == DOWN_FLIP) {
                    //自動執行完下翻到上一頁的動作
                    for (i in rotateF.toInt() downTo -180 step 6) {
                        invalidate()
                    }
                    curPage--
                } else {
                    //自動執行完上翻到下一頁的動作
                    for (i in rotateS.toInt() until 180 step 6) {
                        invalidate()
                    }
                    curPage++
                }
                rotateF = 0f
                rotateS = 0f
                statusFlip = 0
            }
        }
複製程式碼

當距離達到閾值時,就需要程式碼來繼續完成下翻或者上翻的邏輯。這裡我使用迴圈的方式。譬如上翻超過90度了,就迴圈到180度,繼續完成上翻的動作。

3、 陰影部分和繪製順序

onDraw(...)方法內繪製時,一定要注意程式碼順序。因為在這個方法內,順序代表著層次。譬如陰影繪製一定要寫在頁面繪製之前。
陰影部分的繪製也分為上下兩部分。

fun drawFirstShadow(canvas: Canvas?, rotate: Float) {
    canvas切割上半部分,繪製color即可    
}

fun drawSecondShadow(canvas: Canvas?, rotate: Float) {
    canvas切割下半部分,繪製color即可 
}
複製程式碼

onDraw(...)方法內的繪製順序一定要分明

//繪製當前頁底下的一層,翻頁進行中
        if (statusFlip == DOWN_FLIP) {
            //向下翻,滑到上一頁
            drawFirstHalf(canvas, lastBitmap, 0f)
            drawFirstShadow(canvas, rotateF)
        } else if (statusFlip == UP_FLIP) {
            drawSecondHalf(canvas, nextBitmap, 0f)
            drawSecondShadow(canvas, rotateS)
        }

        //繪製當前頁
        drawFirstHalf(canvas, curBitmap, rotateF)
        drawSecondHalf(canvas, curBitmap, rotateS)

        //繪製當前頁之上的一層,翻頁完成後
        if (statusFlip == DOWN_FLIP) {
            if (rotateF <= -90f) {
                //先繪製陰影
                drawSecondShadow(canvas, rotateF + 180f)
                drawSecondHalf(canvas, lastBitmap, rotateF + 180f)
            }
            //繪製覆蓋在翻頁Bitmap之上淡淡透明層,透明度固定
            drawFirstColor(canvas, 20)
        } else if (statusFlip == UP_FLIP) {
            if (rotateS >= 90f) {
                drawFirstShadow(canvas, rotateS - 180f)
                drawFirstHalf(canvas, nextBitmap, rotateS - 180f)
            }
            //淡淡透明度的陰影層
            drawSecondColor(canvas, 20)
        }
複製程式碼

還是得區分一下狀態,當下翻時,我們得先繪製上一頁的上半部分,而且是靜態的。然後再繪製當前頁下翻產生的陰影。再繪製當前頁,然後在當前頁頂上再繪製一層固定淡淡透明度的陰影層,讓頁面層次更加明顯。

4、效果修正

到這裡主要的邏輯業已完成,但我注意到還是有一些小瑕疵。就是旋轉角度和縮放比,變化不明顯。通常要角度變化到超過45度,才會有很明顯的縮放效果展現出來。
最開始我以為是縮放比的演算法問題,後來才發現是camera的機位問題,camera預設的拍攝角度是[0,0,-8],當距離螢幕很近時,變化自然不是很明顯。
當然,camera提供了設定機位的方法setLocation(x, y, z)。最後我調整到[0,0,-20]才滿意這個效果。

下圖,我給出了,預設機位和[0,0,-20]機位的效果區別。

Android一種翻板式互動效果

結語

Demo裡的實現方式並非是唯一,分享出來是為了提供一種思路。路有很多條,選擇即是正確。
以上
專案程式碼在此,大家要是喜歡的話不妨點個贊吧

有一個公眾號,會記錄一些開發的經驗,也會發一些自己的學習日記。歡迎大家關注。

Android一種翻板式互動效果

相關文章