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

惯例,先放效果图,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

时间: 2024-08-28 12:35:02

Android自定义ListView实现侧滑子菜单的相关文章

android自定义listview实现圆角

在项目中我们会经常遇到这种圆角效果,因为直角的看起来确实不那么雅观,可能大家会想到用图片实现,试想上中下要分别做三张图片,这样既会是自己的项目增大也会增加内存使用量,所以使用shape来实现不失为一种更好的实现方式.在这里先看一下shape的使用: [html] view plaincopy <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schema

Android 自定义 ListView 显示网络歌曲列表

本文内容 环境 项目结构 演示自定义 ListView 显示网络歌曲列表 参考资料 本文最开始看的一个国人的文章,没有源代码,根据文中提供的源代码,自己新建的项目(最可气的是,没有图标图片资源,只能自己乱编),但程序不是很稳定,有时能显示出列表中的缩略图,有时显示不出来,还在主线程访问了网络.后看文章评论,作者给出英文原文链接,本来想这下没事了吧,结果下载源代码运行后,还是有问题~仔细看英文原文,原来他也是根据 Github 上一个项目的基础上搞的,只是添加了式样,以及显示完整的歌曲列表,包括歌

Android 自定义 ListView 上下拉动&ldquo;刷新最新&rdquo;和&ldquo;加载更多&rdquo;歌曲列表

本文内容 环境 测试数据 项目结构 演示 参考资料 本文演示,上拉刷新最新的歌曲列表,和下拉加载更多的歌曲列表.所谓"刷新最新"和"加载更多"是指日期.演示代码太多,点击此处下载,自己调试一下. 下载 Demo 环境 Windows 2008 R2 64 位 Eclipse ADT V22.6.2,Android 4.4.3 SAMSUNG GT-I9008L,Android OS 2.2.2 测试数据 本演示的歌曲信息,共有 20 条,包括歌手名.歌曲名.时长.缩

Android自定义ListView下拉刷新

实现的目标是本地有数据并没有刷新.下拉数据及时刷新数据. 我在网上找了某位写的MyListView,这个东西的下拉核心部分还是没有弄明白.非常感谢这位作者. XML布局文件源代码: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layou

Android 自定义ListView实现底部分页刷新与顶部下拉刷新

在项目开发中,由于数据过大时,需要进行分页加载或下拉刷新,来缓解一次性加载的过长等待.本篇博文实例讲解通过自定义的ListView实现底部分页加载和顶部下拉刷新的效果. 其效果图: 一.ListView 底部分页加载 整个底部分页加载,主要分一下几步: 1.加载底部自定义View; 2.响应OnScrollListener监听事件,onScroll方法记录最后可见的View Item以及整个totalItemCount.当onScrollStateChanged状态改变时, 当滑动到底端,并滑动

在android 自定义listView中绘制矩形

我想在android 在listview中绘制自定义的形状,我在网上找了代码但是没有运行通过,我现在有一个可以绘制矩形的 DrawView.java类,我想在我的自定义listView中展示这个对象. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 DrawView.java package com.example.h

Android 自定义ListView实现底部分页刷新与顶部下拉刷新,androidlistview

在项目开发中,由于数据过大时,需要进行分页加载或下拉刷新,来缓解一次性加载的过长等待.本篇博文实例讲解通过自定义的ListView实现底部分页加载和顶部下拉刷新的效果. 其效果图: 一.ListView 底部分页加载 整个底部分页加载,主要分一下几步: 1.加载底部自定义View; 2.响应OnScrollListener监听事件,onScroll方法记录最后可见的View Item以及整个totalItemCount.当onScrollStateChanged状态改变时, 当滑动到底端,并滑动

Android 自定义ListView控件,滑动删除

1.触摸事件 dispatchTouchEvent 判断是否处理触摸动作 onTouchEvent 处理触摸动作 2.Android对于控制和获取View在屏幕很强大 ListView: pointToPosition 根据触摸点获取item的位置 getChildAt 根据索引获取item的View,注意从第一个可视化的item算起 View: getLocationOnScreen获取View在屏幕的坐标 import android.content.Context; import andr

Android自定义ListView的Item无法响应OnItemClick的解决办法

转: 如果你的自定义ListViewItem中有Button或者Checkable的子类控件的话,那么默认focus是交给了子控件,而ListView的Item能被选中的基础是它能获取Focus,也就是说我们可以通过将ListView中Item中包含的所有控件的focusable属性设置为false,这样的话ListView的Item自动获得了Focus的权限,也就可以被选中了 我们可以通过对Item Layout的根控件设置其Android:descendantFocusability=”bl