Android自定义控件仿QQ抽屉效果
其实网上类似的实现已经很多了,原理也并不难,只是网上各种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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。