偶尔看到知乎首页的侧滑删除,感觉还不错。之前用RecyclerView的ItemTouchHelper类来实现了Item的拖动和删除功能,今天带来的则是纯手工打造的一个侧滑删除。老规矩,先看看效果图:
当滑动的距离小于红块的一半,松开手指以后,会自动收缩当前item;当滑动的距离超过一半,松开手指以后,会自动将当前item删除。一起看看怎么实现的吧:
1.准备工作:
(1)数据准备:一个存放数字的List数组来模拟RecyclerView的数据
(2)子Item的布局:整体线性布局水平排列,左侧是显示的部分,右侧是不显示的部分,也就是删除的部分。删除的部分是一个相对布局,然后通过滑动的距离来控制字体与图片的显示与隐藏。
(3)RecyclerView三要素:RecyclerAdapter,RecyclerViewHolder,LayoutManager依次设置即可。
2.View的滑动实现:
(1)滑动方法:
这里我是使用View本身提供的scrollTo/scrollBy方法来实现滑动,scrollBy实际上也是调用了scrollTo方法,
scrollTo实现的是基于所传递参数的绝对滑动,而scrollBy实现的是基于当前位置的相对滑动。
举个例子:
scrollTo(50,50)会将View位置移动到指定位置,多次调用无效
scrollBy(50,50)会将View位置移动到指定位置,每调用一次会在现有位置基础上进行移动
结合这个例子分析一下,
手指滑动的距离就是整体View移动的距离,那我们可以直接使用scrollBy(x,y)方法来进行处理,将手指滑动的距离作为第一个参数传递进去,而不用考虑当前View滑动的位置。
(2)滑动方向
在Android屏幕直角坐标系中,原点在屏幕左上角,向右X为正,向下Y为正。
scrollBy()的参数的正负影响滑动的方向
,这里我们只考虑水平方向上的滑动,所以将第二个参数设置为0。
按我们正常的理解,应该是参数为负的时候,向坐标轴负方向滑动;当参数为正的时候,向坐标轴正方向滑动。
scrollBy()在参数为负的时候,向坐标轴正方向滑动;当参数为正的时候,向坐标轴负方向滑动。
这是因为在scrollBy()源码执行过程的最后,会调用这个方法 :
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
其中l,t,r,b为原来坐标点,scrollX,scrollY为目标坐标点,只有当目标坐标点值是负数时,负负得正,移动到的位置才为正数,这样才会重新绘制,整体的View就会向坐标轴正方向滑动。
综上,我们想让子Item从右往左沿X轴的负方向滑动,scrollBy(X,0)中的X一定是大于0的
(3)滑动实现
现在滑动的方法与方向都已经确定了,接下来的重点就是计算滑动的距离,也就是scrollBy(X,0)中的X的大小了。
public
boolean onTouchEvent(MotionEvent
event
) {
int
x = (
int
)
event
.getX();
int
y = (
int
)
event
.getY();
switch
(
event
.getAction()) {
case
MotionEvent.ACTION_DOWN: { }
break
;
case
MotionEvent.ACTION_MOVE: {
int
scrollX = itemLayout.getScrollX();
int
newScrollX = mStartX - x;
if
(newScrollX <
0
&& scrollX <=
0
) { newScrollX =
0
; }
else
if
(newScrollX >
0
&& scrollX >= maxLength) { newScrollX =
0
; }
if
(scrollX > maxLength /
2
) { textView.setVisibility(GONE); imageView.setVisibility(VISIBLE);
if
(isFirst) { ObjectAnimator animatorX = ObjectAnimator.ofFloat(imageView,
"scaleX"
,
1
f,
1.2
f,
1
f); ObjectAnimator animatorY = ObjectAnimator.ofFloat(imageView,
"scaleY"
,
1
f,
1.2
f,
1
f); AnimatorSet animSet =
new
AnimatorSet(); animSet.play(animatorX).with(animatorY); animSet.setDuration(
800
); animSet.start(); isFirst =
false
; } }
else
{ textView.setVisibility(VISIBLE); imageView.setVisibility(GONE); } itemLayout.scrollBy(newScrollX,
0
); }
break
;
case
MotionEvent.ACTION_UP: { }
break
; mStartX = x;
return
super.onTouchEvent(
event
); }
-
- 47
其中itemLayout为一个水平的LinearLayout,textView为LinearLayout中的”删除”,imageView为LinearLayout中的眼睛图片。
移动计算值 = 最开始点坐标 - 最后移动到的坐标
- 滑动开始的时候,不允许item向右滑动,此时scrollBy(x,0)中的x小于0;滑动的过程中,左右滑动都可以,但getScrollX()小于等于0的时候就不允许继续滑动。此时将x设置为0,代表不再滑动
- 滑动距离大于一半的时候,将文字设置为GONE,图片设置为VISIBLE,否则刚好相反。细心的小伙伴会发现,眼睛图片的显示有一个从小到大再到小的过程,这里用的是属性动画ObjectAnimator加上组合动画AnimatorSet实现的,并且进行了一下判断,让动画在滑动过程中只出现一次
- 滑动的距离超过红块的距离的时候,不允许item向左滑动,此时scrollBy(x,0)中的x是大于0。此时将x设置为0,代表不再滑动
3.RecyclerView的滑动实现
前面已经实现了将一个LinearLayout左右进行滑动,现在关键就是将这个LinearLayout的滑动与我们RecyclerView的滑动相结合。
解决办法就是将这个水平排列的LinearLayout作为子item布局的一部分,然后再获取每一个item的LinearLayout就可以进行滑动了。这里肯定需要一个参数position,只有获取到item的position才能得到item的LinearLayout,才能进行删除操作。
(1)通过触碰的坐标计算当前的position
这里我们肯定要自定义一个MyRecyclerView继承自RecyclerView,然后重写onTouchEvent()方法,在MotionEvent.ACTION_DOWN的时候就要拿到你触碰的item的position。
public
boolean onTouchEvent(MotionEvent
event
) {
int
x = (
int
)
event
.getX();
int
y = (
int
)
event
.getY();
switch
(
event
.getAction()) {
case
MotionEvent.ACTION_DOWN: {
//通过点击的坐标计算当前的position
int
mFirstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition(); Rect frame = mTouchFrame;
if
(frame ==
null
) { mTouchFrame =
new
Rect(); frame = mTouchFrame; } final
int
count = getChildCount();
for
(
int
i = count -
1
; i >=
0
; i--) { final View child = getChildAt(i);
if
(child.getVisibility() == View.VISIBLE) { child.getHitRect(frame);
if
(frame.contains(x, y)) { pos = mFirstPosition + i; } } } }
break
;
-
- 26
在Listview当中,有一个pointToPosition(x, y)方法可以根据坐标获取到当前的position,在RecyclerView中没有这个方法,需要我们自己动手写一个。
这里有一点特别需要注意的是:
这里遍历的是当前可见范围内的子项。使用getChildCount()与getChildAt()进行取值,只能是当前可见区域的子项!取值范围在0到getLastVisiblePosition()减去getFirstVisiblePosition()之间(可取等于)。
(2)通过position得到item的viewHolder
//通过position得到item的viewHolder View view = getChildAt(pos - mFirstPosition)
;
MyViewHolder viewHolder = (MyViewHolder) getChildViewHolder(view)
;
itemLayout = viewHolder
.layout
;
textView = (TextView) itemLayout
.findViewById
(R
.id.item
_delete_txt)
;
imageView = (ImageView) itemLayout
.findViewById
(R
.id.item
_delete_img)
;
- 1
- 2
- 3
- 4
- 5
- 6
viewHolder是存放视图与数据的地方,只要拿到当前item的viewHolder,就可以获取到我们的itemLayout,也就是需要滑动的LinearLayout。RecyclerView提供了一个getChildViewHolder()的方法来获取当前item的viewHolder,传进去的参数就是通过getChildAt(index)获取到的view。
4.RecyclerView的删除实现
我们在上一步已经拿到了item的position与itemLayout,在MotionEvent.ACTION_MOVE的时候使用itemLayout就可以进行滑动,在MotionEvent.ACTION_UP的时候使用position就可以进行删除。
case
MotionEvent.ACTION_UP: {
int
scrollX = itemLayout.getScrollX();
if
(scrollX > maxLength /
2
) { ((RecyclerAdapter) getAdapter()).removeRecycle(pos); } }
break
;
- 7
当滑动的距离大于一半的时候,执行删除操作。 将删除方法写在RecyclerAdapter中:
public
void
removeRecycle(
int
position) { lists.remove(position); notifyDataSetChanged();
if
(lists.size() ==
0
) { Toast.makeText(context,
"已经没数据啦"
, Toast.LENGTH_SHORT).show(); } }
-
- 7
5.RecyclerView的滑动优化
之前说到当滑动的距离小于红块的一半,松开手指以后,会自动收缩当前item,但是这个滑动比较生硬,用户体验很差。我们需要实现渐进式滑动,也就是View的弹性滑动。这里我们使用的是Scroller。
初始化Scroller:
mScroller =
new
Scroller(
context
,
new
LinearInterpolator(
context
,
null
));
- 1
- 2
第二个参数是一个匀速插值器
Scroller的使用方法:
case
MotionEvent.ACTION_UP: {
int
scrollX = itemLayout.getScrollX();
if
(scrollX > maxLength /
2
) { ((RecyclerAdapter) getAdapter()).removeRecycle(pos); }
else
{ mScroller.startScroll(scrollX,
0
, -scrollX,
0
); invalidate(); } isFirst =
true
; }
break
;
startScroll()四个参数依次为:开始移动时的X坐标;开始移动时的Y坐标;沿X轴移动距离,为负时,子控件向右移动;沿Y轴移动距离。如果后面没有duration这个参数,系统会使用默认的时长:250毫秒
然后调用invalidate()是使view进行重绘,在view的onDraw()方法中又会去调用computeScroll()方法,view才能实现弹性滑动
public
void
computeScroll() {
if
(mScroller.computeScrollOffset()) { itemLayout.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } }
- 6
首先向Scroller获取当前的滑动起点,通过scrollTo方法实现滑动,然后再调用invalidate()来进行重绘,又会调用computeScroll()方法,然后再获取当前的起点,使用scrollTo方法滑动到新的位置。如此往复,直到整个滑动结束。
其实Scroller的设计思想就是小幅度滑动组成整个的弹性滑动。
至此,一个漂亮的侧滑删除就已经实现了,零碎的东西不少,记录下来一起学习~~
补充:
评论里有小伙伴说加上点击事件后没有效果,会产生事件冲突。谢谢这位小伙伴的提醒,之前没有考虑这方面的问题。然后周末在家完善了一下,看看怎么解决的吧。
case
MotionEvent.ACTION_UP: { xUp = x; yUp = y;
int
dx = xUp - xDown;
int
dy = yUp - yDown;
if
(Math.
abs
(dy) < mTouchSlop && Math.
abs
(dx) < mTouchSlop) { listener.getPosition(pos); }
else
{
int
scrollX = itemLayout.getScrollX();
if
(scrollX > maxLength /
2
) { ((RecyclerAdapter) getAdapter()).removeRecycle(pos); }
else
{ mScroller.startScroll(scrollX,
0
, -scrollX,
0
); invalidate(); } isFirst =
true
; } }
break
;
RecyclerView的点击事件无非就是接口回调获取position的过程,我们在MotionEvent.ACTION_DOWN的时候已经拿到了position。那么只要在点击的时候将这个position传递给Activity呢。现在只要判断什么动作是点击就可以了!!!其实只要对比一下MotionEvent.ACTION_DOWN与MotionEvent.ACTION_UP的X,Y坐标差,小于默认的滑动最小距离的时候,就认为是点击动作,将得到的position传递即可。最后让Activity实现这个接口,获取参数,进行事件的处理就欧了~