自定义控件: 万能下拉刷新及上拉加载控件SuperRefreshLayout-3

上一节我们实现了一个可以下拉,也能够上拉的基本控件,同时还能够在抬起手指时弹回原位置.
这一节我们需要实现核心逻辑:如何通过子控件状态判定可以下拉了,或者可以上拖了.

由于此逻辑的需要,我们需要限定,本自定义控件只能包含一个子结点.这一点跟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进行下拉/上拉滚动.
即事件分发.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值