=== 搭建带有多种监听自定义ScrollView ===
虽然安卓5.1已经release, 但是ScrollView的封装和对外API依旧少的可怜, 虽然它优化得很好了.
所以问题来了: ScrollView滑动方向是什么, 何时停止? 所以本文的目标出现了: 解决这些看似小, 但是用起来却很燃眉的问题!
首先思考: 如何知道ScrollView是否在滚动, 不过这一点请放心, SDK还是提供了这个功能, 不然SDK也太烂了. 呵呵, 找打了, 首先请继承ScrollView这个父类, 毕竟很多东西拿来主义是没问题的.
public class MyScrollView extends ScrollView {
private final String TAG = this.getClass().getSimpleName();
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}
这个我就不多解释了, 自定义过控件的人肯定知道这三个构造的意义, 多一嘴, 其中的第二个是给XML 来 Render的, 所以一定要写上.
来, 写上刚才说的最关键的:
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
}
简单说明一下, API默认的这四个参数的意思很隐晦, 你可以考虑改写成:
@Override
protected void onScrollChanged(int currentX, int currentY, int oldx, int oldy) {
super.onScrollChanged(currentX, currentY, oldx, oldy);
}
既然需要进行监听状态改变, 那么使用回调的设计进行监听再好不过(之所以选择抽象类而不是使用接口, 是因为这样的话可以更好地让用户进行抉择, 具体需要重写哪一个, 简化代码量和减少流程使用的难度), 这里推荐使用内部类, 当然, 新建一个独立新类也是可以的:
public abstract class MyScrollViewListener {
public void onMyScrollChanged(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollStart(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollStop(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollTop(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollBottom(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollUp(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollDown(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
}
简单解释一下, 自上而下的回调监听的意思分别是:
onMyScrollChanged : 当滚动时
onMyScrollStart : 当开始滚动时
onMyScrollStop : 当滚动停止时
onMyScrollTop : 当滚动到到顶部时
onMyScrollBottom : 当滚动到底部时
onMyScrollUp : 当向上滚动时
onMyScrollDown : 当向下滚动时
这些都是比较常用的, 先写好, 我们一个一个来实现. 注意 内部类的话, 回调方法要使用 public的. 不然外部回调的发起者, 是无法重写回调方法的.
哦, 对了, 进一步集成, 方便外部发起者进行调用:
private MyScrollViewListener myScrollViewListener;
public void setMyScrollViewListener(MyScrollViewListener myScrollViewListener) {
this.myScrollViewListener = myScrollViewListener;
}
很简单, 一个set方法 来对应刚才的回调抽象类, 不解释了.
前方高能, 核心功能!
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (myScrollViewListener != null) {
myScrollViewListener.onMyScrollChanged(this, l, t, oldl, oldt);
}
}
好理解吧?? 这个就是我之前说的SDK自己提供的监听. 既然继承了 ScrollView, 直接重写, 先显式调用父类的方法, 然后正好, 回调一下我们的onMyScrollChanged监听, 满足题意, 当然必须有发起者才可以, 所以加了一个非空判断. 继续!
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (myScrollViewListener != null) {
myScrollViewListener.onMyScrollChanged(this, l, t, oldl, oldt);
if (t - oldt > 0) {
myScrollViewListener.onMyScrollDown(this, l, t, oldl, oldt);
Log.i(TAG, "正在向下滚动");
} else {
myScrollViewListener.onMyScrollUp(this, l, t, oldl, oldt);
Log.i(TAG, "正在向上滚动");
}
}
}
这个也好理解吧, t 之前说过, 是 纵向的偏移量, 你可以用Log看看变化规律, 看代码其实也能明白. 然后根据条件进行回调, OK, 完成!
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (myScrollViewListener != null) {
myScrollViewListener.onMyScrollChanged(this, l, t, oldl, oldt);
if (getScrollY() <= 0) {
myScrollViewListener.onMyScrollTop(this, l, t, oldl, oldt);
Log.i(TAG, "到达了顶部");
}
//====================================================
View view = (View) getChildAt(getChildCount() - 1);// 获取 ScrollView 最后一个控件
int diff = (view.getBottom() - (getHeight() + getScrollY()));
if (diff == 0) {
myScrollViewListener.onMyScrollBottom(this, l, t, oldl, oldt);
Log.i(TAG, "到达了底部");
}
}
}
简单说明一下:
getScrollY 可以获取ScrollView顶部位置的像素值, 具体的看API, 不赘述.
还是看图吧, 不多说了, 自己用画图做的, 比较粗糙,但是顾名思义.
三个分别是滚动到了顶端, 滚动在中间, 滚动到了底部.
至此, 简单的功能都完成了, 剩下一个最难的了, 立马攻克之!
思考: 想知道何时停止的话, 可以使用类似于Observer的方式进行监听, 我第一想法是用for, 后来仔细想想, Thread.sleep等方式尽量避免, UI都会卡掉. 所以当滚动开始的时候, 通过postDelayed()自身延迟自己调用自己反复复执行(安卓中比较常见的设计).
来, 看代码:
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (myScrollViewListener != null) {
myScrollViewListener.onMyScrollChanged(this, l, t, oldl, oldt);
if (!scrollerTaskRunning) {
startScrollerTask(this, l, t, oldl, oldt);
}
}
}
private Runnable scrollerTask;
private int initialPosition;
private int newCheck = 50;
private boolean scrollerTaskRunning = false;
private void startScrollerTask(final MyPullableScrollView scrollView, final int x, final int y, final int oldx, final int oldy) {
if (!scrollerTaskRunning) {
myScrollViewListener.onMyScrollStart(this, x, y, oldx, oldy);
Log.i(TAG, "滚动开始");
}
scrollerTaskRunning = true;
if (scrollerTask == null) {
scrollerTask = new Runnable() {
public void run() {
int newPosition = getScrollY();
if (initialPosition - newPosition == 0) {
if (myScrollViewListener != null) {
scrollerTaskRunning = false;
myScrollViewListener.onMyScrollStop(scrollView, x, y, oldx, oldy);
Log.i(TAG, "滚动结束");
}
} else {
startScrollerTask(scrollView, x, y, oldx, oldy);
}
}
};
}
initialPosition = getScrollY();
postDelayed(scrollerTask, newCheck);
}
最后解释一下, scrollerTask 是一个启动线程的任务, initialPosition 是滚动的初始位置, newCheck 表示postDelayed的推迟频率, 用来实现自身调用的循环. scrollerTaskRunning 表示滚动是否开始, 防止反复执行.
这里(还有向上向下滚动)我之前走了弯路, 使用的是网上提供的 重写 view.onTouch() 或者 view.onTouchEvent(), 在 MotionEvent.ACTION_UP 或者 ACTION_MOVE 的时候调用scrollerTask 来实现, 但是这样 参数(什么oldX 之类的) 不是很好传递, 虽然 Event 也可以简单控制, 但是我不想用太多松散的关系类, 所以直接添加 scrollerTaskRunning 这个变量来具体控制 方法内的判断时机.
判断的原理是, 不停地比较当前的currentScrollY和 上一次的 lastScrollY, 如果一样, 则 停止延迟地自身调用自身, 否则,继续延迟地自身调用自身, 再来一次比较, 以此类推. scrollerTaskRunning 变量的控制要注意, 这样便可以有效地防止多次无意义的执行.
网上提供的方案很多是在 构造函数中 初始化 scrollerTask, 但是这样回调拿不到参数, 而且必须用上面说的重写, 还需要额外的成员属性, 反复计算再使用, 麻烦繁琐. 所以我改造了代码, 就可以避免这个问题(怎么感觉语序有一些乱套… 语文渣).
好吧, 贴一下所有的代码, log 都改成了 英文, 既尊重资料提供者, 又可以避免乱码, 添加package就可以直接用了, 自定义控件怎么用, 我就不说了, 网上有很多, 祝你好运.
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ScrollView;
public class MyScrollView extends ScrollView {
private final String TAG = this.getClass().getSimpleName();
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public abstract class MyScrollViewListener {
public void onMyScrollChanged(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollStart(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollStop(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollTop(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollBottom(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollUp(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
public void onMyScrollDown(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
}
}
private MyScrollViewListener myScrollViewListener;
public void setMyScrollViewListener(MyScrollViewListener myScrollViewListener) {
this.myScrollViewListener = myScrollViewListener;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (myScrollViewListener != null) {
myScrollViewListener.onMyScrollChanged(this, l, t, oldl, oldt);
//====================================================
if (!scrollerTaskRunning) {
startScrollerTask(this, l, t, oldl, oldt);
}
// ====================================================
if (t - oldt > 0) {
myScrollViewListener.onMyScrollDown(this, l, t, oldl, oldt);
Log.i(TAG, "is scrolling down");
} else {
myScrollViewListener.onMyScrollUp(this, l, t, oldl, oldt);
Log.i(TAG, "is scrolling up");
}
//====================================================
if (getScrollY() <= 0) {
myScrollViewListener.onMyScrollTop(this, l, t, oldl, oldt);
Log.i(TAG, "the top has beenreached");
}
// ====================================================
View view = (View) getChildAt(getChildCount() - 1);// We take the last son in the scrollview
int diff = (view.getBottom() - (getHeight() + getScrollY()));
if (diff == 0) {
myScrollViewListener.onMyScrollBottom(this, l, t, oldl, oldt);
Log.i(TAG, "the bottom has beenreached");
}
}
}
private Runnable scrollerTask;
private int initialPosition;
private int newCheck = 50;
private boolean scrollerTaskRunning = false;
private void startScrollerTask(final MyScrollView scrollView, final int x, final int y, final int oldx, final int oldy) {
if (!scrollerTaskRunning) {
myScrollViewListener.onMyScrollStart(this, x, y, oldx, oldy);
Log.i(TAG, "scroll start");
}
scrollerTaskRunning = true;
if (scrollerTask == null) {
scrollerTask = new Runnable() {
public void run() {
int newPosition = getScrollY();
if (initialPosition - newPosition == 0) {
if (myScrollViewListener != null) {
scrollerTaskRunning = false;
myScrollViewListener.onMyScrollStop(scrollView, x, y, oldx, oldy);
Log.i(TAG, "scroll stop");
return;
}
} else {
startScrollerTask(scrollView, x, y, oldx, oldy);
}
}
};
}
initialPosition = getScrollY();
postDelayed(scrollerTask, newCheck);
}
}
======================================================
感谢微软必应, stackoverflow 大神和 Google大神提供的思路. 没有他们, 我, 寸步难行.
======================================================
停止滚动的代码, 参考了这个问题:
http://stackoverflow.com/questions/8181828/android-detect-when-scrollview-stops-scrolling
Presented byimknown
2015-03-19
原文地址:http://blog.csdn.net/imknown/article/details/44457025