不知道大家有沒有發(fā)現(xiàn)璃饱,主流的電商類APP添加商品到購物車時(shí),都會(huì)伴隨一個(gè)小的“添加”動(dòng)畫肪康。你有沒有想過它是怎么實(shí)現(xiàn)的呢荚恶?今天我們就一起來學(xué)習(xí)下。
其實(shí)實(shí)現(xiàn)這個(gè)效果很簡(jiǎn)單磷支,主要涉及到兩個(gè)知識(shí)點(diǎn):貝塞爾曲線和自定義Evaluator估值器谒撼。下面我先簡(jiǎn)單介紹下這兩個(gè)知識(shí)點(diǎn),如果你之前有了解過這兩塊的話齐唆,可以直接翻到下面看下實(shí)現(xiàn)代碼嗤栓。
貝塞爾曲線是數(shù)值分析領(lǐng)域的重要參數(shù)曲線冻河,在我們的生活中隨處都可以看到它的影子箍邮,比如:QQ聊天氣泡的拖拽效果、直播室送花點(diǎn)贊效果叨叙、電量水波紋效果等等锭弊。貝塞爾曲線可以分為一階貝塞爾曲線、二階貝塞爾曲線擂错、三階貝塞爾曲線 .....味滞。在這里我們用到了二階貝塞爾曲線,下面我們先來看下它的計(jì)算公式:
相信大家在開發(fā)過程中都有用到過屬性動(dòng)畫吧哪痰,屬性動(dòng)畫中有兩個(gè)很重要的知識(shí)點(diǎn),那就是差值器Interpolator和估值器Evaluator久妆。簡(jiǎn)單來講晌杰,差值器就類似于我們物理中所學(xué)的“加速度”,比如我們的執(zhí)行動(dòng)畫需要先加速再減速筷弦。Android系統(tǒng)為我們內(nèi)置了幾種常見的差值器肋演,在某些特定情況下不能滿足需要的話,我們就需要實(shí)現(xiàn)TimeInterpolator接口實(shí)現(xiàn)自定義差值器烂琴。估值器Evaluator其實(shí)就是一個(gè)轉(zhuǎn)換器惋啃,他能把小數(shù)進(jìn)度轉(zhuǎn)換成對(duì)應(yīng)的具體數(shù)值,我們可以實(shí)現(xiàn)TypeEvaluator接口來自定義估值器监右。
我們先思考一下边灭,加入購物車動(dòng)畫效果就相當(dāng)于數(shù)學(xué)中常見的拋物線,可以借助二階貝塞爾曲線來實(shí)現(xiàn)健盒,那我們?cè)趺创_定起始點(diǎn)P0绒瘦、控制點(diǎn)P1、終點(diǎn)P2的位置呢扣癣?起始點(diǎn)P0的位置就是我們的商品添加按鈕所在位置惰帽,終點(diǎn)P2位置就是界面左下角購物車Icon圖標(biāo)所在的位置,控制點(diǎn)的位置要怎么選取呢父虑?在這里我們可以沿添加按鈕水平向左该酗,沿購物車Icon圖標(biāo)豎直向上,兩條線的交點(diǎn)處正可以作為我們的控制點(diǎn)坐標(biāo)士嚎,到此三個(gè)點(diǎn)正好構(gòu)成一個(gè)倒立的直角三角形呜魄。控制點(diǎn)P1的橫坐標(biāo)等于終點(diǎn)P2的橫坐標(biāo)莱衩,控制點(diǎn)P1的縱坐標(biāo)等于起始點(diǎn)P0的縱坐標(biāo)爵嗅。下面我們就可以進(jìn)行編碼工作了。
首先自定義估值器CartEvaluator:
public class CartEvaluator implements TypeEvaluator<PointF>{
private PointF pointCur;
private PointF mControlPoint;
public CartEvaluator(PointF mControlPoint) {
this.mControlPoint = mControlPoint;
pointCur = new PointF();
}
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
// 將二階貝塞爾曲線的計(jì)算公式直接代入即可
pointCur.x = (1 - fraction) * (1 - fraction) * startValue.x
+ 2 * fraction * (1 - fraction) * mControlPoint.x + fraction * fraction * endValue.x;
pointCur.y = (1 - fraction) * (1 - fraction) * startValue.y
+ 2 * fraction * (1 - fraction) * mControlPoint.y + fraction * fraction * endValue.y;
return pointCur;
}
}
在這里我們創(chuàng)建了一個(gè)pointCur對(duì)象笨蚁,專門用來存儲(chǔ)當(dāng)前移動(dòng)點(diǎn)的坐標(biāo)睹晒。
下面看下Demo中的布局文件效果,代碼就不貼出來了括细,就是布局左下角放置了一個(gè)購物車圖標(biāo)伪很,右上角放置了三個(gè)添加按鈕,用來模擬添加商品操作:最后看下Activity中的代碼實(shí)現(xiàn):
public class MainActivity extends AppCompatActivity {
private ImageView mAddOne;
private ImageView mAddTwo;
private ImageView mAddThree;
private ImageView mCart;
private ViewGroup mRootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initListener();
}
// 初始化控件
private void initView(){
mRootView = (ViewGroup) getWindow().getDecorView();
mCart = findViewById(R.id.mCart);
mAddOne = findViewById(R.id.mAddOne);
mAddTwo = findViewById(R.id.mAddTwo);
mAddThree = findViewById(R.id.mAddThree);
}
// 初始化監(jiān)聽
private void initListener(){
mAddOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
playAnim(view);
}
});
mAddTwo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
playAnim(view);
}
});
mAddThree.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
playAnim(view);
}
});
}
// 執(zhí)行動(dòng)畫
private void playAnim(final View view){
//創(chuàng)建int數(shù)組奋单,用來接收貝塞爾起始點(diǎn)坐標(biāo)和終點(diǎn)坐標(biāo)值
int[] startPosition = new int[2];
int[] endPosition = new int[2];
view.getLocationInWindow(startPosition);
mCart.getLocationInWindow(endPosition);
PointF startF = new PointF(); //起始點(diǎn) startF
PointF endF = new PointF(); //終點(diǎn) endF
PointF controlF = new PointF(); //控制點(diǎn) controlF
startF.x = startPosition[0];
startF.y = startPosition[1] ;
endF.x = endPosition[0]+mCart.getWidth()/2-view.getWidth()/2; //微調(diào)處理锉试,確保動(dòng)畫執(zhí)行完畢“添加”圖標(biāo)中心點(diǎn)與購物車中心點(diǎn)重合
endF.y = endPosition[1]+mCart.getHeight()/2 - view.getHeight()/2;
controlF.x = endF.x;
controlF.y = startF.y;
// 創(chuàng)建執(zhí)行動(dòng)畫的“添加”圖標(biāo)
final ImageView imageView = new ImageView(this);
mRootView.addView(imageView);
imageView.setImageResource(R.mipmap.cartadd);
imageView.getLayoutParams().width = view.getMeasuredWidth();
imageView.getLayoutParams().height = view.getMeasuredHeight();
ValueAnimator valueAnimator = ValueAnimator.ofObject(new CartEvaluator(controlF), startF, endF);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
PointF pointF = (PointF) animation.getAnimatedValue();
imageView.setX(pointF.x);
imageView.setY(pointF.y);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// 動(dòng)畫執(zhí)行完畢,將執(zhí)行動(dòng)畫的“添加”圖標(biāo)移除掉
mRootView.removeView(imageView);
// 執(zhí)行購物車縮放動(dòng)畫
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator animatorX = ObjectAnimator.ofFloat(mCart, "scaleX", 1f, 1.2f, 1f);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(mCart, "scaleY", 1f, 1.2f, 1f);
animatorSet.play(animatorX).with(animatorY);
animatorSet.setDuration(400);
animatorSet.start();
}
});
valueAnimator.setDuration(800);
valueAnimator.start();
}
}
代碼中相關(guān)地方都標(biāo)上注釋了辱匿,相信大家都能夠理解键痛,整體代碼量還是很少的炫彩。最后我們看下實(shí)現(xiàn)效果: