ListView和RecyclerView侧滑菜单

仿QQ侧滑(实现的效果跟QQ侧滑菜单一样)

当你们都拿着年终开着年会happy的时候,我还在苦逼的赶项目,老板带着外贸部的美女去拉斯维加斯参展马上要回来了,不得不加紧,本来月初就打算上的一篇博客,硬是被推到了现在。侧滑菜单这个功能出来很久了,网上也有很多开源框架,本身也没啥写的,写这个玩意还是源于一个同行网友,他说找不到ListView的侧滑菜单,我说现在大家都用RecyclerView了,你用啥子ListView,他说经理要他们用,好吧,经理都说了,不行也得行了,一年多网友了,我也不好拒绝,只好抽了点时间给他写了个,下面给大家展示下第一个版本:

这里写图片描述

为什么说第一个版本,因为我那网友又说话了,这样不行,要跟QQ的一样,他们经理说的,这年头经理就是牛,写都写了再改改呗。(不过原来的写法确实也存在一些问题,其中一点就是,如果你快速滑动一个item之后,立马快速滑动另一个item,就出现bug了,虽然一般没人这么无聊,下面给大家看看这个bug:)

这里写图片描述

说到这一点,我也下了几个开源的侧滑demo,基本都存在这个问题,所以我觉得QQ那么玩,确实考虑要周全很多,于是为了网友不被经理P,我又抽时间改成了第二个版本,这个版本的操作就跟QQ基本一致了,如果有侧滑菜单打开,那么用户的下一次任何操作都只会让打开的策划菜单关闭,其他啥都做不了,这样也就避免了上图出现的bug,当然为了一劳永逸,我写了ListView和RecycleView版本,思路基本一致,代码稍有差别,避免网友的经理又有新的想法:

这里写图片描述

当然因为这个功能相对单一,就是一个侧滑菜单(侧滑菜单布局内容支持自定义),所以我个人建议你们可以参照我的源码思想,再结合网上开源的功能更强大的框架,在上面稍作修改,这样就两全其美了。因为时间仓促,如果还有其他bug,欢迎留言……接下来我们就开始讲侧滑菜单的实现:

自定义LinearLayout —— PLinearLayout

一、准备工作(Item选择自定义LinearLayout):

这里的准备工作,主要是知识上的,首先你要熟悉事件分发的机制、熟悉让View移动的常用方法scrollTo(绝对)和scrollBy(相对)、Scroll或OverScroll(用于控制View移动的速度),当你这些都基本了解之后,接下来的内容就So Easy!了。因为ListView和RecycleView实现侧滑基本思路一致,所以我只讲解针对ListView,当然一些有差别的地方我也会说明。为了方便各位的使用,Item我选择重写LinearLayout,取名为:PLinearLayout。
我们先来看一下构造函数需要初始化哪些参数:

    public PLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        //用于控制View的移动速度
        mScroller = new Scroller(context, new LinearInterpolator());
        ViewConfiguration viewConfig = ViewConfiguration.get(context);
        //正常滑动下触发move事件的最小距离
        mScaledTouchSlop = viewConfig.getScaledTouchSlop();
        mMaximumFlingVelocity = viewConfig.getScaledMaximumFlingVelocity();//最大滑动速率
        mMinimumFlingVewlocty = viewConfig.getScaledMinimumFlingVelocity();//最小滑动速率
        setClickable(true);//这里必须注明,因为LinearLaytou默认是不可点击的(不加其实也行,因为我最初就是没有加的,只是在重写onTouchEvent方法的时候你就不能直接返回super.onTouchEvent(event)了,至于为什么,有兴趣的自己可以去试下)
    }

二、重写LinearLayout的onTouchEvent方法:

在重写onTouchEvent方法前,我们要讲一下VelocityTracker这个类,从字面意思来讲,它是一个速率追踪者,用来干什么的呢,它是用来监听你手势滑动的速率的,有了这个值,我们就可以处理用户瞬滑事件了,相信一般使用QQ的朋友都会经常习惯这样操作吧,快速向左滑动,菜单就自动弹出,有了这个类,我们就可以愉快的截取到用户的这个操作并做出相应的处理了。下面看下如何初始化以及释放VelocityTracker的引用:

    private VelocityTracker mVelocityTracker;//这个追踪者会在onTouchEvent中Up事件中使用
    private void obtainVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);//这里需要传递事件进去
    }
    private void releaseVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

认识了VelocityTracker,我们就可以重写onTouchEvent方法了:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        obtainVelocityTracker(event);
        //这里需要注意的时候使用PLinearLayout中的第一级子项只能有两级,一级为content部分,一级为hide部分
        maxScroll = getChildAt(1).getWidth();
        int x = (int) event.getX();
        int y = (int) event.getY();
        int scrollX = getScrollX();
        int currentX = scrollX + mPreX - x;//这里是用于计算当前所需要移动到的X坐标(绝对坐标)
        mPreX = x;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = x;
                mStartDownX = x;//这里用于Up事件中的逻辑校验
                mDownY = y;
                Log.e(TAG, "down");
                break;
            case MotionEvent.ACTION_MOVE:
                moveX = Math.abs(mDownX - x);
                moveY = Math.abs(mDownY - y);
                if (!mIsMoveX && moveY > mScaledTouchSlop - 10 && !mIsMoveY) {
                    mIsMoveY = true;
                }
                //如果X轴移动的趋势明显大于Y轴,则说明用户意图是滑动侧滑菜单
                if (!mIsMoveY) {
                    if (moveX > moveY && moveX > mScaledTouchSlop && !mIsMoveX) {
                        mIsMoveX = true;
                    }
                    if (mIsMoveX) {
                        //控制滑动范围
                        if (currentX < 0) {
                            currentX = 0;
                        } else if (currentX > maxScroll) {
                            currentX = maxScroll;
                        }
                        scrollTo(currentX, 0);//这里也可以使用scrollBy(),只是位移计算需要稍作修改
                        mDownX = (int) event.getX();
                        mDownY = (int) event.getY();
                    }
                }
                mIsMove = true;//标记move事件
                break;
            case MotionEvent.ACTION_UP:
                mVelocityTracker.computeCurrentVelocity(1200, mMaximumFlingVelocity);
                int velocityX = (int) mVelocityTracker.getXVelocity();//获取X轴速率
                //判断是否满足X轴的瞬滑操作的最低要求,如果满足,则打开侧滑菜单(还需判断滑动方向)
                if (Math.abs(velocityX) > mMinimumFlingVewlocty) {//快速滑动
                    if (mStartDownX - x > 0) {//向左
                        currentX = maxScroll;
                    } else {//向右
                        currentX = 0;
                    }
                    //这里是长时间的滑动
                } else {//慢慢滑动
                    //判断当前滑动到的位置是否大于50%
                    if (Math.abs(getScrollX()) >= (maxScroll * 0.5f)) {
                        if (mIsOpen && !mIsMove) {//判断此次操作为点击,mIsOpen是用来标记当前item的策划菜单是否打开的,mIsMove上面有提到用于标记产生了滑动事件。如果侧滑菜单打开,且没有产生滑动事件,则说明用户是在Item侧滑菜单打开的情况单击了这个Item,那么我们自然需要将菜单关闭
                            currentX = 0;
                        } else {
                            currentX = maxScroll;//如果不是单击,则打开菜单
                        }
                    } else {//小于则回到原点
                        if (0 != Math.abs(getScaleX())){
                            currentX = 0;
                        }
                    }
                }
                mIsMoveX = false;
                mIsMoveY = false;
                mIsMove = false;
                //注意要想它真正滑动还需要重绘画面,并重写computeScroll()方法
                mScroller.startScroll(scrollX, 0, currentX - scrollX, 0);
                invalidate();//调用computeScroll
                releaseVelocityTracker();
                break;
            case MotionEvent.ACTION_CANCEL:
                //个别朋友可能会好奇,为啥还要监听Cancel事件,这里是为了避免用户在操作过程中触发了这个事件,导致没有进去Up进行事件逻辑校验,这样就可能出现侧滑菜单出来一半就不静止了的情况。
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();//这里是停止mScroller触发的移动动画
                } else {
                    //判断当前滑动到的位置是否大于50%
                    if (Math.abs(getScrollX()) >= (maxScroll * 0.5f)) {
                        if (mIsOpen && !mIsMove) {//点击
                            currentX = 0;
                        } else {
                            currentX = maxScroll;
                        }
                    } else {//小于则回到原点
                        if (0 != Math.abs(getScaleX())){
                            currentX = 0;
                        }
                    }
                    smoothScrollTo(0, currentX);
                }
                mIsStop = true;
                mIsMoveX = false;
                mIsMoveY = false;
                break;
        }
        return super.onTouchEvent(event);
    }

基本的控制逻辑都在onTouchEvent中呈现了,下面再讲一下,computeScroll()方法的重写,以及mIsOpen如何赋值:

    @Override
    public void computeScroll() {
        //这个方法会被调用N次
        if (mScroller.computeScrollOffset())//判断当前已经结束滑动
        {
            scrollTo(mScroller.getCurrX(), 0);//获取mScroller引用x和y轴的值,并滑动到相应位置
            invalidate();//刷新
        }
    }
    @Override
    public void scrollTo(int x, int y) {
        if (x >= maxScroll) {
            mIsOpen = true;
            if (mListener != null) {
                mListener.state(true, this);
            }
        } else {
            mIsOpen = false;
            if (mListener != null) {
                mListener.state(false, this);
            }
        }
        super.scrollTo(x, y);
    }
    private OnScrollListener mListener;
    public void setOnScrollListener(OnScrollListener listener) {
        this.mListener = listener;
    }
    //这个接口用于给ListView判断当前Item菜单的状态
    public interface OnScrollListener {
        void state(boolean isOpen, PLinearLayout linearLayout);
    }

文章开始部分有讲到一个bug,为了避免它发生,我们需要知道当前的Item的菜单是不是处于移动的过程中,如果能够知道,那么我们就能很好的处理掉这个bug了。这里我们用一个全局变量mStop来标记菜单的状态,如果用户触发下一次操作时,还有Item处于滑动过程中,那么我们需要终止用户当前操作,并且让处于滑动状态的Item关闭菜单。(QQ就是这么处理的)我采用的办法就是重写onScrollChanged这个方法。
代码如下:

    //oldl、oldt表示滑动前的x和y
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        if (0 == l || maxScroll == l) {
            mIsStop = true;
        } else {
            mIsStop = false;
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

到这里,Item定义部分就基本讲完了。(更详细可以参照源码,文章结尾处会提供源码下载)

自定义ListView —— PListView

一、重写ListView的onInterceptTouchEvent:

大家都知道onInterceptTouchEvent主要用于控制事件是否向下分发的。这里用它主要有三个用途,第一个就是有Item侧滑菜单打开,但是用户单击的是别的Item,我们需要将打开的侧滑菜单关闭,并且不响应其他事件。第二就是用来处理上文说到的那个bug,第三就是用来定义OnItemClickListner(),为什么需要自定义,你们有时间可以自己去摸索下。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        obtainVelocityTracker(ev);
        isIntercepted = super.onInterceptTouchEvent(ev);
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        //如果默认是不截获,则不需要再做判断
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = (int) ev.getX();
                mDownY = (int) ev.getY();
                if (mListener != null) {
                    position = mListener.isOpen();
                    if (position != -1) {//有item打开侧滑菜单
                        if (position != pointToPosition(mDownX, mDownY)) {//判断按下的位置,如果不是侧滑的条目,则关闭侧滑菜单,且listView不响应任何事件
                            PLinearLayout pLinearLayout = (PLinearLayout) getChildAt(position - getFirstVisiblePosition());
                            pLinearLayout.smoothCloseMenu();
                            mIsOpen = true;
                            isIntercepted = true;
                        } else {
                            mIsOpen = true;
                            isIntercepted = false;
                        }
                    }
                    else {//当item未打开侧滑菜单(这里分两种情况,一种是所有事件都以执行完毕,依然没有菜单打开;另一种则是菜单还在打开的过程中(需要避免快速操作会同时打开两个item侧滑菜单))
                        //记录手指操作的item
                        if (pOldLinearLayout != null) {
                            if (!pOldLinearLayout.ismIsStop() && pOldLinearLayout.getScrollX() != 0) {//如果item还在执行滑动事件
                                //则关闭item,且listView不影响任何事件
                                pOldLinearLayout.smoothCloseMenu();
                                mIsOpen = true;
                                isIntercepted = true;
                            }
                        }
                        mIsOpen = false;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mIsMove = true;
                break;
            case MotionEvent.ACTION_UP:
                moveY = Math.abs(mDownY - y);
                mVelocity.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                int velocityY = (int) mVelocity.getYVelocity();//获取Y轴速率
                if (!mIsMove && position == -1 && moveY < mScaledTouchSlop) {//确定是点击事件,且用户没有滑动意向,则是点击事件
                    if (mOnItemClickListener != null) {
                        mOnItemClickListener.onClick(pointToPosition(mDownX, mDownY));
                    }
                    mIsMove = false;
                    releaseVelocityTracker();
                    return true;
                } else {
                    pOldLinearLayout = (PLinearLayout) getChildAt(pointToPosition(mDownX, mDownY) - getFirstVisiblePosition());
                }
                mIsMove = false;
                releaseVelocityTracker();
                break;
            case MotionEvent.ACTION_CANCEL:
                isIntercepted = moveOrDown((int) ev.getX(), (int) ev.getY(), isIntercepted);
                break;
        }
        return isIntercepted;
    }
    public interface OnItemClickListener {
        void onClick(int position);
    }
    private OnItemClickListener mOnItemClickListener;
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.mOnItemClickListener = onItemClickListener;
    }

讲到这里,还需要讲一下PRecyclerView定义跟PListView的部分差异性,因为在RecyclerView是没有pointToPosition、getFirstVisiblePosition、getLastVisiblePosition三个方法的,所以我们需要自定义:

    private int pointToPosition(int x, int y) {
        return getChildAdapterPosition(findChildViewUnder(x, y));
    }
    private int getFirstVisiblePosition() {
        return getChildAdapterPosition(getChildAt(0));
    }
    private int getLastVisiblePosition() {
        return getChildAdapterPosition(getChildAt(getChildCount() - 1));
    }

这样,PRecyclerView和PListView的定义就基本一致了,当然使用上的差别我就不讲了,相信各位都很清楚了。

二、重写ListView的onTouchEvent方法:

这里就很简单了,三句代码搞定,主要是为了求同QQ侧滑,QQ侧滑的效果是:当有item打开侧滑菜单时,用户的下一次操作无论是针对侧滑菜单的item,还是其他item或者是ListView的上下滑动都会失效,只会触发一个操作,那就是关闭打开的菜单,这样也减少了逻辑上容易出现的bug。

public boolean onTouchEvent(MotionEvent ev) {
        if (mIsOpen) {//如果侧滑菜单打开,则不响应任何事件
            return true;
        }
        return super.onTouchEvent(ev);
    }

使用自定义PListView

这里之所以还讲如何使用,是因为使用过程中有几个注意事项,首先就是我们定义的PLinearLayout需要通过

public interface OnScrollListener {
        void state(boolean isOpen, PLinearLayout linearLayout);
    }

这个接口,将当前item的菜单打开状态传递给PListView,然后通过PListview的

public interface OnMenuListener {
        int isOpen();
    }

获取item的菜单打开状态,当然他们都需要Activity或者fragment做为媒介,下面就看代码部分:

一、布局

<?xml version="1.0" encoding="utf-8"?>
<com.twe.plinearlayout.PLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pl_parent"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="65dp"
    android:orientation="horizontal">
    //这里需要注意的是,我们将PLinearLayout看做两部分,一部分是Content,宽度必须是match_parent,一部分则是侧滑菜单部分(我这里只是简单演示,你们可将他们分别用一个LinearLayout包裹,里面可以随意设置控件布局)
    <TextView 
        android:id="@+id/tv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="25dp" />
    <TextView
        android:id="@+id/tv_delete"
        android:layout_width="100dp"
        android:layout_height="match_parent"
        android:background="@android:color/holo_red_light"
        android:gravity="center"
        android:text="删除"
        android:textColor="#ffffff"
        android:textSize="25dp" />

</com.twe.plinearlayout.PLinearLayout>

二、适配器定义

public class PListViewAdapter extends BaseAdapter {
    private Context mContext;
    private List<String> mList;
    private PLinearLayout.OnScrollListener mListener;//这里则是将PLinearLayout.OnScrollListener通过适配器传给PLinearLayout
    public PListViewAdapter(Context context, List<String> list, PLinearLayout.OnScrollListener listener) {
        this.mContext = context;
        this.mList = list;
        this.mListener = listener;
    }
    @Override
    public int getCount() {
        return mList.size();
    }
    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }
    @Override
    public long getItemId(int position) {
        return position;
    }
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        final ViewHolder holder;
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.item, parent, false);
            holder = new ViewHolder();
            holder.tv_content = (TextView) convertView.findViewById(R.id.tv_content);
            holder.tv_delete = (TextView) convertView.findViewById(R.id.tv_delete);
            holder.pl_parent = (PLinearLayout) convertView.findViewById(R.id.pl_parent);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        holder.pl_parent.setOnScrollListener(mListener);
        holder.tv_content.setText(mList.get(position));
        holder.tv_delete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                holder.pl_parent.smoothCloseMenu();//这里需要注意,在点击删除或者侧滑部分的其他键时,需要现将item的侧滑菜单关闭
                mList.remove(position);
                notifyDataSetChanged();
            }
        });
        return convertView;
    }
    public class ViewHolder {
        TextView tv_content;
        TextView tv_delete;
        PLinearLayout pl_parent;
    }
}

三、Activity中的使用

public class PListViewActivity extends AppCompatActivity implements PLinearLayout.OnScrollListener {
    private PListView mLvScroll;
    private PListViewAdapter mAdapter;
    private List<String> mList;
    private boolean mIsOpen = false;
    private PLinearLayout mLinerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview);
        mLvScroll = (PListView) findViewById(R.id.lv_Scroll);
        mList = new ArrayList<>();
        for (int i = 0; i < 40; i++) {
            mList.add("item" + i);
        }
        mAdapter = new PListViewAdapter(this, mList, this);
        mLvScroll.setAdapter(mAdapter);
        mLvScroll.setOnMenuListener(new PListView.OnMenuListener() {
            @Override
            public int isOpen() {
                if (mIsOpen && mLinerLayout != null) {//返回菜单打开的item的position,以及item本身
                    return mLvScroll.getPositionForView(mLinerLayout);
                }
                return -1;
            }
        });
        mLvScroll.setOnItemClickListener(new PListView.OnItemClickListener() {
            @Override
            public void onClick(int position) {
                Toast.makeText(PListViewActivity.this, "我是第" + position + "条。", Toast.LENGTH_SHORT).show();
            }
        });
    }
    @Override
    public void state(boolean isOpen, PLinearLayout linearLayout) {
        mIsOpen = isOpen;//获取PLinearLayout的侧滑菜单打开状态
        if (mIsOpen) {
            mLinerLayout = linearLayout;
        } else {
            mLinerLayout = null;
        }
    }
}

讲到这里就基本讲完了,这应该也是我目前为止写的最长的一篇博客了,直接将代码copy就可以实现你想要的功能,当然最后我还是会提供源码下载的地址:传送门
最后欢迎各位朋友指正。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
尝试着写web下的listview控件,listview一般有四种显示模式——平铺、图标、列表和详细信息。这些显示模式唯一的共同点就是数据是相同的,显示效果完全不一样。这时候xml的优势就完全体现出来了。 第一步,就是建立一个自定义格式的xml,用来保存listview数据 第二步,xsl可以解析xml生成html,所以就针对listview的每一种显示效果设计了一个对应的xsl。这样前面定义的xml数据和不同的xsl一起就可以显示出不同的效果。 第三步,htc在开发web控件时,非常灵活和功能强大,可以采用客户端脚本如js,可以对控件进行封装,使之有自己的属性、方法和事件等。利用htc封装的listview控件中对外有两个属性CfgXMLSrc(配置文件,设置listview的每一种显示模式对应的xsl文件路径等信息)和View(listview的显示模式),在htc中根据listview的View属性来选择不同的xsl文件和xml数据文件生成html,并输出。 这样就可以通过改变listview控件的view属性来切换listview的不同显示效果。 在线演示 打包下载 以前写换皮肤的控件,都是通过更换css和图片路径来做的(可以看看http://www.stedy.com),局限性很大,例如toolbar,在winxp和win2000下差别很大,只靠通过换css和图片路径无法应付这种情况。通过开发listview的经验,从中悟到了一种更好的开发换皮肤的web控件的模式: 首先将控件的相关数据用xml描述出来,对于每一种Theme(皮肤/主题样式),有一个相关的配置文件,配置文件中记载了该控件所用到的xsl、css、图片路径、htc等信息。在控件相关的htc中,根据Theme属性组合这些。从而可以灵活的应付各种情况。 例如刚才说的toolbar,假如入我们有三种风格:winxp蓝色、winxp银色和windows经典,前面两种基本差不多,只是样式和图片不一样,而后面一种和前面的两种差别比较大。那么我们需要写两个xsl,三个css文件,三个图片文件夹,组合一下就可以生成这三种风格的toolbar了。 这种控件开发模式会慢慢流行起来并在asp.net控件中发挥重要作用的

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值