用 Android 實現一條小金魚游動動畫(超棒) [復制鏈接]

2017-7-26 15:06
liujh 閱讀:4321 評論:8 贊:12
Tag:  

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

圖片描述

用 Android 實現一條小金魚游動動畫(源碼)

http://www.4465984.live/thread-282282-1-1.html

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

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()這三個函數,其中sincos的參數是弧度制角度。說到弧度制可能大家都忘得差不多了,帶大家回顧一下中學數學。角的度量可以用弧度制也可以用角度制表示。其中弧度和角度轉換的橋梁就是圓周率π

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();
}

});

運行結果:

圖片描述

感謝女朋友的默默支持
四、結語

動畫的分析和實現是一個枯燥又費腦筋的過程,時不時還要復習一下還給老師的數學知識,不過當引擎發動的時候看到繪制的東西動起來了你會覺得所有的努力都是值得的。

作者:Jics


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(8)
風醒時分 2017-7-26 16:26
能否把完整的代碼鏈接貼出來學習下,謝謝!
回復
多啦A 2017-7-27 15:25
求代碼鏈接謝謝
回復
liujh 2017-7-27 15:36
風醒時分: 能否把完整的代碼鏈接貼出來學習下,謝謝!
用 Android 實現一條小金魚游動動畫(源碼)
http://www.4465984.live/thread-282282-1-1.html
回復
liujh 2017-7-27 15:36
多啦A: 求代碼鏈接謝謝
用 Android 實現一條小金魚游動動畫(源碼)
http://www.4465984.live/thread-282282-1-1.html
回復
多啦A 2017-7-28 08:53
liujh: 用 Android 實現一條小金魚游動動畫(源碼)
http://www.4465984.live/thread-282282-1-1.html
已刪除???
回復
愛的武器無人 2017-9-28 17:27
能否把完整的代碼鏈接貼出來學習下,謝謝!
回復
安魚兒 2018-4-4 10:00
給力
回復
黃海1916 2019-12-23 20:41
安魚兒: 給力
6的一P
回復
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

白沟网供网包牛牛 安徽11选5遗漏走势图 体彩排列7星彩 浙江6+1开奖结果 股票融资杠杆比例规定 东风汽车股票分析 甘肃十一1选五一定牛 快中彩开奖号码 黑龙江22选5开奖查询 宁夏十一选五彩票app 浙江今天十一选五开