上一节我们实现了一个可以下拉,也能够上拉的基本控件,同时还能够在抬起手指时弹回原位置.
这一节我们需要实现核心逻辑:如何通过子控件状态判定可以下拉了,或者可以上拖了.
由于此逻辑的需要,我们需要限定,本自定义控件只能包含一个子结点.这一点跟ScrollView特点是一致的,因为如果包含多个控件的话,就无法实现上述逻辑目标:
限定: 本布局中只能有一个根子结点,子结点可以是布局,包含众多控件.
于是我们应当在控件加载初始化时进行判定,如果错误的放置了多个结点,应该抛出异常.这个判定工作应该放在哪里呢?经测试放在构造函数中是不合适的,因为构造的时候,还没有解析子控件,因此判定是无效的.
所以应该 放在 attachToWindow中去,即当控件将要显示到窗口上的时候.
同时我们应该在初始的时候就记录下来这个唯一的子控件.以方便后续的判定.
private View mChildView =null;
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// 判定子控件个数,只能有一个.如果有多个,则直接抛出运行时异常.
if (getChildCount() != 1) {
throw new RuntimeException(" SuperRefreshLayout can contain one and only one direct child!");
}
mChildView = getChildAt(0);
}
接下来的问题是: 过度拖动多少距离才触发刷新事件呢?如果仅仅移动很少距离就刷新的话似乎是不合理的,因此,我们需要规定一个触发刷新的最小距离
经验值,你可以固定为200或者300. 但在不同尺寸的屏幕上不一定是最佳值.所以最好的方案是根据屏幕高度来计算一个值最好.
接下来看代码.
private int minDistance = 200;
// 实现初始化内容.
private void onInit() {
mGesture = new GestureDetector(getContext(), mGestureListener);
// mScroller =new Scroller(getContext());//要模拟出弹簧的那种加速递减的效果,需要下面这个构造.
mScroller = new Scroller(getContext(), new DecelerateInterpolator(3));
checkMinDistance();// 设定屏幕高度.
}
private void checkMinDistance() {
// 获取屏幕高度.
DisplayMetrics metrics = getResources().getDisplayMetrics();
if (metrics != null && metrics.heightPixels > 0) {
// 将最小滑动距离设定为屏幕高度的8分之一.
minDistance = metrics.heightPixels / 8;
}
}
最后一个问题: 如何判定该启动刷新动作了.
因为要判定的控件多种多样,所以需要动态的扩展判定流程,
首先我们声明一个接口用来处理这个判定过程:
当需要增加一种控件的判定过程的话,直接实现此接口并添加到判定列表中即可.
// 用于根据child的当前状态判定Refresh控件能否继续下拉或上拉.
public interface CanRefreshListener {
// 如果此child的当前状态不允许下拉/上拉,则返回false, 允许的话返回true. 如果不作判定的话,返回null即可.
public Boolean canRefresh(View child, boolean isUpRefresh);
}
// 保存不重复内容.所以使用HashSet集合.
public static Collection<CanRefreshListener> mCanRefreshListeners = new HashSet<SuperRefreshLayout.CanRefreshListener>();
public static void registerCanRefreshListener(CanRefreshListener l) {
if (l == null)
return;
mCanRefreshListeners.add(l);
}
接下来就来实现一种控件的判定流程.
我们最常用的ListView是必须要支持的,所以首先来讨论ListView的处理.
基本原理:
- 当ListView中无数据时,应该允许下拉和上拉,实现数据加载
- 当ListView中有数据时,如果此时数据正显示第一条,此时允许控件执行下拉动作
- 如果此时最后一条显示的数据是最后一条的时候,则允许控件执行上拉动作.
- 其他时候不允许控件执行任何动作,应该由ListView执行自己的滚动事件.
- 当子控件不是ListView时,不适用此判定流程,因此不作处理.
代码实现:
private final CanRefreshListener listViewRefreshListener = new CanRefreshListener() {
@Override
public Boolean canRefresh(View child, boolean isUpRefresh) {
if (child instanceof AbsListView) {// 只处理child是AbsListView的情况.
// absListView包括listView,GridView等控件.
AbsListView listView = (AbsListView) child;
if (isUpRefresh) {
if (listView.getLastVisiblePosition() == listView.getAdapter().getCount() - 1) {
return true;
}
} else {
if (listView.getFirstVisiblePosition() == 0) {
return true;
}
} // 其他时候返回false.
return false;
}
// 不能处理的时候返回null.
return null;
}
};
{
registerCanRefreshListener(listViewRefreshListener);
}
如果想要实现其他任何控件的刷新判定,都可以仿照以上格式进行编写,并在static代码块中注册进判定列表中去即可.
接下来我们再实现一个常用控件ScrollView的刷新判定过程:
通过查API我们知道 ScrollView中有一个方法:canScrollVertically(int direction)
但是此方法只能在API14以上使用,无法在4.0系统以下版本的手机中使用,通过查看源码,这个函数的实现如下:
/**
* Check if this view can be scrolled vertically in a certain direction.
*
* @param direction Negative to check scrolling up, positive to check scrolling down.
* @return true if this view can be scrolled in the specified direction, false otherwise.
*/
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
这个函数的实现是在View类中的,因此,如果我们能够把以上源码拷贝的话,便可以解决低版本的问题,同时也将会解决绝大多数的子控件的滚动判定问题.
于是我这样搞了.
private static boolean canScrollY(View child, boolean upScroll) {
final int offset = child.computeVerticalScrollOffset();
final int range = child.computeVerticalScrollRange() - child.computeVerticalScrollExtent();
if (range == 0)
return false;
if (upScroll) {
return offset > 0;
} else {
return offset < range - 1;
}
}
很遗憾的是,这其中 computeVerticalScrollOffset和computeVerticalScrollRange以及computeVerticalScrollExtent三个函数都是View类protected方法. 无法在子类以外的地方使用.
万能的Java还是给我们开了口子的.即反射. 我们可以用反射的方式来调用这几个函数.
以下给出代码:
// 这个方法用于View计算垂直滚动的额外距离。但属于protect方法,因此使用反射调用。
private static Method findMethodWithView(View view, String methodName, Class[] params) {
try {
// 要先在本类里边找,有没有重写的方法.
Method[] methods = view.getClass().getDeclaredMethods();
_nextMethod: for (Method method : methods) {
if (methodName.equals(method.getName())) {
// 要求参数完全一致的重写函数.
Class[] dParams = method.getParameterTypes();
if ((params == null || params.length == 0) && dParams.length == 0)
return method;
if (params.length != dParams.length)
continue;
for (int i = 0; i < dParams.length; i++) {
if (!dParams[i].equals(params[i]))
continue _nextMethod;
}
return method;
}
}
if (view.getClass().getName().equals(View.class.getName()))
return null;
// 如果没找到,则再去基类View中去找.
methods = View.class.getDeclaredMethods();
_nextMethod1: for (Method method : methods) {
if (methodName.equals(method.getName())) {
Class[] dParams = method.getParameterTypes();
if ((params == null || params.length == 0) && dParams.length == 0)
return method;
if (params.length != dParams.length)
continue;
for (int i = 0; i < dParams.length; i++) {
if (!dParams[i].getName().equals(params[i].getName()))
continue _nextMethod1;
}
return method;
}
}
} catch (Exception e) {
}
return null;
}
由于反射的效率还是很不理想的,而这个判定要在滚动的时候频繁调用,
于是我们声明了三个变量,用来存储找到的三个函数,并只初始化一次:
// 在什么时候初始化呢。子控件一旦初始化,类型就固定了,因些这个数据也固定起来。只初始化一次。
private Method computeVerticalScrollRange = null;
private Method computeVerticalScrollExtent = null;
private Method computeVerticalScrollOffset = null;
// 这个方法用于View计算垂直滚动的额外距离。但属于protect方法,因此使用反射调用。
private int computeVerticalScrollOffset(View view) {
if (computeVerticalScrollOffset == null) {
computeVerticalScrollOffset = findMethodWithView(view, "computeVerticalScrollOffset", new Class[] {});
if (computeVerticalScrollOffset != null) {
computeVerticalScrollOffset.setAccessible(true);
}
}
if (computeVerticalScrollOffset != null) {
try {
Object o = computeVerticalScrollOffset.invoke(view, (Object[]) null);
return (Integer) o;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return 0;
}
// 这个方法用于View计算垂直滚动的额外距离。但属于protect方法,因此使用反射调用。
private int computeVerticalScrollExtent(View view) {
if (computeVerticalScrollExtent == null) {
computeVerticalScrollExtent = findMethodWithView(view, "computeVerticalScrollExtent", new Class[] {});
if (computeVerticalScrollExtent != null) {
computeVerticalScrollExtent.setAccessible(true);
}
}
if (computeVerticalScrollExtent != null) {
try {
Object o = computeVerticalScrollExtent.invoke(view, (Object[]) null);
return (Integer) o;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return 0;
}
// 这个方法用于View计算垂直滚动的额外距离。但属于protect方法,因此使用反射调用。
private int computeVerticalScrollRange(View view) {
if (computeVerticalScrollRange == null) {
computeVerticalScrollRange = findMethodWithView(view, "computeVerticalScrollRange", new Class[] {});
if (computeVerticalScrollRange != null) {
computeVerticalScrollRange.setAccessible(true);
}
}
if (computeVerticalScrollRange != null) {
try {
Object o = computeVerticalScrollRange.invoke(view, (Object[]) null);
return (Integer) o;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return 0;
}
于是,canScrollY()就变成如下内容:
private boolean canScrollY(View child, boolean upScroll) {
final int offset = computeVerticalScrollOffset(child);
final int range = computeVerticalScrollRange(child) - computeVerticalScrollExtent(child);
if (range == 0)
return false;
if (upScroll) {
return offset > 0;
} else {
return offset < range - 1;
}
}
以上过程实现了各种View的滚动判定, 如果还想实现其他View的滚动判定可以实现CanRefreshListener并注册即可.
实现了滚动判定后该做什么呢? 下一节我们来讲解,如何根据滚动判定来决定是子控件自己内部滚动,还是RefreshLayout进行下拉/上拉滚动.
即事件分发.