Android Listview item向左滑动 显示菜单(仿QQ滑动)

1、说明

1.1 高仿QQ对话列表左滑删除的功能(作者不详,发现于公司年代久远的项目,拿来学习一下,看看思路),滑动item时显示操作菜单,只要划出来了,你想干嘛就好办了。。

2、无图无真相

Android Listview item向左滑动 显示菜单(仿QQ滑动)_第1张图片

3、思路分析

其实思路还是挺简单的,就是每个item多了几个操作菜单,这个ItemView其实就是两部分构成的,一个是本来的ContentView,一个是显示菜单的MenuView。我们要做的只是,找个Framelayout把两个view放在一起。然后通过手势去更新两个view的位置就OK了。

4、代码实现

我们从简单的搞起
第一步,先搞划出的菜单:
4.1
BaseMenuItem.java 这就是单个菜单的实体类。设置菜单icon和文字信息的。别忘了set和get方法。

public class BaseMenuItem {

    private int id;
    private Context mContext;
    private String title;
    private Drawable icon;
    private Drawable background;
    private int titleColor;
    private int titleSize;
    private int width;

    public BaseMenuItem(Context context) {
        mContext = context;
    }
}

4.2
BaseMenuGroups.java 这个类就是BaseMenuItem的集合,这么没什么说的。同样,别忘了set和get方法。

public class BaseMenuGroups {
    private Context mContext;
    private List mItems;
    private int mViewType;

    public BaseMenuGroups(Context context) {
        mContext = context;
        mItems = new ArrayList();
    }
}

4.3
BaseMenuLayout.java 现在就来说这个装多个菜单的容器了。这部分就是划出可见的那部分,我们继承Linearlayout并实现OnClickListener接口,毕竟有菜单要能点击。从BaseMenuLayout的构造方法传入需要的菜单BaseMenuGroups。
a:先准备文字和图标的工具

//显示icon的View
private ImageView createIcon(BaseMenuItem item) {
    ImageView iv = new ImageView(getContext());
    iv.setImageDrawable(item.getIcon());
    return iv;
}
//显示文字的view
private TextView createTitle(BaseMenuItem item) {
    TextView tv = new TextView(getContext());
    tv.setText(item.getTitle());
    tv.setGravity(Gravity.CENTER);
    tv.setTextSize(item.getTitleSize());
    tv.setTextColor(item.getTitleColor());
    return tv;
}

b:创建我们单个菜单的view,这里用一个LinearLayout来装我们的图标和文字,至于是上下还是左右,就是看你喜好了。我们这里设置图标上,文字下。
这个参数id,当然是指BaseMenuLayout中的第几个菜单,不然我们怎么点啊。

private void addItem(BaseMenuItem item, int id) {
        LayoutParams params = new LayoutParams(item.getWidth(),
                LayoutParams.MATCH_PARENT);
        LinearLayout parent = new LinearLayout(getContext());
        parent.setId(id);
        parent.setGravity(Gravity.CENTER);
        parent.setOrientation(LinearLayout.VERTICAL);
        parent.setLayoutParams(params);
        parent.setBackgroundDrawable(item.getBackground());
        parent.setOnClickListener(this);
        //把菜单添加到我们的BaseMenuLayout中去。
        addView(parent);
        //添加图标
        if (item.getIcon() != null) {
            parent.addView(createIcon(item));
        }
        //添加标题
        if (!TextUtils.isEmpty(item.getTitle())) {
            parent.addView(createTitle(item));
        }

    }

这里还需要一个位置信息。不然我怎么知道是点的Listview的哪一行的哪一个菜单啊,so,来自于Listview的item的position。

private int position;
public int getPosition() {
    return position;
}
public void setPosition(int position) {
    this.position = position;
}

光这样还不行,总得把单个小菜单的位置搞出去吧。

private OnSwipeItemClickListener onItemClickListener;
public static interface OnSwipeItemClickListener {
        void onItemClick(BaseMenuLayout view, BaseMenuGroups menu, int index);
    }

其实这里还有其他的判断,就是要在我们的菜单处于显示的时候才能点击菜单,后面会上传demo。
再来说一说构造方法中的代码:把传入的BaseMenuGroups 中的全部菜单都添加到BaseMenuLayout中,按照顺序设置菜单的id

public BaseMenuLayout(BaseMenuGroups menu) {
    super(menu.getContext());
    mMenu = menu;
    List items = menu.getMenuItems();
    int id = 0;
    for (BaseMenuItem item : items) {
        addItem(item, id++);
    }
}

4.4
接下来构建一个接口,BaseMenuCreator.java,只有一个实现方法,便于我们在外部设置菜单。怎么用这个,后面会讲到。

void create(BaseMenuGroups menu);

4.5
现在该做一个组装我们最先提到的ContentView和显示菜单的MenuView的BaseViewAdapter.java了,这个BaseViewAdapter并不是ListView的数据Adapter。在这个adapter里面做的事情有,组装MenuView,并且把MenuView和ContentView传入BaseItemViewLayout(这里就比较复杂了,接下来会提到),由BaseItemViewLayout来组装完整的Listview的item。
这里我们只说getView方法:

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        BaseItemViewLayout layout = null;
        if (convertView == null) {
            View contentView = mAdapter.getView(position, convertView, parent);
            BaseMenuGroups menu = new BaseMenuGroups(mContext);
            menu.setViewType(mAdapter.getItemViewType(position));
            createMenu(menu);   
            BaseMenuLayout menuView = new BaseMenuLayout(menu);
            menuView.setOnSwipeItemClickListener(this);
//但这里都是在组装菜单的数据,接下来就是把contentView 和menuView 传到BaseItemViewLayout去了。剩下的就是处理view的复用了
            DelListView listView = (DelListView) parent;
            layout = new BaseItemViewLayout(contentView, menuView,
                    listView.getCloseInterpolator(),
                    listView.getOpenInterpolator());
            layout.setPosition(position);
        } else {
            layout = (BaseItemViewLayout) convertView;
            layout.closeMenu();
            layout.setPosition(position);
            View view = mAdapter.getView(position, layout.getContentView(),parent);
            /**
             * 可能有一些小伙伴觉得view都没使用,就觉得这样代码没什么用,其实这里是为了复用,关键在于layout.getContentView()。
             */
        }
        return layout;
    }

4.6
下面一起来看看这个BaseItemViewLayout.java了。感觉这就是精华之一所在了。
首先说一下这里面的布局情况:

LayoutParams contentParams = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
mContentView.setLayoutParams(contentParams);
mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
addView(mContentView);
addView(mMenuView);

很显然,感觉我们的menuView被顶到屏幕外面去了。但是,光感觉怎么行呢,看下面的代码就知道,肯定在屏幕外,至少我们看不到。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mMenuView.measure(MeasureSpec.makeMeasureSpec(0,
                MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight(), MeasureSpec.EXACTLY));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mContentView.layout(0, 0, getMeasuredWidth(),
                mContentView.getMeasuredHeight());
        mMenuView.layout(getMeasuredWidth(), 0,
                getMeasuredWidth() + mMenuView.getMeasuredWidth(),
                mContentView.getMeasuredHeight());
    }

其实处理手势是最麻烦的了。因为有几种情况:
第一种,当快速滑动打开、快速滑动关闭、打开状态,点击一下关闭时,动作不跟手:

//关闭,动作不跟手
public void smoothCloseMenu() {
        state = STATE_CLOSE;
        mBaseX = -mContentView.getLeft();
        mCloseScroller.startScroll(0, 0, mBaseX, 0, 350);//这里五个参数的意思是begin(x,y) end(x,y) duration;
        postInvalidate();
    }
//打开,动作不跟手
    public void smoothOpenMenu() {
        state = STATE_OPEN;
mOpenScroller.startScroll(-mContentView.getLeft(), 0,
                mMenuView.getWidth(), 0, 350);
        postInvalidate();
    }

第二种:动作跟手时打开与关闭:这里的这个dis是根据手指滑动的距离算出来的

    private void swipe(int dis) {
        if (dis > mMenuView.getWidth()) {
            dis = mMenuView.getWidth();
        }
        if (dis < 0) {
            dis = 0;
        }
        mContentView.layout(-dis, mContentView.getTop(),
                mContentView.getWidth() - dis, getMeasuredHeight());
        mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(),
                mContentView.getWidth() + mMenuView.getWidth() - dis,
                mMenuView.getBottom());
    }

除开这两个处理滑动的方法之外,还有一个很重要的方法computeScroll(),这个方法会在Scroller.startScroll()的时候自动执行,这个方法的作用是计算滑动量的。没有这个方法,活动的时候,会显得卡顿。

    @Override
    public void computeScroll() {
        if (state == STATE_OPEN) {
            if (mOpenScroller.computeScrollOffset()) {
                swipe(mOpenScroller.getCurrX());
                postInvalidate();
            }
        } else {
            if (mCloseScroller.computeScrollOffset()) {
                swipe(mBaseX - mCloseScroller.getCurrX());
                postInvalidate();
            }
        }
    }

然后就是对手势的监听了:这里的触摸事件肯定来自于最外层的ListView的触摸事件:所以:写一个方法等着MotionEvent传进来处理就好:这里就判断了是快速滑动打开,还是跟手滑动打开了,代码很简单。

public boolean onSwipe(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mDownX = (int) event.getX();
            isFling = false;
            break;
        case MotionEvent.ACTION_MOVE:
            int dis = (int) (mDownX - event.getX());
            if (state == STATE_OPEN) {
                dis += mMenuView.getWidth();
            }
            Log.d("DIS>>>>>>>>>>", dis+"");
            swipe(dis);
            break;
        case MotionEvent.ACTION_UP:
            if (isFling || (mDownX - event.getX()) > (mMenuView.getWidth() / 2)) {
                // open
                smoothOpenMenu();
            } else {
                // close
                smoothCloseMenu();
                return false;
            }
            break;
        }
        return true;
    }

4.7
现在就是精华之二所在了。我们的核心,DelListView.java了
这里面有两个重要的部分了:
第一个就是:我们的BaseMenuCreator终于排上用场了:
重写Listview的SetAdapter方法,把我们set进来的数据adapter传出我们的BaseViewAdapter,组建合成最终的itemView;并且重写BaseViewAdapter的createMenu方法,实现我们的BaseMenuCreator接口。这个接口实例从外部传进来;然后在重写BaseViewAdapter的onItemClick方法,把item的position菜单的position回调回来。

@Override
    public void setAdapter(ListAdapter adapter) {
        adapters = adapter;
        super.setAdapter(new BaseViewAdapter(getContext(), adapter) {
            @Override
            public void createMenu(BaseMenuGroups menu) {
                if (mMenuCreator != null) {
                    mMenuCreator.create(menu);
                }
            }

            @Override
            public void onItemClick(BaseMenuLayout view, BaseMenuGroups menu,
                    int index) {
                if (mOnMenuItemClickListener != null) {
                    mOnMenuItemClickListener.onMenuItemClick(
                            view.getPosition(), menu, index);
                }
                if (mTouchView != null) {
                    mTouchView.smoothCloseMenu();
                }
            }
        });
    }

第二个就是:处理手势,处理完成后传入BaseItemViewLayout.onSwipe(MotionEvent event)方法:
这里要处理的就比BaseItemViewLayout多得多了。这里要判断点击的位置,菜单是否打开,滑动的方向。

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (ev.getAction() != MotionEvent.ACTION_DOWN && mTouchView == null)//有父类处理事件
            return super.onTouchEvent(ev);
        int action = MotionEventCompat.getActionMasked(ev);
        action = ev.getAction();
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            int oldPos = mTouchPosition;
            mDownX = ev.getX();
            mDownY = ev.getY();
            mTouchState = TOUCH_STATE_NONE;
            //通过点击的位置来判断点在listview的那个位置上。
            mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
            if (mTouchPosition == oldPos && mTouchView != null
                    && mTouchView.isOpen()) {
                mTouchState = TOUCH_STATE_X;
                mTouchView.onSwipe(ev);
                return true;
            }

            View view = getChildAt(mTouchPosition - getFirstVisiblePosition());
            ;
            if (mTouchView != null && mTouchView.isOpen()) {
                mTouchView.smoothCloseMenu();
                mTouchView = null;
                return super.onTouchEvent(ev);
            }
            if (view instanceof BaseItemViewLayout) {
                mTouchView = (BaseItemViewLayout) view;
                /**
                 * 可以通过item中某一个子view的显示还是隐藏来控制该item能不能别左滑出菜单
                 * 假如现在item中的tv_value_gone控件处于显示的状态,那么该item则不能被左滑出菜单
                 */
                if(signViewId!=-1){
                    isshow = mTouchView.findViewById(signViewId).isShown();
                }               
            }
            if (!isshow) {
                if (mTouchView != null) {
                    mTouchView.onSwipe(ev);
                }

            }

            break;
        case MotionEvent.ACTION_MOVE:
            float dy = Math.abs((ev.getY() - mDownY));
            float dx = Math.abs((ev.getX() - mDownX));
            if (mTouchState == TOUCH_STATE_X) {
                if (!isshow) {
                    if (mTouchView != null) {
                        mTouchView.onSwipe(ev);
                    }
                }
                getSelector().setState(new int[] { 0 });
                ev.setAction(MotionEvent.ACTION_CANCEL);
                super.onTouchEvent(ev);
                return true;
            } else if (mTouchState == TOUCH_STATE_NONE) {
                if (Math.abs(dy) > MAX_Y) {
                    mTouchState = TOUCH_STATE_Y;
                } else if (dx > MAX_X) {
                    mTouchState = TOUCH_STATE_X;
                    if (mOnSwipeListener != null) {
                        mOnSwipeListener.onSwipeStart(mTouchPosition);
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mTouchState == TOUCH_STATE_X) {
                if (!isshow) {
                    if (mTouchView != null) {
                        mTouchView.onSwipe(ev);
                        if (!mTouchView.isOpen()) {
                            mTouchPosition = -1;
                            mTouchView = null;
                        }
                    }
                }
                if (mOnSwipeListener != null) {
                    mOnSwipeListener.onSwipeEnd(mTouchPosition);
                }
                ev.setAction(MotionEvent.ACTION_CANCEL);
                super.onTouchEvent(ev);
                return true;
            }
            break;
        }
        return super.onTouchEvent(ev);
    }

剩下的就是接口回调参数了。这部分代码请看demo吧。

5、最后

使用方法很简单。大家看demo吧
demo下载地址:http://download.csdn.net/detail/u010886975/9718243
估计有人可能对有些地方有疑问,请自行查看:
1、setViewType:https://www.pocketdigi.com/20130731/1155.html
2、computeScroll():http://www.linuxidc.com/Linux/2016-01/127276.htm

你可能感兴趣的:(自定义view)