前言:本文章适合刚入门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。所以,思考问题,解决问题,我们一定要具体的问,具体的去思考。
完。
网友评论