前言
用过微信的都知道,微信对话列表滑动删除效果是很不错的,这个效果我们也可以有。思路其实很简单,弄个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://download.csdn.net/detail/singwhatiwanna/6760085
另外此博文采用了 ttdevs 所提供代码的部分思想(他的博客是http://blog.csdn.net/ttdevs)