詳解Drawable如何實現(xiàn)靈動的紅鯉魚動畫(上篇)
此篇中的小魚動畫是模仿國外一個大牛做的flash動畫,第一眼就愛上它了,簡約靈動又不失美學,于是抽空試著嘗試了一下,如下是我用Android實現(xiàn)的效果圖:
由于整個繪制分析過程比較繁瑣所以靈動的紅鯉魚準備做成上下兩篇,本篇是小魚兒繪制的實現(xiàn)篇,第二篇是小魚兒游動控制篇。本篇實現(xiàn)如下效果:
?
繪制實現(xiàn)篇用到如下主要的技術:
1)、自定義Drawable動畫?
2)、Android的坐標及角度?
3)、Canvas中l(wèi)ayer的使用?
4)、正余弦函數(shù)的使用以及角度角和弧度角的轉(zhuǎn)換
下圖是我實現(xiàn)小魚兒的分解圖紙:?
一、動畫拆解
拿到動畫需求或者模仿一個動畫首先需要分析動畫主體如何繪制部件如何活動,就此動畫外觀分析如下:?
1)、小魚的身體各個部件都是簡單的半透明幾何圖形?
2)、各個部件都可以活動?
3)、從頭到尾方向的部件擺動幅度越來越大、頻率越來越高
二、技術分析
小魚擺動是周期運動,三角函數(shù)正好有此特性,角度問題也需要和坐標掛鉤,所以我們先來明確一下兩個最重要也是最基本的問題:坐標和角度。與平面直角坐標系不同的是Android的坐標系中Y軸正方向是朝下的,但是角度卻和平面直角坐標系的計算方法一樣,即原點指向X軸正方向為0°,正角度是逆時針旋轉(zhuǎn),負角度是順時針旋轉(zhuǎn)那么問題就來了:坐標系不同,角度轉(zhuǎn)動方式卻一樣,為了讓java中的Math函數(shù)計算出來的角度跟Android的坐標習慣一致我們需要將與Y軸相關的角度都減去180°,這樣解決了既用Android的坐標又用自然角度的問題,即下圖所示的角度和坐標系關系?
?
?
?
統(tǒng)一完角度問題,接下來我們就看看魚的各部件是怎么關聯(lián)在一起的。需要先了解三個重要參數(shù)
1)、魚的重心
因為最終我們要實現(xiàn)魚兒根據(jù)手指點擊的位置而移動的效果,必須確保能讓點擊點成為唯一確定魚兒位置的點,所以我們必須找到一個讓魚兒的各個部件都相對此點繪制的點。參考點可以任意選,但是考慮到轉(zhuǎn)彎的時候或者身體擺動的時候不會往某一邊偏,于是將參考點選在魚的中軸線上,本來選在中軸線和魚兒頭頂橡膠的點但是最后轉(zhuǎn)彎的時候就跟秋名山老司機漂移一樣,那叫一個飄逸,最后將參考點選在了魚的腹部重心處。
2)、魚頭半徑
?
此案例中魚的各個部件都是以魚頭半徑R為單位衡量的,比如魚的身子第一節(jié)長度是3.2R,依次確定好身體的各個部件相對于魚頭半徑的尺寸就能確定整條魚的總長度為6.79R,繼而確定控件的總尺寸。如下圖,經(jīng)過計算控件最小尺寸為8.36R,這樣就保證魚兒轉(zhuǎn)動任意角度都在控件之內(nèi)
3)、魚身角度
此處的魚身角度是指重心到魚頭圓心的連線和X軸正方向的夾角角度,即魚兒前進方向的角度。此方向是確定各個部件方向及位置的的基礎方向,部件的定位、魚身角度以及尾部的擺動角度都是在此角度基礎上通過加減角度來控制左右搖擺。?
下邊我將演示一下如何通過這三個因素來確定頭部以及魚鰭的點坐標(其他部位原理相同)?
先假設魚身角度為0°,即頭朝向X軸正方向。通過重心點以及第一節(jié)身長的一半的長度,以及角度即可計算出頭部的圓心坐標,然后再以頭部圓心坐標和0.9R的長度,順時針旋轉(zhuǎn)80°確定右邊魚鰭的坐標點?
?
魚鰭繪制原理相似,通過上文的右鰭坐標可以計算出右鰭的另一端坐標,魚鰭弧度是通過二階貝塞爾曲線繪制的?
魚尾張合分析。魚尾是內(nèi)外兩個三角形疊加而成的,三角形頂點和三角形底邊中點連線的角度和最后一節(jié)身體的角度一直,三角形底邊左右兩點通過底邊的中點以及動態(tài)計算出來的長度確定的?
?
最后用放出骨架系統(tǒng):黑線為各個部件的主軸,圓圈為各個部件邊界的定位點或貝塞爾曲線的控制點,是不是很酷,像不像電影里的動作捕捉?
?
三、代碼實現(xiàn)
0)自定義Drawable
自定義View可能大家都知道,但是自定義Drawable卻并不是很常見。我們知道Drawable在Android里常常和ImageView配合使用,或者作為某個View的background,它不能通過標簽的方式在xml里定義,所以嚴格意義上來說它不是一個可以獨立展示的控件,需要依附在其他控件中。在attrs.xml里自定義屬性也和它無緣,measure測量也可以省略,這么一看Drawabe好像就只是專著繪制,沒錯,這就是它比View和ViewGroup繪圖的優(yōu)勢 —— 輕量。?
既然說到不用Measure,那么它的大小怎么確定呢??
當ImageView使用我們自定義Drawable的時候,如果設置的是wrap_content,那么content的內(nèi)容寬高從哪里來?Drawable提供了兩個函數(shù)?getIntrinsicHeight()
、getIntrinsicWidth()
,從名字上看是獲得固有寬高,所以我們就可以在這里控制我們的Drawable本來的寬高。如果ImageView的寬高是具體值的話,具體值超過Drawable的固有寬高,那么Drawable就會被拉伸(具體拉伸方案是依據(jù)ImageView的scaleType類型),如果不想讓自己的內(nèi)容因拉伸而導致不清晰的話可以在draw()
函數(shù)里通過canvas.getHeight()和canvas.getWidth()來獲取ImageView的大小。也可以通過getBounds方法獲取到一個Rect邊界來獲取尺寸。?
?
本例中的固有寬高就是可以容納小魚360°旋轉(zhuǎn)的尺寸8.38R
????@Override ????public?int?getIntrinsicHeight()?{ ????????return?(int)?(8.38f?*?HEAD_RADIUS); ????} ????@Override ????public?int?getIntrinsicWidth()?{ ????????return?(int)?(8.38f?*?HEAD_RADIUS); ????}
其次自定義Drawable只需復寫必要的四個函數(shù),比較簡單具體作用見注釋
@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?透明,不顯示繪制內(nèi)容 ????????//PixelFormat.OPAQUE?完全蓋住下邊內(nèi)容 ????????return?PixelFormat.TRANSLUCENT; ????}
主要是復寫draw()方法,利用canvas繪制各種想要的東西。
1)坐標部分
最最最主要的坐標計算代碼,小魚兒所有部件都是通過此方法計算出坐標的 ,功能是計算一個點的坐標,可以理解為一個長度為length的線繞起點startPoint旋轉(zhuǎn)angle角度后線段另一端的坐標
????/** ?????*??輸入起點、長度、旋轉(zhuǎn)角度計算終點 ?????*?@param?startPoint?起點 ?????*?@param?length?長度 ?????*?@param?angle?旋轉(zhuǎn)角度 ?????*?@return?計算結(jié)果點 ?????*/ ????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()這三個函數(shù),其中sincos的參數(shù)是弧度制角度。說到弧度制可能大家都忘得差不多了,帶大家回顧一下中學數(shù)學。角的度量可以用弧度制也可以用角度制表示。其中弧度和角度轉(zhuǎn)換的橋梁就是圓周率π
1角度=(π/180)弧度
比如說想計算30°的正弦值,用Java代碼需要先將角度制的30°轉(zhuǎn)為弧度值即通過Math.toRadians(30)
得到30°對應的弧度,完整代碼如下:
double?sin30?=?Math.sin(?Math.toRadians(30)?);
打印結(jié)果是
0.49999999999999994
如果非要得到0.5的話就強轉(zhuǎn)成float型就行了,可能是由于double的精度問題。
2)、第一節(jié)身體
第一節(jié)身體包括頭部和身體的第一段,代碼如下(虛線部分是身體其他部分的生成方法,暫時不管)
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的初始角度決定發(fā)髻線的高低值越大越低 ????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))
是控制第一節(jié)身體擺動的核心方法,變量currentValue
是ValueAnimator動畫的過程數(shù)值,1.2是用來控制身體擺動的固有頻率,waveFrequence是全局頻率,用于控制魚兒運動時的擺動頻率,因為sin函數(shù)是周期函數(shù),且值域為[-1,1],計算結(jié)果乘2之后這句話就可以生成一個[-2,2]的變化范圍,用這個值加上mainAngle(身體前進方向和X軸正方向夾角)就可以讓魚的第一節(jié)身體在身體主軸左右搖擺2°了。上邊的代碼生成了頭的圓心坐標,第一節(jié)身體的四個頂角以及身體兩側(cè)的貝塞爾曲線控制點,通過這幾個點,就可以畫出魚的頭和第一節(jié)身體了,并且可以根據(jù)動畫控制器的數(shù)值左右擺動身體
?
第二節(jié)第三節(jié)身體思想和第一節(jié)身體一致,不過腰線沒有用貝塞爾曲線,而是直接用直線代替,所以二三節(jié)身體是梯形,需要注意的是在計算第二三節(jié)身體角度的時候擺動核心方法要正余弦相互交替,否則就順拐了
3)、魚鰭
魚鰭的畫法也不難,麻煩的地方在于要判斷魚鰭是左邊的還是右邊的,因為魚鰭的弧線是貝塞爾曲線生成的,而曲線的控制點要分左右。其中fatherAngle是魚身主軸方向和X軸的的夾角,finsAngle是魚鰭向內(nèi)擺動時的偏移角度
????????//魚鰭控制點相對于魚主軸方向的角度 ????????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)、魚尾
魚尾是大小兩個等腰三角形疊加而成的,三角形的頂點重合。繪制原理是根據(jù)三角形底邊中點來確定底邊的兩個點,其中角度和魚尾主方向垂直。其中newWith
變量的是根據(jù)當前動畫的過程值動態(tài)生成的
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); ????????//內(nèi) ????????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)、動畫引擎
接下來就是激動人心的引擎“發(fā)動”時間了,看過上篇文章Android仿百度貼吧客戶端Loading小球的朋友就知道引擎部分是一個ValueAnimator,此篇也是。 動畫周期180秒,數(shù)值變化從0到54000,無限循環(huán)往復運行,將過程值賦值給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(); ????} });
運行結(jié)果:
四、結(jié)語
動畫的分析和實現(xiàn)是一個枯燥又費腦筋的過程,時不時還要復習一下還給老師的數(shù)學知識,不過當引擎發(fā)動的時候看到繪制的東西動起來了你會覺得所有的努力都是值得的。下一篇將分析如何讓魚兒游動起來,希望大家繼續(xù)關注。?
繪制部分源碼:靈動的紅鯉魚Github源碼?
原文鏈接:http://blog.csdn.net/guimianhao9833/article/details/75094010