目录
看到 饿了么, 美团 的添加商品到购物车的效果,一直觉得很不错,虽然网上有很多博客已经实现了相似的效果,但是好像都没有那么全面,然而用到自己的项目中,也并没有那么实用。在此,系统的整理的一下,争取全面实用些!希望对于爱学习的你,也有所帮助。
话不多说,先贴出效果图:
那我们就从效果图,入手分析一下吧:实现如上效果,我们可以分以下几步:
- 左右列表的联动 (重点)
- 右边列表标题的悬停效果(粘性标签)
- 添加和减少商品时,按钮的动画
- 添加商品时的抛物线动画
- 底部弹出购物车清单
- 底部购物车清单列表和右边商品列表的联动 (重点)
下面,我会带大家逐一实现上面的每一步,并争取每一步都有多种实现方案。
由于整篇博客,侧重于思路,只会贴出关键代码,建议大家可一边看 源码 一边看博客。
左右列表的联动
说到左右列表联动,首先需要说明一下,这里左边是RecyclerView,右边也是RecyclerView。
(1)第一步,我们左边列表,点击每一项,都要有选中的效果。这里我们可以用一个 boolean变量来实现,如果点击了某个item,遍历列表时,就给相应item对象中的变量赋值为true,其他赋值为false,然后刷新adapter即可。
关键代码:
/**
* 更新左边菜单类型的状态
*
* @param position
*/
private void updateMenuStatus(int position) {
List<LeftResult> dataList = mLeftAdapter.getData();
for (int i = 0; i < dataList.size(); i++) {
if (i == position) {
dataList.get(i).isSelect = true;
} else {
dataList.get(i).isSelect = false;
}
}
mLeftAdapter.notifyDataSetChanged();
}
(2)那左边列表选中某一项时,右边列表如果滑动到相应类别项呢?我们可以使用LinearLayoutManager中的mLayoutManager.scrollToPositionWithOffset(0, 0);这个属性,就能轻松实现啦。
关键代码:
mLeftAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
updateMenuStatus(position);//更新左边菜单类型的状态
if (position == 0) {
//mLayoutManager.scrollToPosition(0);//测试发现这个,方法是不好的。
mLayoutManager.scrollToPositionWithOffset(0, 0);
} else {
if (productBeanList != null && productBeanList.size() > 0) {
int scrollToPosition = 0;
for (int i = 0; i < position; i++) {
scrollToPosition += productBeanList.get(i).list.size() + 1;
}
Log.i(TAG, "onItemClick: scrollToPosition=" + scrollToPosition);
mLayoutManager.scrollToPositionWithOffset(scrollToPosition, 0);
}
}
}
});
(3)下面这一步我们主要来实现,当右边列表滑动时,滑动到某一类别时,左边列表同时也切换到相同类别项。那如何实现呢?好,我想想看,,,,对,你想的没错,我们肯定要监听RecyclerView的滚动事件啦。
关键代码:
mContentRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == 0) {//解决滑动过快,firstVisibleItemPosition 没有被赋值,导致左边菜单项,状态设置不对的问题。
int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
String strTitle = mContentResultList.get(firstVisibleItemPosition).getTitle();
Log.i(TAG, "onScrollStateChanged: firstVisibleItemPosition=" + firstVisibleItemPosition);
for (LeftResult leftResult : mLeftResultList) {
if (leftResult.name.equals(strTitle)) {
leftResult.isSelect = true;
} else {
leftResult.isSelect = false;
}
}
mLeftAdapter.notifyDataSetChanged();
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
String strTitle = mContentResultList.get(firstVisibleItemPosition).getTitle();
String strName = mContentResultList.get(firstVisibleItemPosition).getName();
if (TextUtils.isEmpty(strName) && !TextUtils.isEmpty(strTitle)) {
//Log.i(TAG, "onScrolled: strTitle=" + strTitle + " firstVisibleItemPosition=" + firstVisibleItemPosition);
for (LeftResult leftResult : mLeftResultList) {
if (leftResult.name.equals(strTitle)) {
leftResult.isSelect = true;
} else {
leftResult.isSelect = false;
}
}
mLeftAdapter.notifyDataSetChanged();
}
//悬浮标题处理 实现
hoverTitleDealWith(recyclerView, firstVisibleItemPosition);
}
});
ps: 你可能对 if (TextUtils.isEmpty(strName) && !TextUtils.isEmpty(strTitle)) {…} 这样的判断,不太明白,觉得这里也很有必要说明一下。其实上面所说的功能,关键思路是:在滑动时,实时获取顶部第一个可见的item,判断这个 item 是否为 类别 item,是的话,就遍历左边的列表比对,符合的就赋值为true,然后刷新adapter即可。 判断这个 item 是否为 类别 item 的方法应该有很多种,我这里用的 比对 item 对象中某些字段值。
这时,也许你可能会有些明白啦,是的,类别item项的title肯定是有值的,而非类别item项的title是没有被赋值的。那这一个字段判断,不就行了吗,为什么还要有一个字段呢?当你用这一个字段的时候,理论上是没有问题的,可很奇怪的是,当你快速滑动列表时,你会发现 firstVisibleItemPosition 有可能会赋不上值。这样就会导致左边菜单项,状态设置不对。
那怎么解决呢?代码上面已经有了,思路是:在列表滚动结束时,判断 第一个可见的item属于哪个类别项,是哪一个,左边的类别项,就更新哪一个的状态。因此这里,不得不在初始化右边列表数据时,给每个列表项,都赋值一个类别title。所以 if (TextUtils.isEmpty(strName) && !TextUtils.isEmpty(strTitle)) {…} 这里要加两个判断才可以。商品名为空,并且 类别title不为空,才肯定是类别item。
右边列表标题的悬停效果(粘性标签)
这里,我们来实现,右边标题悬停的效果。这里网上也有很多开源的框架,你在github上搜类似这样的关键词 StickyHeaders,StickyListHeaders 会有不少。不过这里,推荐一下这个吧:PinnedSectionItemDecoration 。使用起来也比较简单(用法在 源码 中,点击商品列表,进入的界面有展示),关键是如果你右边的列表用的是recycleview,用这个就可以。有的开源框架,是基于 listView 的封装,你用 recycleview 就不行了。
那如果想自己写个呢,该如何实现呢?其实上面效果图中,并没有使用开源框架,而是结合自己的理解写的,效果也是挺不错的吧。下面看看具体思路和关键代码:
/**
* 悬浮标题处理
* <p>
* 实现步骤描述:
* (1):要获取到列表中,第一个显示的item+1 的item的bean类,判断是否为菜单类型
* 是的话,把该item的 position 赋值给一个临时变量 a
* (2):在滑动过程中,当 临时变量 a 等于 列表中第一个显示的item+1 的时候,
* (3):获取列表中( View childView = recyclerView.getChildAt(0);)第一个view, 通过 childView.getBottom()
* 实时获取item显示的距离
* <p>
* (4):通过比较 item显示的距离 和 悬浮标题的高度,来动态设置 悬浮标题 移动 即可!
*
* @param recyclerView
* @param firstVisibleItemPosition
*/
private void hoverTitleDealWith(@NonNull RecyclerView recyclerView, int firstVisibleItemPosition) {
//int section = getSectionForPosition(firstVisibleItemPosition);
//int nextSection = getSectionForPosition(firstVisibleItemPosition + 1);
//int nextSecPosition = getPositionForSection(+nextSection);
int nextSection = 0;//下一个位置
String strNextTitle = mContentResultList.get(firstVisibleItemPosition + 1).getTitle();
String strNextName = mContentResultList.get(firstVisibleItemPosition + 1).getName();
if (TextUtils.isEmpty(strNextName) && !TextUtils.isEmpty(strNextTitle)) {
nextSection = firstVisibleItemPosition + 1;
}
//Log.i(TAG, "onScrolled: firstVisibleItemPosition=" + firstVisibleItemPosition + " lastFirstVisibleItem=" + lastFirstVisibleItem);
if (firstVisibleItemPosition != lastFirstVisibleItem) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) tvHoverTitle.getLayoutParams();
params.topMargin = 0;
tvHoverTitle.setLayoutParams(params);
tvHoverTitle.setText(mContentResultList.get(firstVisibleItemPosition).getTitle());
}
//Log.i(TAG, "onScrolled: nextSection="+nextSection+" nextSecPosition="+nextSecPosition);
if (nextSection == firstVisibleItemPosition + 1) {
View childView = recyclerView.getChildAt(0);
if (childView != null) {
int titleHeight = tvHoverTitle.getHeight();
int bottom = childView.getBottom();//我认为这行代码,很关键。(获取顶部item 显示出来的高度)
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) tvHoverTitle.getLayoutParams();
//Log.i(TAG, "onScrolled: bottom=" + bottom + " titleHeight=" + titleHeight);
if (bottom < titleHeight) {
float pushedDistance = bottom - titleHeight;
params.topMargin = (int) pushedDistance;
tvHoverTitle.setLayoutParams(params);
} else {
if (params.topMargin != 0) {
params.topMargin = 0;
tvHoverTitle.setLayoutParams(params);
}
}
}
}
lastFirstVisibleItem = firstVisibleItemPosition;
}
ps:代码中都已经说的比较明白了,这里就不在废话了。
添加和减少商品时,按钮的动画
这里,效果图中,用的是这个开源库:AnimShopButton 挺实用。用法挺简单,关键代码:
<com.example.shopcatdemo.view.shopcart.AnimShopButton
android:id="@+id/shopCartButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/spacing_10"
app:addEnableBgColor="#f3593c"
app:addEnableFgColor="#ffffff"
app:count="0"
app:gapBetweenCircle="40dp"
app:hintBgColor="#f3593c"
app:hintFgColor="#ffffff"
app:ignoreHintArea="true"
app:maxCount="99" />
同样那如果我想自己实现一个,怎么实现呢?
还是先贴出一下效果图吧:
也不再多说啦,源码 中都有的,关键代码如下:
if (item.getCount() > 0) {
tvNumber.setVisibility(View.VISIBLE);
imageRemove.setVisibility(View.VISIBLE);
tvNumber.setText(item.getCount() + "");
} else {
tvNumber.setVisibility(View.INVISIBLE);
imageRemove.setVisibility(View.INVISIBLE);
tvNumber.setText("0");
}
if (item.getInventory() <= 0) {
tvNumber.setVisibility(View.INVISIBLE);
imageRemove.setVisibility(View.INVISIBLE);
}
//减少
imageRemove.setOnClickListener(v -> {
number = item.getCount();
if (number == 0) return;
number--;
item.setCount(number);
tvNumber.setText(number + "");
if (number == 0) {
tvNumber.setVisibility(View.INVISIBLE);
imageRemove.setVisibility(View.INVISIBLE);
tvNumber.setAnimation(getHiddenAnimation());
imageRemove.setAnimation(getHiddenAnimation());
}
listener.onDelSuccess(imageAdd, number, helper.getLayoutPosition());
});
//添加
imageAdd.setOnClickListener(v -> {
number = item.getCount();
if (number < item.getInventory()) {
number++;
item.setCount(number);
tvNumber.setText(number + "");
if (number == 1) {
tvNumber.setVisibility(View.VISIBLE);
imageRemove.setVisibility(View.VISIBLE);
tvNumber.setAnimation(getShowAnimation());
imageRemove.setAnimation(getShowAnimation());
}
listener.onAddSuccess(imageAdd, number, helper.getLayoutPosition());
} else {
Toast.makeText(mContext, "已超出库存数量", Toast.LENGTH_SHORT).show();
}
});
由于篇幅太多,后面的几步实现,见 购物车完整效果(下),这篇博客吧。
购物车完整效果(下)
源码git地址
参考博客:
Android仿外卖购物车的实现
Android 仿饿了么点餐页面 效果如下: