Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義Switch過程詳解

自定義Switch過程詳解

編輯:關於Android編程

前段時間,我看到了一篇關於Android動畫的文章,十分喜歡文章作者的筆風,可惜每個人的筆風都不同,不過我倒是實現了一個類似的Switch組件,項目地址為https://github.com/ztelur/FunSwitch,就用這篇文章來講述一下實現過程和機制吧。

簡介

?我的自定義Switch是模仿github上的LLSwitch,其UI設計來源於Dribbble,鏈接摸我,其效果圖如下

效果圖

自定義View需要重載的函數

?我們都知道以View為父類來自定義視圖需要重載一系列函數,下面我們就來按照調用順序來介紹一下這些函數。需要重載的函數列表如下:

onMeasure onSizeChanged onDraw onTouchEvent onSaveInstanceState onRestoreInstanceState

?首先就是onMeasure函數,用於確定自定義視圖的長和高。對於本文的Switch,我們讓其高為寬的固定比例大小就可以了,所以重構函數實現得十分簡單。這個函數確定的只是測量的長和高,並不是最終視圖所顯示的長和高

@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)之後要調用的onSizeChanged函數了。這個函數調用之後,draw函數就可能被調用,所以,一般我們在這個函數中計算繪制時所需要的數據。
?接著是draw函數,在這個函數中,我們繪制各種圖像來構成視圖的UI。需要注意的是,這個函數會被頻繁的調用,所以不要在函數內執行耗時的操作。
?最後是onTouchEvent函數,這個函數是用戶觸摸屏幕時才會被調用的,主要進行視圖的觸摸處理,由於我們的自定義Switch支持的觸摸事件比較簡單,只是支持點擊事件,所以此函數的實現也比較簡單。
?最後就是涉及到視圖狀態保存的兩個函數。我們都知道,一定情況下,activity會被銷毀,然後重新建立,比如你旋轉屏幕時。這個時候,你需要保存視圖的一些屬性數據,以備重新建立視圖時使用,來恢復之前的視圖。你需要注意的是,光重載這兩個函數還是不夠的,還需要設置View ID和調用setSaveEnabled函數
?我們接下來就一步一步的來實現這個自定義組件吧。

田徑場式背景

田徑場背景圖

?我們先來看一下這個Switch的背景,它是一個形如田徑場跑道的形狀,由兩個半圓和一個矩形組成,我們先來看一下如何來繪制出這樣的圖案。我們使用Path來構造出這樣的圖案,然後再進行繪制,代碼如下所示:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //TODO:還有padding的問題偶!!!
        mWidth = w;
        mHeight = h;
        float top = 0;
        float left = 0;
        float bottom = h*0.8f; //下邊預留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)這兒函數來繪制田徑場圖案。這個函數,需要傳入一個RectF對象,將要繪制的圓是這個對象所代表矩形的內切圓,我們只要計算出來這個矩形的上下左右四點的坐標就可以了。我們先計算繪制左側半圓所需要的矩形,然後函數後兩個參數為90,和180。注意的是,這個函數中,角度的正方向是順時針的,startAngle為90,也就是我們數學坐標系中角度為270所代表的方向。
?由於Path會自動連接繪制個點之間的連線,所以,我們只需要再繪制出右側半圓的曲線即可。
?我們只需要將繪制左側圓曲線的矩形進行一定距離的平移,就可以繪制出右側曲線。所以矩形的右邊界就等於整個視圖的right,由於矩形的長為bottom,所以矩形的左邊界就為right-bottom。然後再次調用arcTo函數,這次的起始角度就變成270了。
?最後調用Pathclose函數,讓上邊畫的兩段圓弧連接起來,就形成了上述的田徑場圖案。

繪制臉部圖形

臉部表情圖

?笑臉圖案看似復雜,其實就是幾個圖形組合在一起。首先是一個大圓,然後是裡邊的兩個橢圓型的眼睛,然後是嘴巴。我們只要在正確的位置將這些圖形繪制出來即可。
?和繪制背景圖形的順序類似,我們首先在onSizeChanged函數中進行相關函數的計算。
?首先是大圓臉的繪制,我們還是使用drawPath函數,只不過這次Path對象只繪制一個圓;而雙眼則是使用drawOval函數來花橢圓;最後使用drawRect來繪制矩形。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMyBpZD0="switch動畫">Switch動畫

?我們仔細查看自定義Switch的動畫效果,可以發現,主要涉及三部分的動畫效果:

背景顏色動畫轉變。 臉部圖形的平移和轉動(可以看出相當於臉部水平轉動了360度)。 臉部表情動畫,眨眼睛和嘴巴咧開。

?由於動畫涉及的操作比較多,所以我們選擇使用ValueAnimator+AnimatorUpdateListener的動畫實現方式,在onAnimationUpdate函數中記錄下來當前的animatedValue,然後調用invalidate函數來讓界面重繪,在繪制界面計算數據過程中,使用記錄下來的數值,從而產生動畫效果。

    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(); //產生動畫的關鍵步驟
    }

?所以,最終動畫問題又變成了繪制靜態圖像問題,我們根據不同的mAnimationFraction的值來繪制不同的圖像。
?接下來我們就來描述一下幾個比較關鍵的動畫的邏輯。

臉部轉動動畫

?其實這個臉部動畫還是比較難實現的,主要是轉動的這個效果沒有直接的API可以實現。我們的動畫只是讓用戶產生臉部轉動的假象。由於臉部圖案就是一個大圓加上充當眼睛和嘴巴的橢圓和矩形,我們可以讓眼睛和嘴巴向轉動方向平移,讓它們平移出大圓,然後在一定時間後從另外一個方向再平移進入大圓,最終回到原來位置。這樣就實現了一種臉部轉動的效果。
?如何讓眼睛和嘴巴移動到大圓邊緣就消失呢?而且是隨著移動漸漸的一部分一部分的消失呢?我們這裡使用了另外一種思路,使用clipPath函數,將畫布進行裁剪,只留下大圓范圍內的圖案。這樣的話,當眼睛和嘴巴移動出大圓時,就會逐漸消失。
?至於眼睛和嘴巴如何平移呢?大家首先想到的方法一定是根據mAnimationFraction來計算它們的位置,然後在相應位置上將它們繪制出來,但是這樣不是最優的方法,我們可以在繪制這些圖像時,對畫布進行平移,這樣的話,我們繪制眼睛和嘴巴的函數就不會涉及到mAnimationFraction,實現比較簡單。

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:合理的轉動區間,眼睛出現和消失的時間比為1:1,所以當fraction=0.25時,應該只顯示側臉,計算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);
    }

眨眼睛和變笑臉動畫

?眨眼睛動畫十分簡單,我們只要在繪制眼睛之前對畫布進行縮放即可,然後在繪制玩眼睛之後,在將畫布轉變回來。但是後來我發現,畫布縮放的中心點不容易確認,所以,采取了使用mAnimationValue計算橢圓數據的方式來進行橢圓大小的縮放。
?變笑臉動畫主要就是嘴巴的動畫效果,在靜止情況下,我們使用drawRect來繪制嘴部圖形;但在動畫過程中,我們使用drawPathquadTo來共同繪制嘴巴形狀。
?PathquadTo是用來繪制貝塞爾曲線。我們主要使用其二階曲線版本,即兩個數據點,一個控制點。我們計算出A,B這兩個數據點,也就是靜止狀態下矩形的左上點和右上點,然後根據mAnimationValue來計算控制點c的坐標,然後完成繪制。

貝塞爾曲線原理圖

?嘴部圖案的繪制如下所示。

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

總結

?其實還有一些細節問題我沒有在這篇文章上講出,一方面是因為講述起來太過復雜,還是大家自己查閱代碼比較好,另一方面是,我覺得自己實現的方式也不是很好,就不在這裡獻丑了。
?項目還沒有完全完成,比如自定義監聽器和自定義屬性的相關邏輯都沒有添加,希望感興趣的同學可以自行研究代碼並完善它。項目地址摸我我的github。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved