自定義Switch過程詳解

作者: 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)用Pathclose函數(shù)卷扮,讓上邊畫的兩段圓弧連接起來荡澎,就形成了上述的田徑場圖案。

繪制臉部圖形

面部效果圖 ![quadTo.jpg](http://upload-images.jianshu.io/upload_images/623378-38a1da78b35edc0c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

?笑臉圖案看似復(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)畫過程中吧慢,我們使用drawPathquadTo來共同繪制嘴巴形狀涛漂。
?PathquadTo是用來繪制貝塞爾曲線,具體使用方法請查看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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末骚亿,一起剝皮案震驚了整個(gè)濱河市已亥,隨后出現(xiàn)的幾起案子熊赖,更是在濱河造成了極大的恐慌来屠,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件震鹉,死亡現(xiàn)場離奇詭異俱笛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)传趾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門迎膜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人浆兰,你說我怎么就攤上這事磕仅。” “怎么了簸呈?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵榕订,是天一觀的道長。 經(jīng)常有香客問我蜕便,道長劫恒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任轿腺,我火速辦了婚禮两嘴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘族壳。我一直安慰自己憔辫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布仿荆。 她就那樣靜靜地躺著贰您,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赖歌。 梳的紋絲不亂的頭發(fā)上枉圃,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機(jī)與錄音庐冯,去河邊找鬼孽亲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛展父,可吹牛的內(nèi)容都是我干的返劲。 我是一名探鬼主播玲昧,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼篮绿!你這毒婦竟也來了孵延?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤亲配,失蹤者是張志新(化名)和其女友劉穎尘应,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吼虎,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡犬钢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了思灰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玷犹。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖洒疚,靈堂內(nèi)的尸體忽然破棺而出歹颓,到底是詐尸還是另有隱情,我是刑警寧澤油湖,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布巍扛,位于F島的核電站,受9級(jí)特大地震影響肺魁,放射性物質(zhì)發(fā)生泄漏电湘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一鹅经、第九天 我趴在偏房一處隱蔽的房頂上張望寂呛。 院中可真熱鬧,春花似錦瘾晃、人聲如沸贷痪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽劫拢。三九已至,卻和暖如春强胰,著一層夾襖步出監(jiān)牢的瞬間舱沧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工偶洋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留熟吏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像牵寺,于是被迫代替她去往敵國和親悍引。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容