SlidingCheckLayout滑动选择布局实现

背景

项目需求,要求在选择本地图片时,可对图片进行滑动选择。类似小米图库或者图库,现在很多手机系统内置图库都自带了这个功能。最后经过调研,使用自定义View解决。SlidingCheckLayout滑动选择布局,用于嵌套RecyclerView实现滑动选择功能。

效果如图所示:

最后将代码整理了一下,并上传到Jitpack。

源码

https://github.com/huangyu/SlidingCheckLayout

引用方式

在根目录:

1
2
3
4
5
6
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

添加依赖:

1
2
3
4
dependencies {
...
implementation 'com.github.huangyu0:SlidingCheckLayout:x.x.x'
}

使用方法

在layout的xml中,在RecyclerView外层嵌套SlidingCheckLayout作为唯一子节点。

在App中实现SlidingCheckLayout.OnSlidingCheckListener接口即可,具体可参考Demo工程。

原理

通过onInterceptTouchEvent方法拦截滑动请求,找到起始点坐标。

1
2
3
4
5
6
7
8
// 计算起始点坐标
View child = targetRlv.findChildViewUnder(ev.getX(), ev.getY());
if (child != null) {
int position = targetRlv.getChildAdapterPosition(child);
if (position != RecyclerView.*NO_POSITION*&& startPos != position) {
startPos = position;
}
}

RecycleView的findChildViewUnder()方法,可以十分方便返回指定位置的childView。

判断是否处于滑动状态,xTouchSlop和yTouchSlop为横向和纵向两个阈值。

1
2
3
4
5
float xDiff = Math.*abs*(ev.getX() - startX);
float yDiff = Math.*abs*(ev.getY() - startY);
if (yDiff < yTouchSlop && xDiff > xTouchSlop) {
isSliding = true;
}

为了和RecycleView的Item点击事件不冲突,通过自定义一个判断标记isSliding,返回是否拦截滑动请求,即是否进行了滑动选择。

通过onTouchEvent处理具体的拦截逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.*ACTION_DOWN*:
break;
case MotionEvent.*ACTION_MOVE*:
if (!inTopSpot && !inBottomSpot) {
// 处理滑动事件
updateSelectedRange(targetRlv, ev);
}
// 在顶部或者底部触发自动滑动
processAutoScroll(ev);
break;
case MotionEvent.*ACTION_UP*:
resetParams();
stopAutoScroll();
performClick();
return false;

}
return isSliding;
}

既然是滑动,处理事件肯定是在ACTION_MOVE里。这里的inTopSpot和inBottomSpot两个标记用于计算是否滑动到顶部或者底部,用于滑动到顶部或者底部时,使用Scroller进行自动滚动。具体实现在processAutoScroll方法。

updateSelectedRange则处理滑动选择。

1
2
3
4
5
6
7
8
9
10
private void updateSelectedRange(RecyclerView rv, float x, float y) {
View child = rv.findChildViewUnder(x, y);
if (child != null) {
int position = rv.getChildAdapterPosition(child);
if (position != RecyclerView.*NO_POSITION*&& endPos != position) {
endPos = position;
notifySelectRangeChange();
}
}
}

这里同样用findChildViewUnder方法记录结束标志,现在获取到了起始点和结束点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void notifySelectRangeChange() {
if (onSlidingCheckListener == null) {
return;
}

if (startPos == RecyclerView.*NO_POSITION*|| endPos == RecyclerView.*NO_POSITION*) {
return;
}

int newStart, newEnd;
newStart = Math.*min*(startPos, endPos);
newEnd = Math.*max*(startPos, endPos);

if (lastStartPos == RecyclerView.*NO_POSITION*|| lastEndPos == RecyclerView.*NO_POSITION*) {
onSlidingCheckListener.onSlidingCheckPos(newStart, newEnd);
} else {
if (newStart > lastStartPos) {
onSlidingCheckListener.onSlidingCheckPos(lastStartPos, newStart - 1);
} else if (newStart < lastStartPos) {
onSlidingCheckListener.onSlidingCheckPos(newStart, lastStartPos - 1);
}
if (newEnd > lastEndPos) {
onSlidingCheckListener.onSlidingCheckPos(lastEndPos + 1, newEnd);
} else if (newEnd < lastEndPos) {
onSlidingCheckListener.onSlidingCheckPos(newEnd + 1, lastEndPos);
}
}

lastStartPos = newStart;
lastEndPos = newEnd;
}

notifySelectRangeChange为核心处理逻辑,通过当前滑动的起始点和终点计算中间滑动的范围并回调给调用方。

这里自定义了一个回调接口类:

1
2
3
public interface OnSlidingCheckListener {
void onSlidingCheckPos(int startPos, int endPos);
}

最后外部使用回调对起始点至终点部分进行选择处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scl.setOnSlidingCheckListener(new SlidingCheckLayout.OnSlidingCheckListener() {
@Override
public void onSlidingCheckPos(int startPos, int endPos) {
RecyclerView recyclerView = (RecyclerView) scl.getChildAt(0);
for (int I = startPos; I <= endPos; I++) {
View current = recyclerView.getLayoutManager().findViewByPosition(i);
if (current != null) {
View currentLayout = current.findViewById(R.id.*rl_root*);
if (currentLayout != null) {
currentLayout.performClick();
}
}
}
}
});

Done.

0%