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

前言

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

效果

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

高仿微信对话列表滑动删除效果_第1张图片高仿微信对话列表滑动删除效果_第2张图片

代码分析

先看SlideView是如何实现的

看layout xml:

[html]  view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <merge xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <LinearLayout  
  7.         android:id="@+id/view_content"  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="match_parent"  
  10.         android:orientation="horizontal" >  
  11.     </LinearLayout>  
  12.   
  13.     <RelativeLayout  
  14.         android:id="@+id/holder"  
  15.         android:layout_width="120dp"  
  16.         android:layout_height="match_parent"  
  17.         android:clickable="true"  
  18.         android:background="@drawable/holder_bg">  
  19.   
  20.         <TextView  
  21.             android:id="@+id/delete"  
  22.             android:layout_width="wrap_content"  
  23.             android:layout_height="wrap_content"  
  24.             android:drawableLeft="@drawable/del_icon_normal"  
  25.             android:layout_centerInParent="true"  
  26.             android:gravity="center"  
  27.             android:textColor="@color/floralwhite"  
  28.             android:text="删除" />  
  29.     </RelativeLayout>  
  30.   
  31. </merge>  
上述xml文件中,所有的view都会被放在view_content中,而holder是放置诸如删除按钮之类的东西,我们的SlideView会加载这个布局。

再看SlideView.java:

[java]  view plain copy
  1. /** 
  2.  * SlideView 继承自LinearLayout 
  3.  */  
  4. public class SlideView extends LinearLayout {  
  5.   
  6.     private static final String TAG = "SlideView";  
  7.   
  8.     private Context mContext;  
  9.   
  10.     // 用来放置所有view的容器  
  11.     private LinearLayout mViewContent;  
  12.   
  13.     // 用来放置内置view的容器,比如删除 按钮  
  14.     private RelativeLayout mHolder;  
  15.   
  16.     // 弹性滑动对象,提供弹性滑动效果  
  17.     private Scroller mScroller;  
  18.   
  19.     // 滑动回调接口,用来向上层通知滑动事件  
  20.     private OnSlideListener mOnSlideListener;  
  21.   
  22.     // 内置容器的宽度 单位:dp  
  23.     private int mHolderWidth = 120;  
  24.   
  25.     // 分别记录上次滑动的坐标  
  26.     private int mLastX = 0;  
  27.     private int mLastY = 0;  
  28.   
  29.     // 用来控制滑动角度,仅当角度a满足如下条件才进行滑动:tan a = deltaX / deltaY > 2  
  30.     private static final int TAN = 2;  
  31.   
  32.     public interface OnSlideListener {  
  33.         // SlideView的三种状态:开始滑动,打开,关闭  
  34.         public static final int SLIDE_STATUS_OFF = 0;  
  35.         public static final int SLIDE_STATUS_START_SCROLL = 1;  
  36.         public static final int SLIDE_STATUS_ON = 2;  
  37.   
  38.         /** 
  39.          * @param view 
  40.          *            current SlideView 
  41.          * @param status 
  42.          *            SLIDE_STATUS_ON, SLIDE_STATUS_OFF or 
  43.          *            SLIDE_STATUS_START_SCROLL 
  44.          */  
  45.         public void onSlide(View view, int status);  
  46.     }  
  47.   
  48.     public SlideView(Context context) {  
  49.         super(context);  
  50.         initView();  
  51.     }  
  52.   
  53.     public SlideView(Context context, AttributeSet attrs) {  
  54.         super(context, attrs);  
  55.         initView();  
  56.     }  
  57.   
  58.     private void initView() {  
  59.         mContext = getContext();  
  60.         // 初始化弹性滑动对象  
  61.         mScroller = new Scroller(mContext);  
  62.         // 设置其方向为横向  
  63.         setOrientation(LinearLayout.HORIZONTAL);  
  64.         // 将slide_view_merge加载进来  
  65.         View.inflate(mContext, R.layout.slide_view_merge, this);  
  66.         mViewContent = (LinearLayout) findViewById(R.id.view_content);  
  67.         mHolderWidth = Math.round(TypedValue.applyDimension(  
  68.                 TypedValue.COMPLEX_UNIT_DIP, mHolderWidth, getResources()  
  69.                         .getDisplayMetrics()));  
  70.     }  
  71.   
  72.     // 设置按钮的内容,也可以设置图标啥的,我没写  
  73.     public void setButtonText(CharSequence text) {  
  74.         ((TextView) findViewById(R.id.delete)).setText(text);  
  75.     }  
  76.   
  77.     // 将view加入到ViewContent中  
  78.     public void setContentView(View view) {  
  79.         mViewContent.addView(view);  
  80.     }  
  81.   
  82.     // 设置滑动回调  
  83.     public void setOnSlideListener(OnSlideListener onSlideListener) {  
  84.         mOnSlideListener = onSlideListener;  
  85.     }  
  86.   
  87.     // 将当前状态置为关闭  
  88.     public void shrink() {  
  89.         if (getScrollX() != 0) {  
  90.             this.smoothScrollTo(00);  
  91.         }  
  92.     }  
  93.   
  94.     // 根据MotionEvent来进行滑动,这个方法的作用相当于onTouchEvent  
  95.     // 如果你不需要处理滑动冲突,可以直接重命名,照样能正常工作  
  96.     public void onRequireTouchEvent(MotionEvent event) {  
  97.         int x = (int) event.getX();  
  98.         int y = (int) event.getY();  
  99.         int scrollX = getScrollX();  
  100.         Log.d(TAG, "x=" + x + "  y=" + y);  
  101.   
  102.         switch (event.getAction()) {  
  103.         case MotionEvent.ACTION_DOWN: {  
  104.             if (!mScroller.isFinished()) {  
  105.                 mScroller.abortAnimation();  
  106.             }  
  107.             if (mOnSlideListener != null) {  
  108.                 mOnSlideListener.onSlide(this,  
  109.                         OnSlideListener.SLIDE_STATUS_START_SCROLL);  
  110.             }  
  111.             break;  
  112.         }  
  113.         case MotionEvent.ACTION_MOVE: {  
  114.             int deltaX = x - mLastX;  
  115.             int deltaY = y - mLastY;  
  116.             if (Math.abs(deltaX) < Math.abs(deltaY) * TAN) {  
  117.                 // 滑动不满足条件,不做横向滑动  
  118.                 break;  
  119.             }  
  120.   
  121.             // 计算滑动终点是否合法,防止滑动越界  
  122.             int newScrollX = scrollX - deltaX;  
  123.             if (deltaX != 0) {  
  124.                 if (newScrollX < 0) {  
  125.                     newScrollX = 0;  
  126.                 } else if (newScrollX > mHolderWidth) {  
  127.                     newScrollX = mHolderWidth;  
  128.                 }  
  129.                 this.scrollTo(newScrollX, 0);  
  130.             }  
  131.             break;  
  132.         }  
  133.         case MotionEvent.ACTION_UP: {  
  134.             int newScrollX = 0;  
  135.             // 这里做了下判断,当松开手的时候,会自动向两边滑动,具体向哪边滑,要看当前所处的位置  
  136.             if (scrollX - mHolderWidth * 0.75 > 0) {  
  137.                 newScrollX = mHolderWidth;  
  138.             }  
  139.             // 慢慢滑向终点  
  140.             this.smoothScrollTo(newScrollX, 0);  
  141.             // 通知上层滑动事件  
  142.             if (mOnSlideListener != null) {  
  143.                 mOnSlideListener.onSlide(this,  
  144.                         newScrollX == 0 ? OnSlideListener.SLIDE_STATUS_OFF  
  145.                                 : OnSlideListener.SLIDE_STATUS_ON);  
  146.             }  
  147.             break;  
  148.         }  
  149.         default:  
  150.             break;  
  151.         }  
  152.   
  153.         mLastX = x;  
  154.         mLastY = y;  
  155.     }  
  156.   
  157.     private void smoothScrollTo(int destX, int destY) {  
  158.         // 缓慢滚动到指定位置  
  159.         int scrollX = getScrollX();  
  160.         int delta = destX - scrollX;  
  161.         // 以三倍时长滑向destX,效果就是慢慢滑动  
  162.         mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 3);  
  163.         invalidate();  
  164.     }  
  165.   
  166.     @Override  
  167.     public void computeScroll() {  
  168.         if (mScroller.computeScrollOffset()) {  
  169.             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
  170.             postInvalidate();  
  171.         }  
  172.     }  
  173.   
  174. }  
上述代码做了很详细的说明,这就是滑动控件的完整代码,大家要明白的是:你所添加的view都是加在SlideView的子View : view_content中的,而不是直接加在SlideView中,只有这样我们才方便做滑动效果。

接着看ListView的代码:核心就是下面这一个方法,将点击事件发送给SlideView处理。

[java]  view plain copy
  1. @Override  
  2. public boolean onTouchEvent(MotionEvent event) {  
  3.     switch (event.getAction()) {  
  4.     case MotionEvent.ACTION_DOWN: {  
  5.         int x = (int) event.getX();  
  6.         int y = (int) event.getY();  
  7.         //我们想知道当前点击了哪一行  
  8.         int position = pointToPosition(x, y);  
  9.         Log.e(TAG, "postion=" + position);  
  10.         if (position != INVALID_POSITION) {  
  11.             //得到当前点击行的数据从而取出当前行的item。  
  12.             //可能有人怀疑,为什么要这么干?为什么不用getChildAt(position)?  
  13.             //因为ListView会进行缓存,如果你不这么干,有些行的view你是得不到的。  
  14.             MessageItem data = (MessageItem) getItemAtPosition(position);  
  15.             mFocusedItemView = data.slideView;  
  16.             Log.e(TAG, "FocusedItemView=" + mFocusedItemView);  
  17.         }  
  18.     }  
  19.     default:  
  20.         break;  
  21.     }  
  22.   
  23.     //向当前点击的view发送滑动事件请求,其实就是向SlideView发请求  
  24.     if (mFocusedItemView != null) {  
  25.         mFocusedItemView.onRequireTouchEvent(event);  
  26.     }  
  27.   
  28.     return super.onTouchEvent(event);  
  29. }  
最后看Activity的代码:
[java]  view plain copy
  1. public class MainActivity extends Activity implements OnItemClickListener,  
  2.         OnClickListener, OnSlideListener {  
  3.   
  4.     private static final String TAG = "MainActivity";  
  5.   
  6.     private ListViewCompat mListView;  
  7.   
  8.     private List<MessageItem> mMessageItems = new ArrayList<MainActivity.MessageItem>();  
  9.   
  10.     private SlideAdapter mSlideAdapter;  
  11.   
  12.     // 上次处于打开状态的SlideView  
  13.     private SlideView mLastSlideViewWithStatusOn;  
  14.   
  15.     @Override  
  16.     protected void onCreate(Bundle savedInstanceState) {  
  17.         super.onCreate(savedInstanceState);  
  18.         setContentView(R.layout.activity_main);  
  19.         initView();  
  20.     }  
  21.   
  22.     private void initView() {  
  23.         mListView = (ListViewCompat) findViewById(R.id.list);  
  24.   
  25.         for (int i = 0; i < 20; i++) {  
  26.             MessageItem item = new MessageItem();  
  27.             if (i % 3 == 0) {  
  28.                 item.iconRes = R.drawable.default_qq_avatar;  
  29.                 item.title = "腾讯新闻";  
  30.                 item.msg = "青岛爆炸满月:大量鱼虾死亡";  
  31.                 item.time = "晚上18:18";  
  32.             } else {  
  33.                 item.iconRes = R.drawable.wechat_icon;  
  34.                 item.title = "微信团队";  
  35.                 item.msg = "欢迎你使用微信";  
  36.                 item.time = "12月18日";  
  37.             }  
  38.             mMessageItems.add(item);  
  39.         }  
  40.         mSlideAdapter = new SlideAdapter();  
  41.         mListView.setAdapter(mSlideAdapter);  
  42.         mListView.setOnItemClickListener(this);  
  43.     }  
  44.   
  45.     private class SlideAdapter extends BaseAdapter {  
  46.   
  47.         private LayoutInflater mInflater;  
  48.   
  49.         SlideAdapter() {  
  50.             super();  
  51.             mInflater = getLayoutInflater();  
  52.         }  
  53.   
  54.         @Override  
  55.         public int getCount() {  
  56.             return mMessageItems.size();  
  57.         }  
  58.   
  59.         @Override  
  60.         public Object getItem(int position) {  
  61.             return mMessageItems.get(position);  
  62.         }  
  63.   
  64.         @Override  
  65.         public long getItemId(int position) {  
  66.             return position;  
  67.         }  
  68.   
  69.         @Override  
  70.         public View getView(int position, View convertView, ViewGroup parent) {  
  71.             ViewHolder holder;  
  72.             SlideView slideView = (SlideView) convertView;  
  73.             if (slideView == null) {  
  74.                 // 这里是我们的item  
  75.                 View itemView = mInflater.inflate(R.layout.list_item, null);  
  76.   
  77.                 slideView = new SlideView(MainActivity.this);  
  78.                 // 这里把item加入到slideView  
  79.                 slideView.setContentView(itemView);  
  80.                 // 下面是做一些数据缓存  
  81.                 holder = new ViewHolder(slideView);  
  82.                 slideView.setOnSlideListener(MainActivity.this);  
  83.                 slideView.setTag(holder);  
  84.             } else {  
  85.                 holder = (ViewHolder) slideView.getTag();  
  86.             }  
  87.             MessageItem item = mMessageItems.get(position);  
  88.             item.slideView = slideView;  
  89.             item.slideView.shrink();  
  90.   
  91.             holder.icon.setImageResource(item.iconRes);  
  92.             holder.title.setText(item.title);  
  93.             holder.msg.setText(item.msg);  
  94.             holder.time.setText(item.time);  
  95.             holder.deleteHolder.setOnClickListener(MainActivity.this);  
  96.   
  97.             return slideView;  
  98.         }  
  99.   
  100.     }  
  101.   
  102.     public class MessageItem {  
  103.         public int iconRes;  
  104.         public String title;  
  105.         public String msg;  
  106.         public String time;  
  107.         public SlideView slideView;  
  108.     }  
  109.   
  110.     private static class ViewHolder {  
  111.         public ImageView icon;  
  112.         public TextView title;  
  113.         public TextView msg;  
  114.         public TextView time;  
  115.         public ViewGroup deleteHolder;  
  116.   
  117.         ViewHolder(View view) {  
  118.             icon = (ImageView) view.findViewById(R.id.icon);  
  119.             title = (TextView) view.findViewById(R.id.title);  
  120.             msg = (TextView) view.findViewById(R.id.msg);  
  121.             time = (TextView) view.findViewById(R.id.time);  
  122.             deleteHolder = (ViewGroup) view.findViewById(R.id.holder);  
  123.         }  
  124.     }  
  125.   
  126.     @Override  
  127.     public void onItemClick(AdapterView<?> parent, View view, int position,  
  128.             long id) {  
  129.         // 这里处理ListItem的点击事件  
  130.         Log.e(TAG, "onItemClick position=" + position);  
  131.     }  
  132.   
  133.     @Override  
  134.     public void onSlide(View view, int status) {  
  135.         // 如果当前存在已经打开的SlideView,那么将其关闭  
  136.         if (mLastSlideViewWithStatusOn != null  
  137.                 && mLastSlideViewWithStatusOn != view) {  
  138.             mLastSlideViewWithStatusOn.shrink();  
  139.         }  
  140.         // 记录本次处于打开状态的view  
  141.         if (status == SLIDE_STATUS_ON) {  
  142.             mLastSlideViewWithStatusOn = (SlideView) view;  
  143.         }  
  144.     }  
  145.   
  146.     @Override  
  147.     public void onClick(View v) {  
  148.         // 这里处理删除按钮的点击事件,可以删除对话  
  149.         if (v.getId() == R.id.holder) {  
  150.             int position = mListView.getPositionForView(v);  
  151.             if (position != ListView.INVALID_POSITION) {  
  152.                 mMessageItems.remove(position);  
  153.                 mSlideAdapter.notifyDataSetChanged();  
  154.             }  
  155.             Log.e(TAG, "onClick v=" + v);  
  156.         }  
  157.     }  
  158. }  

你可能感兴趣的:(android,仿微信)