购物车

知识点分析

效果图来看不复杂内容并没多少,值得介绍一下的知识点也就下面几个吧 
- 列表标题悬停 
- 左右列表滑动时联动 
- 添加商品时的抛物线动画 
- 底部弹出购物车清单 
- 数据的同步

另外就是实现效果的时候可能会遇到的几个坑。。。

布局很简单直接进入代码

1:列表标题悬停

现在做项目列表什么的基本抛弃了ListView改用RecyclerView,上篇博客中的标题悬停也是使用了一个RecyclerView的开源项目sticky-headers-recyclerview,不过写这个demo的时候遇到了两个坑

1)、sticky-headers-recyclerview做悬停标题的时候scrollToPosition(int position)方法滚动的位置不准确。
2)、当布局复杂点的时候 如果RecyclerView的宽度自适应或者使用权重百分比之类可能会导致header显示空白。

并且该开源项目作者已经停止维护,所以这次又换回了StickyListHeadersListView。

需要购物车Demo的很多都是新手,这里简单介绍下StickyListHeadersListView的使用

1)、AS引用 gradle文件dependencies内添加
    compile 'se.emilsjolander:stickylistheaders:2.7.0'

2)、xml文件中使用StickyListHeadersListView代替ListView

?
1
2
3
4
5
6
< se.emilsjolander.stickylistheaders.StickyListHeadersListView
  android:layout_width = "match_parent"
  android:background = "#fff"
  android:id = "@+id/itemListView"
  android:layout_height = "match_parent" >
se.emilsjolander.stickylistheaders.StickyListHeadersListView >

3)、Adapter继承BaseAdapter和接口StickyListHeadersAdapter
StickyListHeadersAdapter接口包括两个方法

?
1
2
3
View getHeaderView( int position, View convertView, ViewGroup parent);
 
long getHeaderId( int position);

代码中使用和ListView一样,下面是几个特有的方法,看方法名也很容易理解用途

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void setAreHeadersSticky( boolean areHeadersSticky);
public boolean areHeadersSticky();
 
public void setOnHeaderClickListener(OnHeaderClickListener listener);
 
public interface OnHeaderClickListener {
  public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, long headerId, boolean currentlySticky);
}
 
public void setOnStickyHeaderChangedListener(OnStickyHeaderChangedListener listener);
 
public interface OnStickyHeaderChangedListener {
  void onStickyHeaderChanged(StickyListHeadersListView l, View header, int itemPosition, long headerId);
}
 
public View getListChildAt( int index);
public int getListChildCount();

2:左右列表联动

联动主要有两个效果 
- 左侧列表点击选择分类,右侧列表滑动到对应分类 
- 右侧列表滑动过程中左侧列表高亮的分类跟随变化

第一个效果简单,左侧列表item添加点击事件,事件中调用右侧列表的setSelection(int positon) 方法。

第二个效果要给右侧列表添加ScrollListener,根据列表中显示的第一条数据设置左侧选中的分类

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
listView.setOnScrollListener( new AbsListView.OnScrollListener() {
   @Override
   public void onScrollStateChanged(AbsListView view, int scrollState) {
 
   }
 
   @Override
   public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
   //根据firstVisibleItem获取分类ID,根据分类id获取左侧要选中的位置
    GoodsItem item = dataList.get(firstVisibleItem);
    if (typeAdapter.selectTypeId != item.typeId) {
     typeAdapter.selectTypeId = item.typeId;
     typeAdapter.notifyDataSetChanged();
     //左侧列表是个RecyclerView 所以使用smoothScrollToPosition(int position) 使当对应position的item可以滚动显示出来
     rvType.smoothScrollToPosition( int position)(getSelectedGroupPosition(item.typeId));
    }
   }
  });

3:添加商品的动画

添加商品一共有三个动画 
- 当商品从0到1 旋转左移显示出减号按钮 
- 当商品从1到0 减号按钮旋转右移消失 
- 添加商品时抛物线动画添加到购物车图标

前两个动画很简单可以分解成三个补间动画 旋转、平移、透明度。 
可以用xml完成,也可以代码设置,不过有个小坑要注意一下 旋转动画一定要在平移动画前面,否则就不是滚动平移了,而是乱跳。。。

这里贴一下动画的代码设置方法

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//显示减号的动画
private Animation getShowAnimation(){
  AnimationSet set = new AnimationSet( true );
  RotateAnimation rotate = new RotateAnimation( 0 , 720 ,RotateAnimation.RELATIVE_TO_SELF, 0 .5f,RotateAnimation.RELATIVE_TO_SELF, 0 .5f);
  set.addAnimation(rotate);
  TranslateAnimation translate = new TranslateAnimation(
    TranslateAnimation.RELATIVE_TO_SELF,2f
    ,TranslateAnimation.RELATIVE_TO_SELF, 0
    ,TranslateAnimation.RELATIVE_TO_SELF, 0
    ,TranslateAnimation.RELATIVE_TO_SELF, 0 );
  set.addAnimation(translate);
  AlphaAnimation alpha = new AlphaAnimation( 0 , 1 );
  set.addAnimation(alpha);
  set.setDuration( 500 );
  return set;
}
//隐藏减号的动画
private Animation getHiddenAnimation(){
  AnimationSet set = new AnimationSet( true );
  RotateAnimation rotate = new RotateAnimation( 0 , 720 ,RotateAnimation.RELATIVE_TO_SELF, 0 .5f,RotateAnimation.RELATIVE_TO_SELF, 0 .5f);
  set.addAnimation(rotate);
  TranslateAnimation translate = new TranslateAnimation(
    TranslateAnimation.RELATIVE_TO_SELF, 0
    ,TranslateAnimation.RELATIVE_TO_SELF,2f
    ,TranslateAnimation.RELATIVE_TO_SELF, 0
    ,TranslateAnimation.RELATIVE_TO_SELF, 0 );
  set.addAnimation(translate);
  AlphaAnimation alpha = new AlphaAnimation( 1 , 0 );
  set.addAnimation(alpha);
  set.setDuration( 500 );
  return set;
}
 
//执行动画 只需给对应控件setAnimation然后调用setVisibility方法即可
{
  ....
  tvMinus.setAnimation(getHiddenAnimation());
  tvMinus.setVisibility(View.GONE);
}

抛物线动画和上面的差不多可以分解成两个平移动画,不过两个平移动画的差值器一个线性一个加速而已,因为动画界面跨度比较大所以需要在根部局内写,不能写在列表的item中(这样会显示不全)。 
代码中的anim_mask_layout 即为整个布局文件的根布局,这里是一个RelativeLayout

实现过程: 

1、首先点击加号图标,拿到控件在屏幕上的绝对坐标,回调activity显示动画

?
1
2
3
int [] loc = new int [ 2 ];
v.getLocationInWindow(loc);
activity.playAnimation(loc);

2、创建动画的控件并添加到根部局并在动画结束后移除动画view

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public void playAnimation( int [] start_location){
  ImageView img = new ImageView( this );
  img.setImageResource(R.drawable.button_add);
  setAnim(img,start_location);
}
//创建动画 平移动画直接传递偏移量
private Animation createAnim( int startX, int startY){
  int [] des = new int [ 2 ];
  imgCart.getLocationInWindow(des);
 
  AnimationSet set = new AnimationSet( false );
 
  Animation translationX = new TranslateAnimation( 0 , des[ 0 ]-startX, 0 , 0 );
  //线性插值器 默认就是线性
  translationX.setInterpolator( new LinearInterpolator());
  Animation translationY = new TranslateAnimation( 0 , 0 , 0 , des[ 1 ]-startY);
  //设置加速插值器
  translationY.setInterpolator( new AccelerateInterpolator());
  Animation alpha = new AlphaAnimation( 1 , 0 .5f);
  set.addAnimation(translationX);
  set.addAnimation(translationY);
  set.addAnimation(alpha);
  set.setDuration( 500 );
 
  return set;
}
//计算动画view在根部局中的坐标 添加到根部局中
private void addViewToAnimLayout( final ViewGroup vg, final View view,
          int [] location) {
 
  int x = location[ 0 ];
  int y = location[ 1 ];
  int [] loc = new int [ 2 ];
  vg.getLocationInWindow(loc);
  view.setX(x);
  view.setY(y-loc[ 1 ]);
  vg.addView(view);
}
//设置动画结束移除动画view
private void setAnim( final View v, int [] start_location) {
 
  addViewToAnimLayout(anim_mask_layout, v, start_location);
  Animation set = createAnim(start_location[ 0 ],start_location[ 1 ]);
  set.setAnimationListener( new Animation.AnimationListener() {
   @Override
   public void onAnimationStart(Animation animation) {
 
   }
 
   @Override
   public void onAnimationEnd( final Animation animation) {
    //直接remove可能会因为界面仍在绘制中成而报错
    mHanlder.postDelayed( new Runnable() {
     @Override
     public void run() {
      anim_mask_layout.removeView(v);
     }
    }, 100 );
   }
 
   @Override
   public void onAnimationRepeat(Animation animation) {
 
   }
  });
  v.startAnimation(set);
}

4:底部弹出购物车清单

底部弹出的效果大家一定都很熟悉了,几回每个项目中都会用的到,官方没有提供简单的控件实现,一般都需要自己写,不过要做到简单流畅,便于移植推荐使用第三方库,这里向大家推荐一个

bottomsheet

集成简单,效果多样这里简单介绍一下使用方法

集成
compile 'com.flipboard:bottomsheet-core:1.5.1'
使用
xml中使用BottomSheetLayout包裹弹出view时候的背景布局,BottomSheetLayout继承自帧布局:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
< com.flipboard.bottomsheet.BottomSheetLayout
  xmlns:android = "http://schemas.android.com/apk/res/android"
  android:id = "@+id/bottomSheetLayout"
  android:layout_width = "match_parent"
  android:layout_height = "match_parent" >
  < LinearLayout
   android:orientation = "horizontal"
   android:layout_width = "match_parent"
   android:layout_height = "match_parent" >
 
   < android.support.v7.widget.RecyclerView
    android:layout_width = "100dp"
    android:id = "@+id/typeRecyclerView"
    android:layout_height = "match_parent" >
   android.support.v7.widget.RecyclerView >
 
   < se.emilsjolander.stickylistheaders.StickyListHeadersListView
    android:layout_width = "match_parent"
    android:background = "#fff"
    android:id = "@+id/itemListView"
    android:layout_height = "match_parent" >
   se.emilsjolander.stickylistheaders.StickyListHeadersListView >
  LinearLayout >
com.flipboard.bottomsheet.BottomSheetLayout >

代码中使用很简单

?
1
2
3
4
5
//弹出View bottomSheet即是要弹出的view
bottomSheetLayout.showWithSheetView(bottomSheet);
 
//代码隐藏view (点击弹出view以外的地方可以隐藏弹出的view,向下滑动也可以)
bottomSheetLayout.dismissSheet();

5:数据的同步

同步数据,控制界面刷新应该是新手最容易绕弯的地方了,其实只要仔细一点也不难,这里简单提供一种思路(并不一定适合你的项目).

?
1
2
3
4
5
6
7
8
//商品列表
private ArrayList dataList;
//分类列表
private ArrayList typeList;
//已选择的商品
private SparseArray selectedList;
//用于记录每个分组选择的数目
private SparseIntArray groupSelect;

SparseArray这个类其实就是 HashMap< Integer,Object >

不过SparseArray既可以根据key查找Value,也可以根据位置查找value,性能比HashMap高,是官方推荐的替代类, 
同样SparseIntArray 其实是HashMap< Integer,Integer> 的替代者。

Activity里实现了下面几个方法,用于数据统一管理 
列表中显示的商品购买数量统一从activity获取,商品的加减统一调用Activity的方法然后notifiDatasetChanged,由于代码不少具体的还是看源码吧

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
  * Item代表商品的购买数量加一
  * @param item
  * @param refreshGoodList 是否刷新商品list
  */
public void add(GoodsItem item, boolean refreshGoodList){
 
  int groupCount = groupSelect.get(item.typeId);
  if (groupCount== 0 ){
   groupSelect.append(item.typeId, 1 );
  } else {
   groupSelect.append(item.typeId,++groupCount);
  }
 
  GoodsItem temp = selectedList.get(item.id);
  if (temp== null ){
   item.count= 1 ;
   selectedList.append(item.id,item);
  } else {
   temp.count++;
  }
  update(refreshGoodList);
}
/**
  * Item商品的购买数量减一
  * @param item
  * @param refreshGoodList 是否刷新商品list
  */
public void remove(GoodsItem item, boolean refreshGoodList){
 
  int groupCount = groupSelect.get(item.typeId);
  if (groupCount== 1 ){
   groupSelect.delete(item.typeId);
  } else if (groupCount> 1 ){
   groupSelect.append(item.typeId,--groupCount);
  }
 
  GoodsItem temp = selectedList.get(item.id);
  if (temp!= null ){
   if (temp.count< 2 ){
    selectedList.remove(item.id);
   } else {
    item.count--;
   }
  }
  update(refreshGoodList);
}
 
/**
  * 刷新界面 总价、购买数量等
  * @param refreshGoodList 是否刷新商品list
  */
private void update( boolean refreshGoodList){
  ...
}
 
//根据商品id获取当前商品的采购数量
public int getSelectedItemCountById( int id){
  GoodsItem temp = selectedList.get(id);
  if (temp== null ){
   return 0 ;
  }
  return temp.count;
}
//根据类别Id获取属于当前类别的数量
public int getSelectedGroupCountByTypeId( int typeId){
  return groupSelect.get(typeId);
}

具体逻辑还是看代码吧,也许有更简单的实现。。。

Demo下载地址,下载到的文件是个AS module,你可以在自己新建的工程中Import Module.

源码下载:Android仿外卖购物车功能


你可能感兴趣的:(项目代码)