自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

自定義Drawable實(shí)現(xiàn)靈動(dòng)的紅鯉魚動(dòng)畫(上篇)

開發(fā) 開發(fā)工具
此篇中的小魚動(dòng)畫是模仿國(guó)外一個(gè)大牛做的flash動(dòng)畫,第一眼就愛(ài)上它了,簡(jiǎn)約靈動(dòng)又不失美學(xué),于是抽空試著嘗試了一下,如下是我用Android實(shí)現(xiàn)的效果圖。

此篇中的小魚動(dòng)畫是模仿國(guó)外一個(gè)大牛做的flash動(dòng)畫,第一眼就愛(ài)上它了,簡(jiǎn)約靈動(dòng)又不失美學(xué),于是抽空試著嘗試了一下,如下是我用Android實(shí)現(xiàn)的效果圖:

小魚兒

由于整個(gè)繪制分析過(guò)程比較繁瑣所以靈動(dòng)的紅鯉魚準(zhǔn)備做成上下兩篇,本篇是小魚兒繪制的實(shí)現(xiàn)篇,第二篇是小魚兒游動(dòng)控制篇下篇傳送門。本篇實(shí)現(xiàn)如下效果:

原地?cái)[尾版

繪制實(shí)現(xiàn)篇用到如下主要的技術(shù):

1)、自定義Drawable動(dòng)畫

2)、Android的坐標(biāo)及角度

3)、Canvas中l(wèi)ayer的使用

4)、正余弦函數(shù)的使用以及角度角和弧度角的轉(zhuǎn)換

下圖是我實(shí)現(xiàn)小魚兒的分解圖紙:

 

部件分解圖

一、動(dòng)畫拆解

拿到動(dòng)畫需求或者模仿一個(gè)動(dòng)畫首先需要分析動(dòng)畫主體如何繪制部件如何活動(dòng),就此動(dòng)畫外觀分析如下:

1)、小魚的身體各個(gè)部件都是簡(jiǎn)單的半透明幾何圖形

2)、各個(gè)部件都可以活動(dòng)

3)、從頭到尾方向的部件擺動(dòng)幅度越來(lái)越大、頻率越來(lái)越高

二、技術(shù)分析

小魚擺動(dòng)是周期運(yùn)動(dòng),三角函數(shù)正好有此特性,角度問(wèn)題也需要和坐標(biāo)掛鉤,所以我們先來(lái)明確一下兩個(gè)最重要也是最基本的問(wèn)題:坐標(biāo)和角度。與平面直角坐標(biāo)系不同的是Android的坐標(biāo)系中Y軸正方向是朝下的,但是角度卻和平面直角坐標(biāo)系的計(jì)算方法一樣,即原點(diǎn)指向X軸正方向?yàn)?°,正角度是逆時(shí)針旋轉(zhuǎn),負(fù)角度是順時(shí)針旋轉(zhuǎn)那么問(wèn)題就來(lái)了:坐標(biāo)系不同,角度轉(zhuǎn)動(dòng)方式卻一樣,為了讓java中的Math函數(shù)計(jì)算出來(lái)的角度跟Android的坐標(biāo)習(xí)慣一致我們需要將與Y軸相關(guān)的角度都減去180°,這樣解決了既用Android的坐標(biāo)又用自然角度的問(wèn)題,即下圖所示的角度和坐標(biāo)系關(guān)系

Android坐標(biāo)系下的自然角度

統(tǒng)一完角度問(wèn)題,接下來(lái)我們就看看魚的各部件是怎么關(guān)聯(lián)在一起的。需要先了解三個(gè)重要參數(shù)

1)、魚的重心

因?yàn)樽罱K我們要實(shí)現(xiàn)魚兒根據(jù)手指點(diǎn)擊的位置而移動(dòng)的效果,必須確保能讓點(diǎn)擊點(diǎn)成為唯一確定魚兒位置的點(diǎn),所以我們必須找到一個(gè)讓魚兒的各個(gè)部件都相對(duì)此點(diǎn)繪制的點(diǎn)。參考點(diǎn)可以任意選,但是考慮到轉(zhuǎn)彎的時(shí)候或者身體擺動(dòng)的時(shí)候不會(huì)往某一邊偏,于是將參考點(diǎn)選在魚的中軸線上,本來(lái)選在中軸線和魚兒頭頂橡膠的點(diǎn)但是最后轉(zhuǎn)彎的時(shí)候就跟秋名山老司機(jī)漂移一樣,那叫一個(gè)飄逸,最后將參考點(diǎn)選在了魚的腹部重心處。

2)、魚頭半徑

比例示意圖

此案例中魚的各個(gè)部件都是以魚頭半徑R為單位衡量的,比如魚的身子第一節(jié)長(zhǎng)度是3.2R,依次確定好身體的各個(gè)部件相對(duì)于魚頭半徑的尺寸就能確定整條魚的總長(zhǎng)度為6.79R,繼而確定控件的總尺寸。如下圖,經(jīng)過(guò)計(jì)算控件最小尺寸為8.36R,這樣就保證魚兒轉(zhuǎn)動(dòng)任意角度都在控件之內(nèi)

打轉(zhuǎn)圖

3)、魚身角度

此處的魚身角度是指重心到魚頭圓心的連線和X軸正方向的夾角角度,即魚兒前進(jìn)方向的角度。此方向是確定各個(gè)部件方向及位置的的基礎(chǔ)方向,部件的定位、魚身角度以及尾部的擺動(dòng)角度都是在此角度基礎(chǔ)上通過(guò)加減角度來(lái)控制左右搖擺。

下邊我將演示一下如何通過(guò)這三個(gè)因素來(lái)確定頭部以及魚鰭的點(diǎn)坐標(biāo)(其他部位原理相同)

先假設(shè)魚身角度為0°,即頭朝向X軸正方向。通過(guò)重心點(diǎn)以及第一節(jié)身長(zhǎng)的一半的長(zhǎng)度,以及角度即可計(jì)算出頭部的圓心坐標(biāo),然后再以頭部圓心坐標(biāo)和0.9R的長(zhǎng)度,順時(shí)針旋轉(zhuǎn)80°確定右邊魚鰭的坐標(biāo)點(diǎn)

魚鰭定位過(guò)程

魚鰭繪制原理相似,通過(guò)上文的右鰭坐標(biāo)可以計(jì)算出右鰭的另一端坐標(biāo),魚鰭弧度是通過(guò)二階貝塞爾曲線繪制的

魚尾張合分析。魚尾是內(nèi)外兩個(gè)三角形疊加而成的,三角形頂點(diǎn)和三角形底邊中點(diǎn)連線的角度和最后一節(jié)身體的角度一直,三角形底邊左右兩點(diǎn)通過(guò)底邊的中點(diǎn)以及動(dòng)態(tài)計(jì)算出來(lái)的長(zhǎng)度確定的

最后用放出骨架系統(tǒng):黑線為各個(gè)部件的主軸,圓圈為各個(gè)部件邊界的定位點(diǎn)或貝塞爾曲線的控制點(diǎn),是不是很酷,像不像電影里的動(dòng)作捕捉

骨架系統(tǒng)(點(diǎn)擊查看動(dòng)圖)

三、代碼實(shí)現(xiàn)

文章只貼出主要代碼,完整代碼文末提供鏈接

0)自定義Drawable

自定義View可能大家都知道,但是自定義Drawable卻并不是很常見。我們知道Drawable在Android里常常和ImageView配合使用,或者作為某個(gè)View的background,它不能通過(guò)標(biāo)簽的方式在xml里定義,所以嚴(yán)格意義上來(lái)說(shuō)它不是一個(gè)可以獨(dú)立展示的控件,需要依附在其他控件中。在attrs.xml里自定義屬性也和它無(wú)緣,measure測(cè)量也可以省略,這么一看Drawabe好像就只是專著繪制,沒(méi)錯(cuò),這就是它比View和ViewGroup繪圖的優(yōu)勢(shì) —— 輕量。

既然說(shuō)到不用Measure,那么它的大小怎么確定呢?

當(dāng)ImageView使用我們自定義Drawable的時(shí)候,如果設(shè)置的是wrap_content,那么content的內(nèi)容寬高從哪里來(lái)?Drawable提供了兩個(gè)函數(shù) getIntrinsicHeight()、getIntrinsicWidth(),從名字上看是獲得固有寬高,所以我們就可以在這里控制我們的Drawable本來(lái)的寬高。如果ImageView的寬高是具體值的話,具體值超過(guò)Drawable的固有寬高,那么Drawable就會(huì)被拉伸(具體拉伸方案是依據(jù)ImageView的scaleType類型),如果不想讓自己的內(nèi)容因拉伸而導(dǎo)致不清晰的話可以在draw()函數(shù)里通過(guò)canvas.getHeight()和canvas.getWidth()來(lái)獲取ImageView的大小。也可以通過(guò)getBounds方法獲取到一個(gè)Rect邊界來(lái)獲取尺寸。

本例中的固有寬高就是可以容納小魚360°旋轉(zhuǎn)的尺寸8.38R

  1. @Override 
  2.     public int getIntrinsicHeight() { 
  3.         return (int) (8.38f * HEAD_RADIUS); 
  4.     } 
  5.  
  6.     @Override 
  7.     public int getIntrinsicWidth() { 
  8.         return (int) (8.38f * HEAD_RADIUS); 
  9.     } 

其次自定義Drawable只需復(fù)寫必要的四個(gè)函數(shù),比較簡(jiǎn)單具體作用見注釋

  1. @Override 
  2.     public void draw(Canvas canvas) { 
  3.         //和自定義View中的onDraw()異曲同工 
  4.     } 
  5.  
  6.     @Override 
  7.     public void setAlpha(int alpha) { 
  8.         //設(shè)置Drawable的透明度,一般情況下將此alpha值設(shè)置給Paint 
  9.     } 
  10.  
  11.     @Override 
  12.     public void setColorFilter(ColorFilter colorFilter) { 
  13.         //設(shè)置顏色濾鏡,一般情況下將此值設(shè)置給Paint 
  14.     } 
  15.  
  16.     @Override 
  17.     public int getOpacity() { 
  18.         //決定繪制的部分是否遮住Drawable下邊的東西,有點(diǎn)抽象,有幾種模式 
  19.         //PixelFormat.UNKNOWN 
  20.         //PixelFormat.TRANSLUCENT 只有繪制的地方才蓋住下邊 
  21.         //PixelFormat.TRANSPARENT 透明,不顯示繪制內(nèi)容 
  22.         //PixelFormat.OPAQUE 完全蓋住下邊內(nèi)容 
  23.         return PixelFormat.TRANSLUCENT; 
  24.     } 

主要是復(fù)寫draw()方法,利用canvas繪制各種想要的東西。

1)坐標(biāo)部分

最最最主要的坐標(biāo)計(jì)算代碼,小魚兒所有部件都是通過(guò)此方法計(jì)算出坐標(biāo)的 ,功能是計(jì)算一個(gè)點(diǎn)的坐標(biāo),可以理解為一個(gè)長(zhǎng)度為length的線繞起點(diǎn)startPoint旋轉(zhuǎn)angle角度后線段另一端的坐標(biāo)

  1. /** 
  2.      *  輸入起點(diǎn)、長(zhǎng)度、旋轉(zhuǎn)角度計(jì)算終點(diǎn) 
  3.      * @param startPoint 起點(diǎn) 
  4.      * @param length 長(zhǎng)度 
  5.      * @param angle 旋轉(zhuǎn)角度 
  6.      * @return 計(jì)算結(jié)果點(diǎn) 
  7.      */ 
  8.     private static PointF calculatPoint(PointF startPoint, float length, float angle) { 
  9.         float deltaX = (float) Math.cos(Math.toRadians(angle)) * length; 
  10.         //符合Android坐標(biāo)的y軸朝下的標(biāo)準(zhǔn) 
  11.         float deltaY = (float) Math.sin(Math.toRadians(angle-180)) * length; 
  12.         return new PointF(startPoint.x + deltaX, startPoint.y + deltaY); 
  13.     } 

這里要特別說(shuō)明一下Math.sin()、Math.cos()、Math.toRadians()這三個(gè)函數(shù),其中sin\cos的參數(shù)是弧度制角度。說(shuō)到弧度制可能大家都忘得差不多了,帶大家回顧一下中學(xué)數(shù)學(xué)。角的度量可以用弧度制也可以用角度制表示。其中弧度和角度轉(zhuǎn)換的橋梁就是圓周率π

  1. 1角度=(π/180)弧度 

比如說(shuō)想計(jì)算30°的正弦值,用Java代碼需要先將角度制的30°轉(zhuǎn)為弧度值即通過(guò)Math.toRadians(30)得到30°對(duì)應(yīng)的弧度,完整代碼如下:

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

打印結(jié)果是

  1. 0.49999999999999994 

如果非要得到0.5的話就強(qiáng)轉(zhuǎn)成float型就行了,可能是由于double的精度問(wèn)題。

2)、第一節(jié)身體

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

頭身

  1. private void makeBody(Canvas canvas, float headRadius) { 
  2.  
  3.     float angle = mainAngle + (float) Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) * 2; 
  4.     headPoint = calculatPoint(middlePoint, BODY_LENGHT / 2,mainAngle); 
  5.     //畫頭 
  6.     canvas.drawCircle(headPoint.x, headPoint.y, HEAD_RADIUS, mPaint); 
  7.         ........ 
  8.         ....... 
  9.     PointF point1, point2, point3, point4, contralLeft, contralRight; 
  10.     //point1和4的初始角度決定發(fā)髻線的高低值越大越低 
  11.     point1 = calculatPoint(headPoint, headRadius,  angle-80); 
  12.     point2 = calculatPoint(endPoint, headRadius * 0.7f, angle-90); 
  13.     point3 = calculatPoint(endPoint, headRadius * 0.7f, angle +90); 
  14.     point4 = calculatPoint(headPoint, headRadius, angle +80); 
  15.     //決定胖瘦 
  16.     contralLeft = calculatPoint(headPoint, BODY_LENGHT * 0.56f, angle -130); 
  17.     contralRight = calculatPoint(headPoint, BODY_LENGHT * 0.56f, angle +130); 
  18.     mPath.reset(); 
  19.     mPath.moveTo(point1.x, point1.y); 
  20.     mPath.quadTo(contralLeft.x, contralLeft.y, point2.x, point2.y); 
  21.     mPath.lineTo(point3.x, point3.y); 
  22.     mPath.quadTo(contralRight.x, contralRight.y, point4.x, point4.y); 
  23.     mPath.lineTo(point1.x, point1.y); 
  24.  
  25.     mPaint.setColor(Color.argb(BODY_ALPHA, 244, 92, 71)); 
  26.     //畫身子 
  27.     canvas.drawPath(mPath, mPaint); 

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

  1. float angle = mainAngle + (float) Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) * 2;//中心軸線和X軸順時(shí)針?lè)较驃A角 

這里Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence))是控制第一節(jié)身體擺動(dòng)的核心方法,變量currentValue是ValueAnimator動(dòng)畫的過(guò)程數(shù)值,1.2是用來(lái)控制身體擺動(dòng)的固有頻率,waveFrequence是全局頻率,用于控制魚兒運(yùn)動(dòng)時(shí)的擺動(dòng)頻率,因?yàn)閟in函數(shù)是周期函數(shù),且值域?yàn)閇-1,1],計(jì)算結(jié)果乘2之后這句話就可以生成一個(gè)[-2,2]的變化范圍,用這個(gè)值加上mainAngle(身體前進(jìn)方向和X軸正方向夾角)就可以讓魚的第一節(jié)身體在身體主軸左右搖擺2°了。上邊的代碼生成了頭的圓心坐標(biāo),第一節(jié)身體的四個(gè)頂角以及身體兩側(cè)的貝塞爾曲線控制點(diǎn),通過(guò)這幾個(gè)點(diǎn),就可以畫出魚的頭和第一節(jié)身體了,并且可以根據(jù)動(dòng)畫控制器的數(shù)值左右擺動(dòng)身體

第二節(jié)第三節(jié)身體思想和第一節(jié)身體一致,不過(guò)腰線沒(méi)有用貝塞爾曲線,而是直接用直線代替,所以二三節(jié)身體是梯形,需要注意的是在計(jì)算第二三節(jié)身體角度的時(shí)候擺動(dòng)核心方法要正余弦相互交替,否則就順拐了

3)、魚鰭

魚鰭的畫法也不難,麻煩的地方在于要判斷魚鰭是左邊的還是右邊的,因?yàn)轸~鰭的弧線是貝塞爾曲線生成的,而曲線的控制點(diǎn)要分左右。其中fatherAngle是魚身主軸方向和X軸的的夾角,finsAngle是魚鰭向內(nèi)擺動(dòng)時(shí)的偏移角度

  1. private void makeFins(Canvas canvas, PointF startPoint, int type, float fatherAngle) { 
  2.         //魚鰭控制點(diǎn)相對(duì)于魚主軸方向的角度 
  3.         float contralAngle = 115; 
  4.         mPath.reset(); 
  5.         mPath.moveTo(startPoint.x, startPoint.y); 
  6.         //魚鰭的另一端 
  7.         PointF endPoint = calculatPoint(startPoint, FINS_LENGTH, type == FINS_RIGHT ? fatherAngle - finsAngle-180 : fatherAngle + finsAngle+180); 
  8.         //曲線的控制點(diǎn) 
  9.         PointF contralPoint = calculatPoint(startPoint, FINS_LENGTH * 1.8f, type == FINS_RIGHT ? 
  10.                 fatherAngle - contralAngle - finsAngle : fatherAngle + contralAngle + finsAngle); 
  11.         mPath.quadTo(contralPoint.x, contralPoint.y, endPoint.x, endPoint.y); 
  12.         mPath.lineTo(startPoint.x, startPoint.y); 
  13.         mPaint.setColor(Color.argb(FINS_ALPHA, 244, 92, 71)); 
  14.         canvas.drawPath(mPath, mPaint); 
  15.         mPaint.setColor(Color.argb(OTHER_ALPHA, 244, 92, 71)); 
  16.  
  17.     } 

魚鰭定位過(guò)程

4)、魚尾

魚尾是大小兩個(gè)等腰三角形疊加而成的,三角形的頂點(diǎn)重合。繪制原理是根據(jù)三角形底邊中點(diǎn)來(lái)確定底邊的兩個(gè)點(diǎn),其中角度和魚尾主方向垂直。其中newWith變量的是根據(jù)當(dāng)前動(dòng)畫的過(guò)程值動(dòng)態(tài)生成的

  1. private void makeTail(Canvas canvas, PointF mainPoint, float length, float maxWidth, float angle) { 
  2.         float newWidth = (float) Math.abs(Math.sin(Math.toRadians(currentValue * 1.7 * waveFrequence)) * maxWidth + HEAD_RADIUS/5*3); 
  3.         //endPoint為三角形底邊中點(diǎn) 
  4.         PointF endPoint = calculatPoint(mainPoint, length, angle-180); 
  5.         PointF endPoint2 = calculatPoint(mainPoint, length - 10, angle-180); 
  6.         PointF point1, point2, point3, point4; 
  7.         point1 = calculatPoint(endPoint, newWidth, angle-90); 
  8.         point2 = calculatPoint(endPoint, newWidth, angle +90); 
  9.         point3 = calculatPoint(endPoint2, newWidth - 20, angle-90); 
  10.         point4 = calculatPoint(endPoint2, newWidth - 20, angle +90); 
  11.         //內(nèi) 
  12.         mPath.reset(); 
  13.         mPath.moveTo(mainPoint.x, mainPoint.y); 
  14.         mPath.lineTo(point3.x, point3.y); 
  15.         mPath.lineTo(point4.x, point4.y); 
  16.         mPath.lineTo(mainPoint.x, mainPoint.y); 
  17.         canvas.drawPath(mPath, mPaint); 
  18.         //外 
  19.         mPath.reset(); 
  20.         mPath.moveTo(mainPoint.x, mainPoint.y); 
  21.         mPath.lineTo(point1.x, point1.y); 
  22.         mPath.lineTo(point2.x, point2.y); 
  23.         mPath.lineTo(mainPoint.x, mainPoint.y); 
  24.         canvas.drawPath(mPath, mPaint); 
  25.  
  26.     } 

5)、動(dòng)畫引擎

接下來(lái)就是激動(dòng)人心的引擎“發(fā)動(dòng)”時(shí)間了,看過(guò)上篇文章Android仿百度貼吧客戶端Loading小球的朋友就知道引擎部分是一個(gè)ValueAnimator,此篇也是。 動(dòng)畫周期180秒,數(shù)值變化從0到54000,無(wú)限循環(huán)往復(fù)運(yùn)行,將過(guò)程值賦值給currentValue然后刷新Drawable

  1. //引擎部分 
  2. ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 54000); 
  3. valueAnimator.setDuration(180 * 1000); 
  4. valueAnimator.setInterpolator(new LinearInterpolator()); 
  5. valueAnimator.setRepeatCount(ValueAnimator.INFINITE); 
  6. valueAnimator.setRepeatMode(ValueAnimator.REVERSE); 
  7. valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
  8.     @Override 
  9.     public void onAnimationUpdate(ValueAnimator animation) { 
  10.         currentValue = (int) (animation.getAnimatedValue()); 
  11.         invalidateSelf(); 
  12.     } 
  13. }); 

運(yùn)行結(jié)果:

感謝女朋友的默默支持

四、結(jié)語(yǔ)

動(dòng)畫的分析和實(shí)現(xiàn)是一個(gè)枯燥又費(fèi)腦筋的過(guò)程,時(shí)不時(shí)還要復(fù)習(xí)一下還給老師的數(shù)學(xué)知識(shí),不過(guò)當(dāng)引擎發(fā)動(dòng)的時(shí)候看到繪制的東西動(dòng)起來(lái)了你會(huì)覺(jué)得所有的努力都是值得的。下一篇將分析如何讓魚兒游動(dòng)起來(lái),希望大家繼續(xù)關(guān)注。

下篇地址:自定義Drawable實(shí)現(xiàn)靈動(dòng)的紅鯉魚動(dòng)畫(下篇)

繪制部分源碼:靈動(dòng)的紅鯉魚Github源碼

【本文為51CTO專欄作者“季晨生”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)51CTO聯(lián)系作者獲取授權(quán)】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專欄
相關(guān)推薦

2017-07-19 14:59:26

Drawable動(dòng)畫實(shí)現(xiàn)

2022-02-16 15:25:31

JS代碼Canvas鴻蒙

2011-08-09 17:16:56

CoreAnimati動(dòng)畫

2009-09-07 22:00:15

LINQ自定義

2015-02-12 15:33:43

微信SDK

2021-12-02 18:05:21

Android Interpolato動(dòng)畫

2022-12-07 08:56:27

SpringMVC核心組件

2013-01-09 17:22:38

Android開發(fā)Camera

2022-04-01 15:59:22

SQLPostgreSQL審計(jì)

2009-09-03 13:34:03

.NET自定義控件

2022-03-01 16:09:06

OpenHarmon鴻蒙單選組件

2023-01-03 07:40:27

自定義滑塊組件

2015-07-29 10:31:16

Java緩存算法

2009-06-17 16:00:03

Hibernate自定

2023-10-24 13:48:50

自定義注解舉值驗(yàn)證

2022-05-18 07:44:13

自定義菜單前端

2015-10-12 16:47:13

iOS下拉線條動(dòng)畫

2015-01-14 15:06:48

定義相機(jī)

2015-02-12 15:38:26

微信SDK

2024-08-26 11:13:26

字典entry自定義
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)