当前位置: 移动技术网 > IT编程>移动开发>Android > Android自定义控件仿QQ抽屉效果

Android自定义控件仿QQ抽屉效果

2019年07月24日  | 移动技术网IT编程  | 我要评论

p36战斗机,小轩卡盟,秦学士 朱洁

其实网上类似的实现已经很多了,原理也并不难,只是网上各种demo运行下来,多少都有一些问题。折腾了半天,决定自己实现一个。

首先我们看看实现效果:

对比网上各类demo,这次要实现的主要表现在以下几点:

1.侧滑显示抽屉view
2.侧滑抽屉隐藏view控件点击事件
3.单击任意item隐藏显示的抽屉view
4.滑动list隐藏显示的抽屉view
5.增加swipelayout点击事件和swipe touch事件判断处理
6.优化快速划开多个抽屉隐藏view时多个swipelayout滑动状态判断处理,仅显示最后一个滑动的抽屉隐藏view,隐藏前面所有打开的抽屉view(快速滑动时,可能存在多个抽屉view打开情况,网上找的几个demo主要问题都集中在这一块)

实现原理

其实单就一个swipelayout的实现原理来讲的话,还是很简单的,实际上单个swipelayout隐藏抽屉状态时,应该是这样的:

也就是说,最初的隐藏状态,实际上是将hide view区域layout到conten view的右边,达到隐藏效果,而后显示则是根据拖拽的x值变化来动态的layout 2个view,从而达到一个滑动抽屉效果。
当然,直接重写view的ontouchevent来动态的layout 2个view是可以实现我们需要的效果的,但是有更好的方法来实现,就是同过viewdraghelper。
viewdraghelper是google官方提供的一个专门用于手势分析处理的类,关于viewdraghelper的基本使用,网上有一大堆的资源。具体的viewdraghelper介绍以及基本使用方法,本文就不重复造轮子了,此处推荐鸿洋大神的一篇微博:android viewdraghelper完全解析 自定义viewgroup神器

具体实现

下面我们开始具体的实现。
布局比较简单,这里就不贴代码了,最后会贴上本demo的完整代码地址。

首先我们实现一个继承framelayout的自定义swipelauout,重写onfinishinflate方法:
这里我们只允许swipelayout设置2个子view,contentlayout是继承linearlayout的自定义layout,后面会讲到这个,此处先略过;

 @override
 protected void onfinishinflate() {
  super.onfinishinflate();
  if (getchildcount() != 2) {
   throw new illegalstateexception("must 2 views in swipelayout");
  }
  contentview = getchildat(0);
  hideview = getchildat(1);
  if (contentview instanceof contentlayout)
   ((contentlayout) contentview).setswipelayout(this);
  else {
   throw new illegalstateexception("content view must be an instanceof frontlayout");
  }
 }

接着重写onsizechanged,onlayout,onintercepttouchevent方法:

 @override
 protected void onsizechanged(int w, int h, int oldw, int oldh) {
  super.onsizechanged(w, h, oldw, oldh);
  hideviewheight = hideview.getmeasuredheight();
  hideviewwidth = hideview.getmeasuredwidth();
  contentwidth = contentview.getmeasuredwidth();
 }

 @override
 protected void onlayout(boolean changed, int left, int top, int right,
       int bottom) {
  // super.onlayout(changed, left, top, right, bottom);
  contentview.layout(0, 0, contentwidth, hideviewheight);
  hideview.layout(contentview.getright(), 0, contentview.getright()
    + hideviewwidth, hideviewheight);
 }

 @override
 public boolean onintercepttouchevent(motionevent ev) {
  boolean result = viewdraghelper.shouldintercepttouchevent(ev);
  //  log.e("swipelayout", "-----onintercepttouchevent-----");
  return result;
 }

然后是比较关键的,重写ontouchevent方法以及viewdraghelper.callback回调,我们定了一个enum来判断swipelayout的三种状态。在onviewpositionchanged中,有2种方法实现content view和hide view的伴随移动,一种是直接offset view的横向变化量,还有一种就是直接通过layout的方式,两种方式都可以。

 public enum swipestate {
  open, swiping, close;
 }
 @override
 public boolean ontouchevent(motionevent event) {
  //  log.e("swipelayout", "-----ontouchevent-----");
  switch (event.getaction()) {
   case motionevent.action_down:
    downx = event.getx();
    downy = event.gety();
    break;
   case motionevent.action_move:
    // 1.获取x和y方向移动的距离
    float movex = event.getx();
    float movey = event.gety();
    float delatx = movex - downx;// x方向移动的距离
    float delaty = movey - downy;// y方向移动的距离

    if (math.abs(delatx) > math.abs(delaty)) {
     // 表示移动是偏向于水平方向,那么应该swipelayout应该处理,请求listview不要拦截
     this.requestdisallowintercepttouchevent(true);
    }

    // 更新downx,downy
    downx = movex;
    downy = movey;
    break;
   case motionevent.action_up:

    break;
  }
  viewdraghelper.processtouchevent(event);
  return true;
 }

 private viewdraghelper.callback callback = new viewdraghelper.callback() {
  @override
  public boolean trycaptureview(view child, int pointerid) {
   return child == contentview || child == hideview;
  }

  @override
  public int getviewhorizontaldragrange(view child) {
   return hideviewwidth;
  }

  @override
  public int clampviewpositionhorizontal(view child, int left, int dx) {
   if (child == contentview) {
    if (left > 0)
     left = 0;
    if (left < -hideviewwidth)
     left = -hideviewwidth;
   } else if (child == hideview) {
    if (left > contentwidth)
     left = contentwidth;
    if (left < (contentwidth - hideviewwidth))
     left = contentwidth - hideviewwidth;
   }
   return left;
  }

  @override
  public void onviewpositionchanged(view changedview, int left, int top,
           int dx, int dy) {
   super.onviewpositionchanged(changedview, left, top, dx, dy);
   if (changedview == contentview) {
    // 如果手指滑动deleteview,那么也要讲横向变化量dx设置给contentview
    hideview.offsetleftandright(dx);
   } else if (changedview == hideview) {
    // 如果手指滑动contentview,那么也要讲横向变化量dx设置给deleteview
    contentview.offsetleftandright(dx);
   }

   //   if (changedview == contentview) {
   //    // 手动移动deleteview
   //    hideview.layout(hideview.getleft() + dx,
   //      hideview.gettop() + dy, hideview.getright() + dx,
   //      hideview.getbottom() + dy);
   //   } else if (hideview == changedview) {
   //    // 手动移动contentview
   //    contentview.layout(contentview.getleft() + dx,
   //      contentview.gettop() + dy, contentview.getright() + dx,
   //      contentview.getbottom() + dy);
   //   }
   //实时更新当前状态
   updateswipestates();
   invalidate();
  }

  @override
  public void onviewreleased(view releasedchild, float xvel, float yvel) {
   super.onviewreleased(releasedchild, xvel, yvel);
   //根据用户滑动速度处理开关
   //xvel: x方向滑动速度
   //yvel: y方向滑动速度
   //   log.e("tag", "currentstate = " + currentstate);
   //   log.e("tag", "xvel = " + xvel);
   if (xvel < -200 && currentstate != swipestate.open) {
    open();
    return;
   } else if (xvel > 200 && currentstate != swipestate.close) {
    close();
    return;
   }

   if (contentview.getleft() < -hideviewwidth / 2) {
    // 打开
    open();
   } else {
    // 关闭
    close();
   }
  }
 };

open(),close()实现

 public void open() {
  open(true);
 }

 public void close() {
  close(true);
 }

 /**
  * 打开的方法
  *
  * @param issmooth 是否通过缓冲动画的形式设定view的位置
  */
 public void open(boolean issmooth) {
  if (issmooth) {
   viewdraghelper.smoothslideviewto(contentview, -hideviewwidth,
     contentview.gettop());
   viewcompat.postinvalidateonanimation(swipelayout.this);
  } else {
   contentview.offsetleftandright(-hideviewwidth);//直接偏移view的位置
   hideview.offsetleftandright(-hideviewwidth);//直接偏移view的位置
   //   contentview.layout(-hideviewwidth, 0, contentwidth - hideviewwidth, hideviewheight);//直接通过坐标摆放
   //   hideview.layout(contentview.getright(), 0, hideviewwidth, hideviewheight);//直接通过坐标摆放
   invalidate();
  }
 }

 /**
  * 关闭的方法
  *
  * @param issmooth true:通过缓冲动画的形式设定view的位置
  *     false:直接设定view的位置
  */
 public void close(boolean issmooth) {
  if (issmooth) {
   viewdraghelper.smoothslideviewto(contentview, 0, contentview.gettop());
   viewcompat.postinvalidateonanimation(swipelayout.this);
  } else {
   contentview.offsetleftandright(hideviewwidth);
   hideview.offsetleftandright(hideviewwidth);
   invalidate();
   //contentview.layout(0, 0, contentwidth, hideviewheight);//直接通过坐标摆放
   //hideview.layout(contentview.getright(), 0, hideviewwidth, hideviewheight);//直接通过坐标摆放
  }
 }

此上基本实现了单个swipelayout的抽屉滑动效果,但是将此swipelayout作为一个item布局设置给一个listview的时候,还需要做许多的判断。

由于listview的重用机制,我们这里并未针对listview做任何处理,所以一旦有一个item的swipelayout的状态是打开状态,不可避免的其它也必然有几个是打开状态,所以我们这里需要根据检测listview的滑动,当listview滑动时,关闭swipelayout。既然需要在外部控制swipelayout的开关,我们先定义一个swipelayoutmanager用于管理swipelayout的控制。

public class swipelayoutmanager {
 //记录打开的swipelayout集合
 private hashset<swipelayout> munclosedswipelayouts = new hashset<swipelayout>();

 private swipelayoutmanager() {
 }

 private static swipelayoutmanager minstance = new swipelayoutmanager();

 public static swipelayoutmanager getinstance() {
  return minstance;
 }

 /**
  * 将一个没有关闭的swipelayout加入集合
  * @param layout
  */
 public void add(swipelayout layout) {
  munclosedswipelayouts.add(layout);
 }

 /**
  * 将一个没有关闭的swipelayout移出集合
  * @param layout
  */
 public void remove(swipelayout layout){
  munclosedswipelayouts.remove(layout);
 }

 /**
  * 关闭已经打开的swipelayout
  */
 public void closeuncloseswipelayout() {
  if(munclosedswipelayouts.size() == 0){
   return;
  }

  for(swipelayout l : munclosedswipelayouts){
   l.close(true);
  }
  munclosedswipelayouts.clear();
 }

 /**
  * 关闭已经打开的swipelayout
  */
 public void closeuncloseswipelayout(boolean issmooth) {
  if(munclosedswipelayouts.size() == 0){
   return;
  }

  for(swipelayout l : munclosedswipelayouts){
   l.close(issmooth);
  }
  munclosedswipelayouts.clear();
 }
}

这样就可以监听listview的滑动,然后在listview滑动的时候,关闭所有的抽屉view。

 listview.setonscrolllistener(new onscrolllistener() {
   @override
   public void onscrollstatechanged(abslistview view, int scrollstate) {
    swipelayoutmanager.closeuncloseswipelayout();
   }

   @override
   public void onscroll(abslistview view, int firstvisibleitem, int visibleitemcount, int totalitemcount) {
   }
  });

考虑到大多数时候,在我们打开抽屉view和关闭抽屉view的时候,外部需要知道swipelayout的状态值,所以我们需要在swipelayout中增加几个接口,告诉外部当前swipelayout的状态值:

swipelayout.java

----------------
 private void updateswipestates() {
  swipestate lastswipestate = currentstate;
  swipestate swipestate = getcurrentstate();

  if (listener == null) {
   try {
    throw new exception("please setonswipestatechangelistener first!");
   } catch (exception e) {
    e.printstacktrace();
   }
   return;
  }

  if (swipestate != currentstate) {
   currentstate = swipestate;
   if (currentstate == swipestate.open) {
    listener.onopen(this);
    // 当前的swipelayout已经打开,需要让manager记录
    swipelayoutmanager.add(this);
   } else if (currentstate == swipestate.close) {
    listener.onclose(this);
    // 说明当前的swipelayout已经关闭,需要让manager移除
    swipelayoutmanager.remove(this);
   } else if (currentstate == swipestate.swiping) {
    if (lastswipestate == swipestate.open) {
     listener.onstartclose(this);
    } else if (lastswipestate == swipestate.close) {
     listener.onstartopen(this);
     //hideview准备显示之前,先将之前打开的的swipelayout全部关闭
     swipelayoutmanager.closeuncloseswipelayout();
     swipelayoutmanager.add(this);
    }
   }
  } else {
   currentstate = swipestate;
  }
 }

 /**
  * 获取当前控件状态
  *
  * @return
  */
 public swipestate getcurrentstate() {
  int left = contentview.getleft();
  //  log.e("tag", "contentview.getleft() = " + left);
  //  log.e("tag", "hideviewwidth = " + hideviewwidth);
  if (left == 0) {
   return swipestate.close;
  }

  if (left == -hideviewwidth) {
   return swipestate.open;
  }
  return swipestate.swiping;
 }
 private onswipestatechangelistener listener;

 public void setonswipestatechangelistener(
   onswipestatechangelistener listener) {
  this.listener = listener;
 }

 public view getcontentview() {
  return contentview;
 }

 public interface onswipestatechangelistener {
  void onopen(swipelayout swipelayout);

  void onclose(swipelayout swipelayout);

  void onstartopen(swipelayout swipelayout);

  void onstartclose(swipelayout swipelayout);
 }

然后接下来是写一个为listview设置的swipeadapter

swipeadapter.java

------------
public class swipeadapter extends baseadapter implements onswipestatechangelistener {
 private context mcontext;
 private list<string> list;
 private myclicklistener myclicklistener;
 private swipelayoutmanager swipelayoutmanager;

 public swipeadapter(context mcontext) {
  super();
  this.mcontext = mcontext;
  init();
 }

 private void init() {
  myclicklistener = new myclicklistener();
  swipelayoutmanager = swipelayoutmanager.getinstance();
 }

 public void setlist(list<string> list){
  this.list = list;
  notifydatasetchanged();
 }

 @override
 public int getcount() {
  return list.size();
 }

 @override
 public object getitem(int position) {
  return list.get(position);
 }

 @override
 public long getitemid(int position) {
  return position;
 }

 @override
 public view getview(final int position, view convertview, viewgroup parent) {
  if (convertview == null) {
   convertview = uiutils.inflate(r.layout.list_item_swipe);
  }
  viewholder holder = viewholder.getholder(convertview);

  holder.tv_content.settext(list.get(position));
  holder.tv_overhead.setonclicklistener(myclicklistener);
  holder.tv_overhead.settag(position);
  holder.tv_delete.setonclicklistener(myclicklistener);
  holder.tv_delete.settag(position);
  holder.sv_layout.setonswipestatechangelistener(this);
  holder.sv_layout.settag(position);

  holder.sv_layout.getcontentview().setonclicklistener(new view.onclicklistener() {
   @override
   public void onclick(view v) {
    toastutils.showtoast("item click : " + position);
    swipelayoutmanager.closeuncloseswipelayout();
   }
  });

  return convertview;
 }

 static class viewholder {
  textview tv_content, tv_overhead, tv_delete;
  swipelayout sv_layout;

  public viewholder(view convertview) {
   tv_content = (textview) convertview.findviewbyid(r.id.tv_content);
   tv_overhead = (textview) convertview.findviewbyid(r.id.tv_overhead);
   tv_delete = (textview) convertview.findviewbyid(r.id.tv_delete);
   sv_layout = (swipelayout) convertview.findviewbyid(r.id.sv_layout);
  }

  public static viewholder getholder(view convertview) {
   viewholder holder = (viewholder) convertview.gettag();
   if (holder == null) {
    holder = new viewholder(convertview);
    convertview.settag(holder);
   }
   return holder;
  }
 }

 class myclicklistener implements view.onclicklistener {
  @override
  public void onclick(view v) {
   integer position = (integer) v.gettag();
   switch (v.getid()) {
    case r.id.tv_overhead:
     //toastutils.showtoast("position : " + position + " overhead is clicked.");
     }
     break;
    case r.id.tv_delete:
     //toastutils.showtoast("position : " + position + " delete is clicked.");
     }
     break;
    default:
     break;
   }
  }
 }

 @override
 public void onopen(swipelayout swipelayout) {
  //toastutils.showtoast(swipelayout.gettag() + "onopen.");
 }

 @override
 public void onclose(swipelayout swipelayout) {
  //toastutils.showtoast(swipelayout.gettag() + "onclose.");
 }

 @override
 public void onstartopen(swipelayout swipelayout) {
  //   toastutils.showtoast("onstartopen.");
 }

 @override
 public void onstartclose(swipelayout swipelayout) {
  //   toastutils.showtoast("onstartclose.");
 }
}

此时已经基本实现了我们需要的大部分功能了,但是当我们滑动的时候,又发现新的问题,我们的swipelayout和listview滑动判断有问题。由于前面我们仅仅是将touch拦截事件简简单单的丢给了viewdraghelper.shouldintercepttouchevent(ev)来处理,导致swipelayout和listview拦截touch事件时的处理存在一定的问题,这里我们要提到一个知识点:android view事件的传递。
(1)首先由activity分发,分发给根view,也就是decorview(decorview为整个window界面的最顶层view)
(2)然后由根view分发到子的view

view事件拦截如下图所示:

view事件的消费如下图所示:

注:以上2张图借鉴网上总结的比较经典的图

所以这里我们就要谈到一开始出现的contentlayout,主要重写了onintercepttouchevent和ontouchevent。

public class contentlayout extends linearlayout {
 swipelayoutinterface miswipelayout;

 public contentlayout(context context) {
  super(context);
 }

 public contentlayout(context context, attributeset attrs) {
  super(context, attrs);
 }

 public contentlayout(context context, attributeset attrs, int defstyleattr) {
  super(context, attrs, defstyleattr);
 }

 public void setswipelayout(swipelayoutinterface iswipelayout) {
  this.miswipelayout = iswipelayout;
 }

 @override
 public boolean onintercepttouchevent(motionevent ev) {
//  log.e("contentlayout", "-----onintercepttouchevent-----");
  if (miswipelayout.getcurrentstate() == swipestate.close) {
   return super.onintercepttouchevent(ev);
  } else {
   return true;
  }
 }

 @override
 public boolean ontouchevent(motionevent ev) {
//  log.e("contentlayout", "-----ontouchevent-----");
  if (miswipelayout.getcurrentstate() == swipestate.close) {
   return super.ontouchevent(ev);
  } else {
   if (ev.getactionmasked() == motionevent.action_up) {
    miswipelayout.close();
   }
   return true;
  }
 }
}

另外由于在contentlayout中需要拿到父view swipelayout的开关状态以及控制swipelayout的关闭,因此在再写一个接口,用于contentlayout获取swipelayout的开关状态以及更新swipelayout。

public interface swipelayoutinterface {

 swipestate getcurrentstate();

 void open();

 void close();
}

然后接着的是完善swipelayout的onintercepttouchevent,我们在这里增加一个gesturedetectorcompat处理手势识别:

 private void init(context context) {
  viewdraghelper = viewdraghelper.create(this, callback);
  mgesturedetector = new gesturedetectorcompat(context, mongesturelistener);
  swipelayoutmanager = swipelayoutmanager.getinstance();
 }

 private simpleongesturelistener mongesturelistener = new simpleongesturelistener() {
  @override
  public boolean onscroll(motionevent e1, motionevent e2, float distancex, float distancey) {
   // 当横向移动距离大于等于纵向时,返回true
   return math.abs(distancex) >= math.abs(distancey);
  }
 };
  @override
 public boolean onintercepttouchevent(motionevent ev) {
  boolean result = viewdraghelper.shouldintercepttouchevent(ev) & mgesturedetector.ontouchevent(ev);
  //  log.e("swipelayout", "-----onintercepttouchevent-----");
  return result;
 }

如此下来,整个view不管是上下拖动,还是swipelayout的开关滑动,都已经实现完成了。最后增加对应overhead,delete以及item的点击事件,此处完善swipeadapter的代码之后如下。

 class myclicklistener implements view.onclicklistener {
  @override
  public void onclick(view v) {
   integer position = (integer) v.gettag();
   switch (v.getid()) {
    case r.id.tv_overhead:
     //toastutils.showtoast("position : " + position + " overhead is clicked.");
     swipelayoutmanager.closeuncloseswipelayout(false);
     if(onswipecontrollistener != null){
      onswipecontrollistener.onoverhead(position, list.get(position));
     }
     break;
    case r.id.tv_delete:
     //toastutils.showtoast("position : " + position + " delete is clicked.");
     swipelayoutmanager.closeuncloseswipelayout(false);
     if(onswipecontrollistener != null){
      onswipecontrollistener.ondelete(position, list.get(position));
     }
     break;
    default:
     break;
   }
  }
 }

 private onswipecontrollistener onswipecontrollistener;

 public void setonswipecontrollistener(onswipecontrollistener onswipecontrollistener){
  this.onswipecontrollistener = onswipecontrollistener;
 }

 /**
  * overhead 和 delete点击事件接口
  */
 public interface onswipecontrollistener{
  void onoverhead(int position, string itemtitle);

  void ondelete(int position, string itemtitle);
 }

最后贴上mainactivity代码,此处通过onswipecontrollistener接口回调实现item的删除和置顶:

public class mainactivity extends activity implements onswipecontrollistener {
 private listview listview;
 private list<string> list = new arraylist<string>();
 private swipelayoutmanager swipelayoutmanager;
 private swipeadapter swipeadapter;

 protected void oncreate(bundle savedinstancestate) {
  super.oncreate(savedinstancestate);
  setcontentview(r.layout.activity_main);

  initdata();
  initview();
 }

 private void initdata() {
  for (int i = 0; i < 50; i++) {
   list.add("content - " + i);
  }
 }

 private void initview() {
  swipelayoutmanager = swipelayoutmanager.getinstance();
  swipeadapter = new swipeadapter(this);
  swipeadapter.setlist(list);

  listview = (listview) findviewbyid(r.id.list_view);

  listview.setadapter(swipeadapter);
  listview.setonscrolllistener(new onscrolllistener() {
   @override
   public void onscrollstatechanged(abslistview view, int scrollstate) {
    swipelayoutmanager.closeuncloseswipelayout();
   }

   @override
   public void onscroll(abslistview view, int firstvisibleitem, int visibleitemcount, int totalitemcount) {
   }
  });

  swipeadapter.setonswipecontrollistener(this);
 }

 @override
 public void onoverhead(int position, string itemtitle) {
  setitemoverhead(position, itemtitle);
 }

 @override
 public void ondelete(int position, string itemtitle) {
  removeitem(position, itemtitle);
 }

 /**
  * 设置item置顶
  *
  * @param position
  * @param itemtitle
  */
 private void setitemoverhead(int position, string itemtitle) {
  // toastutils.showtoast("position : " + position + " overhead.");
  toastutils.showtoast("overhead ---" + itemtitle + "--- success.");
  string newtitle = itemtitle;
  list.remove(position);//删除要置顶的item
  list.add(0, newtitle);//根据adapter传来的title数据在list 0位置插入title字符串,达到置顶效果
  swipeadapter.setlist(list);//重新给adapter设置list数据并更新
  uiutils.runonuithread(new runnable() {
   @override
   public void run() {
    listview.setselection(0);//listview选中第0项item
   }
  });
 }

 /**
  * 删除item
  *
  * @param position
  * @param itemtitle
  */

 private void removeitem(int position, string itemtitle) {
  //  toastutils.showtoast("position : " + position + " delete.");
  toastutils.showtoast("delete ---" + itemtitle + "--- success.");
  list.remove(position);
  swipeadapter.setlist(list);//重新给adapter设置list数据并更新
 }
}

至此整个demo基本完成,本次完成的功能基本能够直接放到项目中使用。其实最麻烦的地方就在于view的touch事件拦截和处理,不过将本demo的log打开看一下对比之后,也就能够理解整个传递过程了。

完整demo地址:https://github.com/horrarndoo/swipelayout

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网