前言##
通過(guò)之前的《Android 動(dòng)畫(huà)總結(jié)》,對(duì)常用的Android動(dòng)畫(huà)有了一個(gè)整體認(rèn)識(shí)。但是,之前的內(nèi)容都是概念性的,所列的demo也沒(méi)有實(shí)際意義。這里就通過(guò)兩個(gè)實(shí)例了解一下如何在 實(shí)際開(kāi)發(fā)中運(yùn)用Android 動(dòng)畫(huà)來(lái)實(shí)現(xiàn)一些良好的用戶(hù)體驗(yàn)。
這里通過(guò)展示兩個(gè)常見(jiàn)且較為容易實(shí)現(xiàn)的動(dòng)畫(huà)效果:
仿支付寶支付完成動(dòng)畫(huà)
購(gòu)物車(chē)添加商品動(dòng)畫(huà)
動(dòng)畫(huà)實(shí)戰(zhàn)##
仿支付寶支付完成動(dòng)畫(huà)###
首先看一下效果圖。

模擬器截取動(dòng)畫(huà)真是醉了
支付成功動(dòng)畫(huà)####
關(guān)于這個(gè)支付成功的動(dòng)畫(huà),通過(guò)之前所說(shuō)的幀動(dòng)畫(huà)(Frame Animation)是可以實(shí)現(xiàn)的,但前提是需要完善的圖片資源。如果UI 沒(méi)有提供圖片資源,那是否就束手無(wú)策了呢?其實(shí)不然,對(duì)于這種構(gòu)圖比較簡(jiǎn)單的動(dòng)畫(huà),還是可以通過(guò)屬性動(dòng)畫(huà)實(shí)現(xiàn)的。
觀察一下這個(gè)動(dòng)畫(huà),首先繪制一個(gè)圓形,圓形完成的同時(shí)繪制“對(duì)號(hào)”,動(dòng)畫(huà)完成的瞬間再執(zhí)行變色和整個(gè)view縮放的效果,同時(shí)修改button上的文字。
那么我們的動(dòng)畫(huà)實(shí)現(xiàn)也是按照這個(gè)順序:
public void loadCircle(int mRadius) {
mRadius = mRadius <= 0 ? DEFAULT_RADIUS : mRadius;
this.mRadius = mRadius - PADDING;
if (null != mAnimatorSet && mAnimatorSet.isRunning()) {
return;
}
reset();
reMeasure();
Log.e("left", "R is -------->" + mRadius);
mCircleAnim = ValueAnimator.ofInt(0, 360);
mLineLeftAnimator = ValueAnimator.ofFloat(0, this.mRadius / 2f);
mLineRightAnimator = ValueAnimator.ofFloat(0, this.mRadius / 2f);
Log.i(TAG, "mRadius" + mRadius);
mCircleAnim.setDuration(700);
mLineLeftAnimator.setDuration(350);
mLineRightAnimator.setDuration(350);
mCircleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mDegree = (Integer) animation.getAnimatedValue();
invalidate();
}
});
mLineLeftAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mLeftValue = (Float) valueAnimator.getAnimatedValue();
Log.e("left", "-------->" + mLeftValue);
invalidate();
}
});
mLineRightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mRightValue = (Float) animation.getAnimatedValue();
invalidate();
}
});
mAnimatorSet.play(mCircleAnim).before(mLineLeftAnimator);
mAnimatorSet.play(mLineRightAnimator).after(mLineLeftAnimator);
mAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
stop();
if (mEndListner != null) {
mEndListner.onCircleDone();
SuccessAnim();
}
}
});
mAnimatorSet.start();
}
我們定義了mCircleAnim,mLineLeftAnimator和mLineRightAnimator 三個(gè)屬性動(dòng)畫(huà),并依次播放三個(gè)動(dòng)畫(huà),同時(shí)在各自的update方法中獲取動(dòng)畫(huà)當(dāng)前的變化值,同時(shí)調(diào)用invalidate() ,這樣就會(huì)不斷執(zhí)行onDraw 方法,不斷繪制新的視圖,產(chǎn)生動(dòng)畫(huà)效果。而在動(dòng)畫(huà)執(zhí)行結(jié)束的時(shí)候,可以執(zhí)行接口中定義的監(jiān)聽(tīng)動(dòng)畫(huà)結(jié)束的方法,這里這么做是為了方便在Activity中執(zhí)行一些動(dòng)畫(huà)結(jié)束后的操作。同時(shí)執(zhí)行了當(dāng)前view 大小縮放的動(dòng)畫(huà)SuccessAnim()。
這里重點(diǎn)看一下onDraw方法,這個(gè)方法可以說(shuō)是實(shí)現(xiàn)整個(gè)動(dòng)畫(huà)最核心的內(nèi)容。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mRectF.left = mCenterX - mRadius;
mRectF.top = mCenterY - mRadius;
mRectF.right = mCenterX + mRadius;
mRectF.bottom = mCenterY + mRadius;
canvas.drawArc(mRectF, 0, mDegree, false, mCirclePanit);
canvas.drawLine(mCenterX - mRadius / 2, mCenterY,
mCenterX - mRadius / 2 + mLeftValue, mCenterY + mLeftValue, mLinePaint);
canvas.drawLine(mCenterX, mCenterY + mRadius / 2,
mCenterX + mRightValue, mCenterY + mRadius / 2 - (3f / 2f) * mRightValue, mLinePaint);
}
1.第7行canvas.drawArc 的實(shí)現(xiàn)很容易理解,我們?cè)谥暗膶傩詣?dòng)畫(huà)中,實(shí)現(xiàn)一個(gè)初始值為0,結(jié)束值為360 的ValueAnimator,同時(shí)在其執(zhí)行的過(guò)程中,不斷將中間值賦給mDegree,這樣mDegree值就從0變化到360,從而實(shí)現(xiàn)了一個(gè)圓形繪制。
2.第8行中繪制的是”對(duì)號(hào)“中左邊的短線。第10行繪制的是右邊向上的長(zhǎng)線。這里的思路結(jié)合下圖很容易理解(這只是一個(gè)示意圖,實(shí)際繪制時(shí)右邊長(zhǎng)線的斜率由圓心、半徑多個(gè)值所決定)。

兩點(diǎn)確定一條直線,就是這么簡(jiǎn)單。
之前說(shuō)過(guò),屬性動(dòng)畫(huà)的運(yùn)行機(jī)制是通過(guò)不斷地對(duì)值進(jìn)行操作來(lái)實(shí)現(xiàn)的,而初始值和結(jié)束值之間的動(dòng)畫(huà)過(guò)渡就是由ValueAnimator這個(gè)類(lèi)來(lái)負(fù)責(zé)計(jì)算的。
這里我們就是利用這個(gè)原理實(shí)現(xiàn)了這個(gè)動(dòng)畫(huà)。
理解了這點(diǎn),下面支付失敗的動(dòng)畫(huà),也是相似的原理,中間繪制的內(nèi)容不再是一個(gè)“對(duì)號(hào)”,而是一個(gè)巨大的X。這個(gè)很容易實(shí)現(xiàn),以圓心為坐標(biāo)軸中點(diǎn),在四個(gè)象限45度方向繪制四個(gè)點(diǎn),分別作為起始點(diǎn)和終點(diǎn)即可,結(jié)合代碼很容易理解。
int mViewWidth = getWidth();
int mViewHeight = getHeight();
mCenterX = mViewWidth / 2;
mCenterY = mViewHeight / 2;
temp = mRadius / 2.0f * factor;
Path path = new Path();
path.moveTo(mCenterX - temp, mCenterY - temp);
path.lineTo(mCenterX + temp, mCenterY + temp);
pathLeftMeasure = new PathMeasure(path, false);
path = new Path();
path.moveTo(mCenterX + temp, mCenterY - temp);
path.lineTo(mCenterX - temp, mCenterY + temp);
pathRightMeasure = new PathMeasure(path, false);
繪制方法onDraw
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mRectF.left = mCenterX - mRadius;
mRectF.top = mCenterY - mRadius;
mRectF.right = mCenterX + mRadius;
mRectF.bottom = mCenterY + mRadius;
canvas.drawArc(mRectF, 0, mDegree, false, mCirclePanit);
if (mLeftPos[1] > (mCenterY - temp) && mRightPos[1] > (mCenterY - temp)) {
canvas.drawLine(mCenterX - temp, mCenterY - temp, mLeftPos[0], mLeftPos[1], mLinePaint);
canvas.drawLine(mCenterX + temp, mCenterY - temp, mRightPos[0], mRightPos[1], mLinePaint);
}
}
這里的mLeftPos和mRightPos,就是屬性動(dòng)畫(huà)由初始值過(guò)渡到結(jié)束值時(shí),中間變化值所對(duì)應(yīng)的位置。具體可結(jié)合源碼理解。
最后再說(shuō)一下,使用幀動(dòng)畫(huà)的方式實(shí)現(xiàn)這個(gè)動(dòng)畫(huà),為了適配不同的機(jī)型,必然需要多份不同分辨率的圖片,適配效果不得而知,同時(shí)也會(huì)增加應(yīng)用的大小。但是使用幀動(dòng)畫(huà)就不同了,把握好整個(gè)view的大小,適配起來(lái)應(yīng)該相對(duì)會(huì)容易一些。同時(shí)應(yīng)用大小也不會(huì)變化,同時(shí)可擴(kuò)展性也更高。
購(gòu)物車(chē)添加商品動(dòng)畫(huà)###
購(gòu)物添加動(dòng)畫(huà)可以說(shuō)是,屬性動(dòng)畫(huà)最經(jīng)典的例子;很早以前就有人實(shí)現(xiàn)了。這里就從學(xué)習(xí)屬性動(dòng)畫(huà)的角度出發(fā)加以理解。
這里軌跡的繪制并不完全是靠屬性動(dòng)畫(huà)完成,很大一部分的功勞要算在貝塞爾曲線的身上。關(guān)于貝塞爾曲線的理解,可以看看這里。
private void addToCarAnimation(ImageView goodsImg) {
//獲取需要進(jìn)行動(dòng)畫(huà)的ImageView
final ImageView animImg = new ImageView(mContext);
animImg.setImageDrawable(goodsImg.getDrawable());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100);
shellLayout.addView(animImg, params);
//
final int shellLocation[] = new int[2];
shellLayout.getLocationInWindow(shellLocation);
int animImgLocation[] = new int[2];
goodsImg.getLocationInWindow(animImgLocation);
int carLocation[] = new int[2];
carImage.getLocationInWindow(carLocation);
//
// 起始點(diǎn):圖片起始點(diǎn)-父布局起始點(diǎn)+該商品圖片的一半-圖片的marginTop || marginLeft 的值
float startX = animImgLocation[0] - shellLocation[0] + goodsImg.getWidth() / 2 - DpConvert.dip2px(mContext, 10.0f);
float startY = animImgLocation[1] - shellLocation[1] + goodsImg.getHeight() / 2 - DpConvert.dip2px(mContext, 10.0f);
// 商品掉落后的終點(diǎn)坐標(biāo):購(gòu)物車(chē)起始點(diǎn)-父布局起始點(diǎn)+購(gòu)物車(chē)圖片的1/5
float endX = carLocation[0] - shellLocation[0] + carImage.getWidth() / 5;
float endY = carLocation[1] - shellLocation[1];
//控制點(diǎn),控制貝塞爾曲線
float ctrlX = (startX + endX) / 2;
float ctrlY = startY - 100;
Log.e("num", "-------->" + ctrlX + " " + startY + " " + ctrlY + " " + endY);
Path path = new Path();
path.moveTo(startX, startY);
// 使用二階貝塞爾曲線
path.quadTo(ctrlX, ctrlY, endX, endY);
mPathMeasure = new PathMeasure(path, false);
ObjectAnimator scaleXanim = ObjectAnimator.ofFloat(animImg, "scaleX", 1, 0.5f, 0.2f);
ObjectAnimator scaleYanim = ObjectAnimator.ofFloat(animImg, "scaleY", 1, 0.5f, 0.2f);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 這里這個(gè)值是中間過(guò)程中的曲線長(zhǎng)度(下面根據(jù)這個(gè)值來(lái)得出中間點(diǎn)的坐標(biāo)值)
float value = (Float) animation.getAnimatedValue();
// 獲取當(dāng)前點(diǎn)坐標(biāo)封裝到mCurrentPosition
// mCurrentPosition此時(shí)就是中間距離點(diǎn)的坐標(biāo)值
mPathMeasure.getPosTan(value, mCurrentPosition, null);
// 移動(dòng)的商品圖片(動(dòng)畫(huà)圖片)的坐標(biāo)設(shè)置為該中間點(diǎn)的坐標(biāo)
animImg.setTranslationX(mCurrentPosition[0]);
animImg.setTranslationY(mCurrentPosition[1]);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
goodsCount++;
if (goodsCount < 100) {
carCount.setText(String.valueOf(goodsCount));
} else {
carCount.setText("99+");
}
// 把執(zhí)行動(dòng)畫(huà)的商品圖片從父布局中移除
shellLayout.removeView(animImg);
shopCarAnim();
}
});
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(500);
animatorSet.setInterpolator(new AccelerateInterpolator());
animatorSet.playTogether(scaleXanim, scaleYanim, valueAnimator);
animatorSet.start();
}
我們分別獲取了整個(gè)布局在手機(jī)屏幕中的位置: shellLocation
所要進(jìn)行動(dòng)畫(huà)的圖片在手機(jī)屏幕中的位置:animLocation
購(gòu)物車(chē)在整個(gè)手機(jī)屏幕中的位置:carLocation
并由這三個(gè)值及動(dòng)畫(huà)圖片的大小布局等因素確定了三個(gè)點(diǎn):
起始位置(startX,startY)、結(jié)束位置(endX,endY)和控制點(diǎn)(CtrlX,CtrlY)。
并由這三個(gè)點(diǎn)確定了一個(gè)二階貝塞爾曲線 path。
同時(shí)使用PathMeasure 類(lèi)測(cè)量這條path,同時(shí)使用它的長(zhǎng)度length 作為屬性動(dòng)畫(huà)中的終點(diǎn)值。
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
在動(dòng)畫(huà)的update回調(diào)方法中,我們獲取這個(gè)長(zhǎng)度過(guò)渡變化的中間值,然后我們使用了一個(gè)很重要的方法
mPathMeasure.getPosTan(value, mCurrentPosition, null);
可以看一下,這個(gè)方法的具體實(shí)現(xiàn)
/**
* Pins distance to 0 <= distance <= getLength(), and then computes the
* corresponding position and tangent. Returns false if there is no path,
* or a zero-length path was specified, in which case position and tangent
* are unchanged.
*
* @param distance The distance along the current contour to sample
* @param pos If not null, eturns the sampled position (x==[0], y==[1])
* @param tan If not null, returns the sampled tangent (x==[0], y==[1])
* @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float pos[], float tan[]) {
if (pos != null && pos.length < 2 ||
tan != null && tan.length < 2) {
throw new ArrayIndexOutOfBoundsException();
}
return native_getPosTan(native_instance, distance, pos, tan);
}
這個(gè)方法有三個(gè)參數(shù)
第一個(gè)參數(shù),MeasuePath 所測(cè)量的path的長(zhǎng)度的當(dāng)前值,也就是我們動(dòng)畫(huà)變化中的過(guò)渡值。
第二個(gè)參數(shù)是個(gè)數(shù)組,如果不為null,就被賦予當(dāng)前值所對(duì)應(yīng)位置的坐標(biāo)。
第三個(gè)參數(shù)也是數(shù)組,如果不為null,就被賦予當(dāng)前值所對(duì)應(yīng)的切線坐標(biāo)。(這個(gè)沒(méi)搞懂神馬意思)
如果這個(gè)MeasurePath所測(cè)量的path不存在,就會(huì)返回false。
最終這個(gè)方法會(huì)執(zhí)行一個(gè)native方法,具體實(shí)現(xiàn)我們就不得而知了。
回到我們的代碼,這里我們第二參數(shù),傳入了一個(gè)二維的int 數(shù)組,這樣隨著path總長(zhǎng)度的流逝,我們就依次獲取了這條path線路上的坐標(biāo)點(diǎn)mCurrentPosition。然后通過(guò)設(shè)置動(dòng)畫(huà)圖片animImg 的位置就實(shí)現(xiàn)了動(dòng)畫(huà)效果。
這里重點(diǎn)說(shuō)了一下整體的實(shí)現(xiàn)思路,實(shí)際中還有很多細(xì)節(jié)值得考慮,尤其是在切換為GridView模式的時(shí)候,動(dòng)畫(huà)起點(diǎn)在左右兩邊是有差異的,具體細(xì)節(jié)可參考源碼自己思考。
總結(jié)###
看到這里可以發(fā)現(xiàn),ValueAnimator 這個(gè)類(lèi)雖然很簡(jiǎn)單,但是非常有用。他幫我們實(shí)現(xiàn)了一種屬性值從開(kāi)始到結(jié)束的自然過(guò)渡,而且可以獲取到過(guò)渡過(guò)程的中間值,這樣就很方便我們結(jié)合這個(gè)過(guò)渡值做各種各樣的動(dòng)畫(huà)了。
最后 github 源碼歡迎star & fork