listview侧滑菜单的实现——高仿QQ联系人列表

转载请注明出处:http://blog.csdn.net/binbinqq86/article/details/46010951

 

项目用到了ListView的侧滑删除的功能,由于当时项目比较赶,就随便在网上找了一个,但是效果不是太好,最近闲了下来,就想自己实现一个,于是就按照QQ的联系人列表的侧滑菜单做了一个,效果基本上是一模一样的。在这个过程中,自己也学习到了不少的东西,下面就把这个过程跟大家分享出来。

 

废话不多说,首先上效果图。

 

 

 

看完了图如果感觉效果不好,请不要拍砖,好的话请继续往下看~

 

下面结合代码说说实现的原理:首先自定义一个ViewGroup来实现item的滑动效果。

 

 

package com.binbin.slidedelmenu.item;  
  
import android.content.Context;  
import android.os.Build;  
import android.support.annotation.RequiresApi;  
import android.support.v4.widget.ViewDragHelper;  
import android.util.AttributeSet;  
import android.util.Log;  
import android.view.GestureDetector;  
import android.view.MotionEvent;  
import android.view.VelocityTracker;  
import android.view.View;  
import android.view.ViewConfiguration;  
import android.view.ViewGroup;  
import android.widget.BaseAdapter;  
import android.widget.FrameLayout;  
import android.widget.ListAdapter;  
import android.widget.ListView;  
import android.widget.Scroller;  
  
/** 
 * Created by -- on 2016/11/3. 
 * 带侧滑菜单的自定义item 
 */  
  
public class MenuItem extends ViewGroup {  
    private int contentWidth;  
    private Scroller mScroller;  
    private int maxWidth,maxHeight;//viewGroup的宽高  
    private static final int MIN_FLING_VELOCITY = 600; // dips per second  
    /**最小滑动距离,超过了,才认为开始滑动  */  
    private int mTouchSlop = 0 ;  
    /**上次触摸的X坐标*/  
    private float mLastX = -1;  
    /**第一次触摸的X坐标*/  
    private float mFirstX = -1;  
    private int ratio;  
    //防止多只手指一起滑动的flag 在每次down里判断, touch事件结束清空  
    private static boolean isTouching;  
    private int mRightMenuWidths;//右侧菜单总宽度  
    private VelocityTracker mVelocityTracker;  
    private float mMaxVelocity;  
    private int mPointerId;//多点触摸只算第一根手指的速度  
    private boolean isQQ=true;//是否是qq效果  
    private boolean qqInterceptFlag;//qq效果判断标志  
    private static MenuItem mViewCache;//存储的是当前正在展开的View  
    private boolean isUserSwiped;// 判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件  
    //仿QQ,侧滑菜单展开时,点击除侧滑菜单之外的区域,关闭侧滑菜单。  
    //增加一个布尔值变量,dispatch函数里,每次down时,为true,move时判断,如果是滑动动作,设为false。  
    //在Intercept函数的up时,判断这个变量,如果仍为true 说明是点击事件,则关闭菜单。  
    private boolean isUnMoved = true;  
  
    public MenuItem(Context context) {  
        this(context,null);  
    }  
  
    public MenuItem(Context context, AttributeSet attrs) {  
        this(context, attrs,0);  
    }  
  
    public MenuItem(Context context, AttributeSet attrs, int defStyleAttr) {  
        super(context, attrs, defStyleAttr);  
        init();  
    }  
  
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)  
    public MenuItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {  
        super(context, attrs, defStyleAttr, defStyleRes);  
        init();  
    }  
  
    private void init(){  
        contentWidth=getContext().getResources().getDisplayMetrics().widthPixels;  
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();  
        mScroller=new Scroller(getContext());  
        mMaxVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity();  
    }  
  
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        setClickable(true);//令自己可点击,从而获取触摸事件(很重要,必须写在onMesaure中)  
        mRightMenuWidths=0;//由于ViewHolder的复用机制,每次这里要手动恢复初始值  
        /** 
         * 根据childView计算的出的宽和高,计算容器的宽和高,主要用于容器是warp_content时 
         */  
        for (int i = 0,count = getChildCount(); i < count; i++) {  
            View childView = getChildAt(i);  
            //令每一个子View可点击,从而获取触摸事件  
            childView.setClickable(true);  
            if(childView.getVisibility()!=View.GONE){  
                //获取每个子view的自己高度宽度,取最大的就是viewGroup的大小  
                measureChild(childView, widthMeasureSpec, heightMeasureSpec);  
                maxWidth = Math.max(maxWidth,childView.getMeasuredWidth());  
                maxHeight = Math.max(maxHeight,childView.getMeasuredHeight());  
                if(i>0){//第一个是content,后面的都是菜单  
                    if(childView.getLayoutParams().width== LayoutParams.MATCH_PARENT){  
                        //菜单的宽不能MATCH_PARENT  
                        throw new IllegalArgumentException("======menu'width can't be MATCH_PARENT=====");  
                    }  
                    mRightMenuWidths+=childView.getMeasuredWidth();  
                }else{  
                    if(childView.getLayoutParams().width!= LayoutParams.MATCH_PARENT){  
                        //content的宽必须MATCH_PARENT  
                        throw new IllegalArgumentException("======content'width must be MATCH_PARENT=====");  
                    }  
                }  
            }  
        }  
        //为ViewGroup设置宽高  
        setMeasuredDimension(maxWidth,maxHeight);  
        ratio=mRightMenuWidths/3;//可能每个item的菜单不同,所以ratio要每次计算  
  
        /** 
         * 根据最大宽高重新设置子view,保证高度充满 
         */  
        //首先判断params.width的值是多少,有三种情况。  
        //如果是大于零的话,及传递的就是一个具体的值,那么,构造MeasupreSpec的时候可以直接用EXACTLY。  
        //如果为-1的话,就是MatchParent的情况,那么,获得父View的宽度,再用EXACTLY来构造MeasureSpec。  
        //如果为-2的话,就是wrapContent的情况,那么,构造MeasureSpec的话直接用一个负数就可以了。  
        for (int i = 0,count = getChildCount(); i < count; i++) {  
            View childView = getChildAt(i);  
            if(childView.getVisibility()!=View.GONE){  
                //宽度采用测量好的  
                int widthSpec = MeasureSpec.makeMeasureSpec(childView.getMeasuredWidth(), MeasureSpec.EXACTLY);  
                //高度采用最大的  
                int heightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);  
                childView.measure(widthSpec, heightSpec);  
            }  
        }  
    }  
  
    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
//        Log.e("tianbin",content.getMeasuredWidth()+"#"+content.getMeasuredHeight()+"$$$"+menu.getMeasuredWidth()+"#"+menu.getMeasuredHeight());  
        int left=0;  
        for (int i = 0; i < getChildCount(); i++) {  
            View childView = getChildAt(i);  
            if (childView.getVisibility() != GONE) {  
                if (i == 0) {//第一个子View是内容 宽度设置为全屏  
                    childView.layout(0, 0, maxWidth, maxHeight);  
                    left += maxWidth;  
                } else {  
                    childView.layout(left, 0, left + childView.getMeasuredWidth(), getPaddingTop() + maxHeight);  
                    left += childView.getMeasuredWidth();  
                }  
            }  
        }  
    }  
  
    @Override  
    public boolean dispatchTouchEvent(MotionEvent ev) {  
        acquireVelocityTracker(ev);  
        final VelocityTracker verTracker = mVelocityTracker;  
        switch (ev.getAction()){  
            case MotionEvent.ACTION_DOWN:  
//                Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_DOWN==="+isTouching);  
                if (isTouching) {//如果有别的指头摸过了,那么就return false。这样后续的move...等事件也不会再来找这个View了。  
                    return false;  
                } else {  
                    isTouching = true;//第一个摸的指头,赶紧改变标志,宣誓主权。  
                }  
                mLastX=ev.getRawX();  
                mFirstX=ev.getRawX();  
                //求第一个触点的id, 此时可能有多个触点,但至少一个,计算滑动速率用  
                mPointerId = ev.getPointerId(0);  
                isUserSwiped = false;  
                qqInterceptFlag=false;  
                isUnMoved=true;  
                //如果down,view和cacheview不一样,则立马让它还原。且把它置为null  
                if (mViewCache != null) {  
                    if (mViewCache != this) {  
                        mViewCache.smoothClose();  
                        qqInterceptFlag = isQQ;//当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。  
                    }  
                    //只要有一个侧滑菜单处于打开状态, 就不给外层布局上下滑动了  
                    getParent().requestDisallowInterceptTouchEvent(true);  
                }  
                break;  
            case MotionEvent.ACTION_MOVE:  
                if(qqInterceptFlag){//当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。滑动也不该出现  
                    break;  
                }  
//                Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_MOVE===11111111111111");  
                float deltaX= ev.getRawX()-mLastX;  
                mLastX=ev.getRawX();  
                //为了在水平滑动中禁止父类ListView等再竖直滑动  
                if (Math.abs(deltaX) > 10 || Math.abs(getScrollX()) > 10) {//使屏蔽父布局滑动更加灵敏,  
                    getParent().requestDisallowInterceptTouchEvent(true);  
//                    Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_MOVE===222222222222222");  
                }  
                if (Math.abs(deltaX) > mTouchSlop) {  
                    isUnMoved = false;  
                }  
                scrollBy(-(int)deltaX,0);  
                //越界修正  
                if (getScrollX() < 0) {  
                    scrollTo(0, 0);  
                }  
                if (getScrollX() > mRightMenuWidths) {  
                    scrollTo(mRightMenuWidths, 0);  
                }  
                break;  
            case MotionEvent.ACTION_CANCEL:  
            case MotionEvent.ACTION_UP:  
            default:  
//                Log.e("tianbin","======MenuItem dispatchTouchEvent======ACTION_UP===");  
                if (Math.abs(ev.getRawX() - mFirstX) > mTouchSlop) {  
                    isUserSwiped = true;  
                }  
                if(!qqInterceptFlag){  
                    //求伪瞬时速度  
                    verTracker.computeCurrentVelocity(1000, mMaxVelocity);  
                    final float velocityX = verTracker.getXVelocity(mPointerId);  
//                    Log.e("tianbin",qqInterceptFlag+"=============velocityX:"+velocityX);  
                    if (Math.abs(velocityX) > 1000) {//滑动速度超过阈值  
                        if (velocityX < -1000) {  
                            //平滑展开Menu  
                            smoothExpand();  
                        } else {  
                            // 平滑关闭Menu  
                            smoothClose();  
                        }  
                    } else {  
                        if (Math.abs(getScrollX()) > ratio) {//否则就判断滑动距离  
                            //平滑展开Menu  
                            smoothExpand();  
                        } else {  
                            // 平滑关闭Menu  
                            smoothClose();  
                        }  
                    }  
                }  
                //释放  
                releaseVelocityTracker();  
                isTouching = false;//没有手指在摸我了  
                break;  
        }  
        return super.dispatchTouchEvent(ev);  
    }  
  
    @Override  
    public boolean onInterceptTouchEvent(MotionEvent ev) {  
        switch (ev.getAction()){  
            case MotionEvent.ACTION_MOVE:  
                //屏蔽滑动时的事件(长按事件和侧滑的冲突)  
//                Log.e("tianbin","======MenuItem onInterceptTouchEvent======ACTION_MOVE===111111111111111");  
                if (Math.abs(ev.getRawX() - mFirstX) > mTouchSlop) {  
//                    Log.e("tianbin","======MenuItem onInterceptTouchEvent======ACTION_MOVE===22222222222222222");  
                    return true;  
                }  
                break;  
            case MotionEvent.ACTION_UP:  
                if (getScrollX() > mTouchSlop) {  
                    //这里判断落点在内容区域屏蔽点击,内容区域外,允许传递事件继续向下的。。。  
                    if (ev.getX() < getWidth() - getScrollX()) {  
                        //仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。  
                        if (isUnMoved) {  
                            smoothClose();  
                        }  
                        return true;//true表示拦截  
                    }  
                }  
                if (isUserSwiped) {  
                    return true;  
                }  
                break;  
        }  
        if(qqInterceptFlag){  
            return true;  
        }  
        return super.onInterceptTouchEvent(ev);  
    }  
  
    @Override  
    public void computeScroll() {  
        if(mScroller.computeScrollOffset()) {  
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
            invalidate();  
        }  
    }  
  
    private void smoothExpand(){  
        mViewCache=MenuItem.this;  
        mScroller.startScroll(getScrollX(),0,mRightMenuWidths-getScrollX(),0);  
        invalidate();  
    }  
  
    private void smoothClose(){  
        mViewCache=null;  
        mScroller.startScroll(getScrollX(),0,-getScrollX(),0);  
        invalidate();  
    }  
  
    /** 
     * 快速关闭。 
     * 用于 点击侧滑菜单上的选项,同时想让它快速关闭(删除 置顶)。 
     * 这个方法在ListView里是必须调用的, 
     * 在RecyclerView里,视情况而定,如果是mAdapter.notifyItemRemoved(pos)方法不用调用。 
     */  
    public void quickClose() {  
        if (this == mViewCache) {  
//            mViewCache.scrollTo(0, 0);//关闭  
            mScroller.startScroll(0,0,0,0,0);  
            mViewCache = null;  
        }  
    }  
  
    //每次ViewDetach的时候,判断一下 ViewCache是不是自己,如果是自己,关闭侧滑菜单,且ViewCache设置为null,  
    // 理由:1 防止内存泄漏(ViewCache是一个静态变量)  
    // 2 侧滑删除后自己后,这个View被Recycler回收,复用,下一个进入屏幕的View的状态应该是普通状态,而不是展开状态。  
    @Override  
    protected void onDetachedFromWindow() {  
        if (this == mViewCache) {  
            mViewCache.smoothClose();  
            mViewCache = null;  
        }  
        super.onDetachedFromWindow();  
    }  
  
    //展开时,禁止长按  
//    @Override  
//    public boolean performLongClick() {  
//        if (Math.abs(getScrollX()) > mTouchSlop) {  
//            return false;  
//        }  
//        return super.performLongClick();  
//    }  
  
    /** 
     * @param event 向VelocityTracker添加MotionEvent 
     * @see VelocityTracker#obtain() 
     * @see VelocityTracker#addMovement(MotionEvent) 
     */  
    private void acquireVelocityTracker(final MotionEvent event) {  
        if (null == mVelocityTracker) {  
            mVelocityTracker = VelocityTracker.obtain();  
        }  
        mVelocityTracker.addMovement(event);  
    }  
  
    /** 
     * * 释放VelocityTracker 
     * 
     * @see VelocityTracker#clear() 
     * @see VelocityTracker#recycle() 
     */  
    private void releaseVelocityTracker() {  
        if (null != mVelocityTracker) {  
            mVelocityTracker.clear();  
            mVelocityTracker.recycle();  
            mVelocityTracker = null;  
        }  
    }  
}  

 

 

 

一个最简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE->ACTION_MOVE...->ACTION_MOVE->ACTION_UP,Android系统中的每个View的子类都具有下面三个和TouchEvent处理密切相关的方法:

public boolean dispatchTouchEvent(MotionEvent ev)  这个方法用来分发TouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) 这个方法用来拦截TouchEvent(ViewGroup才有)

public boolean onTouchEvent(MotionEvent ev)       这个方法用来处理TouchEvent

 

当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最先到达最顶层view的 dispatchTouchEvent,然后由dispatchTouchEvent方法进行分发,如果dispatchTouchEvent返回true或者false,事件均不会继续向下传递,如果down后返回false,则move和up都不会被接受,事件向上传递给Activity处理。这里为什么特别指定的down事件呢,因为如果down返回true,说明后续事件会被传递于此,被自己消费,但是move返回false呢?哈哈,这个就不会影响了,因此说down才是关键。此方法一般用于初步处理事件,因为动作是由此分发,所以通常会调用super.dispatchTouchEvent,这样就会继续调用onInterceptTouchEvent,再由onInterceptTouchEvent决定事件流向。如果interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent 再来开始这个事件的分发。如果事件传递到某一层的子 view 的 onTouchEvent 上了,这个方法返回了 false ,那么这个事件会从这个 view 往上传递,都是 onTouchEvent 来接收。而如果传递到最上面的 onTouchEvent  
 也返回 false 的话,这个事件就会“消失”,而且接收不到下一次事件。

 

下面一张图可以说明一切

 

listview侧滑菜单的实现——高仿QQ联系人列表_第1张图片

 

里面有个重要方法在此要特别说明一下:requestDisallowInterceptTouchEvent。当检测到水平有移动距离的时候,则调用此方法,将滑动事件交给子View来处理,而ListView自身不再进行垂直滑动,否则会出现水平跟垂直滑动有冲突。它在源码中的定义如下:

/**
     * Called when a child does not want this parent and its ancestors to
     * intercept touch events with
     * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
     *
     * 

This parent should pass this call onto its parents. This parent must obey * this request for the duration of the touch (that is, only clear the flag * after this parent has received an up or a cancel.

* * @param disallowIntercept True if the child does not want the parent to * intercept touch events. */ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

这是viewparent接口,viewgroup中的实现如下:(viewgroup实现了viewparent接口)

@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

自定义item讲完了,下面就说说怎么用(终于派上用场了)

 

 

package com.binbin.slidedelmenu.item;  
  
import android.content.Context;  
import android.os.Bundle;  
import android.os.PersistableBundle;  
import android.support.v7.app.AppCompatActivity;  
import android.support.v7.widget.DefaultItemAnimator;  
import android.support.v7.widget.LinearLayoutManager;  
import android.support.v7.widget.RecyclerView;  
import android.view.LayoutInflater;  
import android.view.View;  
import android.view.ViewGroup;  
import android.widget.Button;  
import android.widget.LinearLayout;  
import android.widget.TextView;  
import android.widget.Toast;  
  
import com.binbin.slidedelmenu.DividerItemDecoration;  
import com.binbin.slidedelmenu.R;  
  
import java.util.ArrayList;  
import java.util.List;  
  
/** 
 * Created by -- on 2016/12/19. 
 */  
  
public class RecyclerViewActivity extends AppCompatActivity {  
  
    private RecyclerView mRecyclerView;  
    private List str = new ArrayList<>();  
    private MyAdapter adapter;  
  
    @Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        mRecyclerView = new RecyclerView(this);  
        setContentView(mRecyclerView);  
        for (int i = 0; i < 20; i++) {  
            str.add(i + "个");  
        }  
        //设置adapter  
        adapter=new MyAdapter(str,this);  
        mRecyclerView.setAdapter(adapter);  
        //设置布局管理器  
        LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this);  
        mRecyclerView.setLayoutManager(linearLayoutManager);  
        //设置Item增加、移除动画  
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());  
        //添加分割线  
        mRecyclerView.addItemDecoration(new DividerItemDecoration(this, linearLayoutManager.getOrientation(),R.drawable.divider2));  
    }  
  
    class MyAdapter extends RecyclerView.Adapter {  
  
        private List datas;  
        private Context mContext;  
        public MyAdapter(List datas,Context mContext){  
            this.datas=datas;  
            this.mContext=mContext;  
        }  
        @Override  
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
            return new MyViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item3, parent,  
                    false));  
        }  
  
        @Override  
        public void onBindViewHolder(final MyViewHolder holder, final int position) {  
            holder.tv.setText(datas.get(position));  
            holder.bt.setOnClickListener(new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    Toast.makeText(RecyclerViewActivity.this,"bt========onClick",Toast.LENGTH_SHORT).show();  
                }  
            });  
            holder.tv.setOnClickListener(new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    Toast.makeText(RecyclerViewActivity.this,"tv========onClick",Toast.LENGTH_SHORT).show();  
                }  
            });  
            holder.tvHello.setOnClickListener(new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    Toast.makeText(RecyclerViewActivity.this,"hello=====onClick",Toast.LENGTH_SHORT).show();  
                }  
            });  
            holder.tvDel.setOnClickListener(new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    Toast.makeText(RecyclerViewActivity.this,holder.getAdapterPosition()+"del========onClick"+position,Toast.LENGTH_SHORT).show();  
//                    ((MenuItem)holder.itemView).quickClose();  
                    str.remove(holder.getAdapterPosition());  
                    notifyItemRemoved(holder.getAdapterPosition());  
//                    notifyDataSetChanged();  
                }  
            });  
            holder.tv.setOnLongClickListener(new View.OnLongClickListener() {  
                @Override  
                public boolean onLongClick(View v) {  
                    Toast.makeText(RecyclerViewActivity.this,"tv==========onLongClick",Toast.LENGTH_SHORT).show();  
                    return true;  
                }  
            });  
            holder.tvHello.setOnLongClickListener(new View.OnLongClickListener() {  
                @Override  
                public boolean onLongClick(View v) {  
                    Toast.makeText(RecyclerViewActivity.this,"hello============onLongClick",Toast.LENGTH_SHORT).show();  
                    return true;  
                }  
            });  
        }  
  
        @Override  
        public int getItemCount() {  
            return datas.size();  
        }  
  
        class MyViewHolder extends RecyclerView.ViewHolder {  
            TextView tv;  
            TextView tvHello;  
            TextView tvDel;  
            Button bt;  
  
            public MyViewHolder(View view) {  
                super(view);  
                tv = (TextView) view.findViewById(R.id.tv);  
                tvDel=(TextView) view.findViewById(R.id.tv_del);  
                tvHello=(TextView) view.findViewById(R.id.tv_hello);  
                bt= (Button) view.findViewById(R.id.bt);  
            }  
        }  
    }  
}  

 

 

 

 

 

以上就是整个项目的代码,不知道大家理解了没有,写的不好的地方,请大家多多包容与指正~~~

 

好了,今天的讲解到此结束,有疑问的朋友请在下面留言。

 

源码下载

 

最后要感谢张旭童同学的思路,参考文章:http://blog.csdn.net/zxt0601/article/details/53157090

 

 

你可能感兴趣的:(Android开发)