作者: remcarpediem
聯(lián)系方式:segmentfault稽寒,csdn,簡書
本文轉(zhuǎn)載請注明作者趟章、文章來源杏糙,鏈接,版權(quán)歸作者所有蚓土。
?前段時(shí)間宏侍,我看到了一篇關(guān)于Android動(dòng)畫的文章Android View 仿iOS SwitchButton Material Design,十分喜歡文章作者的筆風(fēng)北戏,可惜每個(gè)人的筆風(fēng)都不同负芋,不過我倒是實(shí)現(xiàn)了一個(gè)類似的Switch組件,項(xiàng)目地址為https://github.com/ztelur/FunSwitch,就用這篇文章來講述一下實(shí)現(xiàn)過程和機(jī)制吧嗜愈。
簡介
?我的自定義Switch是模仿github上的LLSwitch,其UI設(shè)計(jì)來源于Dribbble,鏈接摸我,其效果圖如下
自定義View需要重載的函數(shù)
?我們都知道以View
為父類來自定義視圖需要重載一系列函數(shù)莽龟,下面我們就來按照調(diào)用順序來介紹一下這些函數(shù)蠕嫁。需要重載的函數(shù)列表如下:
onMeasure
onSizeChanged
onDraw
onTouchEvent
onSaveInstanceState
onRestoreInstanceState
?首先就是onMeasure
函數(shù),用于確定自定義視圖的長和高毯盈。對于本文的Switch,我們讓其高為寬的固定比例大小就可以了剃毒,所以重構(gòu)函數(shù)實(shí)現(xiàn)得十分簡單。這個(gè)函數(shù)確定的只是測量的長和高,并不是最終視圖所顯示的長和高赘阀。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int) (width * DEFAULT_WIDTH_HEIGHT_PERCENT);
setMeasuredDimension(width,height);
}
?然后就是視圖確定真正大小(onLayout
)之后要調(diào)用的onSizeChanged
函數(shù)了益缠。這個(gè)函數(shù)調(diào)用之后,draw
函數(shù)就可能被調(diào)用基公,所以幅慌,一般我們在這個(gè)函數(shù)中計(jì)算繪制時(shí)所需要的數(shù)據(jù)。
?接著是draw
函數(shù)轰豆,在這個(gè)函數(shù)中胰伍,我們繪制各種圖像來構(gòu)成視圖的UI。需要注意的是酸休,這個(gè)函數(shù)會(huì)被頻繁的調(diào)用骂租,所以不要在函數(shù)內(nèi)執(zhí)行耗時(shí)的操作。
?最后是onTouchEvent
函數(shù)斑司,這個(gè)函數(shù)是用戶觸摸屏幕時(shí)才會(huì)被調(diào)用的渗饮,主要進(jìn)行視圖的觸摸處理,由于我們的自定義Switch支持的觸摸事件比較簡單宿刮,只是支持點(diǎn)擊事件抽米,所以此函數(shù)的實(shí)現(xiàn)也比較簡單。
?最后就是涉及到視圖狀態(tài)保存的兩個(gè)函數(shù)糙置。我們都知道云茸,一定情況下,activity會(huì)被銷毀谤饭,然后重新建立标捺,比如你旋轉(zhuǎn)屏幕時(shí)。這個(gè)時(shí)候揉抵,你需要保存視圖的一些屬性數(shù)據(jù)亡容,以備重新建立視圖時(shí)使用,來恢復(fù)之前的視圖冤今。你需要注意的是闺兢,光重載這兩個(gè)函數(shù)還是不夠的,還需要設(shè)置View ID和調(diào)用setSaveEnabled函數(shù)戏罢。
?我們接下來就一步一步的來實(shí)現(xiàn)這個(gè)自定義組件吧屋谭。
田徑場式背景
?我們先來看一下這個(gè)Switch的背景,它是一個(gè)形如田徑場跑道的形狀龟糕,由兩個(gè)半圓和一個(gè)矩形組成桐磁,我們先來看一下如何來繪制出這樣的圖案。我們使用Path
來構(gòu)造出這樣的圖案讲岁,然后再進(jìn)行繪制我擂,代碼如下所示:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//TODO:還有padding的問題偶3囊浴!校摩!
mWidth = w;
mHeight = h;
float top = 0;
float left = 0;
float bottom = h*0.8f; //下邊預(yù)留0.2空間來畫陰影
float right = w;
RectF backgroundRecf = new RectF(left,top,bottom,bottom);
mBackgroundPath = new Path();
//TODO:???????????
mBackgroundPath.arcTo(backgroundRecf,90,180);
backgroundRecf.left = right - bottom;
backgroundRecf.right = right;
mBackgroundPath.arcTo(backgroundRecf,270,180);
mBackgroundPath.close();
........
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBackground(canvas);
drawForeground(canvas);
}
private void drawBackground(Canvas canvas) {
mPaint.setColor(mCurrentColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mBackgroundPath,mPaint);
mPaint.reset();
}
?我們使用arcTo(RectF oval, float startAngle, float sweepAngle)
這兒函數(shù)來繪制田徑場圖案看峻。這個(gè)函數(shù),需要傳入一個(gè)RectF對象衙吩,將要繪制的圓是這個(gè)對象所代表矩形的內(nèi)切圓互妓,我們只要計(jì)算出來這個(gè)矩形的上下左右四點(diǎn)的坐標(biāo)就可以了。我們先計(jì)算繪制左側(cè)半圓所需要的矩形分井,然后函數(shù)后兩個(gè)參數(shù)為90,和180车猬。注意的是,這個(gè)函數(shù)中尺锚,角度的正方向是順時(shí)針的珠闰,startAngle
為90,也就是我們數(shù)學(xué)坐標(biāo)系中角度為270所代表的方向。
?由于Path會(huì)自動(dòng)連接繪制個(gè)點(diǎn)之間的連線瘫辩,所以伏嗜,我們只需要再繪制出右側(cè)半圓的曲線即可。
?我們只需要將繪制左側(cè)圓曲線的矩形進(jìn)行一定距離的平移伐厌,就可以繪制出右側(cè)曲線承绸。所以矩形的右邊界就等于整個(gè)視圖的right
,由于矩形的長為bottom
,所以矩形的左邊界就為right-bottom
挣轨。然后再次調(diào)用arcTo
函數(shù)军熏,這次的起始角度就變成270了。
?最后調(diào)用Path
的close
函數(shù)卷扮,讓上邊畫的兩段圓弧連接起來荡澎,就形成了上述的田徑場圖案。
繪制臉部圖形
?笑臉圖案看似復(fù)雜晤锹,其實(shí)就是幾個(gè)圖形組合在一起摩幔。首先是一個(gè)大圓,然后是里邊的兩個(gè)橢圓型的眼睛鞭铆,然后是嘴巴或衡。我們只要在正確的位置將這些圖形繪制出來即可。
?和繪制背景圖形的順序類似车遂,我們首先在onSizeChanged
函數(shù)中進(jìn)行相關(guān)函數(shù)的計(jì)算封断。
?首先是大圓臉的繪制,我們還是使用drawPath
函數(shù)艰额,只不過這次Path
對象只繪制一個(gè)圓澄港;而雙眼則是使用drawOval
函數(shù)來花橢圓;最后使用drawRect
來繪制矩形柄沮。
Switch動(dòng)畫
?我們仔細(xì)查看自定義Switch的動(dòng)畫效果,可以發(fā)現(xiàn),主要涉及三部分的動(dòng)畫效果:
- 背景顏色動(dòng)畫轉(zhuǎn)變祖搓。
- 臉部圖形的平移和轉(zhuǎn)動(dòng)(可以看出相當(dāng)于臉部水平轉(zhuǎn)動(dòng)了360度)狱意。
- 臉部表情動(dòng)畫,眨眼睛和嘴巴咧開拯欧。
?由于動(dòng)畫涉及的操作比較多详囤,所以我們選擇使用ValueAnimator
+AnimatorUpdateListener
的動(dòng)畫實(shí)現(xiàn)方式,在onAnimationUpdate
函數(shù)中記錄下來當(dāng)前的animatedValue
镐作,然后調(diào)用invalidate
函數(shù)來讓界面重繪藏姐,在繪制界面計(jì)算數(shù)據(jù)過程中,使用記錄下來的數(shù)值该贾,從而產(chǎn)生動(dòng)畫效果羔杨。
private void startCloseAnimation() {
mValueAnimator = ValueAnimator.ofFloat(NORMAL_ANIM_MAX_FRACTION,0);
mValueAnimator.setDuration(mOffAnimationDuration);
mValueAnimator.addUpdateListener(this);
mValueAnimator.addListener(this);
mValueAnimator.setInterpolator(mInterpolator);
mValueAnimator.start();
startColorAnimation();
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimationFraction = (float)animation.getAnimatedValue();
invalidate(); //產(chǎn)生動(dòng)畫的關(guān)鍵步驟
}
?所以,最終動(dòng)畫問題又變成了繪制靜態(tài)圖像問題杨蛋,我們根據(jù)不同的mAnimationFraction
的值來繪制不同的圖像兜材。
?接下來我們就來描述一下幾個(gè)比較關(guān)鍵的動(dòng)畫的邏輯。
臉部轉(zhuǎn)動(dòng)動(dòng)畫
?其實(shí)這個(gè)臉部動(dòng)畫還是比較難實(shí)現(xiàn)的逞力,主要是轉(zhuǎn)動(dòng)的這個(gè)效果沒有直接的API可以實(shí)現(xiàn)曙寡。我們的動(dòng)畫只是讓用戶產(chǎn)生臉部轉(zhuǎn)動(dòng)的假象。由于臉部圖案就是一個(gè)大圓加上充當(dāng)眼睛和嘴巴的橢圓和矩形寇荧,我們可以讓眼睛和嘴巴向轉(zhuǎn)動(dòng)方向平移举庶,讓它們平移出大圓,然后在一定時(shí)間后從另外一個(gè)方向再平移進(jìn)入大圓揩抡,最終回到原來位置户侥。這樣就實(shí)現(xiàn)了一種臉部轉(zhuǎn)動(dòng)的效果。
?如何讓眼睛和嘴巴移動(dòng)到大圓邊緣就消失呢捅膘?而且是隨著移動(dòng)漸漸的一部分一部分的消失呢添祸?我們這里使用了另外一種思路,使用clipPath
函數(shù)寻仗,將畫布進(jìn)行裁剪刃泌,只留下大圓范圍內(nèi)的圖案。這樣的話署尤,當(dāng)眼睛和嘴巴移動(dòng)出大圓時(shí)耙替,就會(huì)逐漸消失。
?至于眼睛和嘴巴如何平移呢曹体?大家首先想到的方法一定是根據(jù)mAnimationFraction
來計(jì)算它們的位置俗扇,然后在相應(yīng)位置上將它們繪制出來,但是這樣不是最優(yōu)的方法箕别,我們可以在繪制這些圖像時(shí)铜幽,對畫布進(jìn)行平移滞谢,這樣的話,我們繪制眼睛和嘴巴的函數(shù)就不會(huì)涉及到mAnimationFraction
除抛,實(shí)現(xiàn)比較簡單狮杨。
public void drawFace(Canvas canvas,float fraction) {
mPaint.setAntiAlias(true);
//面部背景
mPaint.setColor(mFaceColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mFacePath,mPaint);
//先裁剪并平移畫布,然后再繪制眼部五官
translateAndClipFace(canvas,fraction);
drawEye(canvas,fraction);
drawMouth(canvas,fraction);
}
private void translateAndClipFace(Canvas canvas,float fraction) {
//截掉超出face的部分到忽。
canvas.clipPath(mFacePath);
float faceTransition ;
//TODO:合理的轉(zhuǎn)動(dòng)區(qū)間橄教,眼睛出現(xiàn)和消失的時(shí)間比為1:1,所以當(dāng)fraction=0.25時(shí),應(yīng)該只顯示側(cè)臉,計(jì)算faceTransition喘漏。
if (fraction >=0.0f && fraction <0.5f) {
faceTransition = fraction * mFaceRadius *4;
} else if (fraction <=NORMAL_ANIM_MAX_FRACTION){
faceTransition = - (NORMAL_ANIM_MAX_FRACTION - fraction) * mFaceRadius * 4;
} else if (fraction <=(NORMAL_ANIM_MAX_FRACTION+FACE_ANIM_MAX_FRACTION)/2) {
faceTransition = (fraction - NORMAL_ANIM_MAX_FRACTION) * mFaceRadius * 2;
} else {
faceTransition = (FACE_ANIM_MAX_FRACTION - fraction) * mFaceRadius * 2;
}
canvas.translate(faceTransition,0);
}
眨眼睛和變笑臉動(dòng)畫
?眨眼睛動(dòng)畫十分簡單护蝶,我們只要在繪制眼睛之前對畫布進(jìn)行縮放即可,然后在繪制玩眼睛之后翩迈,在將畫布轉(zhuǎn)變回來持灰。但是后來我發(fā)現(xiàn),畫布縮放的中心點(diǎn)不容易確認(rèn)帽馋,所以搅方,采取了使用mAnimationValue
計(jì)算橢圓數(shù)據(jù)的方式來進(jìn)行橢圓大小的縮放。
?變笑臉動(dòng)畫主要就是嘴巴的動(dòng)畫效果绽族,在靜止情況下姨涡,我們使用drawRect
來繪制嘴部圖形;但在動(dòng)畫過程中吧慢,我們使用drawPath
和quadTo
來共同繪制嘴巴形狀涛漂。
?Path
的quadTo
是用來繪制貝塞爾曲線,具體使用方法請查看Path之貝塞爾曲線检诗。我們主要使用其二階曲線版本匈仗,即兩個(gè)數(shù)據(jù)點(diǎn),一個(gè)控制點(diǎn)逢慌。我們計(jì)算出A,B這兩個(gè)數(shù)據(jù)點(diǎn)悠轩,也就是靜止?fàn)顟B(tài)下矩形的左上點(diǎn)和右上點(diǎn),然后根據(jù)mAnimationValue
來計(jì)算控制點(diǎn)c的坐標(biāo)攻泼,然后完成繪制火架。
?嘴部圖案的繪制如下所示。
private void drawMouth(Canvas canvas,float fraction) {
.......
//嘴巴
if (fraction <=0.75) { //
canvas.drawRect(mouthLeft, mouthTop, mouthLeft + mouthWidth, mouthTop + mouthHeight, mPaint);
} else {
Path path = new Path();
path.moveTo(mouthLeft,mouthTop);
float controlX = mouthLeft + mouthWidth/2;
float controlY = mouthTop + mouthHeight + mouthHeight * 15 * (fraction - 0.75f);
path.quadTo(controlX,controlY,mouthLeft+mouthWidth,mouthTop);
path.close();
canvas.drawPath(path,mPaint);
}
}
總結(jié)
?其實(shí)還有一些細(xì)節(jié)問題我沒有在這篇文章上講出忙菠,一方面是因?yàn)橹v述起來太過復(fù)雜何鸡,還是大家自己查閱代碼比較好,另一方面是牛欢,我覺得自己實(shí)現(xiàn)的方式也不是很好骡男,就不在這里獻(xiàn)丑了。
?項(xiàng)目還沒有完全完成傍睹,比如自定義監(jiān)聽器和自定義屬性的相關(guān)邏輯都沒有添加隔盛,希望感興趣的同學(xué)可以自行研究代碼并完善它犹菱。項(xiàng)目地址摸我我的github。