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

来源:互联网 发布:amazfit华米手表 知乎 编辑:程序博客网 时间:2024/04/27 23:25

1、说明

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

2、无图无真相

这里写图片描述

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<BaseMenuItem> mItems;    private int mViewType;    public BaseMenuGroups(Context context) {        mContext = context;        mItems = new ArrayList<BaseMenuItem>();    }}

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

//显示icon的Viewprivate ImageView createIcon(BaseMenuItem item) {    ImageView iv = new ImageView(getContext());    iv.setImageDrawable(item.getIcon());    return iv;}//显示文字的viewprivate 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<BaseMenuItem> 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

1 0
原创粉丝点击