1. 程式人生 > >兩種方式實現購物車動畫

兩種方式實現購物車動畫

公司專案有新增商品到購物車的需求,需要一個新增商品的動畫效果。參照了一些當下主流APP的效果,最後實現了以下效果:

這裡寫圖片描述

點選Item,顯示點選第幾項;點選購買,新增商品到購物車,同時購物車商品總數加一。

實現過程:
首先是商品新增到購物車的軌跡,類似於一條拋物線,好在Android已經為我們提供了相關的方法–Path類(封裝了貝塞爾曲線)。具體關於貝塞爾曲線,大家可以自行百度。這裡我們主要研究Path為我們提供的構造路徑的方法。

1.moveTo(float,float)
用於設定移動路徑的起始點Point(x,y),對於android系統來說,螢幕的左上角的座標是 (0,0) , 我們在做一些操作的時候預設基準點也是 (0,0)。Path 的moveTo 方法可以與此進行一個類比,就是為了改變 Path 的起始點。
2.quadTo(float x1, float y1, float x2, float y2 )
android 只對低階貝塞爾曲線進行了封裝,這是用於設定二次貝塞爾曲線的方法,先上圖說明:
這裡寫圖片描述

x3、y3 代表控制點的 x、y,即動態圖中的P1,x2、y2 代表目標點的 x、y,即動態圖中的P2。繪製路徑軌跡已經找到了對應的類與方法,接下來就是在自己專案裡的具體應用了。如下圖:
這裡寫圖片描述

(x0,y0)代表父佈局的座標,(x1,y1)代表商品,(x2,y2)代表購物車,(x3,y3)代表控制點。需要的點已經確定好,接下來就是程式碼實現了:

public class SecondActivity extends AppCompatActivity implements addListener {

    private int i;
    private TextView txt;
    private
ImageView cartImg; private RelativeLayout relativeLayout; private ListView list; private LayoutInflater inflater; private ListAdapter adapter; private int[] imgs = new int[]{R.drawable.cake, R.drawable.milk, R.drawable.coffee, R.drawable.kettle, R.drawable.mobile}; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); initviews(); } private void initviews() { relativeLayout = (RelativeLayout) findViewById(R.id.rl); txt = (TextView) findViewById(R.id.second_txt); cartImg = (ImageView) findViewById(R.id.cart_img); inflater = LayoutInflater.from(this); list = (ListView) findViewById(R.id.list); adapter = new ListAdapter(); adapter.setListener(this); list.setAdapter(adapter); list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(SecondActivity.this, "你點選了第" + String.valueOf(position + 1) + "項", Toast.LENGTH_SHORT).show(); } }); } public class ListAdapter extends BaseAdapter { private addListener listener; public void setListener(addListener listener) { this.listener = listener; } @Override public int getCount() { if (imgs != null) { return imgs.length; } else { return 0; } } @Override public Object getItem(int position) { return (position); } @Override public long getItemId(int id) { // TODO Auto-generated method stub return id; } @Override public View getView(final int position, View convertView, ViewGroup parent) { final ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, null); viewHolder = new ViewHolder(); viewHolder.itemimg = (ImageView) convertView.findViewById(R.id.item_img); viewHolder.itemtxt = (TextView) convertView.findViewById(R.id.item_txt); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.itemimg.setImageResource(imgs[position]); viewHolder.itemtxt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { listener.addCart(position, viewHolder.itemimg); } }); return convertView; } public class ViewHolder { public ImageView itemimg; public TextView itemtxt; } }

資料的準備,Item的單擊事件,介面回撥處理“購買”點選事件

這裡需要拿到具體商品的圖片進行動畫處理,傳遞了一個ImageView過去,Position則是方便我們進行其他具體的業務處理。接下來就是最重要的動畫實現了:

        //得到起始點座標
        int parentLoc[] = new int[2];
        relativeLayout.getLocationInWindow(parentLoc);
        int startLoc[] = new int[2];
        imgview.getLocationInWindow(startLoc);
        int endLoc[] = new int[2];
        cartImg.getLocationInWindow(endLoc);

getLocationInWindow :獲取該檢視在整個視窗內的絕對坐,parentLoc [0]代表x座標,parentLoc [1]代表y座標。

        float startX = startLoc[0] - parentLoc[0] + imgview.getWidth() / 2;
        float startY = startLoc[1] - parentLoc[1] + imgview.getHeight() / 2;
        float toX = endLoc[0] - parentLoc[0] + cartImg.getWidth() / 3;
        float toY = endLoc[1] - parentLoc[1];

通過起始點座標計算出控制點與目標點的座標。

        final ImageView goods = new ImageView(getApplicationContext());
        goods.setImageDrawable(imgview.getDrawable());
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(60, 60);
        relativeLayout.addView(goods, params);

動態在父佈局中新增一個執行新增動畫的檢視,也就是效果圖中的商品縮圖。之前自己用的是傳遞過來的ImageView,發現每次動畫一執行,Item的圖片相應也會消失。所以用這種方法來替代這個商品,記住最後在動畫完成的時候將父佈局中動態新增的這個view移除即可。

        Path path = new Path();
        path.moveTo(startX, startY);
        path.quadTo((startX + toX) / 2, startY, toX, toY);

呼叫Path類對應的方法模擬出這一條拋物線

現在路徑曲線有了,還需要一個非常重要的輔助類:路徑測量PathMeasure,無論Path路徑多麼複雜,PathMeasure也會將所有path中的路徑看成一個直線,取出某一點的位置,然後計算出對應的座標。

構造方法:
PathMeasure(Path path, boolean forceClosed)
常用方法:
float getLength() :測量path的長度
boolean getPosTan(float distance, float[] pos, float[] tan) :傳入一個距離distance(0<=distance<=getLength()),然後會計算當前距離的座標點和切線,pos會自動填充上座標,這個方法很重要。

路徑上每一點的座標都能夠獲取到,接下來就是動畫的實現了,其實就是商品每次根據不同的點座標移動到不同的位置,這樣就實現了想要的效果。我這裡用的是自定義的一個動畫:

/**
 * Created by tangyangkai on 16/4/20.
 */
public class PathAnimation extends Animation {

    private PathMeasure measure;
    private float[] pos = new float[2];

    public PathAnimation(Path path) {
        measure = new PathMeasure(path, false);
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        measure.getPosTan(measure.getLength() * interpolatedTime, pos, null);
        t.getMatrix().setTranslate(pos[0], pos[1]);

    }
}

通過重寫Animation的 applyTransformation (float interpolatedTime, Transformation t)函式來實現自定義動畫效果。在繪製動畫的過程中會反覆的呼叫applyTransformation 函式,每次呼叫引數interpolatedTime值都會變化,該引數從0漸變為measure.getLength() ,當該引數為measure.getLength() 時表明動畫結束。通過引數Transformation 來獲取變換的矩陣(matrix),通過改變矩陣就可以實現各種複雜的效果。
通過getMatrix().setTranslate函式來實現移動,該函式的兩個引數代表商品的x座標與y座標,由於interpolatedTime是從0到measure.getLength() 變化,所在這裡實現的效果就是商品會沿著制定的路徑進行移動。

PathAnimation animation = new PathAnimation(path);
        animation.setDuration(1000);
        animation.setInterpolator(new LinearInterpolator());
        animation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                i++;
                txt.setText(String.valueOf(i));
                relativeLayout.removeView(goods);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });

        goods.startAnimation(animation);

自定義動畫完成,然後就是呼叫。依次設定動畫持續時間,勻速動畫線性插值器,動畫監聽。記住在動畫完成的時候,將商品數量加一,同時移除動態新增的view。最後開啟動畫,達到最後的效果。

參考資料:

忙著出效果,所以沒有研究用屬性動畫來實現,有時間會去完成的,然後更新部落格。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~分割線

之前說到想使用屬性動畫來實現,週末回去好好看了看這方面的知識,最後一樣的效果,不同的動畫實現。廢話不多說,看程式碼(記得設定成區域性變數,不然會出現重疊現象):

final PathMeasure mPathMeasure= new PathMeasure(path, false);
  final float[] mCurrentPosition = new float[2];

路徑測量輔助類path measure,陣列存放x,y座標

//新增購物車動畫實現
    @Override
    public void addCart(int position, ImageView imgview) {
        //得到起始點座標
        int parentLoc[] = new int[2];
        relativeLayout.getLocationInWindow(parentLoc);
        int startLoc[] = new int[2];
        imgview.getLocationInWindow(startLoc);
        int endLoc[] = new int[2];
        cartImg.getLocationInWindow(endLoc);


        final ImageView goods = new ImageView(getApplicationContext());
        goods.setImageDrawable(imgview.getDrawable());
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(60, 60);
        relativeLayout.addView(goods, params);

        float startX = startLoc[0] - parentLoc[0] + imgview.getWidth() / 2;
        float startY = startLoc[1] - parentLoc[1] + imgview.getHeight() / 2;
        float toX = endLoc[0] - parentLoc[0] + cartImg.getWidth() / 3;
        float toY = endLoc[1] - parentLoc[1];


        Path path = new Path();
        path.moveTo(startX, startY);
        path.quadTo((startX + toX) / 2, startY, toX, toY);
        mPathMeasure = new PathMeasure(path, false);

Path路徑以及PathMeasure路徑測量的構造

//屬性動畫實現
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
        valueAnimator.setDuration(1000);
        // 勻速插值器
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                // 獲取當前點座標封裝到mCurrentPosition
                mPathMeasure.getPosTan(value, mCurrentPosition, null);
                goods.setTranslationX(mCurrentPosition[0]);
                goods.setTranslationY(mCurrentPosition[1]);
            }
        });
        valueAnimator.start();


        valueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                i++;
                txt.setText(String.valueOf(i));
                relativeLayout.removeView(goods);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

這裡是屬性動畫實現的核心,著重學習一下。
科普時間(引用郭神部落格):
ValueAnimator是整個屬性動畫機制當中最核心的一個類,前面我們已經提到了,屬性動畫的執行機制是通過不斷地對值進行操作來實現的,而初始值和結束值之間的動畫過渡就是由ValueAnimator這個類來負責計算的。它的內部使用一種時間迴圈的機制來計算值與值之間的動畫過渡,我們只需要將初始值和結束值提供給ValueAnimator,並且告訴它動畫所需執行的時長,那麼ValueAnimator就會自動幫我們完成從初始值平滑地過渡到結束值這樣的效果。除此之外,ValueAnimator還負責管理動畫的播放次數、播放模式、以及對動畫設定監聽器等,確實是一個非常重要的類。

其實新增購物車的動畫實現就是對商品的x,y座標不斷賦值,不斷更新,達到拋物線的效果。
1.呼叫ValueAnimator的ofFloat()方法就可以構建出一個ValueAnimator的例項,ofFloat()方法當中允許傳入多個float型別的引數,這裡傳入0和mPathMeasure.getLength()就表示將值從0平滑過渡到mPathMeasure.getLength()。
2.通過addUpdateListener()方法來新增一個動畫的監聽器,在動畫執行的過程中會不斷地進行回撥,我們只需要在回撥方法當中通過mPathMeasure.getPosTan()方法將當前的值取出並設定給商品,就可以達到動畫效果了。
3.addListener方法來監聽動畫完成以後的操作,移除動態新增的view,購物車數量加一。

參考部落格: