天天看點

JJSearchViewAnim源碼分析

項目位址:JJSearchViewAnim,本文分析版本: 3a19d94

1.簡介

JJSearchViewAnim源碼分析

JJSearchViewAnim是CJJ同學這周剛放出來的一個實作了各種搜尋互動動畫的動畫庫,一共實作了8種不同的搜尋互動動畫,短短4天

github

上的

star

就已經900+。可見此項目的受歡迎程度。我也第一時間把代碼

clone

下來看了一遍,并和

CJJ

交流了一些心得,這篇文章我們就來分析

JJSearchViewAnim

到底是如何實作的,以及該怎麼更好的運用的項目中去呢?

2.使用方法

JJSearchViewAnim

實作的效果部分如下,更詳細的請參照這裡:

JJSearchViewAnim源碼分析
JJSearchViewAnim源碼分析
JJSearchViewAnim源碼分析

JJSearchViewAnim

使用方法相當簡單:

1.先在布局檔案xml中聲明

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.cjj.jjsearchviewanim.MainActivity">

    <com.cjj.sva.JJSearchView
        android:id="@+id/jjsv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
 </RelativeLayout>
           

2.再在Java代碼中設定你需要顯示的動畫類型

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        JJSearchView mJJSearchView = (JJSearchView) findViewById(R.id.jjsv);
        mJJSearchView.setController(new JJChangeArrowController());
    }
           

3.設定動畫開啟及恢複

mJJSearchView.startAnim();
mJJSearchView.resetAnim();
           

3.類關系圖

JJSearchViewAnim源碼分析

從上面的類圖中可以清晰的看到

JJSearchViewAnim

的項目結構:

JJSearchView

是繼承自

View

的,其内部持有一個

JJBaseController

的對象,

JJBaseController

有8個子類的實作,應該就是對應這8個動畫的具體實作了。通過給

JJSearchView

設定不同的

Controller

就能實作對應的動畫效果了。

是不是覺得跟我們以前分析過的HTextView很相似?因為這兩個項目都使用了政策模式來設定不同的

Controller

進而實作不同的動畫效果,如果你以後也想開發這種類型的項目,那麼這種架構是相當适合你的。

4.源碼分析

在分析源碼之前,我們要知道其實所有的動畫無非是:

在規定的動畫持續時間内,在特定時間繪制出目前需要展示的畫面,并随時間變化而改變繪制的畫面進而形成動畫。

我們在開發中使用屬性動畫時,我們隻需要傳遞需要變換的參數和時間等等,

Android

已經為我們封裝好了繪制過程。但是如果我們需要開發例如上圖中的幾個效果的時候,屬性動畫已經不能滿足我們,這時候我們就要負責整個動畫每一幀的繪制了。這就要運用到自定義

View

,

Canvas

,

Paint

,

Path

PathMeasure

等等相關知識了。那到底該如何實作呢?下面我們就先從整體上分析

JJSearchView

的整體結構,然後再具體分析兩個動畫的具體實作,相信你看完之後就會明白。

1.JJSearchView的實作

JJSearchView

的部分代碼如下:

public class JJSearchView extends View {
    private JJBaseController mController = new JJChangeArrowController();

    public JJSearchView(Context context) {
        this(context, null);
    }

    public JJSearchView(Context context, AttributeSet attrs) {
        this(context, attrs, );
    }

    public void setController(JJBaseController controller) {
        this.mController = controller;
        mController.setSearchView(this);
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mController.draw(canvas, mPaint);
    }

    public void startAnim() {
        if (mController != null)
            mController.startAnim();
    }

    public void resetAnim() {
        if (mController != null)
            mController.resetAnim();
    }

}
           

可以看出在

onDraw()

方法中直接調用了

JJBaseController

draw()

方法說明具體的繪制都是交給

JJBaseController

的實作類去做的,同時又提供了

startAnim()

resetAnim()

也都是調用

mController

了對應方法。代碼很簡單就不再多說了,我們繼續來看看

JJBaseController

2.JJBaseController的實作

JJBaseController

的部分代碼如下:

public abstract class JJBaseController {

    public abstract void draw(Canvas canvas, Paint paint);

    //開啟搜尋動畫
    public void startAnim() {
    }

    //重置搜尋動畫
    public void resetAnim() {
    }

    public ValueAnimator startSearchViewAnim() {
        ValueAnimator valueAnimator = startSearchViewAnim(, , );
        return valueAnimator;
    }

    public ValueAnimator startSearchViewAnim(float startF, float endF, long time) {
        ValueAnimator valueAnimator =startSearchViewAnim(startF, endF, time, null);
        return valueAnimator;
    }

    public ValueAnimator startSearchViewAnim(float startF, float endF, long time, final PathMeasure pathMeasure) {
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(startF, endF);
        valueAnimator.setDuration(time);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mPro = (float) valueAnimator.getAnimatedValue();
                if (null != pathMeasure)
                    pathMeasure.getPosTan(mPro, mPos, null);
                getSearchView().invalidate();
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });
        if (!valueAnimator.isRunning()) {
            valueAnimator.start();
        }
        mPro = ;
        return valueAnimator;
    }
}
           

JJBaseController

是一個抽象類,

draw(Canvas canvas, Paint paint);

方法是一個抽象方法,是以實作類必須實作

draw()

方法來完成具體動畫的繪制,此外

startAnim()

resetAnim()

也是空方法子類可以重寫實作具體的功能。

值得注意的是這裡實作的一個

ValueAnimator

,從代碼上看上去是一個在500毫秒中從0-1的一個不斷變化的值,最後在

onAnimationUpdate()

回調方法中指派給了

mPro

并調用了

invalidate();

方法。從這裡應該可以看出,在

JJSearchViewAnim

中所有具體的

Controller

應該都是根據不斷變化的

mPro

的值來繪制對應的圖像,最終形成動畫。

此外

JJBaseController

還定義了三種狀态:

public static final int STATE_ANIM_NONE = ;
    public static final int STATE_ANIM_START = ;
    public static final int STATE_ANIM_STOP = ;

    @IntDef({STATE_ANIM_NONE,STATE_ANIM_START, STATE_ANIM_STOP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface State {
    }
           

分别對應目前

View

無動畫,動畫開始和動畫結束的狀态。下面我們就具體分析兩個

Controller

的具體實作,來看看具體怎麼實作的,分别是

JJAroundCircleBornTailController

JJCircleToLineAlphaController

3.JJAroundCircleBornTailController的實作

在看具體的實作之前,我們先看一下這個動畫的設計圖:

JJSearchViewAnim源碼分析

我們可以分解成兩種狀态:

  1. 正常狀态:就是一個搜尋放大鏡。
  2. 動畫狀态:圓弧形進度不斷圍繞圓環旋轉,并且進度完成之後放大鏡的把手不斷變長最終變成和正常狀态一樣。

再來看看具體的代碼實作:

public class JJAroundCircleBornTailController extends JJBaseController {
    private int mAngle = ;
    private RectF mRectF;
    private int cx, cy, cr;

    @Override
    public void draw(Canvas canvas, Paint paint) {
        //先設定一個背景
        canvas.drawColor(Color.parseColor(mColor));
        //根據目前狀态的不同調用不同的繪制方法
        switch (mState) {
            case STATE_ANIM_NONE:
                drawNormalView(paint, canvas);
                break;
            case STATE_ANIM_START:
                drawStartAnimView(paint, canvas);
                break;
            case STATE_ANIM_STOP:
                drawStopAnimView(paint, canvas);
                break;
        }
    }

    private void drawStartAnimView(Paint paint, Canvas canvas) {
        //設定paint的狀态
        paint.setAntiAlias(true);
        paint.setColor(Color.parseColor(mColorTran));
        paint.setStrokeWidth();
        paint.setStyle(Paint.Style.STROKE);
        canvas.rotate(, cx, cy);
        //繪制旋轉時的外環
        canvas.drawCircle(cx, cy, cr, paint);
        //給mRectF指派為圓形成的矩形的值
        mRectF.left = cx - cr;
        mRectF.right = cx + cr;
        mRectF.top = cy - cr;
        mRectF.bottom = cy + cr;

        //當mPro小于0.2時,繪制一個不斷變短的直線以及一個弧形
        if (mPro <= ) {
            canvas.drawLine(cx + cr, cy, cx + cr + cr * (f - mPro),
                    cy, paint);
            canvas.save();
            paint.setAntiAlias(true);
            paint.setColor(Color.WHITE);
            canvas.drawArc(mRectF, , -, false, paint);
            canvas.restore();
        } else if (mPro >  && mPro < ) {
            canvas.save();
            paint.setColor(Color.WHITE);
            //不斷增加mAngle的值
            mAngle += ;
            //不斷的旋轉畫布再繪制弧形,就可以形成旋轉進度
            canvas.rotate(mAngle, getWidth() / , getHeight() / );
            canvas.drawArc(mRectF, , mAngle / , false, paint);
            canvas.restore();
        } else {
            //當mPro的值大于4.5時
            canvas.save();
            paint.setAntiAlias(true);
            paint.setColor(Color.WHITE);
            paint.setStrokeWidth();
            paint.setStyle(Paint.Style.STROKE);
            //繪制出放大鏡的把手,這裡通過mPro來時把手的長度不斷增加
            canvas.drawLine(cx + cr, cy, cx + cr + cr * ((mPro - f) * ), cy, paint);
            canvas.drawCircle(cx, cy, cr, paint);
            canvas.restore();
        }

    }

    private void drawNormalView(Paint paint, Canvas canvas) {
        //cr 表示圓環半徑
        cr = getWidth() / ;
        //cx 表示圓心的x坐标
        cx = getWidth() / ;
        //cy 表示圓心得y坐标
        cy = getHeight() / ;
        paint.reset();
        paint.setAntiAlias(true);
        // 儲存目前canvas的狀态
        canvas.save();
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth();
        paint.setStyle(Paint.Style.STROKE);
        //将canvas旋轉45度
        canvas.rotate(, cx, cy);
        //畫斜線
        canvas.drawLine(cx + cr, cy, cx + cr * , cy, paint);
        //畫圓形
        canvas.drawCircle(cx, cy, cr, paint);
        //恢複canvas的狀态到上次save()方法調用的狀态
        canvas.restore();
    }

    @Override
    public void startAnim() {
        if (mState == STATE_ANIM_START) return;
        //設定狀态
        mState = STATE_ANIM_START;
        //開啟ValueAnimator
        startSearchViewAnim(, , );
    }

    @Override
    public void resetAnim() {
        if (mState == STATE_ANIM_STOP) return;
        mState = STATE_ANIM_STOP;
        mAngle = ;
        startSearchViewAnim();
    }
}
           

以上就是大部分

JJAroundCircleBornTailController

的代碼,由于這個動畫的初始狀态和完成動畫後的狀态是一樣的是以

drawStopAnimView(paint, canvas);

drawStartAnimView(paint, canvas);

方法是相同的實作,這裡就省略了。

從上面的代碼注釋中可以看出,當我們調用

startAnim()

方法時會通過

startSearchViewAnim(0, 5, 2000);

開啟

ValueAnimator

,這裡是在2000毫秒中将

mPro

的值從0-5勻速變換,然後再回調方法中又回不斷的調用

invalidate()

方法進而不斷調用

JJAroundCircleBornTailController

draw()

方法,進而就可以通過判斷

mPro

的值來繪制不同狀态的圖像。進而就達到了動畫效果。相當清晰的實作,下面讓我們來看看

JJCircleToLineAlphaController

是不是也是類似的實作方法呢?

4.JJCircleToLineAlphaController的實作

再看一下這次的動畫設計圖:

JJSearchViewAnim源碼分析

同樣可以分解成兩種狀态:

  1. 正常狀态: 一個放大鏡以及外面有一個圓環
  2. 動畫狀态: 整體不斷向右平移,并且圓環會不斷減少最後變為輸入框的橫線。

下面我們來看看具體實作:

public class JJCircleToLineAlphaController extends JJBaseController {
    private String mColor = "#673AB7";
    private int cx, cy, cr;
    private RectF mRectF, mRectF2;
    private float sign = f;
    private float tran = ;

    public JJCircleToLineAlphaController() {
        mRectF = new RectF();
        mRectF2 = new RectF();
    }

    @Override
    public void draw(Canvas canvas, Paint paint) {
        canvas.drawColor(Color.parseColor(mColor));
        switch (mState) {
            case STATE_ANIM_NONE:
                drawNormalView(paint, canvas);
                break;
            case STATE_ANIM_START:
                drawStartAnimView(paint, canvas);
                break;
            case STATE_ANIM_STOP:
                drawStopAnimView(paint, canvas);
                break;
        }
    }

    private void drawStopAnimView(Paint paint, Canvas canvas) {
        canvas.save();
        if (mPro > ) {
            paint.setAlpha((int) (mPro * ));
            drawNormalView(paint, canvas);
        }
        canvas.restore();
    }

    private void drawStartAnimView(Paint paint, Canvas canvas) {
        ...
    }

    private void drawNormalView(Paint paint, Canvas canvas) {
        ...
    }

    @Override
    public void startAnim() {
        if (mState == STATE_ANIM_START) return;
        mState = STATE_ANIM_START;
        startSearchViewAnim();
    }

    @Override
    public void resetAnim() {
        if (mState == STATE_ANIM_STOP) return;
        mState = STATE_ANIM_STOP;
        startSearchViewAnim();
    }
}
           

好像和第一個動畫是一個套路是嗎?是的,其實這些動畫經過我們的分析,無非是兩種或三種狀态,再根據動畫期間不斷變換的

mPro

的值再做具體的動畫就可以了,是以我們在具體看看這裡的

drawNormalView(Paint paint, Canvas canvas);

方法和

drawStartAnimView(Paint paint, Canvas canvas);

方法的實作:

private void drawNormalView(Paint paint, Canvas canvas) {
        //圓的半徑
        cr = getWidth() / ;
        //圓心x坐标
        cx = getWidth() / ;
        //圓心y坐标
        cy = getHeight() / ;
        //内圓所占的矩形區域
        mRectF.left = cx - cr;
        mRectF.right = cx + cr;
        mRectF.top = cy - cr;
        mRectF.bottom = cy + cr;
        //外圓所占的矩形局域
        mRectF2.left = cx -  * cr;
        mRectF2.right = cx +  * cr;
        mRectF2.top = cy -  * cr;
        mRectF2.bottom = cy +  * cr;

        canvas.save();
        paint.reset();
        paint.setAntiAlias(true);
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth();
        paint.setStyle(Paint.Style.STROKE);

        canvas.rotate(, cx, cy);
        //繪制放大鏡把手
        canvas.drawLine(cx + cr, cy, cx + cr * , cy, paint);
        //繪制内圓,也就是組成放大鏡的圓
        canvas.drawArc(mRectF, , , false, paint);
        //繪制外圓
        canvas.drawArc(mRectF2, , , false, paint);
        canvas.restore();
    }

    private void drawStartAnimView(Paint paint, Canvas canvas) {
        canvas.save();
        //根據目前的mRectF來繪制放大鏡的把手
        canvas.drawLine(mRectF.left + cr + (cr * sign), mRectF.top + cr + (cr * sign),
                mRectF.left + cr + ( * cr * sign), mRectF.top + cr + ( * cr * sign), paint);
        //繪制放大鏡的圓
        canvas.drawArc(mRectF, , , false, paint);
        //繪制外圓,由于mPro是從0-1不斷增加,這裡的繪制的角度就會不斷變小,
        //進而形成動畫
        canvas.drawArc(mRectF2, , - * ( - mPro), false, paint);
        //當mPro 大于 0.7時開始繪制橫線,會不斷變長
        if (mPro >= f) {
            canvas.drawLine(( - mPro + f) * (mRectF2.right -  * cr), mRectF2.bottom,
                    (mRectF2.right -  * cr), mRectF2.bottom, paint);
        }
        canvas.restore();
        //tran表示平移的距離,同樣會不斷變化然後再給兩個RectF指派
        mRectF.left = cx - cr + tran * mPro;
        mRectF.right = cx + cr + tran * mPro;
        mRectF2.left = cx -  * cr + tran * mPro;
        mRectF2.right = cx +  * cr + tran * mPro;
    }
           

注釋相當清晰,這裡就不再解釋了。看到了這裡大家應該都已經明白了

JJSearchViewAnim

具體是如何實作的了,而且也已經掌握了該如何開發此類動畫效果。但是我們學習這些動畫最終是想要應用到項目中去的,那麼拿剛剛這種動畫來說,目前在項目中應該是無法使用的,那麼我們怎麼才能又快又簡單的應用到項目中去呢?接下來我們就講如何簡單封裝

JJCircleToLineAlphaController

并實際應用到項目中去。

5.項目應用

再拿出這張設計圖。。。:

JJSearchViewAnim源碼分析

JJCircleToLineAlphaController

實作的動畫,看上出應該是本身是一個圓環加一個放大鏡,當我們點選之後,然後執行動畫,最終形成一個白色橫線的輸入框,當我們輸入文字之後,點選搜尋就應該可以進行搜尋了。

這裡我提供一個比較簡單的實作思路:就是我們自己實作一個布局将

EditText

和這個

JJSearchView

疊加放置,這裡要注意

EditText

的寬度要比

JJSearchView

要短,最好是一個放大鏡的寬度。首先隐藏

EditText

,點選

JJSearchView

執行動畫,然後顯示

EditText

再點選搜尋按鈕時,這時候我們通過

JJSearchView

的狀态來判斷是否需要搜尋.這樣就簡單的完成了一個帶動畫的

SearchView

實作了.下面我們大緻貼出具體的實作,大家也可以到我

fork

的分支上去檢視:位址在這

1.首先是布局檔案

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <com.cjj.sva.JJSearchView
        android:id="@+id/search_view"
        android:layout_width="200dp"
        android:layout_height="60dp"
        android:layout_centerInParent="true"/>

    <EditText
        android:id="@+id/edit_text"
        android:layout_width="150dp"
        android:layout_height="50dp"
        android:layout_alignParentLeft="true"
        android:background="@null"
        android:layout_centerInParent="true"
        android:textCursorDrawable="@null"
        android:textColor="@android:color/white"
        android:textSize="14sp"
        android:singleLine="true"
        android:visibility="invisible"
        android:layout_marginLeft="12dp"/>

</merge>
           

2.CircleSearchView的具體實作

public class CircleSearchView extends RelativeLayout {
    private Context mContext;
    private JJSearchView mSearchView;
    private EditText mEditText;

    public CircleSearchView(Context context) {
        this(context, null);
    }

    public CircleSearchView(Context context, AttributeSet attrs) {
        this(context, attrs, );
    }

    public CircleSearchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initLayout(context);
    }

    private void initLayout(Context context) {
        this.mContext = context;
        LayoutInflater.from(mContext).inflate(R.layout.view_circle_search, this);
        mSearchView = (JJSearchView) findViewById(R.id.search_view);
        mSearchView.setController(new JJCircleToLineAlphaController());
        mEditText = (EditText) findViewById(R.id.edit_text);

        mSearchView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mSearchView.getState() == JJBaseController.STATE_ANIM_NONE) {
                    mSearchView.startAnim();
                    mEditText.setVisibility(View.VISIBLE);
                    mEditText.bringToFront();
                } else if (mSearchView.getState() == JJBaseController.STATE_ANIM_START) {
                    Toast.makeText(mContext, "正在搜尋", Toast.LENGTH_LONG).show();
                }
            }
        });
    }
}
           

主要看

onClick(View v);

方法中的實作。

2.JJCircleToLineAlphaController的修改

要想很好的實作平移的距離以及最終橫線的長度,需要修改一些

JJCircleToLineAlphaController

中的方法,這裡就不再貼代碼了,有需要的可以去這裡檢視。

6.個人評價

JJSearchViewAnim

實作了多種酷炫的搜尋動畫,我們不僅能從項目裡學到大量的動畫相關的用法,更能學到如何去分解和思考一個動畫的實作。是一個非常值得我們學習的項目。不過項目中可能有一些數字或者變量沒有做注釋,有可能會影響代碼的閱讀,不過

CJJ

同學已經着手開始優化了,很快就會更新。

另外關于實作的這些效果并沒有辦法直接在項目中使用的問題,

CJJ

同學的初衷是想讓大家從項目中學習動畫實作的思路與技巧,修改之後放到自己的項目中去而不是做一個伸手黨總想直接使用最好。在修改的過程中更能提升自己的程式設計能力。這點我很贊同

CJJ

(其實我們就是懶=。=)。好了今天的文章就寫到這兒,如果有什麼想看的開源項目歡迎給我留言,如果是我目前的能力能分析好的開源項目我都會考慮去寫,最後謝謝大家。周末愉快^_^

我每周會寫一篇源代碼分析的文章,以後也可能會有其他主題.

如果你喜歡我寫的文章的話,歡迎關注我的新浪微網誌@達達達達sky

位址: http://weibo.com/u/2030683111

每周我會第一時間在微網誌分享我寫的文章,也會積極轉發更多有用的知識給大家.