Android動(dòng)畫之萌萌噠蠟燭吹蠟燭動(dòng)畫
一、簡(jiǎn)介
最近開始寫一些文章記錄一下以前的一些自己寫的小項(xiàng)目或者是定義View積累,積灰的東西還是要多翻出來整理整理看看的。這個(gè)只完成了一部分燃起熄滅的動(dòng)畫,沒有為何燃起火焰的動(dòng)畫,希望有興趣的同學(xué)也可以接著完成并分享,話不多說,我們來看這兩根萌萌的小蠟燭。
小蠟燭憋足氣把火焰燃起,一下被旁邊的哥們吹滅了 0^0 ,看起來還是萌氣十足的啊。看著圖大家應(yīng)該能想到應(yīng)該怎么實(shí)現(xiàn)了吧,自定義View!對(duì)了,但是具體要怎么把這個(gè)過程做好呢,跟著腳步一起來看一看吧。代碼稍微有點(diǎn)多,大家耐心觀看,有興趣的同學(xué)可以從我的GITHUB上clone下來,對(duì)著代碼看。
二、過程實(shí)現(xiàn)
蠟燭的繪制和動(dòng)畫
- 本著面向?qū)ο蟮乃枷?,很明顯這里就是兩個(gè)蠟燭嘛!既然是這樣那我們就定義一個(gè)蠟燭類具有蠟燭的基本屬性。
- public abstract class ICandle {
- //蠟燭底部左下坐標(biāo)
- protected int mCurX;
- protected int mCurY;
- //蠟燭寬高
- protected int mCandleWidth;
- protected int mCandleHeight;
- //蠟燭左眼坐標(biāo)
- protected Point mEyeLPoint;
- //蠟燭右眼坐標(biāo)
- protected Point mEyeRPoint;
- //蠟燭眼睛半徑
- protected int mEyeRadius;
- //眼睛間隔距離
- protected int mEyeDevide;
- //身體顏色
- protected int mCandleColor;
- //是否停止動(dòng)畫中
- protected boolean mIsAnimStoping = false;
- //蠟燭芯坐標(biāo)
- protected Point mCandlewickPoint;
- public void initAnim(){
- }
- public void stopAnim(){
- }
- public void drawSelf(Canvas canvas){
- }
- }
對(duì)應(yīng)著這蠟燭還有代碼,那就一目了然了。可能需要解釋的應(yīng)該就是下面的幾個(gè)方法了public void initAnim(), stopAnim()初始化開始和結(jié)束動(dòng)畫需要的數(shù)據(jù),小蠟燭將會(huì)實(shí)現(xiàn)這個(gè)方法,drawSelf(Canvas canvas)把畫布傳進(jìn)來然后蠟燭自己繪制自己。
現(xiàn)在就是讓我們來看一看小蠟燭身體內(nèi)部構(gòu)造的時(shí)候了,hiahiahiahia!
不對(duì),和蠟燭生死相隨的還有火焰呢!先來看看火焰吧,等下小蠟燭還要燃燒自己呢。+10086s
Flame
- 一樣先來一睹我們的富勒姆真容
好像也沒什么毛病,首先是里面的區(qū)域,就是Flame啦,外面的呢,就是Flame先生燃燒自己散發(fā)的人性之光和飄散的骨灰(手動(dòng)抹眼淚)。
來看一下Flame的實(shí)現(xiàn)吧。我們一步步分析。
- private static float CHANGE_FACTOR = 20;
- private Paint mPaint;
- private Path mPath;
- //左下點(diǎn)坐標(biāo)
- private int mCurX;
- private int mCurY;
- //火焰寬度
- private int mWidth;
- //火焰高度
- private int mHeight;
- //記錄初始高度
- private int mPreHeight;
- //記錄初始寬度
- private int mPreWidth;
- //火焰頂部貝塞爾曲線控制點(diǎn)變化參數(shù)
- private int mTopXFactor;
- private int mTopYFactor;
- //用于實(shí)現(xiàn)火焰的抖動(dòng)
- private Random mRandom;
- //光環(huán)半徑
- private int mHaloRadius;
- //正在燃燒
- private boolean mIsFiring;
- //是否啟動(dòng)停止動(dòng)畫
- private boolean mIsStopAnim = false;
- private boolean mFlagStop = false;
- private LinearGradient mLinearGradient;
- private RadialGradient mRadialGradient;
- private ValueAnimator mFlameAnimator;
- private ValueAnimator mHaloAnimator;
參數(shù)就是這些了,主要是我們的動(dòng)畫實(shí)現(xiàn)過程,也就是我們的屬性動(dòng)畫ValueAnimator這里還有兩個(gè)渲染類不知道大家用過沒有,LinearGradient和RadialGradient不了解的同學(xué)可以直接點(diǎn)我的博文了解一下。LinearGradient繪制出了火焰,RadialGradient繪制除了發(fā)散的光芒。
初始化的過程我就不寫了,大家對(duì)這代碼看吧。那主要的就是小火焰的是怎么繪制出來的呢 show the code
- mPaint.setStyle(Paint.Style.FILL);
- mPaint.setShader(mLinearGradient);
- mPath.reset();
- mPath.moveTo(mCurX, mCurY);
- mPath.quadTo(mCurX + mWidth / 2,
- mCurY + mHeight / 3,
- mCurX + mWidth, mCurY);
- mPath.quadTo(mCurX + mWidth / 2 + ((1 - mRandom.nextFloat()) * CHANGE_FACTOR) + mTopXFactor,
- mCurY - 2 * mHeight + mTopYFactor,
- mCurX, mCurY);
- canvas.drawPath(mPath, mPaint);
這就是火焰flame的繪制,可以看到這里用到了二次貝塞爾曲線的繪制,不太清楚貝塞爾曲線的同學(xué)也可以點(diǎn)這波浪Loading動(dòng)畫(貝塞爾曲線)有簡(jiǎn)單的介紹,當(dāng)時(shí)是用在一個(gè)水波的view里面。這里的繪制是以前面那個(gè)圖里面的矩形為參照,我們?cè)賮砜匆幌逻@個(gè)圖(當(dāng)然是加強(qiáng)版hiahia)。
那為什么上面的x坐標(biāo)還加了mRandom.nextFloat()) * CHANGE_FACTOR呢?你想啊,火焰不是會(huì)左右晃動(dòng)嗎,利用一個(gè)隨機(jī)來控制左右擺動(dòng)咯。
- mFlameAnimator = ValueAnimator.ofFloat(0, 4).setDuration(4000);
- mFlameAnimator.setRepeatCount(ValueAnimator.INFINITE);
- mFlameAnimator.addUpdateListener(
- new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- float zeroToOne = (float) animation.getAnimatedValue();
- if (zeroToOne >= 1.0f && zeroToOne <= 1.2f) {
- //火焰燃起
- zeroToOne = 1.0f - 5 * (zeroToOne - 1.0f);//1-0
- mHeight = (int) (mPreHeight * (1 - zeroToOne));
- mIsFiring = true;
- } else if (zeroToOne >= 3.5f) {
- if (mFlagStop) {
- mFlameAnimator.cancel();
- return;
- }
- //火焰被吹滅
- zeroToOne = 2 * (zeroToOne - 3.5f);//0-2
- mTopXFactor = (int) (-20 * zeroToOne);
- mTopYFactor = (int) (160 * zeroToOne);
- / mWidth = (int) (mPreWidth * (1 -zeroToOne));
- mIsFiring = false;
- }
- }
- });
在4秒的時(shí)間內(nèi),火焰進(jìn)行了一系列活動(dòng),從下面隨著燈芯移上來,不斷的改變火焰的位置,分為了兩部分,火焰燃起和火焰熄滅,從代碼中可以看到,火焰燃起時(shí)mHeight慢慢變大,然后就是有了升起的過程辣,另外一個(gè)就是火焰被吹滅的時(shí)候,因?yàn)榇禍绲臅r(shí)候火焰的高度肯定是保持之前的值,所以不需要改變,而是用了mTopXFactor和mTopYFactor這個(gè)兩個(gè)因子來控制火焰的位置。好了,既然火焰有了,蠟炬成灰淚始干啊,生命之光也該出場(chǎng)了。
光圈的繪制和動(dòng)畫就相對(duì)簡(jiǎn)單了
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setStrokeWidth(5);
- mPaint.setShader(mRadialGradient);
- canvas.drawCircle(mCurX + mWidth / 2,
- mCurY - mHeight / 2, mHaloRadius, mPaint);
- canvas.drawCircle(mCurX + mWidth / 2,
- mCurY - mHeight / 2, mHaloRadius + 5, mPaint);
- canvas.drawCircle(mCurX + mWidth / 2,
- mCurY - mHeight / 2, mHaloRadius - 5, mPaint);
這里改變的只有一個(gè)參數(shù),mHaloRadius也就是光圈的半徑。但是不要忘了,其他參數(shù)同時(shí)也是在改變的呢,只不過是放在了mFlameAnimator里面。
好了介紹到這Flame的介紹完了,任重而道遠(yuǎn)啊,寫了這么多卻還沒完結(jié),讓我想到一某位古人說過,不是我。
還未老死,就先累死
- FireCandle
這名字有點(diǎn)奇怪,火燭,厲害了Word哥。前面已經(jīng)介紹過ICandle了,現(xiàn)在來看一下他的實(shí)現(xiàn)類,蠟燭兩兄弟之FireCandle。
初始化照例也就不說了,來看該有的變量。
- private Paint mPaint; //中心X坐標(biāo)
- private int mCenterX; //記錄初始寬
- private int mPreWidth; //記錄初始高
- private int mPreHeight; //蠟燭芯旋轉(zhuǎn)角
- private int mCandlewickDegrees = 0; private Flame mFlame; private boolean mIsFire = false; private boolean mIsStateOnStart = false; private boolean mIsStateOnEnd = false; private boolean mFlagStop = false; private ValueAnimator mCandlesAnimator;
命名還是挺規(guī)范的,應(yīng)該一看就知道是干嘛的。
我們還是來主要看繪制和屬性動(dòng)畫的配合,繪制就不看了(光速打臉)。來看動(dòng)畫。
- mCandlesAnimator = ValueAnimator.ofFloat(0, 4).setDuration(4000);
- mCandlesAnimator.addUpdateListener(
- new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- float zeroToOne = (float) animation.getAnimatedValue();
- if (zeroToOne <= 1.0f) {
- //蠟燭芯蓄力下拉
- mIsFire = true;
- mCandleWidth = mPreWidth + (int) (zeroToOne * 40);
- mCandleHeight = mPreHeight - (int) (zeroToOne * 30);
- mCandlewickDegrees = (int) (-60 + (180 + 60) * zeroToOne);
- refreshEyePosition();
- } else if (zeroToOne <= 2.0f) {
- zeroToOne = zeroToOne - 1.0f;
- //蠟燭芯上擺
- if (zeroToOne <= 0.2f) {
- zeroToOne = 1.0f - 5 * zeroToOne;
- mIsFire = false;
- mCandleWidth = mPreWidth + (int) (zeroToOne * 40);
- mCandleHeight = mPreHeight - (int) (zeroToOne * 30);
- mCandlewickDegrees = (int) (180 * zeroToOne);
- } else {
- if (mFlameStateListener != null && !mIsStateOnStart) {
- mFlameStateListener.flameStart();
- mIsStateOnStart = true;
- }
- mCandleWidth = mPreWidth;
- mCandleHeight = mPreHeight;
- mCandlewickDegrees = 0;
- if (mFlagStop) {
- mCandlesAnimator.cancel();
- }
- }
- refreshEyePosition();
- } else if (zeroToOne >= 3.5f) {
- //蠟燭芯被吹歪
- zeroToOne = 2 * (zeroToOne - 3.5f);//0-1
- mCandlewickDegrees = (int) (-60 * zeroToOne);
- if (mFlameStateListener != null && !mIsStateOnEnd) {
- mFlameStateListener.flameEnd();
- mIsStateOnEnd = true;
- }
- }
- }
- });
這個(gè)就過程就有點(diǎn)多了,但是其實(shí)一點(diǎn)都不復(fù)雜,,首先我們看動(dòng)畫里面的小蠟燭,一開始,他來了一個(gè)變胖紅臉深蹲,所以呢mCandleWidth是變大的,mCandleHeight是變小的,后面那個(gè)燈芯隨著深蹲來了一個(gè)大角度旋轉(zhuǎn),燈芯的如何旋轉(zhuǎn)大家也看到了,改變坐標(biāo)系然后就可以了。用到了
canvas.rotate(mCandlewickDegrees, mCenterX, mCurY - mCandleHeight);這個(gè)方法。上擺過程也是一樣的,就不多說了。refreshEyePosition();這個(gè)方法是改變眼睛位置的,兩個(gè)地方都用到了所以稍微獨(dú)立出來了。注意mIsFire這個(gè)變量,沒有火焰的時(shí)候就做其他繪制,比如說紅眼睛等等。好了好了,介紹到這,小蠟燭的部分就結(jié)束了。
SecCandle
大蠟燭,帥蠟燭鎮(zhèn)樓,實(shí)際的繪制和小蠟燭的就差不多了,這里就不解釋了。
共同繪制View和控制器
AnimControler
這個(gè)類的功能很簡(jiǎn)單,繪制地板部分還有就是把計(jì)算后傳過來的高度寬度賦給兩支蠟燭,然后控制兩支蠟燭各自開始動(dòng)畫。
- mFirCandle = new FirCandle(mRelativeX + mWidth / 6, mRelativeY + mHeight);
- mFirCandle.initCandle(mFirCandleWidth, mFirCandleHeight);
- mFirCandle.initAnim();
- mSecCandle = new SecCandle(mRelativeX + mWidth / 2, mRelativeY + mHeight);
- mSecCandle.initCandle(mSecCandleWidth, mSecCandleHeight - 80);
- mSecCandle.initAnim();
***的***,就是我們的View了
- CandlesAnimView
- //16ms刷新Canvas
- mInvalidateAnimator = ValueAnimator.ofInt(0, 1).setDuration(16);
- mInvalidateAnimator.setRepeatCount(ValueAnimator.INFINITE);
- mInvalidateAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationRepeat(Animator animation) {
- invalidate();
- }
- });
- mInvalidateAnimator.start();
這個(gè)屬性動(dòng)畫履行的任務(wù)就是快速的刷新界面,是Candle的動(dòng)畫能夠及時(shí)顯示。
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = measureDimension(WIDTH_DEFAULT * mDensity,
- widthMeasureSpec); int height = measureDimension(HEIGHT_DEFAULT *mDensity,
- heightMeasureSpec);
- setMeasuredDimension(width, height);
- } public int measureDimension(int defaultSize, int measureSpec) { int result; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) {
- result = specSize;
- } else {
- result = defaultSize; if (specMode == MeasureSpec.AT_MOST) {
- result = Math.min(result, specSize);
- }
- } return result;
- } @Override
- protected void onDraw(Canvas canvas) { if (!mIsInit) {
- initConfig();
- mIsInit = true;
- }
- mAnimControler.drawMyView(canvas);
- }
可以看到***在view里面調(diào)用了我們的控制器,把cavas傳過去了。
***的tip:大家有沒有發(fā)現(xiàn)每個(gè)動(dòng)畫的duration都是一樣的。
三、***
好了至此,本來一個(gè)簡(jiǎn)單的view自定義被我說了這么多。初次在簡(jiǎn)書上寫,望大家多支持支持。
希望大家有什么建議和意見都可以提出。望斧正。