成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

此篇中的小魚動畫是模仿國外一個大牛做的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

  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只需復寫必要的四個函數,比較簡單具體作用見注釋

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

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

1)坐標部分

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

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

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

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

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

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

打印結果是

  1. 0.49999999999999994 

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

2)、第一節身體

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

頭身

  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的初始角度決定發髻線的高低值越大越低 
  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); 

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

  1. 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是魚鰭向內擺動時的偏移角度

  1. private void makeFins(Canvas canvas, PointF startPoint, int type, float fatherAngle) { 
  2.         //魚鰭控制點相對于魚主軸方向的角度 
  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.         //曲線的控制點 
  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.     } 

魚鰭定位過程

4)、魚尾

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

  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為三角形底邊中點 
  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.         //內 
  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)、動畫引擎

接下來就是激動人心的引擎“發動”時間了,看過上篇文章Android仿百度貼吧客戶端Loading小球的朋友就知道引擎部分是一個ValueAnimator,此篇也是。 動畫周期180秒,數值變化從0到54000,無限循環往復運行,將過程值賦值給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. }); 

運行結果:

感謝女朋友的默默支持

四、結語

動畫的分析和實現是一個枯燥又費腦筋的過程,時不時還要復習一下還給老師的數學知識,不過當引擎發動的時候看到繪制的東西動起來了你會覺得所有的努力都是值得的。下一篇將分析如何讓魚兒游動起來,希望大家繼續關注。

下篇地址:自定義Drawable實現靈動的紅鯉魚動畫(下篇)

繪制部分源碼:靈動的紅鯉魚Github源碼

【本文為51CTO專欄作者“季晨生”的原創稿件,轉載請通過51CTO聯系作者獲取授權】

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

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2017-07-19 14:59:26

Drawable動畫實現

2022-02-16 15:25:31

JS代碼Canvas鴻蒙

2011-08-09 17:16:56

CoreAnimati動畫

2022-05-18 07:44:13

自定義菜單前端

2009-09-07 22:00:15

LINQ自定義

2015-10-12 16:47:13

iOS下拉線條動畫

2024-08-26 11:13:26

字典entry自定義

2015-02-12 15:33:43

微信SDK

2009-07-06 16:20:50

JSP自定義標簽

2022-08-22 20:10:59

自定義計數器CSS

2009-06-17 16:00:03

Hibernate自定

2009-09-03 13:34:03

.NET自定義控件

2013-01-09 17:22:38

Android開發Camera

2022-03-01 16:09:06

OpenHarmon鴻蒙單選組件

2015-07-29 10:31:16

Java緩存算法

2022-12-07 08:56:27

SpringMVC核心組件

2022-04-01 15:59:22

SQLPostgreSQL審計

2023-01-03 07:40:27

自定義滑塊組件

2023-10-24 13:48:50

自定義注解舉值驗證

2021-12-02 18:05:21

Android Interpolato動畫
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产高清精品一区 | 日韩精品中文字幕一区二区三区 | 亚洲电影一区二区三区 | 夜夜爽99久久国产综合精品女不卡 | 国产精品视频久久 | 国产在线中文字幕 | 黄色精品 | 亚洲综合国产 | 久久成人精品 | 天天影视网天天综合色在线播放 | 羞羞视频在线观看 | 国产成都精品91一区二区三 | 国产一区二区精品在线观看 | 久久av网站 | av在线免费播放 | 久久久久久久久综合 | 日日干综合 | 欧美天堂 | 国产a爽一区二区久久久 | 欧美一区日韩一区 | 午夜私人影院 | 日本在线观看视频 | 国产91视频免费 | 性网址| 天天av天天好逼 | 欧美xxxx黑人又粗又长 | 亚洲精品区 | 日韩免费视频一区二区 | 美国一级片在线观看 | av手机在线播放 | 日本亚洲一区二区 | 综合久久av | 亚洲一区二区视频 | 国产成人午夜电影网 | 一区二区三区在线观看视频 | 91精品国产乱码久久久久久久 | 久久av一区二区 | 国产乱人伦精品一区二区 | 午夜精品久久久久久久久久久久 | 日本涩涩视频 | 色婷婷久久久亚洲一区二区三区 |