知识点分析
效果图来看不复杂内容并没多少,值得介绍一下的知识点也就下面几个吧
- 列表标题悬停
- 左右列表滑动时联动
- 添加商品时的抛物线动画
- 底部弹出购物车清单
- 数据的同步
另外就是实现效果的时候可能会遇到的几个坑。。。
布局很简单直接进入代码
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
//分类列表
private
ArrayList
//已选择的商品
private
SparseArray
//用于记录每个分组选择的数目
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仿外卖购物车功能