您的位置:首页 > 移动开发 > Android开发

Android 中实现 ScrollView 的滚动事件监听

2014-11-06 17:15 661 查看
转自:http://www.rincliu.com/blog/2013/11/16/scrollview/

最近在自己实现一个类似 Pinterest 瀑布流展示效果的组件,GitHub 上其实有类似项目,比如 PinterestLikeAdapterViewPinterestListView
但都或多或少有些不足(详见这篇文章的分析),然后自己想基于
ScrollView
去嵌套多列
LinearLayout
实现。

坑爹的是系统自带的
ScrollView
功能相当粗糙:连个最基本的
setOnScrollListener()
的方法都没有,仅有个
onScrollChanged()
方法,而且还是
protected
的。不得不吐槽下,Google
真够懒的,这货纯粹就是个毛胚啊,完全得靠开发者去继承后自己打磨。

首先当然是继承
ScrollView
,然后把最原始的
onScrollChanged()
方法暴露给外部:

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
32
33
34
35
36

public class RLScrollView extends ScrollView{

public RLScrollView(Context context) {
super(context);
}

public RLScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public RLScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

public interface OnScrollChangedListener{
public void onScrollChanged(int x, int y, int oldxX, int oldY);
}

private OnScrollChangedListener onScrollChangedListener;

/**
*
* @param onScrollChangedListener
*/
public void setOnScrollListener(OnScrollChangedListener onScrollChangedListener){
this.onScrollChangedListener=onScrollChangedListener;
}

@Override
protected void onScrollChanged(int x, int y, int oldX, int oldY){
super.onScrollChanged(x, y, oldX, oldY);
if(onScrollChangedListener!=null){
onScrollChangedListener.onScrollChanged(x, y, oldX, oldY);
}
}
}

然后需要两个判断位置的方法(top or bottom),比如要实现一个滚动到底部自动加载更多数据的功能,这个就是必须的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

/**
*
* @return
*/
public boolean isAtTop(){
return getScrollY()<=0;
}

/**
*
* @return
*/
public boolean isAtBottom(){
return getScrollY()==getChildAt(getChildCount()-1).getBottom()+getPaddingBottom()-getHeight();
}

顶部好说了,就是 Y 方向滚动为 0 。底部判断:Y 方向滚动等于最后一个 child 元素底部相对 parent 的位置减去 parent 高度,然后还要考虑到 parent 自身可能有
paddingBottom


然后像瀑布流这种几乎没有底的东西,得需要个判断 child 是否处于屏幕可见范围内的方法,不然将所有 item 的
View
Bitmap
都放在内存肯定是相当占用资源的,既影响滑动流畅性,又很容易
OOM 。

1
2
3
4
5
6
7
8
9
10
11
12
13

/**
*
* @param child
* @return
*/
public boolean isChildVisible(View child){
if(child==null){
return false;
}
Rect scrollBounds = new Rect();
getHitRect(scrollBounds);
return child.getLocalVisibleRect(scrollBounds);
}

这里用到了
View
getLocalVisibleRect()
方法,官方文档没有给出任何相关说明。

不过看方法名应该不难了解其功能:获取可见的矩形,参数也是个矩形,这里传过去的是 parent 边界所在的矩形,返回值
boolean
类型。也很好理解:不可见的 child 当然得不到可见的矩形了。

顺便提一下,StackOverflow 上有对这个神秘方法的讨论,提到了一个
getLocalVisibleRect()
方法,这里就不深挖了。

回到这个用于实现瀑布流的加强版 ScrollView 上来,为了实现滚动到底部自动加载还有顶部下拉刷新这些功能,肯定还需要监听滚动是否停止。

首先想到的是
onScrollChanged()
方法,在这个里面去判断
Y
oldY
是否相等,发现效果不是很理想。

然后就想到自己去后台实时检测这个
scrollY
的变化,详细思路如下:

首先设置
onTouchListener
,去监听手指屏幕操作;

一旦检测到
ACTION_UP
事件(即手指离开屏幕)就记下当前滚动位置,然后延时
post 一个
Runnable
到主线程;

在这个
Runnable
中判断当前滚动位置是否和延时之前的位置相等(即 Y 方向位置不再变化),如果不相等则延时后继续
post 同样的
Runnable
去检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1516
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

setOnTouchListener(new OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
currentScroll = sv.getScrollY();
postDelayed(scrollCheckTask, 300);
}
return false;
}
});

Runnable scrollCheckTask = new Runnable() {
@Override
public void run() {
int newScroll = sv.getScrollY();
if(currentScroll==newScroll){
if(onWaterfallScrollListener!=null){
//TODO: onScrollStopped;
if(sv.isAtTop()){
//TODO: onScrollStoppedAtTop;
}
if(isScrollViewAtBottom(sv)){
//TODO: onScrollStoppedAtBottom;
}
}
}else{
currentScroll=sv.getScrollY();
postDelayed(scrollCheckTask, 300);
}
}
};

可能有人会担心这样频繁的 post 会不会是开启了大量的线程很影响性能?

答案是否定的,
Runnable
不同于
Thread
,它只是一个可执行对象,具体在哪个线程执行看情况。

比如这里实际上是发送了一个延时消息到UI线程的消息队列,由其
Looper
按照 FIFO 规则一个个抽取后给
Handler
去处理,而
UI 线程只有一个,所以并不是开启了很多线程。

亲测效果不错,不仅检测到的滚动 stop 状态很准确,而且也并不会影响滚动流畅性。

项目完整代码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: