androidAndroid知识Android开发

小demo —— RecyclerView侧滑菜单的实现(重点是

2017-04-26  本文已影响1171人  thinkChao

前言:本文章适合刚入门Android开发的新同学,有一定的Android基础,但在开发时,存在“项目经验少,缺乏思路,教程看了就忘,不知如何Google”等问题。本人目前正在努力逃脱这个阶段,所以目前水平还比较菜。不过编程最重要的还是要思考、要有清晰的思路和逻辑、要总结,所以我就将一些思考总结在我的文章里,希望也能够引发大家的思考。也许将来回过头看这篇文章写的很傻×,但将自己的心路历程记录下来这件事还是挺有用的。

类似的文章已经有很多很多大牛写过并大量传播,无论技术和代码都是除了官方文档以外算是比较权威的参考了。但是有一个很重要的问题,就是我们看大牛的博客,但永远成为不了他们。为什么?因为他们的内容给你呈现了某一项目或功能实现的完美解决方案,但他们却很少提及这套方案和代码是怎么搞出来的,其实最最重要的是他们的这个思考过程,博文只是他们思考的产物而已,但这个过程他们没有详说,但对像我这样的菜鸟来说就缺乏这样的思考过程,所以我也只能靠自己的去琢磨了。但他们的博客,仍然是我们重要的参考,我们要做的是尝试去琢磨整个实现过程。

文章内容存在明显错误或不足之处,还望批评指正。

demo目标##

demo目标效果.gif

这个小demo只实现了侧滑菜单的功能,一般这个项目都会给侧滑菜单配个删除操作,不过我这里没有实现这个功能。

代码地址:https://github.com/thinkChao/SwipeRecyclerView

1、demo实现步骤规划##

Tips:
当我们想要实现每一个功能时,最好先规划好大体步骤,先做什么,再做什么。把一个模糊的功能描述,转化为较为具体的实现步骤,否则对于我这种菜鸟来说,几乎每次想实现某个功能,都有种无从下手的感觉。很多时候我们遇到一个问题之所以会有这种感觉,就是因为我们的头脑中只有模糊的问题描述或者功能描述(例如这里的:如何实现列表的侧滑菜单?),而没有任何的规划。所以,拿到一个问题,我们一定要必须要将其尽量具体化,再复杂的问题,那也是一步步规划和实现来的,我们需要做的就是针对每一步来各个击破。这个是我们自己要独立思考的,我们要在得出我们的答案之后,再去看看别人是怎么做的.

我们这个demo从总体上分为三个大步骤:

1、实现列表功能,每一个item的数据暂且写死在程序里。

2、实现侧滑菜单的布局,并将其放到“合适的位置”。为什么这么说,因为侧滑菜单的滑出效果有两种。一种是像QQ那种的,给人的感觉是侧滑菜单是直接放在每一个Item右边,用户左滑时直接带出菜单,该模式被称为Scroll模式。还有一种像网易邮箱的,感觉是侧滑菜单是放在每一个Item下边的,左滑每一个Item,菜单就被揭露出来了,所以该模式被称为Reveal模式。所以这里我们可以猜想,用户行为是一样的(都是向左滑动),但效果却不同,很有可能与侧滑菜单布局的存放位置有关。

3、最后一步就是实现用户的滑动操作了,当向左滑时,显现滑动菜单,向右隐藏菜单。

2、逐一实现,各个击破##

Tips:
1、在实现一个具体的不太好再细分的功能时,例如这里的列表实现,我们应该怎么做?我认为,应该是搜索实现该功能的Android相关类和相应教程。毫无疑问,要实现某一功能,一定是通过Android框架层为我们提供的各种API来实现的。这里针对个人分两种情况:如果你已经知道该功能是借助哪个API实现的,你只需要搜索这个API的用法或者已经很熟悉了就直接写代码就好了。如果不知道需要用哪个API,那就再多一步搜索过程,先搜索该功能需要借助哪些API来实现,再搜索这个API的用法。

2、关于学习某个组件或API的使用,个人建议如果英文还可以的话,直接Google搜索相关教程和官方开发文档,而不是学习其它大牛的博客。为什么呢?拿RecyclerView的使用举例,我们想一想, 那些大牛们是如何知道和了解它的用法的,我不相信当官方一发布RecyclerView,他们牛到不参考任何资料就可以拿来直接用,他们肯定也要看官方文档和教程的嘛。所以,我们尽力在这方面做到与大牛同步,自己动手,丰衣足食。

3、在具体行动上面,我个人主要是参考两类资源:官网文档+相关教程。官方文档这个不用说,最权威的参考,但是它一般都是罗列各个方法的具体参数和功能介绍,没有使用教程。所以我们还需要一份权威教程,个人用的较多的是“CodePath”,个人感觉内容质量很高,页面布局看着也让人舒服。如果一个满足不了,还有Vogella、TutorialPoint也很不错,可以几个同时参考着。

2.1、实现列表功能###

回到我们的项目,在实现列表功能时,有过开发经验的同学,就会知道列表的实现一定是通过ListView或者RecyclerView,毕竟基本上所有的APP都会实现列表功能,很常用。我们这里采用RecyclerView,因为它是ListView的继承和发展,完全替代ListView,而且比它更灵活。

好了,实现该功能的相关组件我们已经选定,下面就是具体实现了。如果之前使用过RecyclerView,而且比较熟悉了,可以直接写代码或者懒得写有现成的项目可以用,那就拿来直接用好了。但是像我这种菜鸟,没怎么用过的,就得麻烦一下,就得要先学习RecyclerView的使用了。不过我认为,还是以参考官方文档和高质量教程为主,下面给出我的参考链接,我个人还是以看教程为主,官方文档作为辅助。这些组件也是多用几次才会熟练,第一次用也不可能理解太透彻,多用就好了。

官方文档:https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html
教程:https://guides.codepath.com/android/using-the-recyclerview

只通过上面那个教程链接,即使不参考官方文档,完全可以实现基本的列表效果,而且不用参考其它教程,本人已亲测,过程这里不再赘述,而且相关博客也是太多太多了,多我一个没什么意思,而且还没大牛写的好。

2.2、实现Item的布局###

Item布局.png

列表中每一行我们称之为一个Item,所以我们要设计每一个Item的布局。其实这个很简单啦,我们把每一个Item布局分为两部分。左边为列表布局,右边为侧滑菜单的布局,中间放一个宽度为1dp的view。所以我们在设计时,只需要将左边列表布局的宽度覆盖整个屏幕,右边的侧滑菜单放在其右。我们这里的布局就相当简单了,代码很短,所以附在可以下面。

<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:id="@+id/item_layout">

        <TextView
            android:id="@+id/content_name"
            android:layout_width="match_parent"    //宽度覆盖屏幕
            android:layout_height="match_parent"
            android:layout_margin="25dp"
            android:text="item" />

        <View
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="@android:color/background_dark"/>

        <TextView
            android:id="@+id/content_delete"
            android:layout_width="150dp"
            android:layout_height="match_parent"
            android:padding="15dp"
            android:background="@android:color/holo_green_light"
            android:text="侧滑菜单"
            android:textSize="20sp"
            android:paddingTop="30dp"/>
    </LinearLayout>

2.3、实现滑动操作(1)###

这一步是整个项目的重点,一开始我们肯定是没什么思路和想法的,不过没关系,我们需要做的就是,认真想一想这个过程,在我们手指按下,开始滑动的那一刻,我们对系统做了什么,我们应该让系统如何反应我们的操作。下面我们尝试进行整个过程的思考。

1、首先我们应该想到的是,我们的滑动操作其实是一个touch事件,一定有相应的View来响应我们的动作。那现在的问题就是,用户的动作应该交给谁来响应:是RecyclerView?还是每一个ItemView?很显然,我们滑动的是每一个Item,所以应该是交给每一个Item来实现。然后我就去百度了一下,但是看到很多案例是交给RecyclerView来响应的,而不是每一个Item。所以,这里暂且也使用他们的思路吧。不过,肯定也有别的实现方式的,不过这个方法比较普遍,所以先按这个来吧,以后有机会再尝试其它方法。我们接着这个思路往下走。

2、其次想到,既然让RecyclerView来响应用户的操作,也就是RecyclerView中一定有一个方法,当用户滑动时会被处罚。这个方法,一定是个touch事件,肯定不是click事件。这里简单说一下,click事件是指用户的点击行为,点击我们都清楚,只有按下和松开两个动作。而touch事件的行为更宽泛,不仅限于点击,还包括滑动。所以很明显,我们这里的是touch事件。

接着我们就可以搜索RecyclerView的开发文档,查找与touch事件有关的方法,结果发现也就onTouchEvent()了,所以这里,我们的滑动事件就在这个方法里实现了。

通过查看文档发现,该方法需要传入一个MotionEvent参数,MotionEvent是做什么的呢。查看官方文档,第一句话就写着“用于报告动作事件的对象。运动事件可以持有绝对或相对的移动 ,以及其它的一些数据,具体哪些数据取决于具体的设备”。也就是说,MotionEvent可以判断用户做了哪些动作,比如:按下(ACTION_DOWN)、松开(ACTION_UP)、滑动(ACTION_MOVE)等等。在我们这个项目中,就只用到这三个事件,然后分别在这三个事件中,处理不同的事务就可以了。

3、想到这里之后呢,接下来我们就要开始调用或者实现onTouchEvent方法了,但是我们应该把这个代码写在哪里呢?很多时候,当我知道了该用哪个方法或者哪个类的时候,但是却不知道代码该往哪里写。以我目前的理解认为:方法要么定义在类里面,当触发某个事件时由系统自动调用(也就是回调函数);要么开发者自己通过对象调用,目前我只想到这两个。很明显,onTouchEvent是回调函数,我们无法主动调用,只能在类里面重写了,但是我们又不能去原始类里面写,那怎么办?只能继承RecyclerView,自己写一个MyRecyclerView了。

4、现在捋一捋思路:滑动的事件处理,我们交给RecyclerView的onTouchEvent()方法,这是一个回调方法,所以我们需要继承RecyclerView并重写该方法。然后我们在该方法中,需要用到MotionEvent类,来根据用户行为来分别进行处理,行为包括按下(ACTION_DOWN)、松开(ACTION_UP)、滑动(ACTION_MOVE)。我们需要做的就是在这三个case下分别实现我们想要的操作或者数据。
代码思路如下:

package com.cn.chao.swiperecyclerview;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;

/**
 * Created by aChao on 2017/4/25.
 */

public class MyRecyclerView extends RecyclerView {
    
    public MyRecyclerView(Context context) {
        super(context);
        scroller = new Scroller(context,new LinearInterpolator());
    }

    public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context,new LinearInterpolator());
    }

    public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        
    }


      /*重写onTouchEvent()方法*/
    @Override
   public boolean onTouchEvent(MotionEvent e) {
 
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:

                //当用户“按下”时应该进行的操作和获取需要的数据

                break;
            case MotionEvent.ACTION_MOVE:

                //当用户“滑动”时应该进行的操作和获取需要的数据

                break;
            case MotionEvent.ACTION_UP:

                //当用户“抬起”时应该进行的操作和获取需要的数据

                break;

        }
    }
}

2.4、实现滑动操作(2)###

通过上面的总结,我们已经大体明白了实现滑动的操作应该交由谁来实现,需要继承哪些类,代码架构也搭建好,接下来就是像三个case中填充具体的操作了。这里提一点我的想法,当我们在思考在每一个case(DOWN、MOVE、UP)中,应该进行哪些操作,我们可以分两点来思考:需要获取的数据和进行哪些操作。这两点概括了一切。以下我在进行讲解时,也从这两个方面来说明,具体的代码逻辑就不细说了,挺好理解,我会附上代码,注释也很详细。

1、涉及的类和方法:

从我们的demo想实现的效果可以看出,只有滑动这么一个用户交互。细分的话又分两种:一个是ItemView跟随用户的手指而滑动,手指动View就动,否则不动;另一个就是ItemView自己完成滑动,在我们项目中,当我们滑动到一半,松开手指时,根据菜单滑出来的距离来决定这个菜单是显现还是隐藏,此时让这个view自己去完成剩余的滑动(这种滑动被称为“弹性滑动”)。所以,我们接下来要做的就是为不同的滑动调用不同的方法。我之前也不知道该如何实现这两种滑动效果,也是先搜索的再用的,下面就简单介绍一下吧,详细教程大家可以去搜一搜。

跟随用户的滑动:使用srollTo()/scrollBy()方法来实现。这两个方法是每一个View都自带的,srollTo()是滑动到指定位置,scrollBy()是滑动指定的位移量。

弹性滑动:使用Scroller这个类来实现弹性滑动,需要调用它的startScroll()方法。

2、ACTION_DOWN:

需要获取的数据:
1)根据坐标找到用户触摸点对应的ItemView
2)得到相应的ViewHolder
3)获取侧滑菜单的宽度
…………
执行的操作:
1)判断用户触摸时,有没有被打开的菜单。如果有,则判断触摸的是否为被打开菜单的那个Item,如果是,则交给MOVE来处理,如果不是,则通过scrollTo关闭那个菜单。如果没有打开的菜单,则该获取哪些数据就获取哪些数据。
…………

   public boolean onTouchEvent(MotionEvent e) {
        int x = (int)e.getX();//获得当前点击的X坐标
        int y = (int)e.getY();//获得当前点击的Y坐标
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(state == 0){//state=0表示菜单没被打开,state=1表示菜单被打开
                    view = findChildViewUnder(x,y);//根据用户点击的坐标,找到RecyclerView下的子View,这里也就是每一个Item
                    viewHolder = (ContentAdapter.ViewHolder) getChildViewHolder(view);//获得每一个Item的ViewHolder
                    itemLayout = viewHolder.layout;//获得ViewHolder相应的布局
                    deleteTextView = viewHolder.deleteTextView;//得到菜单栏里的控件,这里我们只有一个textview
                    deleteWidth = deleteTextView.getWidth();//获得侧滑菜单的宽度
                }else if(state == 1){
                    View view1 = findChildViewUnder(x,y);
                    ContentAdapter.ViewHolder viewHolder1 = (ContentAdapter.ViewHolder) getChildViewHolder(view1);
                    LinearLayout itemLayout1 = viewHolder1.layout;
                    Boolean bool = viewHolder.equals(viewHolder1);//判断当前用户指向的Item是否为之前打开的那个Item
                    if(bool){
                        break;
                    }else {
                        scroller.startScroll(itemLayout.getScrollX(), 0, -deleteWidth, 0, 100);//弹性滑动
                        invalidate();
                        state = 0;
                        return true;   //加上这一句会好一些,
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                
                break;
            case MotionEvent.ACTION_UP:

                break;

    lastX = x;
        lastY = y;
        return super.onTouchEvent(e);//返回调用父类的方法,来处理我们没有处理的操作,比如上下滑动操作
    }

3、ACTION_MOVE:

需要获取的数据:
1)获得用户相对上次移动点,移动的距离dx,也就是实时的移动距离
2)获得用户自按下以后,滑过的总距离scrollX
…………
执行的操作:
1)根据实时移动的距离,调用scrollBy()方法来实现跟随用户的实时滑动,这个是MOVE中的主要操作
2)判断边界问题,ItemView移动的距离不能超过菜单的宽度,否则就滑动超过边界了
…………

   public boolean onTouchEvent(MotionEvent e) {
        
                break;
            case MotionEvent.ACTION_MOVE:
                int scrollX = itemLayout.getScrollX();  //获得用户在滑动后,View相对初始位置移动的距离
                int dx = lastX - x; //得到用户实时移动的举例(横向)
                int dy = lastY - y;
                if(Math.abs(dx)>Math.abs(dy)){  //只要左右移动的举例比上下移动的距离大,就执行滑动菜单操作
                    if(scrollX + dx >= deleteWidth){    //检测右边界
                        itemLayout.scrollTo(deleteWidth,0); //scrollTo()中的参数是指要“移动到的位置”
                        state = 1;
                        return true;    //表示已经消费这个事件,不必再传递了
                    }else if(scrollX + dx <= 0){    //检测左边界
                        itemLayout.scrollTo(0,0);
                        state = 0;
                        return true;
                    }
                    itemLayout.scrollBy(dx,0);  //scrollBy()中的参数是指要“移动的距离(也就是像素的数量)”
                }
                break;
            case MotionEvent.ACTION_UP:

                break;

    lastX = x;
        lastY = y;
        return super.onTouchEvent(e);//返回调用父类的方法,来处理我们没有处理的操作,比如上下滑动操作
    }

4、ACTION_UP:

需要获取的数据:
1)获得菜单被滑动出来显现的宽度
…………
执行的操作:
1)判断当手指抬起时,被划出的宽度是否超过一般,超过一般则打开菜单,否则关闭菜单,这里的打开和关闭就是弹性滑动。
…………

   public boolean onTouchEvent(MotionEvent e) {
        
                break;
            case MotionEvent.ACTION_MOVE:
                
                break;
            case MotionEvent.ACTION_UP:
        int deltaX = 0;
                int upScrollX = itemLayout.getScrollX();//获得Item总共移动的距离
                Log.e("hehe","scrollX2"+upScrollX);
                if(upScrollX >= deleteWidth/2) {  //如果显示超过一半,则弹性滑开
                    deltaX = deleteWidth - upScrollX;
                    state = 1;
                }else if(upScrollX < deleteWidth/2){//否则关闭
                    deltaX = -upScrollX;//在startScroll()方法中,第三个参数小于0,表示向右滑。
                    state = 0;
                }
                scroller.startScroll(upScrollX,0,deltaX,0,100);//弹性滑动
                invalidate();
                break;

    lastX = x;
        lastY = y;
        return super.onTouchEvent(e);//返回调用父类的方法,来处理我们没有处理的操作,比如上下滑动操作
    }

3、其它问题##

在这个过程当中,一开始出现了一个问题。就是当我手指按下时,一直触发不了RecyclerView的onTouchEvent中的ACTION_DOWN,一开始毫无头绪,后来捋一捋,为什么触发不了?是什么原因?仔细想一想就知道,RecyclerView没有接受到用户的ACTION_DOWN,那肯定和事件分发机制有关了,这个事件一定分发给了别的View。后来发现,就连ACTION_MOVE事件也无法很好的响应。

解决方法就是重写RecyclerView中的onInterceptTouchEvent方法,劫持这个事件,不交给其它的View去响应。这个就涉及事件分发机制了,可以去认真去学习下。

    public boolean onInterceptTouchEvent(MotionEvent e) {
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                onTouchEvent(e);
                break;
            case MotionEvent.ACTION_MOVE:
                onTouchEvent(e);
                break;
            case MotionEvent.ACTION_UP:
                onTouchEvent(e);
                break;
    }
        return super.onInterceptTouchEvent(e);
    }

这个问题也给我一个启发,就是当出现一个问题时,一定要定准问题是什么,再去找答案。比如这里,一开始我一直想问“为什么当我按下没有触发RecyclerView的ACTION_DOWN事件?为什么当我按下没有触发RecyclerView的ACTION_DOWN事件? 为什么当我按下没有触发RecyclerView的ACTION_DOWN事件? ”我问再多遍也没用,我一直的问这个问题是找不到答案的,我应该想的是“当我按下的那一刻,系统做了什么,导致RecyclerView没有该事件。”那系统做了什么呢???系统没有把该事件分发给RecyclerView,而是给了其它的View,具体是哪个我也没去探究,以后再说。我猜测应该是交给了每一个Item的View。所以,思考问题,解决问题,我们一定要具体的问,具体的去思考。

完。

上一篇下一篇

猜你喜欢

热点阅读