1. 程式人生 > >仿餓了麼購物車效果(UI效果)

仿餓了麼購物車效果(UI效果)

images

  • 右列表標題懸停
  • 左右列表滑動時聯動
  • 新增購物車時動畫效果

右列表標題懸停&左右列表滑動時聯動

gradle新增依賴

    compile 'se.emilsjolander:stickylistheaders:2.7.0'

主Layout簡單,基本LinearLayout實現,左邊列表直接用RecyclerView實現

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.gaoyy.stickylistdemo.MainActivity" >
<LinearLayout android:layout_width
="match_parent" android:layout_height="300dp" android:layout_above="@+id/linearLayout" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" android:orientation="horizontal">
<android.support.v7.widget.RecyclerView
android:id="@+id/left" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.3"/>
<se.emilsjolander.stickylistheaders.StickyListHeadersListView android:id="@+id/list" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.7" /> </LinearLayout> <LinearLayout android:id="@+id/linearLayout" android:layout_width="match_parent" android:layout_height="50dp" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:background="#433d3d" android:orientation="horizontal"> <ImageView android:id="@+id/cart" android:layout_width="50dp" android:layout_height="50dp" android:padding="10dp" android:layout_marginLeft="10dp" android:src="@mipmap/icon_cart"/> </LinearLayout> </RelativeLayout>

資料模擬,左Type右Sample,唯一需要注意的是,在初始化資料時,資料必須按照分組順排列

public class Sample
{
    //item id
    private String id;
    //所屬組id
    private String groupId;
    //item title
    private String title;
    //item desc
    private String desc;
    //組 title
    private String groupTitle;
    //數量
    private int count;

    public Sample(String id, String groupId, String title, String desc, String groupTitle,int count)
    {
        this.id = id;
        this.groupId = groupId;
        this.title = title;
        this.desc = desc;
        this.groupTitle = groupTitle;
        this.count= count;
    }

    //setter and getter...
}

public class Type
{
    private int status;
    private String type;

    public Type(int status, String type)
    {
        this.status = status;
        this.type = type;
    }

    //setter and getter...
}

...

private void initData()
{
    int random = (int) (Math.random() * 100 + 1);
    for (int i = 0; i < 15; i++)
    {
        headerData.add(new Type(0, "種類" + i));
    }

    for (int i = 0; i < headerData.size(); i++)
    {
        for (int k = 0; k < 10; k++)
        {
            data.add(new Sample("" + k, "" + i, "種類" + i + "   分類" + k, (int) (Math.random() * 100 + 1) + "", "種類" + i, 0));
        }
    }
}

StickyListHeadersListView的介面卡需實現StickyListHeadersAdapter介面的getHeaderView() getHeaderId()方法,其中getHeaderView()與普通ListView的getView()實現一致,getHeaderId()則需要返回資料的組id

public class SampleAdapter extends BaseAdapter implements StickyListHeadersAdapter
{
    ...
    @Override
    public View getHeaderView(int position, View convertView, ViewGroup parent)
    {
        HeaderViewHolder headerViewHolder;
        if (convertView == null)
        {
            headerViewHolder = new HeaderViewHolder();
            convertView = inflater.inflate(R.layout.header, parent, false);
            headerViewHolder.header = (TextView) convertView.findViewById(R.id.header);
            convertView.setTag(headerViewHolder);
        }
        else
        {
            headerViewHolder = (HeaderViewHolder) convertView.getTag();
        }
        headerViewHolder.header.setText(data.get(position).getGroupTitle());
        return convertView;
    }

    @Override
    public long getHeaderId(int position)
    {
        //return the first character of the country as ID because this is what headers are based upon
        return Long.parseLong(data.get(position).getGroupId());
    }
}

實現滑動時左右聯動(左-ReListAdapter,右-SampleAdapter)
1.左邊RecyclerView點選item時可呼叫StickyListHeadersListView的setSelection(position)來實現右邊列表的顯示。注意,這裡的position傳入的是item的position,不是header的組id。左列表item狀態變化在adapter中實現,由Type中status控制(1-選中,0-未選中);
2.右邊列表滑動時,左邊列表item的選中狀態也相應變化。右列表滑動時,設定setOnScrollListener來監聽item位置變化,更新左列表item選中狀態;

ReListAdapter

if(data.get(position).getStatus() == 1)
{
    leftViewHolder.layout.setBackgroundColor(context.getResources().getColor(android.R.color.white));
    leftViewHolder.tv.setTextColor(context.getResources().getColor(android.R.color.black));
}
if(data.get(position).getStatus() == 0)
{
    leftViewHolder.layout.setBackgroundColor(context.getResources().getColor(R.color.gray));
    leftViewHolder.tv.setTextColor(context.getResources().getColor(R.color.gray_text));
}

...
//更新資料,重新整理列表
public void updateData(List<Type> data)
{
    this.data = data;
    notifyDataSetChanged();
}

MainActivity

    reListAdapter.setOnItemClickListener(new ReListAdapter.OnItemClickListener()
    {
        @Override
        public void onItemClick(View view, int position)
        {
            int select = -1;
            for (int i = 0; i < data.size(); i++)
            {
                if (Integer.valueOf(data.get(i).getGroupId()) == position)
                {
                    select = i;
                    break;
                }
            }
            updateTypeList(position);
            list.setSelection(select);

        }
    });

    list.setOnScrollListener(new AbsListView.OnScrollListener()
    {
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState)
        {

        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
        {
            Sample sample = data.get(firstVisibleItem);
            int groupId = Integer.valueOf(sample.getGroupId());
            left.smoothScrollToPosition(groupId);
            updateTypeList(groupId);
        }
    });

    private void updateTypeList(int position)
    {
        for (int i = 0; i < headerData.size(); i++)
        {
            Type type = headerData.get(i);

            if (position == i)
            {
                type.setStatus(1);
                headerData.remove(i);
                headerData.add(i, type);
            }
            else
            {
                type.setStatus(0);
                headerData.remove(i);
                headerData.add(i, type);
            }
        }
        reListAdapter.updateData(headerData);
    }

這裡有個BUG,當每組的資料量比較少時,即一屏顯示完所有的資料或者餘下的資料時,左邊列表的選中狀態會預設選中右邊列表出現在頂部的item的組,下面的組選不到。原因在於右邊列表的OnScrollListener,在onScroll方法中取到的是位於頂部item的position,暫時沒有想到解決辦法。

images

左右聯動基本實現(>.<)

新增購物車動畫效果實現

首先,新增add的點選監聽,這裡用介面的方式實現,在Activity中setListener,在BasicOnClickListener的onClick中傳入itemViewHolder.parent,為了方便後續在Activity中新增動畫效果。

item佈局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/item_parent"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="12dp"
        android:layout_marginStart="12dp"
        android:layout_marginTop="5dp"
        android:layout_toEndOf="@+id/imageView"
        android:layout_toRightOf="@+id/imageView"
        android:text="TextView"
        android:textSize="20sp"/>

    <TextView
        android:id="@+id/desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/imageView"
        android:layout_alignLeft="@+id/title"
        android:layout_alignStart="@+id/title"
        android:layout_marginBottom="5dp"
        android:text="TextView"
        android:textColor="@color/colorAccent"
        android:textSize="18sp"/>

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:scaleType="fitXY"
        app:srcCompat="@mipmap/ic_launcher"/>

    <ImageView
        android:id="@+id/add"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_alignBottom="@+id/desc"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_marginEnd="24dp"
        android:layout_marginRight="24dp"
        app:srcCompat="@mipmap/button_add"/>

    <TextView
        android:id="@+id/count"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/desc"
        android:layout_alignBottom="@+id/desc"
        android:layout_marginEnd="12dp"
        android:layout_marginRight="12dp"
        android:layout_toLeftOf="@+id/add"
        android:layout_toStartOf="@+id/add"
        android:text="1"
        android:textSize="20sp"
        android:visibility="visible"/>

    <ImageView
        android:id="@+id/minus"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_alignBottom="@+id/count"
        android:layout_marginRight="12dp"
        android:layout_toLeftOf="@+id/count"
        android:layout_toStartOf="@+id/count"
        android:visibility="visible"
        app:srcCompat="@mipmap/button_minus"/>


</RelativeLayout>

SampleAdapter

    private OnOperationClickListener onOperationClickListener;

    public interface OnOperationClickListener
    {
        void onOperationClick(View parent,View view, int position);
    }

    public void setOnOperationClickListener(OnOperationClickListener listener)
    {
        this.onOperationClickListener = listener;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        //...

        if(onOperationClickListener != null)
        {
            itemViewHolder.add.setOnClickListener(new BasicOnClickListener(itemViewHolder,position));
            itemViewHolder.miuns.setOnClickListener(new BasicOnClickListener(itemViewHolder,position));
        }
    }
    public class BasicOnClickListener implements View.OnClickListener
    {
        ItemViewHolder itemViewHolder;
        int position;

        public BasicOnClickListener(ItemViewHolder itemViewHolder,int position)
        {
            this.itemViewHolder = itemViewHolder;
            this.position = position;
        }

        @Override
        public void onClick(View view)
        {
            switch (view.getId())
            {
                case R.id.add:
                    onOperationClickListener.onOperationClick(itemViewHolder.parent,itemViewHolder.add,position);
                    break;
                case R.id.minus:
                    onOperationClickListener.onOperationClick(itemViewHolder.parent,itemViewHolder.miuns,position);
                    break;
            }
        }
    }

MainActivity
1.點選add時count加1,更新adapter,miuns顯現,點選minus時count減1,當減到0時miuns隱藏,通過設定miuns的tag來實現。

SampleAdapter
getView()
{
        if(data.get(position).getCount() == 0)
        {
            itemViewHolder.miuns.setAlpha(0f);
            itemViewHolder.count.setAlpha(0f);
            itemViewHolder.miuns.setTag(true);
        }
        else
        {
            itemViewHolder.count.setAlpha(1f);
            itemViewHolder.miuns.setAlpha(1f);
            itemViewHolder.count.setText(data.get(position).getCount()+"");
            itemViewHolder.miuns.setTag(false);
        }
}

2.新增動畫

  • 獲取根佈局,item中的add,購物車cart的座標
  • 新增一個add的ImageView到根佈局中,位置與item中的add相同
  • 為新增的add新增拋物線效果,設定transitionX和transitionY的屬性動畫,結束點的位於cart的位置,X軸上線性,Y軸上加速,同時設定漸隱動畫。(這裡的拋物線效果也可以用貝塞爾曲線實現)
  • 新增add在動畫結束時從根佈局remove掉
  • 新增add在動畫結束時購物車cart設定一個放大的屬性動畫
        sampleAdapter.setOnOperationClickListener(new SampleAdapter.OnOperationClickListener()
        {
            @Override
            public void onOperationClick(View parent, View view, int position)
            {
                ImageView add = (ImageView) parent.findViewById(R.id.add);
                ImageView miuns = (ImageView) parent.findViewById(minus);

                Sample sample = data.get(position);
                int count = sample.getCount();
                switch (view.getId())
                {
                    case R.id.add:
                        //新增時動畫效果
                        createAnim(add);

                        count = count + 1;
                        data.get(position).setCount(count);

                        boolean tag = ((boolean) miuns.getTag());
                        if (tag)
                        {
                            minusAnim(miuns);
                        }
                        else
                        {
                            sampleAdapter.updateDataCount(data);
                        }
                        break;
                    case R.id.minus:
                        count = count - 1;
                        if (count < 0)
                        {
                            count = 0;
                        }
                        data.get(position).setCount(count);
                        sampleAdapter.updateDataCount(data);
                        break;
                }
            }
        });


    /**
     * 新增時動畫效果
     * @param add
     */
    private void createAnim(ImageView add)
    {
        //獲取+號的座標
        int[] addPoint = new int[2];
        add.getLocationInWindow(addPoint);

        //獲取購物車的座標
        int[] cartPoint = new int[2];
        cart.getLocationInWindow(cartPoint);

        //獲父佈局的座標
        int[] parentPoint = new int[2];
        activityMain.getLocationInWindow(parentPoint);

        final ImageView newAdd = new ImageView(MainActivity.this);
        newAdd.setLayoutParams(new RelativeLayout.LayoutParams(dip2px(MainActivity.this, 25), dip2px(MainActivity.this, 25)));
        newAdd.setImageResource(R.mipmap.button_add);
        newAdd.setX(addPoint[0]);
        newAdd.setY(addPoint[1] - parentPoint[1]);
        activityMain.addView(newAdd);

        //X軸平移動畫
        ValueAnimator x = ValueAnimator.ofInt((int) newAdd.getX(), cartPoint[0]);
        x.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator)
            {
                int value = (int) valueAnimator.getAnimatedValue();
                newAdd.setTranslationX(value);
            }
        });
        //設定線性插值器
        x.setInterpolator(new LinearInterpolator());

        //Y軸平移動畫
        ValueAnimator y = ValueAnimator.ofInt((int) newAdd.getY(), cartPoint[1] - parentPoint[1]);
        y.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator)
            {
                int value = (int) valueAnimator.getAnimatedValue();
                newAdd.setTranslationY(value);
            }
        });
        //設定加速插值器
        y.setInterpolator(new AccelerateInterpolator());


        //新增ImageView加號的漸隱動畫
        ObjectAnimator newAddAlpha = ObjectAnimator.ofFloat(newAdd, "Alpha", 1.0f, 0.0f);
        newAddAlpha.addListener(new AnimatorListenerAdapter()
        {
            @Override
            public void onAnimationEnd(Animator animation)
            {
                super.onAnimationEnd(animation);
                //動畫結束移除view
                activityMain.removeView(newAdd);
            }
        });

        //動畫集合
        AnimatorSet set = new AnimatorSet();
        set.playTogether(x, y, newAddAlpha);
        set.start();

        set.addListener(new AnimatorListenerAdapter()
        {
            @Override
            public void onAnimationEnd(Animator animation)
            {
                super.onAnimationEnd(animation);
                //購物車的放大動畫
                cartAnim();
            }
        });
    }

    /**
     * 減號平移動畫
     *
     * @param minus
     */
    public void minusAnim(final View minus)
    {
        ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(minus, "TranslationX", 100, 0);
        ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(minus, "Alpha", 0f, 1f);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(translationXAnim, alphaAnim);
        set.addListener(new AnimatorListenerAdapter()
        {
            @Override
            public void onAnimationEnd(Animator animation)
            {
                super.onAnimationEnd(animation);
                sampleAdapter.updateDataCount(data);
            }
        });
        set.start();
    }

    /**
     * 購物車放大動畫
     */
    private void cartAnim()
    {
        ObjectAnimator cartScale = ObjectAnimator.ofFloat(cart, "ScaleXY", 0.6f, 1.0f);
        cartScale.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator)
            {
                float value = (float) valueAnimator.getAnimatedValue();
                cart.setScaleX(value);
                cart.setScaleY(value);
            }
        });
        cartScale.start();
    }

這裡需要注意的是,座標計算。
座標圖

動畫效果基本實現(>.<)