欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android仿外卖购物车功能

程序员文章站 2024-03-03 22:31:58
先看看效果图: 知识点分析 效果图来看不复杂内容并没多少,值得介绍一下的知识点也就下面几个吧 - 列表标题悬停 - 左右列表滑动时联动 - 添加商品时的...

先看看效果图:

Android仿外卖购物车功能

知识点分析

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

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

布局很简单直接进入代码

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

 <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接口包括两个方法

 view getheaderview(int position, view convertview, viewgroup parent);

 long getheaderid(int position);

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

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,根据列表中显示的第一条数据设置左侧选中的分类

 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完成,也可以代码设置,不过有个小坑要注意一下 旋转动画一定要在平移动画前面,否则就不是滚动平移了,而是乱跳。。。

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

 

 //显示减号的动画
 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显示动画

 int[] loc = new int[2];
 v.getlocationinwindow(loc);
 activity.playanimation(loc);

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

 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继承自帧布局:

<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>

代码中使用很简单

 //弹出view bottomsheet即是要弹出的view
 bottomsheetlayout.showwithsheetview(bottomsheet);

 //代码隐藏view (点击弹出view以外的地方可以隐藏弹出的view,向下滑动也可以)
 bottomsheetlayout.dismisssheet();

5:数据的同步

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

 //商品列表
 private arraylist<goodsitem> datalist;
 //分类列表
 private arraylist<goodsitem> typelist;
 //已选择的商品
 private sparsearray<goodsitem> selectedlist;
 //用于记录每个分组选择的数目
 private sparseintarray groupselect;

sparsearray这个类其实就是 hashmap< integer,object >

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

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

 /**
  * 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仿外卖购物车功能

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。