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

Android高仿微信对话列表滑动删除效果

程序员文章站 2024-03-06 15:51:56
前言  用过微信的都知道,微信对话列表滑动删除效果是很不错的,这个效果我们也可以有。思路其实很简单,弄个listview,然后里面的每个item做成一个可以...

前言 

用过微信的都知道,微信对话列表滑动删除效果是很不错的,这个效果我们也可以有。思路其实很简单,弄个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做滑动,这种实现的确可以达到效果,而且代码很简单,根本不需要处理什么复杂的滑动冲突。 

效果 

下面分别为微信和高仿效果 

Android高仿微信对话列表滑动删除效果

代码分析 

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

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