Android高仿微信对话列表滑动删除效果
前言
用过微信的都知道,微信对话列表滑动删除效果是很不错的,这个效果我们也可以有。思路其实很简单,弄个listview,然后里面的每个item做成一个可以滑动的自定义控件即可。由于listview是上下滑动而item是左右滑动,因此会有滑动冲突,也许你需要了解下android中点击事件的派发流程,请参考android源码分析-点击事件派发机制。我的解决思路是这样的:重写listview的onintercepttouchevent方法,在move的时候做判断,如果是左右滑动就返回false,否则返回true;重写slideview(即自定义item控件)的ontouchevent方法来处理滑动。整个思路没有问题,滑动冲突也解决了,可是listview无法得到焦点了,也就是listview无法处理点击事件了。让我们回想下问题出在哪里:我的理解是这样的,上述处理滑动本身没有问题,但是有一个副作用,就是会让外层view失去焦点且无法处理点击事件。常见的滑动冲突场景,比如launcher内部嵌入listview却是没有问题的,因为这个时候launcher不需要获得焦点,需要获得焦点的是内部的listview。因此,上述处理方式对于外部需要获得焦点的情况(比如外部是listview)就不太适合了。于是我就和ttdevs探讨,发现他采用了另外一种思路,我从来没有想过还可以这样玩。下面介绍他的思路。
新的思路
不考虑那么复杂,不采用主流玩法,所有的事件均由外层的listview做拦截,同时把事件传递给slideview做滑动,这种实现的确可以达到效果,而且代码很简单,根本不需要处理什么复杂的滑动冲突。
效果
下面分别为微信和高仿效果
代码分析
先看slideview是如何实现的
看layout xml:
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <linearlayout android:id="@+id/view_content" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > </linearlayout> <relativelayout android:id="@+id/holder" android:layout_width="120dp" android:layout_height="match_parent" android:clickable="true" android:background="@drawable/holder_bg"> <textview android:id="@+id/delete" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableleft="@drawable/del_icon_normal" android:layout_centerinparent="true" android:gravity="center" android:textcolor="@color/floralwhite" android:text="删除" /> </relativelayout> </merge>
上述xml文件中,所有的view都会被放在view_content中,而holder是放置诸如删除按钮之类的东西,我们的slideview会加载这个布局。
再看slideview.java:
/** * slideview 继承自linearlayout */ public class slideview extends linearlayout { private static final string tag = "slideview"; private context mcontext; // 用来放置所有view的容器 private linearlayout mviewcontent; // 用来放置内置view的容器,比如删除 按钮 private relativelayout mholder; // 弹性滑动对象,提供弹性滑动效果 private scroller mscroller; // 滑动回调接口,用来向上层通知滑动事件 private onslidelistener monslidelistener; // 内置容器的宽度 单位:dp private int mholderwidth = 120; // 分别记录上次滑动的坐标 private int mlastx = 0; private int mlasty = 0; // 用来控制滑动角度,仅当角度a满足如下条件才进行滑动:tan a = deltax / deltay > 2 private static final int tan = 2; public interface onslidelistener { // slideview的三种状态:开始滑动,打开,关闭 public static final int slide_status_off = 0; public static final int slide_status_start_scroll = 1; public static final int slide_status_on = 2; /** * @param view * current slideview * @param status * slide_status_on, slide_status_off or * slide_status_start_scroll */ public void onslide(view view, int status); } public slideview(context context) { super(context); initview(); } public slideview(context context, attributeset attrs) { super(context, attrs); initview(); } private void initview() { mcontext = getcontext(); // 初始化弹性滑动对象 mscroller = new scroller(mcontext); // 设置其方向为横向 setorientation(linearlayout.horizontal); // 将slide_view_merge加载进来 view.inflate(mcontext, r.layout.slide_view_merge, this); mviewcontent = (linearlayout) findviewbyid(r.id.view_content); mholderwidth = math.round(typedvalue.applydimension( typedvalue.complex_unit_dip, mholderwidth, getresources() .getdisplaymetrics())); } // 设置按钮的内容,也可以设置图标啥的,我没写 public void setbuttontext(charsequence text) { ((textview) findviewbyid(r.id.delete)).settext(text); } // 将view加入到viewcontent中 public void setcontentview(view view) { mviewcontent.addview(view); } // 设置滑动回调 public void setonslidelistener(onslidelistener onslidelistener) { monslidelistener = onslidelistener; } // 将当前状态置为关闭 public void shrink() { if (getscrollx() != 0) { this.smoothscrollto(0, 0); } } // 根据motionevent来进行滑动,这个方法的作用相当于ontouchevent // 如果你不需要处理滑动冲突,可以直接重命名,照样能正常工作 public void onrequiretouchevent(motionevent event) { int x = (int) event.getx(); int y = (int) event.gety(); int scrollx = getscrollx(); log.d(tag, "x=" + x + " y=" + y); switch (event.getaction()) { case motionevent.action_down: { if (!mscroller.isfinished()) { mscroller.abortanimation(); } if (monslidelistener != null) { monslidelistener.onslide(this, onslidelistener.slide_status_start_scroll); } break; } case motionevent.action_move: { int deltax = x - mlastx; int deltay = y - mlasty; if (math.abs(deltax) < math.abs(deltay) * tan) { // 滑动不满足条件,不做横向滑动 break; } // 计算滑动终点是否合法,防止滑动越界 int newscrollx = scrollx - deltax; if (deltax != 0) { if (newscrollx < 0) { newscrollx = 0; } else if (newscrollx > mholderwidth) { newscrollx = mholderwidth; } this.scrollto(newscrollx, 0); } break; } case motionevent.action_up: { int newscrollx = 0; // 这里做了下判断,当松开手的时候,会自动向两边滑动,具体向哪边滑,要看当前所处的位置 if (scrollx - mholderwidth * 0.75 > 0) { newscrollx = mholderwidth; } // 慢慢滑向终点 this.smoothscrollto(newscrollx, 0); // 通知上层滑动事件 if (monslidelistener != null) { monslidelistener.onslide(this, newscrollx == 0 ? onslidelistener.slide_status_off : onslidelistener.slide_status_on); } break; } default: break; } mlastx = x; mlasty = y; } private void smoothscrollto(int destx, int desty) { // 缓慢滚动到指定位置 int scrollx = getscrollx(); int delta = destx - scrollx; // 以三倍时长滑向destx,效果就是慢慢滑动 mscroller.startscroll(scrollx, 0, delta, 0, math.abs(delta) * 3); invalidate(); } @override public void computescroll() { if (mscroller.computescrolloffset()) { scrollto(mscroller.getcurrx(), mscroller.getcurry()); postinvalidate(); } } }
上述代码做了很详细的说明,这就是滑动控件的完整代码,大家要明白的是:你所添加的view都是加在slideview的子view : view_content中的,而不是直接加在slideview中,只有这样我们才方便做滑动效果。
接着看listview的代码:核心就是下面这一个方法,将点击事件发送给slideview处理。
@override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: { int x = (int) event.getx(); int y = (int) event.gety(); //我们想知道当前点击了哪一行 int position = pointtoposition(x, y); log.e(tag, "postion=" + position); if (position != invalid_position) { //得到当前点击行的数据从而取出当前行的item。 //可能有人怀疑,为什么要这么干?为什么不用getchildat(position)? //因为listview会进行缓存,如果你不这么干,有些行的view你是得不到的。 messageitem data = (messageitem) getitematposition(position); mfocuseditemview = data.slideview; log.e(tag, "focuseditemview=" + mfocuseditemview); } } default: break; } //向当前点击的view发送滑动事件请求,其实就是向slideview发请求 if (mfocuseditemview != null) { mfocuseditemview.onrequiretouchevent(event); } return super.ontouchevent(event); }
最后看activity的代码:
public class mainactivity extends activity implements onitemclicklistener, onclicklistener, onslidelistener { private static final string tag = "mainactivity"; private listviewcompat mlistview; private list<messageitem> mmessageitems = new arraylist<mainactivity.messageitem>(); private slideadapter mslideadapter; // 上次处于打开状态的slideview private slideview mlastslideviewwithstatuson; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); initview(); } private void initview() { mlistview = (listviewcompat) findviewbyid(r.id.list); for (int i = 0; i < 20; i++) { messageitem item = new messageitem(); if (i % 3 == 0) { item.iconres = r.drawable.default_qq_avatar; item.title = "腾讯新闻"; item.msg = "青岛爆炸满月:大量鱼虾死亡"; item.time = "晚上18:18"; } else { item.iconres = r.drawable.wechat_icon; item.title = "微信团队"; item.msg = "欢迎你使用微信"; item.time = "12月18日"; } mmessageitems.add(item); } mslideadapter = new slideadapter(); mlistview.setadapter(mslideadapter); mlistview.setonitemclicklistener(this); } private class slideadapter extends baseadapter { private layoutinflater minflater; slideadapter() { super(); minflater = getlayoutinflater(); } @override public int getcount() { return mmessageitems.size(); } @override public object getitem(int position) { return mmessageitems.get(position); } @override public long getitemid(int position) { return position; } @override public view getview(int position, view convertview, viewgroup parent) { viewholder holder; slideview slideview = (slideview) convertview; if (slideview == null) { // 这里是我们的item view itemview = minflater.inflate(r.layout.list_item, null); slideview = new slideview(mainactivity.this); // 这里把item加入到slideview slideview.setcontentview(itemview); // 下面是做一些数据缓存 holder = new viewholder(slideview); slideview.setonslidelistener(mainactivity.this); slideview.settag(holder); } else { holder = (viewholder) slideview.gettag(); } messageitem item = mmessageitems.get(position); item.slideview = slideview; item.slideview.shrink(); holder.icon.setimageresource(item.iconres); holder.title.settext(item.title); holder.msg.settext(item.msg); holder.time.settext(item.time); holder.deleteholder.setonclicklistener(mainactivity.this); return slideview; } } public class messageitem { public int iconres; public string title; public string msg; public string time; public slideview slideview; } private static class viewholder { public imageview icon; public textview title; public textview msg; public textview time; public viewgroup deleteholder; viewholder(view view) { icon = (imageview) view.findviewbyid(r.id.icon); title = (textview) view.findviewbyid(r.id.title); msg = (textview) view.findviewbyid(r.id.msg); time = (textview) view.findviewbyid(r.id.time); deleteholder = (viewgroup) view.findviewbyid(r.id.holder); } } @override public void onitemclick(adapterview<?> parent, view view, int position, long id) { // 这里处理listitem的点击事件 log.e(tag, "onitemclick position=" + position); } @override public void onslide(view view, int status) { // 如果当前存在已经打开的slideview,那么将其关闭 if (mlastslideviewwithstatuson != null && mlastslideviewwithstatuson != view) { mlastslideviewwithstatuson.shrink(); } // 记录本次处于打开状态的view if (status == slide_status_on) { mlastslideviewwithstatuson = (slideview) view; } } @override public void onclick(view v) { // 这里处理删除按钮的点击事件,可以删除对话 if (v.getid() == r.id.holder) { int position = mlistview.getpositionforview(v); if (position != listview.invalid_position) { mmessageitems.remove(position); mslideadapter.notifydatasetchanged(); } log.e(tag, "onclick v=" + v); } } }
代码我都特意写了注释,就不多说了。
代码下载:http://xiazai.jb51.net/201608/yuanma/androidslide(jb51.net).rar
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: java实现MD5加密方法汇总
推荐阅读