Android Recycleview与Item之间的事件分发以及如何防止更新数据时图片抖动

最近做的项目是一个RecycleView里面Item可以左划出菜单按钮的需求,开始从网上找了一个demo,重写了Item的布局的最外层layout,命名为了SwipeMenuLayout,使之能通过监听OnTouch事件实现既可以支持左划并显示菜单按钮,并且能点击后自动合上,还要支持Item中的原色点击菜单按钮的响应,此处用到的知识点有:RecycleView的上下滑动(父View)的触摸事件监听,SwipeMenuLayout(也就是recycleview的item的跟布局)对于横向滑动的监听,以及SwipeMenuLayout中textview元素点击时的Click响应事件

首先讲一下Touch事件的事件分发,其次是Click事件跟touch事件的区别,然后是Down和Up和Cancel在父子View之间的传递机制,最后讲一下这个过程中遇到的各种坑(子View不可以左划、RecycleView根据位置获取position和hodler以及view,实现自动关闭上次左划出来的菜单功能、SwipeMenuLayout的里面的TextView点击事件经常无法获取到焦点、Recyceview刷新数据时SwipeMenuLayout出现菜单的滑动抖动)

 

Touch事件的分发机制:

一、DispatchTouchEvent:

Touch事件分为ViewGroup和View的事件分发:他们组成一个View树,ViewGroup有DispatchTouchEvent、IntercepterTouchEvent、OnTouchEvent三个相关方法,View没有拦截事件的方法;当ViewGroup收到Touch事件后,首先由DispatchTouch进行分发给所有的子View,子ViewGroup收到后继续往下分发,只有当子View不处理时,才由ViewGroup处理,走ViewGroup的onTouchEvent事件,而子View收到DisapatchTouch后,会在这个方法里面调用onTouchEvent方法,如果子Viwe处理就返回True,否则返回false;

二、IntercepterTouchEvent:

只有ViewGroup才有IntercepteTouchEvent方法,它通过拦截ACTIN_DOWN、ACTION_UP、ACTION_MOVE、其实还有个ACTION_CANCEL(他是在别人拦截了这个事件后自己触发的,还有一种情况就是这个View拦截了事件后action之间没有break,导致不会跳出case选项,顺势向下执行导致的,一般有Break的走了Break后如果没有返回值,会走方法最后的super.OnTouchEvent也就是交给父亲View来处理),一般处理父子View的Touch拦截事件就从这里这几个ACTION处理

 

父View Recycleview的拦截事件和触摸事件代码如下


@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean isIntercepted = super.onInterceptTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("swipe","recycleview ACTION_DOWN");
mLastX = (int) event.getX();
mLastY = (int) event.getY();
mDownX = (int) event.getX();
mDownY = (int) event.getY();
isIntercepted = false;
//根据MotionEvent的X Y值得到子View

View view = findChildViewUnder(mLastX, mLastY);
Log.i("swipe","recycle find childview "+(view==null));
if (view == null) return false;
//点击的子View所在的位置
final int touchPos = getChildAdapterPosition(view);
Log.i("swipe","recycle find childview "+"touchpos:"+touchPos+"lasttouchpos"+mLastTouchPosition);
if (touchPos != mLastTouchPosition && mLastMenuLayout != null
&& mLastMenuLayout.currentState != STATE_CLOSED) {
// if (mLastMenuLayout.isMenuOpen()) {
//如果之前的菜单栏处于打开状态,则关闭它
mLastMenuLayout.smoothToCloseMenu();
mLastMenuLayout.currentState=STATE_CLOSED;
Log.i("swipe","recycle childview 打开状态");
// }
Log.i("swipe","recycle childview 点击其他item");
// isIntercepted = true;

//根据点击位置获得相应的子View
ViewHolder holder = findViewHolderForAdapterPosition(touchPos);
Log.i("swipe","recycle childview holder"+(holder==null));
if (holder != null) {
View childView = holder.itemView;
if (childView != null && childView instanceof SwipeMenuLayout) {
mLastMenuLayout = (SwipeMenuLayout) childView;
mLastTouchPosition = touchPos;
Log.i("swipe","recycle childview holder!=null"+mLastTouchPosition);
}
}


} else {
//根据点击位置获得相应的子View
ViewHolder holder = findViewHolderForAdapterPosition(touchPos);
Log.i("swipe","recycle childview holder"+(holder==null));
if (holder != null) {
View childView = holder.itemView;
if (childView != null && childView instanceof SwipeMenuLayout) {
mLastMenuLayout = (SwipeMenuLayout) childView;
mLastTouchPosition = touchPos;
Log.i("swipe","recycle childview holder!=null"+mLastTouchPosition);
}
}
}

break;
case MotionEvent.ACTION_MOVE:
Log.i("swipe","recycleview move");
case MotionEvent.ACTION_UP:
int dxup = (int) (mDownX - event.getX());
int dyup = (int) (mDownY - event.getY());
Log.i("swipe","DXup:+dxup"+dxup+"dyup:"+dyup+"dxup>dyup?"+(Math.abs(dxup) > Math.abs(dyup))+(mLastMenuLayout.currentState)+mLastTouchPosition);

if ((Math.abs(dxup) > mScaleTouchSlop) &&(Math.abs(dxup)>5.0)&& (Math.abs(dxup) > Math.abs(dyup))) {
//如果X轴偏移量大于Y轴偏移量 或者上一个打开的菜单还没有关闭 则禁止RecycleView滑动 RecycleView不去拦截事件
Log.i("swipe","swipe view dule move");
if( (mLastMenuLayout != null && mLastMenuLayout.currentState != STATE_CLOSED)){
return false;
}else{
return true;
}
}else if( (mLastMenuLayout != null && mLastMenuLayout.currentState != STATE_CLOSED)&&(dxup==dyup)&&(dxup==0)){
Log.i("swipe","swipe view 点击移除");
return false;
}
case MotionEvent.ACTION_CANCEL:
int dx = (int) (mDownX - event.getX());
int dy = (int) (mDownY - event.getY());
Log.i("swipe","DX:+dx"+dx+"dy:"+dy+"dx>dy?"+(Math.abs(dx) > Math.abs(dy))+(mLastMenuLayout.currentState)+mLastTouchPosition);

if ((Math.abs(dx) > mScaleTouchSlop) &&(Math.abs(dx)>5.0)&& (Math.abs(dx) > Math.abs(dy))) {
//如果X轴偏移量大于Y轴偏移量 或者上一个打开的菜单还没有关闭 则禁止RecycleView滑动 RecycleView不去拦截事件
Log.i("swipe","swipe view dule move");
if( (mLastMenuLayout != null && mLastMenuLayout.currentState != STATE_CLOSED)){
return false;
}else{
return true;
}
}else if((Math.abs(dy)>0)&&(Math.abs(dy) > Math.abs(dx))){
Log.i("swipe","recycle view dule move");
return true;
}
break;
}
return isIntercepted;
}

@Override
public boolean onTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
//若某个Item的菜单还没有关闭,则RecycleView不能滑动
if (mLastMenuLayout!=null&&(!mLastMenuLayout.isMenuClosed())) {
return false;
}
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
if (mLastMenuLayout != null && mLastMenuLayout.isMenuOpen()) {
mLastMenuLayout.smoothToCloseMenu();
}
break;
}
return super.onTouchEvent(e);
}

子View的TouchEvent事件监听
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("swipe","swipe ACTION_DOWN");
mDownX = (int) event.getX();
mDownY = (int) event.getY();
mLastX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:

int dx = (int) (mDownX - event.getX());
int dy = (int) (mDownY - event.getY());
Log.i("swipe","swipe ACTION_MOVE"+currentState+(Math.abs(dy) > Math.abs(dx)));
//如果Y轴偏移量大于X轴偏移量 不再滑动
if (Math.abs(dy) > Math.abs(dx)) return false;

int deltaX = (int) (mLastX - event.getX());
if (deltaX > 0) {
//向左滑动
currentState = STATE_MOVING_LEFT;
Log.i("swipe","swipe ACTION_MOVE state"+currentState);
if (deltaX >= menuWidth || getScrollX() + deltaX >= menuWidth) {
//右边缘检测
scrollTo(menuWidth, 0);
currentState = STATE_OPEN;
break;
}
} else if (deltaX < 0) {
//向右滑动
currentState = STATE_MOVING_RIGHT;
if (deltaX + getScrollX() <= 0) {
//左边缘检测
scrollTo(0, 0);
currentState = STATE_CLOSED;
break;
}
}
Log.i("swipe","deltaXdeltaX:"+deltaX);
scrollBy(deltaX, 0);
mLastX = (int) event.getX();
break;
case MotionEvent.ACTION_UP:
rightMenuView.setFocusable(true);
rightMenuView.setFocusableInTouchMode(true);
rightMenuView.setClickable(true);
Log.i("swipe","swipe ACTION_UP:");
case MotionEvent.ACTION_CANCEL:
Log.i("swipe","swipe action_cancel:");
if (currentState == STATE_MOVING_LEFT) {
//左滑打开
mScroller.startScroll(getScrollX(), 0, menuWidth - getScrollX(), 0);
invalidate();
} else if (currentState == STATE_MOVING_RIGHT || currentState == STATE_OPEN) {
//右滑关闭0
smoothToCloseMenu();
}
//如果小于滑动距离并且菜单是关闭状态 此时Item可以有点击事件
int deltx = (int) (mDownX - event.getX());
boolean isdeule=!(Math.abs(deltx) < mScaledTouchSlop && isMenuClosed()) || super.onTouchEvent(event);
return isdeule;
}
return super.onTouchEvent(event);
}

通过跟踪log发现:

父类RecycelVCiew首先捕获拦截action down事件,action down有break,而且没有设置return,那么直接走方法最后的 return isIntercepted,也就是return false,那么就是不处理,交给子View处理,但是这个case里其实已经做了一些工作,就是获取recycelview点击的子View的对象,和点击的position;交给子View处理,子View获取到坐标后,返回给父View处理,父View捕捉到ACITON_MOVE事件,同时没有break,直接走到up里面,判断该给子View,就return false,子View获取到后获取move事件,滑动view到指定的距离,同时return给父View,这样父View继续获取下一段距离的移动,然后通过UP里面进行判断,return false给子View进行移动一定距离,父View中的up里面的移动距离不断增大,最后抬起手指的时候,将事件还是传给子View,这时子View收到Up事件,进行scroller的自动滑动动画补齐实现,同时没有break,自动走到ACTION_CANCEL,代码是在ACTION_CANCEL中处理上面的逻辑;这样有点类似于踢皮球一样,特别是move的时候,父亲扔给孩子,孩子处理了还给父亲,父亲再扔给孩子,孩子处理了再扔给父亲,知道抬起手指后,将up事件传递给子View,由子View来处理;如果满足是上下滑动的条件,那么up里面就不会返回给孩子,直接父亲来处理,并return true;特别注意,action move事件父View里面如果有break,那么就是返回false的,交给子View处理了,ACTION_MOVE的子View和父亲View之间互相传皮球,最后父亲传给孩子,孩子传给父亲,父亲又传给孩子,孩子不处理走了action_cancel。MOVE事件一旦交给子View处理,那么父VIEW就监听不到UP事件,所以为了防止监听不到UP事件,在每一次move移动的时候就开始走UP事件;

03-25 17:10:52.693 13297-13297/t I/swipe: recycleview ACTION_DOWN
03-25 17:10:52.694 13297-13297/ I/swipe: recycle find childview false
recycle find childview touchpos:1lasttouchpos1
recycle childview holderfalse
recycle childview holder!=null1
swipe ACTION_DOWN
03-25 17:10:52.708 13297-13297/I/swipe: recycleview move
03-25 17:10:52.709 13297-13297I/swipe: DXup:+dxup0dyup:0dxup>dyup?false01
DX:+dx0dy:0dx>dy?false01
swipe ACTION_MOVE0false
deltaXdeltaX:0
03-25 17:10:52.758 13297-13297/ I/swipe: recycleview move
DXup:+dxup18dyup:0dxup>dyup?true01
DX:+dx18dy:0dx>dy?true01
swipe ACTION_MOVE0false
swipe ACTION_MOVE state2
deltaXdeltaX:18
03-25 17:10:52.769 13297-13297 I/swipe: recycleview move
DXup:+dxup75dyup:2dxup>dyup?true21
swipe view dule move
swipe ACTION_MOVE2false
swipe ACTION_MOVE state2
deltaXdeltaX:56
03-25 17:10:52.793 13297-13297 I/swipe: recycleview move
DXup:+dxup168dyup:0dxup>dyup?true21
swipe view dule move
swipe ACTION_MOVE2false
swipe ACTION_MOVE state2
deltaXdeltaX:92
03-25 17:10:52.809 13297-13297I/swipe: recycleview move
DXup:+dxup245dyup:-14dxup>dyup?true21
swipe view dule move
swipe ACTION_MOVE2false
03-25 17:10:52.810 13297-13297 I/swipe: swipe ACTION_MOVE state2
deltaXdeltaX:76
03-25 17:10:52.828 13297-13297 I/swipe: recycleview move
DXup:+dxup325dyup:-28dxup>dyup?true21
swipe view dule move
swipe ACTION_MOVE2false
swipe ACTION_MOVE state2
deltaXdeltaX:79
03-25 17:10:52.845 13297-13297 I/swipe: recycleview move
DXup:+dxup377dyup:-40dxup>dyup?true21
swipe view dule move
swipe ACTION_MOVE2false
swipe ACTION_MOVE state2
03-25 17:10:52.846 13297-13297t I/swipe: DXup:+dxup371dyup:-39dxup>dyup?true11
swipe view dule move
swipe ACTION_UP:
swipe action_cancel:
03-25 17:10:52.848 13297-13297 I/swipe: swipe 动画中 state open
03-25 17:10:53.360 13297-13297t I/swipe: swipe 动画中 state close
03-25 17:10:53.427 13297-13297t I/swipe: swipe 动画中 state close
03-25 17:10:53.443 13297-13297I/swipe: swipe 动画中 state close
03-25 17:10:53.460 13297-13297/I/swipe: swipe 动画中 state close
03-25 17:11:25.460 13297-13297 I/swipe: swipe 动画中 state close
03-25 17:11:25.461 13297-13297 I/swipe: swipe 动画中 state close
03-25 17:11:25.461 13297-13297I/swipe: swipe 动画中 state close
swipe 动画中 state close

综上所述,父子View之间的事件传递全靠

 

 

将父亲View的move中加上break后,

打印log:

03-25 17:52:18.789 22047-22047/ I/swipe: recycleview ACTION_DOWN
recycle find childview false
03-25 17:52:18.790 22047-22047/t I/swipe: recycle find childview touchpos:0lasttouchpos2
recycle childview holderfalse
recycle childview holder!=null0
swipe ACTION_DOWN
03-25 17:52:18.803 22047-22047/t I/swipe: recycleview move
swipe ACTION_MOVE0false
deltaXdeltaX:0
03-25 17:52:18.901 22047-22047/t I/swipe: recycleview move
swipe ACTION_MOVE0false
deltaXdeltaX:0
03-25 17:52:18.909 22047-22047/ I/swipe: recycleview move
03-25 17:52:18.910 22047-22047/I/swipe: swipe ACTION_MOVE0false
swipe ACTION_MOVE state2
deltaXdeltaX:37
03-25 17:52:18.916 22047-22047/ I/swipe: recycleview move
03-25 17:52:18.917 22047-22047/ I/swipe: swipe ACTION_MOVE2false
swipe ACTION_MOVE state2
deltaXdeltaX:32
03-25 17:52:18.942 22047-22047/ I/swipe: recycleview move
swipe action_cancel:
03-25 17:52:19.437 22047-22047/I/swipe: swipe 动画中 state open
03-25 17:52:19.453 22047-22047/ I/swipe: swipe 动画中 state open
03-25 17:52:19.470 22047-22047/ I/swipe: swipe 动画中 state open
03-25 17:52:19.570 22047-22047/ I/swipe: swipe 动画中 state open
03-25 17:53:06.571 22047-22047/ I/swipe: swipe 动画中 state open
03-25 17:53:06.572 22047-22047/ I/swipe: swipe 动画中 state close
03-25 17:53:06.574 22047-22047/t I/swipe: swipe 动画中 state close
03-25 17:54:05.344 22047-22047/t I/swipe: swipe 动画中 state open
03-25 17:54:05.345 22047-22047/ I/swipe: swipe 动画中 state close
swipe 动画中 state close
03-25 17:54:05.346 22047-22047/ I/swipe: swipe 动画中 state close
03-25 17:54:05.346 22047-22047t I/swipe: swipe 动画中 state close
03-25 17:54:05.347 22047-22047/ I/swipe: swipe 动画中 state close

特别注意,可以看出:action move事件父View里面如果有break,那么就是返回false的,交给子View处理了,ACTION_MOVE的子View和父亲View之间互相传皮球,最后父亲传给孩子,孩子传给父亲,父亲又传给孩子,孩子不处理走了action_cancel。MOVE事件一旦交给子View处理,那么父VIEW就监听不到UP事件,所以为了防止监听不到UP事件,在每一次move移动的时候就开始走UP事件;

 

运行了一下发现这样虽然子view不走up事件了,但是走了action_cancel,我们的逻辑是写在action_cance里面的,所以还是执行了,没有收到影响,单如果非要在action_up里面处理的话,就得去掉break了

 

三、OnTouchEvent:

这个在拦截了事件后处理就可以了

下面讲一下遇到得有些坑

一、子View不可以左划

 

这个问题可以看上面得代码,View得事件分发中有一个非常重要得点就是,如果父View的ACTION_DOWN返回true,那么所以子View的targetview==null都是等于null的,那么即使你将touch事件传递给子View也处理不了,所以要想让子View也能收到事件,必须将父View的ACITON_DOWN的时候返回false,这样所有的子View和ViewGroup和这个父亲View都可以收到aciton_down的事件,后面的谁来处理再在move和up里面进行判断,这种设置fa'l'se后所有子vIEW都可以收到的情况仅限于ACTION_DOWN,其他的事件不可以,必须走踢皮球路线

参考地址:https://www.cnblogs.com/linjzong/p/4191891.html

二\

RecycleView根据位置获取position和hodler以及view,实现自动关闭上次左划出来的菜单功能,这个的问题是每次滑动开了第一个后,再滑动第二个第一个收不回去,所以这就要用到Recycelview的功能:

1、根据点击的view获取所在的子View的position

                //点击的子View所在的位置
final int touchPos = getChildAdapterPosition(view);
2.根据position获取Viewhodler
//根据点击位置获得相应的子View
ViewHolder holder = findViewHolderForAdapterPosition(touchPos);
Log.i("swipe","recycle childview holder"+(holder==null));
3.根据holder获取子View布局
if (holder != null) {
View childView = holder.itemView;
if (childView != null && childView instanceof SwipeMenuLayout) {
mLastMenuLayout = (SwipeMenuLayout) childView;
mLastTouchPosition = touchPos;
Log.i("swipe","recycle childview holder!=null"+mLastTouchPosition);
}
}

三、SwipeMenuLayout的里面的TextView点击事件经常无法获取到焦点

 

首先要清楚click事件跟Touch事件的关系,click事件是在Touch事件的MOVE_UP事件里面启动的调用的clicklistener事件,那么要想能响应子view的点击事件,那么必须要走子View的ACITON_UP事件,这样才可以,所以要从父类View的action up 入手,判断点击的事件的swipelayouot不为空,且点击的dx和dx都为0,那么可以判定是点击的子View的菜单按钮了,return false就可以了

 

参考地址:https://blog.csdn.net/xw13782513621/article/details/76648557

 

四、Recyceview刷新数据时SwipeMenuLayout出现菜单的滑动抖动

 

这个坑我估计很多人遇到过,我刚开始以为是position错位和SWIPE_open状态不对导致的,去掉更新adapter的数据的代码发现就没有问题,那就说明即使刷新recycyelview导致的

我就想有没有部分刷新的方法

搜了下果真有

 

1.RecycleView删除 指定item

 

在adapter里面加入这几句代码,这样就会只刷新当前这条数据,后面的通过动画补上来,也没有抖动了,我觉得这是recycyelview的一个bug,不过他们找到了解决方法就是单条更新
public void removePositionData(int position) {
if (contactBeanList.size() > 0) {
Log.i("swipe","contactBeanList.SIZE()"+contactBeanList.size()+"position:"+position);
contactBeanList.remove(position);
Log.i("swipe","Remove 后 contactBeanList.SIZE()"+contactBeanList.size());
notifyItemRemoved(position);
notifyItemChanged(position,1);
notifyItemRangeChanged(0,contactBeanList.size());
}

}
在recycelview中传入position直接调用就饿可以,其实
contactBeanList.remove(position);
notifyItemRemoved(position);

这两句就可以的,但是运行了发现有坑,打印log发现删除了列表中间的item后,最后一个的positon没有更新,还是以前的,删除的时候出现position越界问题,删除不了最后一个,后来查资料发现
可以加入上面最后依据
 notifyItemRangeChanged(0,contactBeanList.size());它可以更新一下psoition的范围

参考地址:https://blog.csdn.net/wangkai0681080/article/details/50082825

2.recycelview更新某条数据不抖动

参考地址:https://www.jianshu.com/p/57e569087ffc

public void updatePositionData(int position,String ishelmetfriend) {
if (contactBeanList.size() > 0) {
contactBeanList.get(position).setHelmetFriend(ishelmetfriend);
}
notifyItemChanged(position,1);
}
后面的1估计是丢失率,我觉得

这样也不抖动了

3.偶然的机会发现还有插入一条数据

// 添加数据
public void addData(int position) {
// 在list中添加数据,并通知条目加入一条
list.add(position, "我是商品" + position);
//添加动画
notifyItemInserted(position);
}

以后也用用

参考连接:https://blog.csdn.net/qq_34908107/article/details/77847985,这个连接记录了动画刷新recycleview的方法

 

 

有空了我觉得我应该记录下recycleview使用ItemDecorator这个类实现list列表分类分title显示,我觉得不错,主要原理是通过给数据加一个tag,相同tag的数据就只显示第一条数据的tag,遇到不同的tag时再显示其他的tag标题

 

posted @ 2019-03-25 18:57  shiyiling  阅读(1266)  评论(0)    收藏  举报