項(xiàng)目地址:JJSearchViewAnim,本文分析版本: 3a19d94
1.簡(jiǎn)介

JJSearchViewAnim是CJJ同學(xué)這周剛放出來(lái)的一個(gè)實(shí)現(xiàn)了各種搜索交互動(dòng)畫的動(dòng)畫庫(kù),一共實(shí)現(xiàn)了8種不同的搜索交互動(dòng)畫,短短4天github上的star就已經(jīng)900+??梢姶隧?xiàng)目的受歡迎程度。我也第一時(shí)間把代碼clone下來(lái)看了一遍,并和CJJ交流了一些心得,這篇文章我們就來(lái)分析JJSearchViewAnim到底是如何實(shí)現(xiàn)的,以及該怎么更好的運(yùn)用的項(xiàng)目中去呢?
2.使用方法
JJSearchViewAnim實(shí)現(xiàn)的效果部分如下,更詳細(xì)的請(qǐng)參照這里:



JJSearchViewAnim使用方法相當(dāng)簡(jiǎn)單:
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代碼中設(shè)置你需要顯示的動(dòng)畫類型
@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.設(shè)置動(dòng)畫開啟及恢復(fù)
mJJSearchView.startAnim();
mJJSearchView.resetAnim();
3.類關(guān)系圖

從上面的類圖中可以清晰的看到
JJSearchViewAnim的項(xiàng)目結(jié)構(gòu):
JJSearchView是繼承自View的,其內(nèi)部持有一個(gè)JJBaseController的對(duì)象,JJBaseController有8個(gè)子類的實(shí)現(xiàn),應(yīng)該就是對(duì)應(yīng)這8個(gè)動(dòng)畫的具體實(shí)現(xiàn)了。通過(guò)給JJSearchView設(shè)置不同的Controller就能實(shí)現(xiàn)對(duì)應(yīng)的動(dòng)畫效果了。
是不是覺得跟我們以前分析過(guò)的HTextView很相似?因?yàn)檫@兩個(gè)項(xiàng)目都使用了策略模式來(lái)設(shè)定不同的Controller從而實(shí)現(xiàn)不同的動(dòng)畫效果,如果你以后也想開發(fā)這種類型的項(xiàng)目,那么這種架構(gòu)是相當(dāng)適合你的。
4.源碼分析
在分析源碼之前,我們要知道其實(shí)所有的動(dòng)畫無(wú)非是:
在規(guī)定的動(dòng)畫持續(xù)時(shí)間內(nèi),在特定時(shí)間繪制出當(dāng)前需要展示的畫面,并隨時(shí)間變化而改變繪制的畫面從而形成動(dòng)畫。
我們?cè)陂_發(fā)中使用屬性動(dòng)畫時(shí),我們只需要傳遞需要變換的參數(shù)和時(shí)間等等,Android已經(jīng)為我們封裝好了繪制過(guò)程。但是如果我們需要開發(fā)例如上圖中的幾個(gè)效果的時(shí)候,屬性動(dòng)畫已經(jīng)不能滿足我們,這時(shí)候我們就要負(fù)責(zé)整個(gè)動(dòng)畫每一幀的繪制了。這就要運(yùn)用到自定義View,Canvas,Paint,Path和PathMeasure等等相關(guān)知識(shí)了。那到底該如何實(shí)現(xiàn)呢?下面我們就先從整體上分析JJSearchView的整體結(jié)構(gòu),然后再具體分析兩個(gè)動(dòng)畫的具體實(shí)現(xiàn),相信你看完之后就會(huì)明白。
1.JJSearchView的實(shí)現(xiàn)
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, 0);
}
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()方法中直接調(diào)用了JJBaseController的draw()方法說(shuō)明具體的繪制都是交給JJBaseController的實(shí)現(xiàn)類去做的,同時(shí)又提供了startAnim()和resetAnim()也都是調(diào)用mController了對(duì)應(yīng)方法。代碼很簡(jiǎn)單就不再多說(shuō)了,我們繼續(xù)來(lái)看看JJBaseController。
2.JJBaseController的實(shí)現(xiàn)
JJBaseController的部分代碼如下:
public abstract class JJBaseController {
public abstract void draw(Canvas canvas, Paint paint);
//開啟搜索動(dòng)畫
public void startAnim() {
}
//重置搜索動(dòng)畫
public void resetAnim() {
}
public ValueAnimator startSearchViewAnim() {
ValueAnimator valueAnimator = startSearchViewAnim(0, 1, 500);
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 = 0;
return valueAnimator;
}
}
JJBaseController是一個(gè)抽象類,draw(Canvas canvas, Paint paint);方法是一個(gè)抽象方法,所以實(shí)現(xiàn)類必須實(shí)現(xiàn)draw()方法來(lái)完成具體動(dòng)畫的繪制,此外startAnim()和resetAnim()也是空方法子類可以重寫實(shí)現(xiàn)具體的功能。
值得注意的是這里實(shí)現(xiàn)的一個(gè)ValueAnimator,從代碼上看上去是一個(gè)在500毫秒中從0-1的一個(gè)不斷變化的值,最后在onAnimationUpdate()回調(diào)方法中賦值給了mPro并調(diào)用了invalidate();方法。從這里應(yīng)該可以看出,在JJSearchViewAnim中所有具體的Controller應(yīng)該都是根據(jù)不斷變化的mPro的值來(lái)繪制對(duì)應(yīng)的圖像,最終形成動(dòng)畫。
此外JJBaseController還定義了三種狀態(tài):
public static final int STATE_ANIM_NONE = 0;
public static final int STATE_ANIM_START = 1;
public static final int STATE_ANIM_STOP = 2;
@IntDef({STATE_ANIM_NONE,STATE_ANIM_START, STATE_ANIM_STOP})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
}
分別對(duì)應(yīng)當(dāng)前View無(wú)動(dòng)畫,動(dòng)畫開始和動(dòng)畫結(jié)束的狀態(tài)。下面我們就具體分析兩個(gè)Controller的具體實(shí)現(xiàn),來(lái)看看具體怎么實(shí)現(xiàn)的,分別是JJAroundCircleBornTailController與JJCircleToLineAlphaController。
3.JJAroundCircleBornTailController的實(shí)現(xiàn)
在看具體的實(shí)現(xiàn)之前,我們先看一下這個(gè)動(dòng)畫的設(shè)計(jì)圖:

我們可以分解成兩種狀態(tài):
- 正常狀態(tài):就是一個(gè)搜索放大鏡。
- 動(dòng)畫狀態(tài):圓弧形進(jìn)度不斷圍繞圓環(huán)旋轉(zhuǎn),并且進(jìn)度完成之后放大鏡的把手不斷變長(zhǎng)最終變成和正常狀態(tài)一樣。
再來(lái)看看具體的代碼實(shí)現(xiàn):
public class JJAroundCircleBornTailController extends JJBaseController {
private int mAngle = 10;
private RectF mRectF;
private int cx, cy, cr;
@Override
public void draw(Canvas canvas, Paint paint) {
//先設(shè)置一個(gè)背景
canvas.drawColor(Color.parseColor(mColor));
//根據(jù)當(dāng)前狀態(tài)的不同調(diào)用不同的繪制方法
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) {
//設(shè)置paint的狀態(tài)
paint.setAntiAlias(true);
paint.setColor(Color.parseColor(mColorTran));
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.STROKE);
canvas.rotate(45, cx, cy);
//繪制旋轉(zhuǎn)時(shí)的外環(huán)
canvas.drawCircle(cx, cy, cr, paint);
//給mRectF賦值為圓形成的矩形的值
mRectF.left = cx - cr;
mRectF.right = cx + cr;
mRectF.top = cy - cr;
mRectF.bottom = cy + cr;
//當(dāng)mPro小于0.2時(shí),繪制一個(gè)不斷變短的直線以及一個(gè)弧形
if (mPro <= 0.2) {
canvas.drawLine(cx + cr, cy, cx + cr + cr * (.2f - mPro),
cy, paint);
canvas.save();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
canvas.drawArc(mRectF, 6, -14, false, paint);
canvas.restore();
} else if (mPro > 0.2 && mPro < 4.5) {
canvas.save();
paint.setColor(Color.WHITE);
//不斷增加mAngle的值
mAngle += 20;
//不斷的旋轉(zhuǎn)畫布再繪制弧形,就可以形成旋轉(zhuǎn)進(jìn)度
canvas.rotate(mAngle, getWidth() / 2, getHeight() / 2);
canvas.drawArc(mRectF, 0, mAngle / 4, false, paint);
canvas.restore();
} else {
//當(dāng)mPro的值大于4.5時(shí)
canvas.save();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(14);
paint.setStyle(Paint.Style.STROKE);
//繪制出放大鏡的把手,這里通過(guò)mPro來(lái)時(shí)把手的長(zhǎng)度不斷增加
canvas.drawLine(cx + cr, cy, cx + cr + cr * ((mPro - 4.5f) * 2), cy, paint);
canvas.drawCircle(cx, cy, cr, paint);
canvas.restore();
}
}
private void drawNormalView(Paint paint, Canvas canvas) {
//cr 表示圓環(huán)半徑
cr = getWidth() / 15;
//cx 表示圓心的x坐標(biāo)
cx = getWidth() / 2;
//cy 表示圓心得y坐標(biāo)
cy = getHeight() / 2;
paint.reset();
paint.setAntiAlias(true);
// 保存當(dāng)前canvas的狀態(tài)
canvas.save();
paint.setColor(Color.WHITE);
paint.setStrokeWidth(14);
paint.setStyle(Paint.Style.STROKE);
//將canvas旋轉(zhuǎn)45度
canvas.rotate(45, cx, cy);
//畫斜線
canvas.drawLine(cx + cr, cy, cx + cr * 2, cy, paint);
//畫圓形
canvas.drawCircle(cx, cy, cr, paint);
//恢復(fù)canvas的狀態(tài)到上次save()方法調(diào)用的狀態(tài)
canvas.restore();
}
@Override
public void startAnim() {
if (mState == STATE_ANIM_START) return;
//設(shè)置狀態(tài)
mState = STATE_ANIM_START;
//開啟ValueAnimator
startSearchViewAnim(0, 5, 2000);
}
@Override
public void resetAnim() {
if (mState == STATE_ANIM_STOP) return;
mState = STATE_ANIM_STOP;
mAngle = 0;
startSearchViewAnim();
}
}
以上就是大部分JJAroundCircleBornTailController的代碼,由于這個(gè)動(dòng)畫的初始狀態(tài)和完成動(dòng)畫后的狀態(tài)是一樣的所以drawStopAnimView(paint, canvas);和drawStartAnimView(paint, canvas);方法是相同的實(shí)現(xiàn),這里就省略了。
從上面的代碼注釋中可以看出,當(dāng)我們調(diào)用startAnim()方法時(shí)會(huì)通過(guò)startSearchViewAnim(0, 5, 2000);開啟ValueAnimator,這里是在2000毫秒中將mPro的值從0-5勻速變換,然后再回調(diào)方法中又回不斷的調(diào)用invalidate()方法從而不斷調(diào)用JJAroundCircleBornTailController的draw()方法,進(jìn)而就可以通過(guò)判斷mPro的值來(lái)繪制不同狀態(tài)的圖像。從而就達(dá)到了動(dòng)畫效果。相當(dāng)清晰的實(shí)現(xiàn),下面讓我們來(lái)看看JJCircleToLineAlphaController是不是也是類似的實(shí)現(xiàn)方法呢?
4.JJCircleToLineAlphaController的實(shí)現(xiàn)
再看一下這次的動(dòng)畫設(shè)計(jì)圖:

同樣可以分解成兩種狀態(tài):
- 正常狀態(tài): 一個(gè)放大鏡以及外面有一個(gè)圓環(huán)
- 動(dòng)畫狀態(tài): 整體不斷向右平移,并且圓環(huán)會(huì)不斷減少最后變?yōu)檩斎肟虻臋M線。
下面我們來(lái)看看具體實(shí)現(xiàn):
public class JJCircleToLineAlphaController extends JJBaseController {
private String mColor = "#673AB7";
private int cx, cy, cr;
private RectF mRectF, mRectF2;
private float sign = 0.707f;
private float tran = 120;
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 > 0.7) {
paint.setAlpha((int) (mPro * 255));
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();
}
}
好像和第一個(gè)動(dòng)畫是一個(gè)套路是嗎?是的,其實(shí)這些動(dòng)畫經(jīng)過(guò)我們的分析,無(wú)非是兩種或三種狀態(tài),再根據(jù)動(dòng)畫期間不斷變換的mPro的值再做具體的動(dòng)畫就可以了,所以我們?cè)诰唧w看看這里的drawNormalView(Paint paint, Canvas canvas);方法和drawStartAnimView(Paint paint, Canvas canvas);方法的實(shí)現(xiàn):
private void drawNormalView(Paint paint, Canvas canvas) {
//圓的半徑
cr = getWidth() / 50;
//圓心x坐標(biāo)
cx = getWidth() / 2;
//圓心y坐標(biāo)
cy = getHeight() / 2;
//內(nèi)圓所占的矩形區(qū)域
mRectF.left = cx - cr;
mRectF.right = cx + cr;
mRectF.top = cy - cr;
mRectF.bottom = cy + cr;
//外圓所占的矩形局域
mRectF2.left = cx - 3 * cr;
mRectF2.right = cx + 3 * cr;
mRectF2.top = cy - 3 * cr;
mRectF2.bottom = cy + 3 * cr;
canvas.save();
paint.reset();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
canvas.rotate(45, cx, cy);
//繪制放大鏡把手
canvas.drawLine(cx + cr, cy, cx + cr * 2, cy, paint);
//繪制內(nèi)圓,也就是組成放大鏡的圓
canvas.drawArc(mRectF, 0, 360, false, paint);
//繪制外圓
canvas.drawArc(mRectF2, 0, 360, false, paint);
canvas.restore();
}
private void drawStartAnimView(Paint paint, Canvas canvas) {
canvas.save();
//根據(jù)當(dāng)前的mRectF來(lái)繪制放大鏡的把手
canvas.drawLine(mRectF.left + cr + (cr * sign), mRectF.top + cr + (cr * sign),
mRectF.left + cr + (2 * cr * sign), mRectF.top + cr + (2 * cr * sign), paint);
//繪制放大鏡的圓
canvas.drawArc(mRectF, 0, 360, false, paint);
//繪制外圓,由于mPro是從0-1不斷增加,這里的繪制的角度就會(huì)不斷變小,
//從而形成動(dòng)畫
canvas.drawArc(mRectF2, 90, -360 * (1 - mPro), false, paint);
//當(dāng)mPro 大于 0.7時(shí)開始繪制橫線,會(huì)不斷變長(zhǎng)
if (mPro >= 0.7f) {
canvas.drawLine((1 - mPro + 0.7f) * (mRectF2.right - 3 * cr), mRectF2.bottom,
(mRectF2.right - 3 * cr), mRectF2.bottom, paint);
}
canvas.restore();
//tran表示平移的距離,同樣會(huì)不斷變化然后再給兩個(gè)RectF賦值
mRectF.left = cx - cr + tran * mPro;
mRectF.right = cx + cr + tran * mPro;
mRectF2.left = cx - 3 * cr + tran * mPro;
mRectF2.right = cx + 3 * cr + tran * mPro;
}
注釋相當(dāng)清晰,這里就不再解釋了??吹搅诉@里大家應(yīng)該都已經(jīng)明白了JJSearchViewAnim具體是如何實(shí)現(xiàn)的了,而且也已經(jīng)掌握了該如何開發(fā)此類動(dòng)畫效果。但是我們學(xué)習(xí)這些動(dòng)畫最終是想要應(yīng)用到項(xiàng)目中去的,那么拿剛剛這種動(dòng)畫來(lái)說(shuō),目前在項(xiàng)目中應(yīng)該是無(wú)法使用的,那么我們?cè)趺床拍苡挚煊趾?jiǎn)單的應(yīng)用到項(xiàng)目中去呢?接下來(lái)我們就講如何簡(jiǎn)單封裝JJCircleToLineAlphaController并實(shí)際應(yīng)用到項(xiàng)目中去。
5.項(xiàng)目應(yīng)用
再拿出這張?jiān)O(shè)計(jì)圖。。。:

JJCircleToLineAlphaController實(shí)現(xiàn)的動(dòng)畫,看上出應(yīng)該是本身是一個(gè)圓環(huán)加一個(gè)放大鏡,當(dāng)我們點(diǎn)擊之后,然后執(zhí)行動(dòng)畫,最終形成一個(gè)白色橫線的輸入框,當(dāng)我們輸入文字之后,點(diǎn)擊搜索就應(yīng)該可以進(jìn)行搜索了。
這里我提供一個(gè)比較簡(jiǎn)單的實(shí)現(xiàn)思路:就是我們自己實(shí)現(xiàn)一個(gè)布局將EditText和這個(gè)JJSearchView疊加放置,這里要注意EditText的寬度要比JJSearchView要短,最好是一個(gè)放大鏡的寬度。首先隱藏EditText,點(diǎn)擊JJSearchView執(zhí)行動(dòng)畫,然后顯示EditText再點(diǎn)擊搜索按鈕時(shí),這時(shí)候我們通過(guò)JJSearchView的狀態(tài)來(lái)判斷是否需要搜索.這樣就簡(jiǎn)單的完成了一個(gè)帶動(dòng)畫的SearchView實(shí)現(xiàn)了.下面我們大致貼出具體的實(shí)現(xiàn),大家也可以到我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的具體實(shí)現(xiàn)
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, 0);
}
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);方法中的實(shí)現(xiàn)。
2.JJCircleToLineAlphaController的修改
要想很好的實(shí)現(xiàn)平移的距離以及最終橫線的長(zhǎng)度,需要修改一些JJCircleToLineAlphaController中的方法,這里就不再貼代碼了,有需要的可以去這里查看。
6.個(gè)人評(píng)價(jià)
JJSearchViewAnim實(shí)現(xiàn)了多種酷炫的搜索動(dòng)畫,我們不僅能從項(xiàng)目里學(xué)到大量的動(dòng)畫相關(guān)的用法,更能學(xué)到如何去分解和思考一個(gè)動(dòng)畫的實(shí)現(xiàn)。是一個(gè)非常值得我們學(xué)習(xí)的項(xiàng)目。不過(guò)項(xiàng)目中可能有一些數(shù)字或者變量沒有做注釋,有可能會(huì)影響代碼的閱讀,不過(guò)CJJ同學(xué)已經(jīng)著手開始優(yōu)化了,很快就會(huì)更新。
另外關(guān)于實(shí)現(xiàn)的這些效果并沒有辦法直接在項(xiàng)目中使用的問(wèn)題,CJJ同學(xué)的初衷是想讓大家從項(xiàng)目中學(xué)習(xí)動(dòng)畫實(shí)現(xiàn)的思路與技巧,修改之后放到自己的項(xiàng)目中去而不是做一個(gè)伸手黨總想直接使用最好。在修改的過(guò)程中更能提升自己的編程能力。這點(diǎn)我很贊同CJJ(其實(shí)我們就是懶=。=)。好了今天的文章就寫到這兒,如果有什么想看的開源項(xiàng)目歡迎給我留言,如果是我目前的能力能分析好的開源項(xiàng)目我都會(huì)考慮去寫,最后謝謝大家。周末愉快_
我每周會(huì)寫一篇源代碼分析的文章,以后也可能會(huì)有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達(dá)達(dá)達(dá)達(dá)sky
地址: http://weibo.com/u/2030683111
每周我會(huì)第一時(shí)間在微博分享我寫的文章,也會(huì)積極轉(zhuǎn)發(fā)更多有用的知識(shí)給大家.