歡迎大家前往騰訊雲社區,獲取更多騰訊海量技術實踐干貨哦~
前言
開門見山,先來看下效果吧。
看到這么酷炫的效果圖,不得不贊嘆一下我們的設計師。然而,站在程序員的角度上看,除了酷炫之外更多的是復雜。但是,上面我們所看到的還只是最簡單的一種形態而已。更加復雜的情況是當存在多個人臉的時候進行主次臉動畫的切換,攝像頭移動的時候動畫的追蹤,多個動畫的之間的時序控制等問題,總之,UI展示加上各種業務邏輯使得這個動畫變得異常復雜。今天我們要講解的是剔除業務邏輯之外的單純UI上的實現。
為什么是SurfaceView
選擇一種方案的同時要給出為什么不選擇另一種的理由是什么。沒錯,為什么這里不用自定義Vew來完成繪圖呢?既然自定義View也可以實現一般的動畫效果,為什么還要引入SurfaceView呢?可以把View理解為一個經過系統優化的,可以用來高效執行一些幀數比較低動畫的對象,但是對於靈活性更高的動畫來說,View並不是最好的選擇。同時,對於普通的View它們都是在應用程序的主線程中進行繪制的,我們知道在Android系統上我們不能夠在主線程做一些耗時的操作,否則會引起ANR。對於一些游戲畫面,或者攝像頭預覽、視頻播放來說,它們的UI都比較復雜,而且要求能夠進行高效的繪制,因此,它們的UI就不適合在應用程序的主線程中進行繪制。這時候就必須要給那些需要復雜而高效UI的視圖生成一個獨立的繪圖表面,以及使用一個獨立的線程來繪制這些視圖的UI,所以SurfaceView華麗登場了。
SurfaceView,它擁有獨立的繪圖表面,即它不與其宿主窗口共享同一個繪圖表面。由於擁有獨立的繪圖表面,因此SurfaceView的UI就可以在一個獨立的線程中進行繪制。又由於不會占用主線程資源,SurfaceView一方面可以實現復雜而高效的UI,另一方面又不會導致用戶輸入得不到及時響應。SurfaceView 一般與SurfaceHolder.Callback配合使用,需要重寫以下三個方法:
@Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { }
我們可以通過SurfaceView的getHolder()函數可以獲取SurfaceHolder對象,然后在子線程中通過 mHolder.lockCanvas()來獲得 canvas,繪制完畢之后調用mHolder.unlockCanvasAndPost(canvas)來釋放canvas,並把繪制的內容顯示出來。
你所知道的ObjectAnimator
所謂屬性動畫:改變一切能改變的對象的屬性值。ObjectAnimator內部的工作機制是通過尋找特定屬性的get和set方法,然后通過方法不斷地對值進行改變,從而實現動畫效果的。ObjectAnimator提供了ofInt、ofFloat、ofObject等幾個方法來對屬性進行插值操作,這幾個方法都是設置動畫作用的元素、作用的屬性、動畫開始、結束、以及中間的任意個屬性值。動畫更新的過程中,會不斷調用setPropName更新元素的屬性,所以使用ObjectAnimator更新某個屬性,必須得有setter方法。ObjectAnimator經常配合AnimatorSet進行使用:
if (null == mBCRotateAnimator1) { mBCRotateAnimator1 = ObjectAnimator.ofFloat(this, "bCRotate", 0f, 360f); mBCRotateAnimator1.setInterpolator(new LinearInterpolator()); mBCRotateAnimator1.setDuration(866); } if (null == mBCRotateAnimator2) { mBCRotateAnimator2 = ObjectAnimator.ofFloat(this, "bCRotate", 360f, 720f); mBCRotateAnimator2.setInterpolator(new LinearInterpolator()); mBCRotateAnimator2.setDuration(334); } if (null == mBCRotateAnimator3) { mBCRotateAnimator3 = ObjectAnimator.ofFloat(this, "bCRotate", 720f, 360f); mBCRotateAnimator3.setInterpolator(new LinearInterpolator()); mBCRotateAnimator3.setDuration(12000); mBCRotateAnimator3.setRepeatCount(ValueAnimator.INFINITE); } if (null == mBCRotateAnimatorSet) { mBCRotateAnimatorSet = new AnimatorSet(); ArrayList<Animator> animatorArrayList = new ArrayList<Animator>(); animatorArrayList.add(mBCRotateAnimator1); animatorArrayList.add(mBCRotateAnimator2); animatorArrayList.add(mBCRotateAnimator3); mBCRotateAnimatorSet.playSequentially(animatorArrayList); mBCRotateAnimatorSet.setStartDelay(800); }
實用又強大的三角函數
大家有沒有注意到轉圈動畫里面有一對小三角形呢?再細心點你會發現,這是個正三角形,並且其中的一個頂點正對着大圓的圓心,然后隨着大圓一起轉動,有木有?標題也說了,這里所有的元素都是自繪的,所以這兩個三角形它不是設計同學給的icon資源,而是在Canvas上面繪制出來的,那么這里問題就來了:
-
如何確定三角形所在的位置?(總不能把它畫到圓外面去吧)
-
如何確定三角形三個頂點的位置?(總不能把它畫歪了吧)
這里先拋一下數學思路,后面會進行更詳細的講解。
//繪制三角形/** * 數學問題:已知圓心(a, b),半徑r,和角度m,求圓周上點的坐標? * 解:假設圓心的坐標為(a, b),那么圓的方程是(x-a)^2+(y-b)^2=r^2 * 根據方程可以求出圓上的各點坐標 * 又已知角度m,則圓上點的坐標分別是(r*cos(m*Math.PI/180)+a, r*sin(m*Math.PI/180)+b) * * 拓展:已知正三角形中心點的坐標和其外接圓的半徑,求其他三個頂點的坐標? * 解:可畫出該三角形的外接圓,然后可把問題轉化為求圓上三個點的坐標,又因為是正三角形,所以每個點的角度已知。解法同上。 */
人臉識別動畫完全解析
所有的動畫元素可以分解為以下幾種,這里我們主要講解第一種——掃描控件,因為這個是難度最大的
先來粗略看下掃描控件的設計稿(這還不是全部的,一共有好幾張,看不清的同學可以放大來看)
轉圈動畫是整個動畫的核心,我們先來看下,其實它是分了好幾層圓圈,圓弧,圓環,圓點,然后組合在一起根據各自不同的方向和速率進行轉動,同時還伴隨着alpha值的淡入淡出以及scale值的放大縮小,還有顏色的改變。so,幾乎平時接觸到的動畫里所有能改變的元素都摻和了進來。(alpha,scale,rotate,color,shadow,speed,direction)
重點來分析下那個大藍圈(暫且叫它為大藍圈):由兩個半圓弧組成,顏色為漸變藍(最后變為白色),中間夾着對稱的兩個三角形,然后在不同的時間段里以不同的速率進行旋轉和縮放。嘿嘿,畫圓弧是件很簡單的事情,調用canvas.drawArc()方法就可以了,圓弧的起始結束角度設計稿都有給了。這里的難點是在圓弧的不同部位繪制出漸變藍色以及陰影效果。對於漸變顏色的填充,我們可以使用API提供的 來實現,參數填入圓心的坐標,顏色數組,顏色的比例。最后調用Paint.setShader()方法即可。
public SweepGradient(float cx, float cy, int colors[], float positions[]) {} int[] colors = new int[]{Color.argb(255, 0x1E, 0xFF, 0xEC), Color.argb(255, 0x00, 0xBF, 0xFF)}; //藍圈的漸變顏色float[] positions = new float[]{0f, 0.5f}; //漸變顏色占的位置Matrix matrix = new Matrix(); SweepGradient sweepGradient = new SweepGradient(scanningData.centerX, scanningData.centerY, colors, positions); matrix.setRotate(scanningData.bCRightAngleStart+scanningData.bCRotate - 5, scanningData.centerX, scanningData.centerY);//這里居然需要減5°來矯正?sweepGradient.setLocalMatrix(matrix); scanningData.mBCPaint.setShader(sweepGradient);
陰影效果可以調用API提供的 來實現,參數填入陰影半徑以及Blur type。最后調用Paint.setMaskFilter()方法即可。
public BlurMaskFilter(float radius, Blur style) {}
現在來看下大藍圈上的三角形如何繪制出來,我們只分析左上角那個,右下角那個處於對稱位置。以順時針X軸正方向為0°角,那么根據設計稿的初始狀態,可計算出左上角三角形的初始角度是位於225°左右,右下角三角形的初始角度是45°左右。這里我們的最終目標是求出三角形三個頂點的坐標,然后用線條連起來使其成為一個三角形,但是根據這些條件我們直接計算三角形的頂點坐標是做不到的。現在我們來分解下:先根據三角函數求出三角形的中心坐標,然后又由於這個是正三角形,再根據三角函數求出各個頂點的坐標。
由於三角形是在圓周上,假設圓心(a, b),半徑r,和三角形所在的角度m,其實這幾個變量都是知道的,圓心坐標(a,b)則是人臉的中心點,可以通過人臉識別后的矩形坐標返回,半徑r則是設計稿給的初始半徑,角度m就是剛剛我們計算出來的225°,那么根據三角函數可得該三角形的圓心坐標則是 x = rcos(mMath.PI/180)+a, y = rsin(mMath.PI/180)+b。下一步計算三角形三個頂點的坐標。思路還是一樣的:畫出該三角形的外接圓,三角形的中心坐標即是外接圓的圓心,問題可轉化為求外接圓上三個點的坐標,是不是又回到了上面的求解過程?是的。我們已知了這個外接圓的半徑(設計稿給出),圓心坐標,現在要知道的是三角形三個頂點的角度,然后我們就可以分別算出它們的坐標了。我們知道這是個正三角形,而且其中一個頂點指向大圓圈的圓心,暫且把這個頂點命名為P。畫出三角形的外接圓,即頂點P相對於外接圓的位置就是右下角那個三角形相對於大藍圈的位置,因為這兩個三角形是對稱的,頂點是相對的。也就是說頂點P相對於外接圓的角度是45°。如圖:
那么這樣就好辦了,第一個頂點的角度為45°,第二個頂點的角度為45°+120° = 165°,第三個頂點的坐標為165°+120°=285°,再次運用三角函數可求得三個頂點的坐標,然后調用Path.moveTo(),Path.lineTo(),Path.close()等方法把三角形的路徑描述出來,最后調用Canvas.drawPath()把三角形繪制出來,三角形的繪制過程就到此結束了。
//繪制三角形/** * 數學問題:已知圓心(a, b),半徑r,和角度m,求圓周上點的坐標? * 解:假設圓心的坐標為(a, b),那么圓的方程是(x-a)^2+(y-b)^2=r^2 * 根據方程可以求出圓上的各點坐標 * 又已知角度m,則圓上點的坐標分別是(r*cos(m*Math.PI/180)+a, r*sin(m*Math.PI/180)+b) * * 拓展:已知正三角形中心點的坐標和其外接圓的半徑,求其他三個頂點的坐標? * 解:可畫出該三角形的外接圓,然后可把問題轉化為求圓上三個點的坐標,又因為是正三角形,所以每個點的角度已知。解法同上。 */ scanningData.bTLeftTopX = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.cos((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerX); scanningData.bTLeftTopY = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.sin((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerY); float tempAngle1 = scanningData.bTRightBottomAngle + scanningData.bCRotate; //取對稱的角度,因為這是在三角形小圓里面的角度,不是大圓的角度 float tempAngle2 = tempAngle1 + 120f; //正三角形,所以加120° float tempAngle3 = tempAngle2 + 120f; if (tempAngle1 >= 360) { tempAngle1 = tempAngle1 - 360;} if (tempAngle2 >= 360) { tempAngle2 = tempAngle2 - 360;} if (tempAngle3 >= 360) { tempAngle3 = tempAngle3 - 360;} //問題轉化,求三角形三個頂點的坐標(看上面的數學解析) float vertex1_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopX); float vertex1_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopY); float vertex2_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopX); float vertex2_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopY); float vertex3_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopX); float vertex3_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopY); Path path = new Path(); path.moveTo(vertex1_x, vertex1_y); path.lineTo(vertex2_x, vertex2_y); path.lineTo(vertex3_x, vertex3_y); path.close(); scanningData.mBTPaint.setColor(scanningData.bTLeftTopColor); scanningData.mBTPaint.setAlpha((int) (scanningData.bTAlpha * 255)); canvas.drawPath(path, scanningData.mBTPaint);
好了,最難的一塊繪制完了,其他的外圈,內圈,透明圈等等也都問題不大了。嗯,接下來,我們應該讓它動起來。為了讓動畫旋轉起來,我們可以用一個變量rotate運用到ObjectAnimator上面,然后在繪制的時候加上rotate的變化值即可。scale,alpha,等等都一樣,需要在繪制的時候把這些值給加上去。
if (null == mBCScaleAnimator1) { mBCScaleAnimator1 = ObjectAnimator.ofFloat(this, "bCScale", 0f, 1.04f); mBCScaleAnimator1.setInterpolator(new LinearInterpolator()); mBCScaleAnimator1.setDuration(200); } if (null == mBCScaleAnimator2) { mBCScaleAnimator2 = ObjectAnimator.ofFloat(this, "bCScale", 1.04f, 1f); mBCScaleAnimator2.setInterpolator(new LinearInterpolator()); mBCScaleAnimator2.setDuration(66); } if (null == mBCScaleAnimator3) { mBCScaleAnimator3 = ObjectAnimator.ofFloat(this, "bCScale", 1f, 1.02f); mBCScaleAnimator3.setInterpolator(new LinearInterpolator()); mBCScaleAnimator3.setDuration(66); }
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180) scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180) scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180) scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180) scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180) scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180)
人物名稱的繪制以及指示線條以及下面的資料浮窗就不說了,調用Canvas.drawLine(),Canvas.drawText(),Canvas.drawCircle()等等就可以實現(其實線條的位置以及角度也需要運用三角函數進行計算),下面簡單分析下右邊小圖片的波紋效果。
其實這里又有一個需要自繪的三角形,有沒有感覺到崩潰?沒關系,還是按照我們上面的套路,改變下初始角度就可以了,算法在手,三角形我有!其實波紋效果的繪制也比較簡單,調用Canvas.drawCircle(),然后通過ObjectAnimator不斷地去改變圓圈的alpha值和scale值,從而達到波紋的效果。見如下代碼
if (null == mFPOutCircleAlphaAnimator) { mFPOutCircleAlphaAnimator = ObjectAnimator.ofFloat(this, "fPOutCircleAlpha", 1f, 0f, 0f); mFPOutCircleAlphaAnimator.setInterpolator(new LinearInterpolator()); mFPOutCircleAlphaAnimator.setDuration(1500); mFPOutCircleAlphaAnimator.setRepeatCount(ValueAnimator.INFINITE); } if (null == mFPOutCircleAlphaAnimatorSet) { mFPOutCircleAlphaAnimatorSet = new AnimatorSet(); mFPOutCircleAlphaAnimatorSet.play(mFPOutCircleAlphaAnimator); mFPOutCircleAlphaAnimatorSet.setStartDelay(2200); } if (null == mFPOutCircleScaleAnimator) { mFPOutCircleScaleAnimator = ObjectAnimator.ofFloat(this, "fPOutCircleScale", 1f, 1.3f, 1.3f); mFPOutCircleScaleAnimator.setInterpolator(new LinearInterpolator()); mFPOutCircleScaleAnimator.setRepeatCount(ValueAnimator.INFINITE); mFPOutCircleScaleAnimator.setDuration(1500); } if (null == mFPOutCircleScaleAnimatorSet) { mFPOutCircleScaleAnimatorSet = new AnimatorSet(); mFPOutCircleScaleAnimatorSet.play(mFPOutCircleScaleAnimator); mFPOutCircleScaleAnimatorSet.setStartDelay(2200); } scanningData.mFPOutCirclePaint.setAlpha((int)(scanningData.fPOutCircleAlpha * 255 * 0.5)); canvas.drawCircle(scanningData.fPCenterX, scanningData.fPCenterY, scanningData.fPOutCircleRadius * scanningData.fPOutCircleScale, scanningData.mFPOutCirclePaint);
到此,整個動畫繪制完畢。