仿餓了麼購物車效果(UI效果)
- 右列表標題懸停
- 左右列表滑動時聯動
- 新增購物車時動畫效果
右列表標題懸停&左右列表滑動時聯動
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,暫時沒有想到解決辦法。
左右聯動基本實現(>.<)
新增購物車動畫效果實現
首先,新增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();
}
這裡需要注意的是,座標計算。
動畫效果基本實現(>.<)