Android自定义ListView实现侧滑子菜单

来源:互联网 发布:在线课堂软件 编辑:程序博客网 时间:2024/05/22 06:15

惯例,先放效果图,DEMO在最后



想当年博主刚接触Android的时候,看到这个效果心中只有膜拜啊,如果慢慢的自己水平也上来了,就把当年的一个想法给圆满了吧。

好了,废话不多说,先总结总结这个效果:

  • 首先是需要自定义ListView,这点是必须的,然后在ListView的onTouchEvent方法中对事件进行处理
  • 普通的Item的话,是没办法实现这样侧滑的,即使你塞一个HorizontalScrollView进去都不行,所以也必须自定义一个ItemView实现左右侧滑
  • 由于ListView的layout_width不一定是MATCH_PARENT,也可能是定值比如300dp,这个时候我们就需要建立一种机制来保证ItemView的宽度和ListView的宽度匹配,毕竟ItemView包含了两个View,一个是正文的ContentView,一个是菜单的MenuView。
首先我从自定义ListView开始讲起,这个ListView需要完成两件事:事件分发和高度匹配。首先来看高度匹配:
    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int width = MeasureSpec.getSize(widthMeasureSpec);        //宽度适配,改变ItemView的宽度        SlideItemView.Width = width;        for(int i = 0; i < getChildCount(); i++){            SlideItemView item = (SlideItemView) getChildAt(i);            item.resetWidth();        }        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }

这个方法并没有什么难度,得到了ListView的宽度,并且将所有在内存中的ItemView的宽度进行重设。这一步是非常必要的,上面也说了,因为你并不知道实际ListView的宽度,那么还谈什么左右滑动。SlideItemView的resetWidth方法我们放在后面讲解。这里就大概了解一下。
然后是ListView的事件分发,这里就比较重要了:
    @Override    public boolean onTouchEvent(MotionEvent ev) {        float dx = 0;        float dy = 0;        switch (ev.getAction()){            case MotionEvent.ACTION_DOWN:                mTouchX = ev.getX();                mTouchY = ev.getY();                mMoveX = ev.getX();                mMoveY = ev.getY();                mTouchPosition = pointToPosition((int)ev.getX(), (int)ev.getY());                break;            case MotionEvent.ACTION_MOVE:                dx = ev.getX() - mMoveX;                dy = ev.getY() - mMoveY;                if(Math.abs(dx) > Math.abs(dy)){                    //根据坐标点得到索引值                    int position = pointToPosition((int)ev.getX(), (int)ev.getY());                    if(mTouchPosition != ListView.INVALID_POSITION && position == mTouchPosition){                        //得到内存中真实的Item                        SlideItemView itemView = (SlideItemView) getChildAt(position - getFirstVisiblePosition());                        itemView.scroll((int) dx);                    }                }                mMoveX = ev.getX();                mMoveY = ev.getY();                break;            case MotionEvent.ACTION_UP:                dx = ev.getX() - mTouchX;                dy = ev.getY() - mTouchY;                if(Math.abs(dx) > Math.abs(dy) && Math.abs(dx) >= mTouchSlop){                    int position = pointToPosition((int)ev.getX(), (int)ev.getY());                    if(mTouchPosition != ListView.INVALID_POSITION && position == mTouchPosition){                        //得到真正在内存中的Item                        SlideItemView itemView = (SlideItemView) getChildAt(position - getFirstVisiblePosition());                        //根据当前scrollX以及dx判断是否显示正文内容                        if (itemView.shouldShowContent((int) dx)){                            itemView.showContent();                        }else{                            itemView.showMenu();                        }                    }else if(position != mTouchPosition){                        SlideItemView itemView = (SlideItemView) getChildAt(mTouchPosition - getFirstVisiblePosition());                        //根据当前scrollX以及dx判断是否显示正文内容                        if (itemView.shouldShowContent((int) dx)){                            itemView.showContent();                        }else{                            itemView.showMenu();                        }                    }                }else{                    SlideItemView itemView = (SlideItemView) getChildAt(mTouchPosition - getFirstVisiblePosition());                    //根据当前scrollX以及dx判断是否显示正文内容                    if (itemView.shouldShowContent((int) dx)){                        itemView.showContent();                    }else{                        itemView.showMenu();                    }                }                break;            case MotionEvent.ACTION_CANCEL:                if(mTouchPosition != ListView.INVALID_POSITION){                    SlideItemView itemView = (SlideItemView) getChildAt(mTouchPosition - getFirstVisiblePosition());                    itemView.showContent();                }                break;        }        return super.onTouchEvent(ev);    }

首先啊,在ACTION_DOWN中实现对坐标点的记录,这里需要记录两套坐标点,一套是表示DOWN时的坐标,一套是表示MOVE时的坐标,MOVE时的坐标初始化是ACTION_MOVE中所必须的。然后根据当前按下的点,得出ListView的position索引。这里还有一点非常重要,不要习惯性的在ACTION_DOWN中返回true,如果这里返回true,那么久表示ListView将消耗掉这个事件,并且后续的MOVE事件和UP事件都只会传递到ListView而不会分发到子Item中,那么子Item就无法点击了。具体请参考【Android事件分发】。
然后是ACTION_MOVE,如果滑动时的X坐标的绝对值比Y坐标的绝对值大,才进行下一步操作。得出当前滑动坐标所对应的ListView的position索引值,如果DOWN适合的position索引值和MOVE的position索引值相等,才开始进行滑动。这里有一个重要的概念就是getChildAt(position - getFirstVisiblePosition()),我们知道ListView是有缓存机制的,内存中不可能存在getCount()这么多数量的View存在,内存中只存放从getFirstVisiablePosition()到getLastVisiablePosition()这么多个Item在内存中,那么如果我们想得到当前的position所对应内存中的Item,就需要position - getFirstVisiblePosition()得到真正的索引。具体请参考【ListView的缓存策略】。然后呢就开始滑动吧,滑动被封装到了ItemView里操作,这里主要有个了解就行了。
然后是ACTION_UP,这里除了x坐标的绝度值比y坐标的绝对值要大意外,还需要一个额外条件就是x坐标的绝对值要大于一个阈值,只有大于这个阈值,我们才认为是滑动,这个概念很重要,这个是区别点击操作还是滑动操作的条件。满足条件后,我们对当前的dx偏移值进行一个判断,如果需要展示正文Content,就展示正文,如果需要展示菜单Menu,就展示菜单。当前,这个判断操作和展示操作都封装在了ItemView里。如果当前的position与ACTION_DOWN时的position不同的话,我们认定此时已经划出这个Item了,那么我们就需要对ACTION_DOWN所对应的ItemView进行一个判断和展示操作。如果连滑动操作的条件都不满足,我们认定此时是在对ListView进行上下的滚动操作,则同样对ACTION_DOWN事件对应的ItemView进行一个判断和展示。
最后是ACTION_CANCEL,写这个事件呢主要是为了防止滑动过程中事件被中断后造成滑动到一半就卡在那里。处理方法和上面一致。

接下来就展示ItemView的部分了。这个部分需要一个必要的知识点是Scroller,如果不会请参考【Android Scroller】,假设你已经了解了Scroller。那么就可以接着往下看了。首先展示设置正文Content和菜单Menu的部分:
public void setView(SlideListView listView, int contentId, int menuId, float menuScale){        this.listView = listView;        this.content = View.inflate(getContext(), contentId, null);        this.menu = View.inflate(getContext(), menuId, null);        this.scale = menuScale;        LayoutParams param1 = new LayoutParams(Width, LayoutParams.MATCH_PARENT);        addView(content, param1);        LayoutParams param2 = new LayoutParams((int) (Width * menuScale), LayoutParams.MATCH_PARENT);        addView(menu, param2);    }    public View getContent(){        return content;    }    public View getMenu(){        return menu;    }

很简单,非常简单,就只是将对应的layoutId实例化,然后addView而已,唯一需要注意的就是LayoutParams这里的Width,它是个静态变量,它的值就是外界ListView的宽度值。接下来我们来看滑动相关的代码:
    public void showContent(){        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -mScroller.getFinalX(), 0);        invalidate();    }    public void showMenu(){        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), menu.getWidth() - mScroller.getFinalX(), 0);        invalidate();    }    public boolean shouldShowContent(int dx){        //初始化        if(menu.getWidth() == 0){            resetWidth();        }        if(dx > 0){            //右滑,当滑过1/4的时候开始变化            if(mScroller.getFinalX() < menu.getWidth() * 3 / 4){                return true;            }else{                return false;            }        }else{            //左滑,当滑过1/4的时候开始变化            if(mScroller.getFinalX() < menu.getWidth() / 4){                return true;            }else{                return false;            }        }    }

首先看shouldShowContent方法,这里有一个初始化操作,我们待会再讲。如果dx大于0,则说明是往右滑动,那么scrollX的值只要小于menu宽度的3/4,也就是滑动超过了1/4,我们就认为需要显示正文Content了,否则就显示菜单Menu。对于dx小于0的情况也同理。然后是showContent和showMenu方法,这里直接是将scrollX滚动到两者的起始位置,也就是说这两个方法是在ListView的ACTION_UP方法中调用的。这里需要注意的是invalidate()方法一定要调用。否则可能会不刷新,别问我是怎么知道的。。我为了知道这个原因花了一个小时。。
然后是ListView中的ACTION_MOVE方法所需要调用的scroll方法:
public void scroll(int dx){        if(dx > 0){            //右滑            if(mScroller.getFinalX() > 0){                if(dx > mScroller.getFinalX()){                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -mScroller.getFinalX(), 0);                }else{                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);                }            }else{                mScroller.setFinalX(0);            }            invalidate();        }else{            //左滑            if(mScroller.getFinalX() < menu.getWidth()){                if(mScroller.getFinalX() - dx > menu.getWidth()){                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), menu.getWidth()- mScroller.getFinalX(), 0);                }else{                    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);                }            }else{                mScroller.setFinalX(menu.getWidth());            }            invalidate();        }    }

这里主要是有一个左右边界值的判断问题,大家直接看代码吧。文字说不清楚的。逻辑其实也并不困难。
最后就是resetWidth方法了:
    /**     * 重设宽度,在ListView的onMeasure方法中调用。     * 此方法是为了动态适配ListView的宽度,因为ListView的layout_width不一定等于MATCH_PARENT     * 也可能是定值比如300dp     */    public void resetWidth(){        ViewGroup.LayoutParams param1 = content.getLayoutParams();        if(param1 == null){            param1 = new LayoutParams(Width, LayoutParams.MATCH_PARENT);        }else{            param1.width = Width;        }        content.setLayoutParams(param1);        ViewGroup.LayoutParams param2 = menu.getLayoutParams();        if(param2 == null){            param2 = new LayoutParams((int) (Width * scale), LayoutParams.MATCH_PARENT);        }else{            param2.width = (int) (Width * scale);        }        menu.setLayoutParams(param2);    }

其实也没什么,也就是重新改变正文Content和菜单Menu的宽度值罢了。

好了,源码讲解完毕,下面给出测试例子:
首先是activity_main.xml:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <cc.wxf.slide.SlideListView        android:id="@+id/listView"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:cacheColorHint="@android:color/transparent"        android:listSelector="@android:color/transparent"        android:dividerHeight="1dp"        android:divider="@android:color/darker_gray"        /></RelativeLayout>

接着是MainActivity:
public class MainActivity extends Activity {    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        SlideListView listView = (SlideListView) findViewById(R.id.listView);        listView.setAdapter(new SlideAdapter(this, listView));    }}

然后是自定义Adapter,这里需要好好看一下,特别是getView和ViewHolder的处理:
public class SlideAdapter extends BaseAdapter {    private Context context;    private SlideListView listView;    public SlideAdapter(Context context, SlideListView listView){        this.context = context;        this.listView = listView;    }    private String[] data = new String[]{            "1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231","1231231","232131231"    };    @Override    public int getCount() {        return data.length;    }    @Override    public Object getItem(int position) {        return data[position];    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        ViewHolder holder = null;        if(convertView == null){            SlideItemView itemView = new SlideItemView(context);            itemView.setView(listView, R.layout.item_content, R.layout.item_menu, 2.0f / 3);            holder = new ViewHolder(itemView);            itemView.setTag(holder);            convertView = itemView;        }else{            holder = (ViewHolder) convertView.getTag();        }        holder.textView.setText(data[position]);        final SlideItemView itemView = (SlideItemView) convertView;        holder.imageView.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Toast.makeText(context, "点击了imageview", Toast.LENGTH_SHORT).show();                itemView.showContent();            }        });        holder.textView.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Toast.makeText(context, "点击了textview", Toast.LENGTH_SHORT).show();                itemView.showContent();            }        });        holder.btn1.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Toast.makeText(context, "点击了btn1", Toast.LENGTH_SHORT).show();                itemView.showContent();            }        });        holder.btn2.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Toast.makeText(context, "点击了btn2", Toast.LENGTH_SHORT).show();                itemView.showContent();            }        });        return convertView;    }    public class ViewHolder {        public ImageView imageView;        public TextView textView;        public TextView btn1;        public TextView btn2;        public ViewHolder(SlideItemView view){            View content = view.getContent();            imageView = (ImageView) content.findViewById(R.id.imageView);            textView = (TextView) content.findViewById(R.id.textView);            View menu = view.getMenu();            btn1 = (TextView) menu.findViewById(R.id.btn1);            btn2 = (TextView) menu.findViewById(R.id.btn2);        }    }}

最后就是两个布局文件了
item_content.xml:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content"    >    <ImageView        android:id="@+id/imageView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:src="@mipmap/ic_launcher"        android:layout_centerVertical="true"        android:layout_marginLeft="10dp"        />    <TextView        android:id="@+id/textView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="@string/app_name"        android:textSize="15sp"        android:textColor="@android:color/black"        android:layout_centerVertical="true"        android:layout_alignParentRight="true"        android:layout_marginRight="10dp"        /></RelativeLayout>

item_menu.xml:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:orientation="horizontal"    android:gravity="center"    android:padding="20dp"    android:background="@android:color/holo_red_light"    >    <TextView        android:id="@+id/btn1"        android:layout_width="match_parent"        android:layout_weight="1"        android:layout_height="match_parent"        android:gravity="center"        android:text="@string/btn1"        android:textSize="15sp"        android:textColor="@android:color/black"        />    <TextView        android:id="@+id/btn2"        android:layout_width="match_parent"        android:layout_weight="1"        android:layout_height="match_parent"        android:gravity="center"        android:text="@string/btn2"        android:textSize="15sp"        android:textColor="@android:color/black"        /></LinearLayout>

好了,所有都讲完了。放出Demo:
点我去下载DEMO

2 0
原创粉丝点击