Android 写一个可以横向滑动条目的列表

在开发中,会发现很多列表希望条目能够侧滑,侧滑出来一两个按钮什么的,例如QQ就可以侧滑出删除按钮。这边文章就是教大家写一个可以侧滑的自定义控件。另外,本文的内容不是属于Android中比较高深的内容,高手可以略过。通过阅读本文,你可能学习到的知识有:

  1. 自定义侧滑控件的实现
  2. Android事件传递简要内容
  3. 属性动画ValueAnimator的使用

先来看一下要实现的侧滑是什么样的效果:

sample.gif

因为这个例子很简单所以代码就不单独拿出来了,想看代码的可以去这个仓库:https://github.com/Lee-swifter/UToolBox 。 这个一个工具箱的APP,里面包含有一些查询的工具,选择“电视节目表”,然后随便进入一个频道,就可以看到这个例子了。代码的话请搜索类 TvChannelItem

这个例子其实就是类似于QQ消息界面的侧滑,只是QQ是滑出来两个按钮,而我的只是一个按钮,另外在滑动出来后再进行上下滑动时我没有做处理。下面看一下这个控件是怎么做出来的吧。


功能分析

首先可以看到,这个侧滑是列表中的条目的功能,那么需不需要对ListViewRecyclerView做一做手脚呢?这个是不需要的,因为这个侧滑只是属于条目的功能,虽然是放在列表里面的,但是并非需要列表特别支持。另外如果要修改列表控件的话,势必会降低这个功能的可移植性。所以我们仅仅是针对每一个条目写一个可侧滑的自定义控件。虽然很多人会说上下滑动的东西里面再加上左右滑动,肯定会出现事件冲突的情况。没错,这个问题是有的,但是也很好解决。

说句题外话,ListView已经有些过时,现在应该转到RecyclerView上了,这个例子中的代码使用的都是RecyclerView

创建自定义控件

既然已经确定只需要创建自定义控件,那么就开始考虑这个自定义控件要怎么去设计。

控件的设计

  1. 首先,能看到这个控件是由一些其他基本控件组成,因此我们需要写的只是一个组合控件,而非继承自View类;
  2. 再次,控件中所有的基本控件整体是横向排列的,使用LinearLayout可以方便的做出这种布局。因此我们继承LinearLayout
  3. 关于滑动:因为控件的内容是要大于控件的宽度的,因此再滑动的时候应该移动的是控件的内容,而不是控件的位置。这个说是好说,但是这里有一些函数和变量如果弄混了,就很容易卡在这里;
  4. 上下滑动与左右滑动的冲突:首先,我们必须要判断用户当前是要进行上下滑动还是左右滑动,如果是上下滑动,可以交由RecyclerView来处理;如果是左右滑动,那么我们必须屏蔽列表的事件拦截,至于怎么滑动,就是我们自己说的算了。

控件的实现

整体就是这些问题了,下面就开始编码,如果在写代码过程中出现了什么问题,那就再去解决什么问题。

  1. 自定义控件的布局:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

   <!-- 注意下面这个布局的宽度是match_parent的,也就是占用整个控件宽度-->
    <LinearLayout
        android:orientation="vertical"
        android:padding="5dip"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/widget_channel_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:lines="1"
            android:textSize="16sp"/>

        <TextView
            android:id="@+id/widget_channel_rel"
            android:layout_marginTop="5dip"
            android:layout_width="wrap_content"
            android:lines="1"
            android:layout_height="wrap_content"/>

    </LinearLayout>

    <!-- 这个Button是放在上面的布局的右边,也就是超出了控件显示部分-->
    <Button
        android:id="@+id/widget_channel_live_button"
        android:layout_width="100dip"
        android:layout_height="match_parent"
        android:text="@string/live"
        android:textSize="16sp"
        android:background="@android:color/holo_green_light"/>

</merge>

这里需要注意的第一个LinearLayout的宽度和下面的Button的宽度,这两个宽度是这个布局的重点。

  1. 创建自定义控件:
public class TvChannelItem extends LinearLayout {

    private int touchSlop;

    public TvChannelItem(Context context) {
        super(context, null);
    }

    public TvChannelItem(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TvChannelItem(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setOrientation(HORIZONTAL);

        LayoutInflater.from(context).inflate(R.layout.widget_tv_channel, this);
        name = ButterKnife.findById(this, R.id.widget_channel_name);
        url = ButterKnife.findById(this, R.id.widget_channel_rel);
        button = ButterKnife.findById(this, R.id.widget_channel_live_button);

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }   
}

在创建自定义控件的时候,不需要什么特别的操作,只是将布局通过LayoutInflater引入进来,并找到响应的控件。注意在构造时初始化了一个变量touchSlop,这个变量指的是可以考虑为用户进行滑动操作的最小像素距离。也就是说如果滑动超过了这个值,那么认为是滑动操作;如果是小于这个值,则认为是点击操作。

  1. 滑动处理

滑动的处理是这个控件的关键部分,内容移动、冲突处理都在这里做。下面贴出代码,并在代码中给出注释:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录按下的位置
            downX = event.getRawX();
            downY = event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            float nowX = event.getRawX();
            float nowY = event.getRawY();

            //判断用户是上下滑动还是左右滑动
            if (!touchMode && (Math.abs(nowX - downX) > touchSlop || Math.abs(nowY - downY) > touchSlop)) {
                touchMode = true;   //一旦该变量被置为true,则滑动方向确定
                if (Math.abs(nowX - downX) > touchSlop && Math.abs(nowY - downY) <= touchSlop) {
                    slide = true;   //此时认为是左右滑动
                    getParent().requestDisallowInterceptTouchEvent(true);   //请求父控件不要拦截触摸事件

                    //以下代码避免出发点击事件
                    MotionEvent cancelEvent = MotionEvent.obtain(event);
                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (event.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
                    onTouchEvent(cancelEvent);
                }
            }

            if (slide) {
                float diffX = downX - nowX + lastScrollX;
                if (diffX < 0)  //设置阻尼
                    diffX /= 3;
                else if (diffX > button.getWidth())
                    diffX = (diffX - button.getWidth()) / 3 + button.getWidth();

                scrollTo((int) diffX, 0);   //滑动到手指位置
            }

            break;
        case MotionEvent.ACTION_UP:
            if (slide) {    //如果是左右滑动,那么松手时需要自动滑到指定位置
                ValueAnimator animator;     //使用的是ValueAnimator,而非Scroller
                if (getScrollX() > button.getWidth() / 2) {
                    animator = ValueAnimator.ofInt(getScrollX(), button.getWidth());
                } else {
                    animator = ValueAnimator.ofInt(getScrollX(), 0);
                }
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        scrollTo((Integer) animation.getAnimatedValue(), 0);
                    }
                });
                animator.start();
                slide = false;
            }
            touchMode = false;  //重置变量
            break;
    }

    return super.onTouchEvent(event);
}

以上就是本控件的关键代码。由几个地方需要讲解一下:

  1. getParent().requestDisallowInterceptTouchEvent(true); 此代码用于避免父控件拦截事件。因为在Android的事件传递过程中,如果一个控件的onTouchEvent函数返回true,那么后续的事件都会传递到这个控件中处理,但是此时父控件仍然可以拦截事件,而子控件会接收到一个ACTION_CANCEL的事件。在本例中,此行代码可以避免在横向滑动时触发上下滑动。
  1. 此处移动是移动的控件中的内容,而控件本身没有移动,scrollX就是只内容距控件左边的相对距离。如果你使用了translationX,那么你会发现控件位置在移动,而右边的按钮并没有被移动出来。如果对这个问题有疑问,可以看看这篇文章http://blog.csdn.net/whsdu929/article/details/52152520
  2. 在手指抬起来的时候,滑出来的控件将滑回指定位置,此时可以用Scroller来实现,但本例中使用的是ValueAnimator,也仅仅是这个要比用Scroller方便一些。至于ValueAnimator的用法,本例子只是最简单的,其详细用法可以自行搜索。

使用

控件已经写好了,那么就看看怎么使用。因为这个控件仅仅是一个LinearLayout,因此其使用也没有需要额外注意的地方,只要会使用RecyclerView,就会使用这个。只不过把布局换成了单个自定义控件而已。

下面是布局代码:

<?xml version="1.0" encoding="utf-8"?>
<lic.swifter.box.widget.TvChannelItem 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_tv_channel"
    android:layout_width="match_parent"
    android:layout_height="70dip" />

Adapter代码:

public class TvChannelAdapter extends RecyclerView.Adapter<TvChannelHolder> {

    public class TvChannelHolder extends RecyclerView.ViewHolder {
        private TvChannelItem channelItem;
    
        public TvChannelHolder(View itemView) {
            super(itemView);
            channelItem = ButterKnife.findById(itemView, R.id.item_tv_channel);
        }
    
        public void setChannel(TvChannel channel) {
            channelItem.setChannel(channel);
        }
    }


    private List<TvChannel> list;

    public TvChannelAdapter(List<TvChannel> list) {
        this.list = list;
    }


    @Override
    public TvChannelHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View rootView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tv_channel, parent, false);
        return new TvChannelHolder(rootView);
    }

    @Override
    public void onBindViewHolder(TvChannelHolder holder, int position) {
        holder.setChannel(list.get(position));
    }

    @Override
    public int getItemCount() {
        return list.size();
    }
}

RecyclerView布局代码:

recycler.setLayoutManager(new LinearLayoutManager(this));
recycler.setAdapter(new TvChannelAdapter(response.result));

以上代码中包含了我项目中的一些内容,但是那些只是数据的封装。总体使用方式就是这样,就是RecyclerView正常使用而已。

代码

本文章中的代码都可以在https://github.com/Lee-swifter/UToolBox 中找到,搜索TvChannelItem就可以找到本文中描述的自定义控件。
也可以从这里直接下载应用http://fir.im/tobox ,在应用中查看效果(选择“电视节目表”,然后随便进入一个频道,就可以看到本例子)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,428评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,705评论 22 664
  • 前阵子网上有个大家都比较关注的话题,说一个上海姑娘过年和男朋友回家,男生家里条件不好,甚至是晒出了几张她觉...
    洋果子小姐阅读 464评论 0 1
  • 她从未见过那样大而明亮的月亮,天上一轮,水面一轮。暗黑的海水翻滚出银色浪花,一次又一次像漫不经心,又像刻意而为似的...
    Monica爱夹馍阅读 283评论 0 0
  • 近段时间还是觉得自己心里堵得慌,一想到他就难受,想到他对自己的种种伤害,还是会愤愤不平。想到他现在竟然搬出去住了,...
    丽丽丫丫阅读 153评论 0 0