1. 程式人生 > >[轉]自定義Drawable實現靈動的紅鯉魚動畫(上篇)

[轉]自定義Drawable實現靈動的紅鯉魚動畫(上篇)

此篇中的小魚動畫是模仿國外一個大牛做的flash動畫,第一眼就愛上它了,簡約靈動又不失美學,於是抽空試著嘗試了一下,如下是我用Android實現的效果圖:

  小魚兒

由於整個繪製分析過程比較繁瑣所以靈動的紅鯉魚準備做成上下兩篇,本篇是小魚兒繪製的實現篇,第二篇是小魚兒遊動控制篇下篇傳送門。本篇實現如下效果:

  原地擺尾版

繪製實現篇用到如下主要的技術:

1)、自定義Drawable動畫
2)、Android的座標及角度
3)、Canvas中layer的使用
4)、正餘弦函式的使用以及角度角和弧度角的轉換

下圖是我實現小魚兒的分解圖紙:


  部件分解圖

一、動畫拆解

拿到動畫需求或者模仿一個動畫首先需要分析動畫主體如何繪製部件如何活動,就此動畫外觀分析如下:
1)、小魚的身體各個部件都是簡單的半透明幾何圖形
2)、各個部件都可以活動
3)、從頭到尾方向的部件擺動幅度越來越大、頻率越來越高

二、技術分析

小魚擺動是週期運動,三角函式正好有此特性,角度問題也需要和座標掛鉤,所以我們先來明確一下兩個最重要也是最基本的問題:座標和角度。與平面直角座標系不同的是Android的座標系中Y軸正方向是朝下的,但是角度卻和平面直角座標系的計算方法一樣,即原點指向X軸正方向為0°,正角度是逆時針旋轉,負角度是順時針旋轉那麼問題就來了:座標系不同,角度轉動方式卻一樣,為了讓java中的Math函式計算出來的角度跟Android的座標習慣一致我們需要將與Y軸相關的角度都減去180°,這樣解決了既用Android的座標又用自然角度的問題,即下圖所示的角度和座標系關係
  

 
Android座標系下的自然角度
  
  統一完角度問題,接下來我們就看看魚的各部件是怎麼關聯在一起的。需要先了解三個重要引數

 

1)、魚的重心

因為最終我們要實現魚兒根據手指點選的位置而移動的效果,必須確保能讓點選點成為唯一確定魚兒位置的點,所以我們必須找到一個讓魚兒的各個部件都相對此點繪製的點。參考點可以任意選,但是考慮到轉彎的時候或者身體擺動的時候不會往某一邊偏,於是將參考點選在魚的中軸線上,本來選在中軸線和魚兒頭頂橡膠的點但是最後轉彎的時候就跟秋名山老司機漂移一樣,那叫一個飄逸,最後將參考點選在了魚的腹部重心處。

2)、魚頭半徑

 

  比例示意圖
此案例中魚的各個部件都是以 魚頭半徑R為單位衡量的,比如魚的身子第一節長度是3.2R,依次確定好身體的各個部件相對於魚頭半徑的尺寸就能確定整條魚的總長度為6.79R,繼而確定控制元件的總尺寸。如下圖,經過計算控制元件最小尺寸為8.36R,這樣就保證魚兒轉動任意角度都在控制元件之內

 

  打轉圖

3)、魚身角度

此處的魚身角度是指重心到魚頭圓心的連線和X軸正方向的夾角角度,即魚兒前進方向的角度。此方向是確定各個部件方向及位置的的基礎方向,部件的定位、魚身角度以及尾部的擺動角度都是在此角度基礎上通過加減角度來控制左右搖擺。
 下邊我將演示一下如何通過這三個因素來確定頭部以及魚鰭的點座標(其他部位原理相同)
 先假設魚身角度為0°,即頭朝向X軸正方向。通過重心點以及第一節身長的一半的長度,以及角度即可計算出頭部的圓心座標,然後再以頭部圓心座標和0.9R的長度,順時針旋轉80°確定右邊魚鰭的座標點
 

  魚鰭定位過程

 

魚鰭繪製原理相似,通過上文的右鰭座標可以計算出右鰭的另一端座標,魚鰭弧度是通過二階貝塞爾曲線繪製的

魚尾張合分析。魚尾是內外兩個三角形疊加而成的,三角形頂點和三角形底邊中點連線的角度和最後一節身體的角度一直,三角形底邊左右兩點通過底邊的中點以及動態計算出來的長度確定的
    
  最後用放出骨架系統:黑線為各個部件的主軸,圓圈為各個部件邊界的定位點或貝塞爾曲線的控制點,是不是很酷,像不像電影裡的動作捕捉
  

  骨架系統

 

三、程式碼實現

文章只貼出主要程式碼,完整程式碼文末提供連結

0)自定義Drawable

自定義View可能大家都知道,但是自定義Drawable卻並不是很常見。我們知道Drawable在Android裡常常和ImageView配合使用,或者作為某個View的background,它不能通過標籤的方式在xml裡定義,所以嚴格意義上來說它不是一個可以獨立展示的控制元件,需要依附在其他控制元件中。在attrs.xml裡自定義屬性也和它無緣,measure測量也可以省略,這麼一看Drawabe好像就只是專著繪製,沒錯,這就是它比View和ViewGroup繪圖的優勢 —— 輕量。
既然說到不用Measure,那麼它的大小怎麼確定呢?
  當ImageView使用我們自定義Drawable的時候,如果設定的是wrap_content,那麼content的內容寬高從哪裡來?Drawable提供了兩個函式 getIntrinsicHeight()getIntrinsicWidth(),從名字上看是獲得固有寬高,所以我們就可以在這裡控制我們的Drawable本來的寬高。如果ImageView的寬高是具體值的話,具體值超過Drawable的固有寬高,那麼Drawable就會被拉伸(具體拉伸方案是依據ImageView的scaleType型別),如果不想讓自己的內容因拉伸而導致不清晰的話可以在draw()函式裡通過canvas.getHeight()和canvas.getWidth()來獲取ImageView的大小。也可以通過getBounds方法獲取到一個Rect邊界來獲取尺寸。
  
本例中的固有寬高就是可以容納小魚360°旋轉的尺寸8.38R

    @Override
    public int getIntrinsicHeight() { return (int) (8.38f * HEAD_RADIUS); } @Override public int getIntrinsicWidth() { return (int) (8.38f * HEAD_RADIUS); } 

其次自定義Drawable只需複寫必要的四個函式,比較簡單具體作用見註釋

@Override
    public void draw(Canvas canvas) { //和自定義View中的onDraw()異曲同工 } @Override public void setAlpha(int alpha) { //設定Drawable的透明度,一般情況下將此alpha值設定給Paint } @Override public void setColorFilter(ColorFilter colorFilter) { //設定顏色濾鏡,一般情況下將此值設定給Paint } @Override public int getOpacity() { //決定繪製的部分是否遮住Drawable下邊的東西,有點抽象,有幾種模式 //PixelFormat.UNKNOWN //PixelFormat.TRANSLUCENT 只有繪製的地方才蓋住下邊 //PixelFormat.TRANSPARENT 透明,不顯示繪製內容 //PixelFormat.OPAQUE 完全蓋住下邊內容 return PixelFormat.TRANSLUCENT; } 

主要是複寫draw()方法,利用canvas繪製各種想要的東西。

1)座標部分

最最最主要的座標計算程式碼,小魚兒所有部件都是通過此方法計算出座標的 ,功能是計算一個點的座標,可以理解為一個長度為length的線繞起點startPoint旋轉angle角度後線段另一端的座標

  
    /**
     *  輸入起點、長度、旋轉角度計算終點
     * @param startPoint 起點
     * @param length 長度
     * @param angle 旋轉角度
     * @return 計算結果點 */ private static PointF calculatPoint(PointF startPoint, float length, float angle) { float deltaX = (float) Math.cos(Math.toRadians(angle)) * length; //符合Android座標的y軸朝下的標準 float deltaY = (float) Math.sin(Math.toRadians(angle-180)) * length; return new PointF(startPoint.x + deltaX, startPoint.y + deltaY); } 

這裡要特別說明一下Math.sin()、Math.cos()、Math.toRadians()這三個函式,其中sin\cos的引數是弧度制角度。說到弧度制可能大家都忘得差不多了,帶大家回顧一下中學數學。角的度量可以用弧度制也可以用角度製表示。其中弧度和角度轉換的橋樑就是圓周率π

1角度=(π/180)弧度

比如說想計算30°的正弦值,用Java程式碼需要先將角度制的30°轉為弧度值即通過Math.toRadians(30)得到30°對應的弧度,完整程式碼如下:

double sin30 = Math.sin( Math.toRadians(30) );

列印結果是

0.49999999999999994

如果非要得到0.5的話就強轉成float型就行了,可能是由於double的精度問題。

2)、第一節身體

第一節身體包括頭部和身體的第一段,程式碼如下(虛線部分是身體其他部分的生成方法,暫時不管)

  頭身
private void makeBody(Canvas canvas, float headRadius) { float angle = mainAngle + (float) Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) * 2; headPoint = calculatPoint(middlePoint, BODY_LENGHT / 2,mainAngle); //畫頭 canvas.drawCircle(headPoint.x, headPoint.y, HEAD_RADIUS, mPaint); ........ ....... PointF point1, point2, point3, point4, contralLeft, contralRight; //point1和4的初始角度決定髮髻線的高低值越大越低 point1 = calculatPoint(headPoint, headRadius, angle-80); point2 = calculatPoint(endPoint, headRadius * 0.7f, angle-90); point3 = calculatPoint(endPoint, headRadius * 0.7f, angle +90); point4 = calculatPoint(headPoint, headRadius, angle +80); //決定胖瘦 contralLeft = calculatPoint(headPoint, BODY_LENGHT * 0.56f, angle -130); contralRight = calculatPoint(headPoint, BODY_LENGHT * 0.56f, angle +130); mPath.reset(); mPath.moveTo(point1.x, point1.y); mPath.quadTo(contralLeft.x, contralLeft.y, point2.x, point2.y); mPath.lineTo(point3.x, point3.y); mPath.quadTo(contralRight.x, contralRight.y, point4.x, point4.y); mPath.lineTo(point1.x, point1.y); mPaint.setColor(Color.argb(BODY_ALPHA, 244, 92, 71)); //畫身子 canvas.drawPath(mPath, mPaint); } 

其中最難理解的是角度的計算這句話:

    float angle = mainAngle + (float) Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) * 2;//中心軸線和X軸順時針方向夾角 

這裡Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence))是控制第一節身體擺動的核心方法,變數currentValue是ValueAnimator動畫的過程數值,1.2是用來控制身體擺動的固有頻率,waveFrequence是全域性頻率,用於控制魚兒運動時的擺動頻率,因為sin函式是周期函式,且值域為[-1,1],計算結果乘2之後這句話就可以生成一個[-2,2]的變化範圍,用這個值加上mainAngle(身體前進方向和X軸正方向夾角)就可以讓魚的第一節身體在身體主軸左右搖擺2°了。上邊的程式碼生成了頭的圓心座標,第一節身體的四個頂角以及身體兩側的貝塞爾曲線控制點,通過這幾個點,就可以畫出魚的頭和第一節身體了,並且可以根據動畫控制器的數值左右擺動身體

第二節第三節身體思想和第一節身體一致,不過腰線沒有用貝塞爾曲線,而是直接用直線代替,所以二三節身體是梯形,需要注意的是在計算第二三節身體角度的時候擺動核心方法要正餘弦相互交替,否則就順拐了

3)、魚鰭

魚鰭的畫法也不難,麻煩的地方在於要判斷魚鰭是左邊的還是右邊的,因為魚鰭的弧線是貝塞爾曲線生成的,而曲線的控制點要分左右。其中fatherAngle是魚身主軸方向和X軸的的夾角,finsAngle是魚鰭向內擺動時的偏移角度

    private void makeFins(Canvas canvas, PointF startPoint, int type, float fatherAngle) { //魚鰭控制點相對於魚主軸方向的角度 float contralAngle = 115; mPath.reset(); mPath.moveTo(startPoint.x, startPoint.y); //魚鰭的另一端 PointF endPoint = calculatPoint(startPoint, FINS_LENGTH, type == FINS_RIGHT ? fatherAngle - finsAngle-180 : fatherAngle + finsAngle+180); //曲線的控制點 PointF contralPoint = calculatPoint(startPoint, FINS_LENGTH * 1.8f, type == FINS_RIGHT ? fatherAngle - contralAngle - finsAngle : fatherAngle + contralAngle + finsAngle); mPath.quadTo(contralPoint.x, contralPoint.y, endPoint.x, endPoint.y); mPath.lineTo(startPoint.x, startPoint.y); mPaint.setColor(Color.argb(FINS_ALPHA, 244, 92, 71)); canvas.drawPath(mPath, mPaint); mPaint.setColor(Color.argb(OTHER_ALPHA, 244, 92, 71)); } 
  魚鰭定位過程

4)、魚尾

魚尾是大小兩個等腰三角形疊加而成的,三角形的頂點重合。繪製原理是根據三角形底邊中點來確定底邊的兩個點,其中角度和魚尾主方向垂直。其中newWith變數的是根據當前動畫的過程值動態生成的

private void makeTail(Canvas canvas, PointF mainPoint, float length, float maxWidth, float angle) { float newWidth = (float) Math.abs(Math.sin(Math.toRadians(currentValue * 1.7 * waveFrequence)) * maxWidth + HEAD_RADIUS/5*3); //endPoint為三角形底邊中點 PointF endPoint = calculatPoint(mainPoint, length, angle-180); PointF endPoint2 = calculatPoint(mainPoint, length - 10, angle-180); PointF point1, point2, point3, point4; point1 = calculatPoint(endPoint, newWidth, angle-90); point2 = calculatPoint(endPoint, newWidth, angle +90); point3 = calculatPoint(endPoint2, newWidth - 20, angle-90); point4 = calculatPoint(endPoint2, newWidth - 20, angle +90); //內 mPath.reset(); mPath.moveTo(mainPoint.x, mainPoint.y); mPath.lineTo(point3.x, point3.y); mPath.lineTo(point4.x, point4.y); mPath.lineTo(mainPoint.x, mainPoint.y); canvas.drawPath(mPath, mPaint); //外 mPath.reset(); mPath.moveTo(mainPoint.x, mainPoint.y); mPath.lineTo(point1.x, point1.y); mPath.lineTo(point2.x, point2.y); mPath.lineTo(mainPoint.x, mainPoint.y); canvas.drawPath(mPath, mPaint); } 

5)、動畫引擎

接下來就是激動人心的引擎“發動”時間了,看過上篇文章Android仿百度貼吧客戶端Loading小球的朋友就知道引擎部分是一個ValueAnimator,此篇也是。 動畫週期180秒,數值變化從0到54000,無限迴圈往復執行,將過程值賦值給currentValue然後重新整理Drawable

//引擎部分
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 54000);
valueAnimator.setDuration(180 * 1000); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.setRepeatMode(ValueAnimator.REVERSE); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentValue = (int) (animation.getAnimatedValue()); invalidateSelf(); } }); 

執行結果:

  感謝女朋友的默默支援

四、結語

動畫的分析和實現是一個枯燥又費腦筋的過程,時不時還要複習一下還給老師的數學知識,不過當引擎發動的時候看到繪製的東西動起來了你會覺得所有的努力都是值得的。下一篇將分析如何讓魚兒遊動起來,希望大家繼續關注。
繪製部分原始碼:靈動的紅鯉魚Github原始碼
CSDN同步分析文章連結: 自定義Drawable實現靈動的紅鯉魚動畫(上篇)
下篇連結: 自定義Drawable實現靈動的紅鯉魚動畫(下篇)



作者:Jics
連結:https://www.jianshu.com/p/3dd3d1524851
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。