开源项目:Android-PullToRefresh
2014-02-17 15:12
281 查看
项目链接:https://github.com/chrisbanes/Android-PullToRefresh
此项目的文件目录是:
核心库是library,其余3个目录是使用pulltorefresh库的例子。
分别写了10个实例供用户开发参考:ListView、GridView、ScrollView、ExpandListView、ListFragment、ViewPager、HorizontalScrollView、ListView in ViewPager、WebView、WebView2。
2. 核心库library:
功能实现总览:
整个项目采用策略模式,把公共的部分实现为抽象类,并提供抽象接口(策略)让具体类来实现:
(1)PullToRefreshBase抽象类,实现的功能:scrollview、下拉/上拉逻辑,并把需要PullToRefresh的View(如:ListView、ScrollView等)提供接口给独立出来,让实现者提供,并把在哪里需要上拉和下拉也提供出接口、提供支持上下滚动或左右滚动的接口。
(2)LoadingLayout抽象类,实现的功能:基本Loading界面框架,但是把界面的元素(子View)独立出来,提供成接口,让实现者提供。
overscroll(弹簧阻尼效果)是通过ViewGroup的onInterceptTouchEvent()和onTouchEvent()函数通过过滤手势、捕捉手势,并且使用View的scrollTo()函数来实现的,当手势向下滑动或向上滑动的时候,使用scrollTo()函数做overscroll运动,当松开手势的时候,利用一个独立线程来scrollTo()滚动动画(采用View的post(Runnable)函数实现)到header,当refresh完成后,scrollTo()到0(不显示header/footer);上拉/下拉刷新的footer/header是通过ViewGroup的addView根据是否是下拉/上拉或双向来加入的,header或footer是在PullToRefreshBase初始化的时候一开始就被addView()到要PollToRefresh的View(ListView或GradView)之前和之后的,也就是说header/footer刚开始时是塞在了ListView或GridView之前的顶部栏/之后的底部栏后隐藏的,所以下拉/上拉刷新的时候直接把隐藏的header/footer从顶部栏/底部栏之后拉出来了。
这3个子目录分别是com.handmark.pulltorefresh.library是客户端开发人员直接使用的类,./extras也是客户端开发人员直接使用的类,是对library的补充,./interal是library和./extras内部使用的自定义View类。其中,在library包中,PullToRefreshBase是基类(抽象类),PullToRefreshListView等是客户端类,ILoadingLayout接口是针对Loading界面的回调接口,策略模式用于设置Loading界面的图像和文字,是被LoadingLayout继承实现的;IPullToRefresh是刷新动作回调接口,只在PullToRefreshBase中实现,IPullToRefresh类写成接口的目的应该是为了更加通用,目的是即使再重写一个PullToRefreshBase类都是可以的,并且PullToRefreshBase抽象类中提供了几个抽象方法:
(其实,对于继承AbsListView的ListView和GridView来说,该项目又封装了一层PullToRefreshAdapterViewBase)。
LoadingLayout是Loading界面的基类,也是抽象类,该抽象类根据x拉刷新方向,加载生成了一个基本Loading界面,可以让FlipLoadingLayout和RotateLoadingLayout继承,这2个类主要是指定了Loading界面中的图片动画,并实现以下抽象方法,指定各个状态时的图片:
3. PullToRefreshListFragment目录是pulltoRefresh在ListFragment中使用的例子,供用户参考使用。
4. PullToRefreshViewPager目录是pulltoRefresh在ViewPager中使用的例子,供用户参考使用。
核心类PullToRefreshBase,代码分析如下:
核心类LoadingLayout代码分析:
此项目的文件目录是:
核心库是library,其余3个目录是使用pulltorefresh库的例子。
分别写了10个实例供用户开发参考:ListView、GridView、ScrollView、ExpandListView、ListFragment、ViewPager、HorizontalScrollView、ListView in ViewPager、WebView、WebView2。
2. 核心库library:
功能实现总览:
整个项目采用策略模式,把公共的部分实现为抽象类,并提供抽象接口(策略)让具体类来实现:
(1)PullToRefreshBase抽象类,实现的功能:scrollview、下拉/上拉逻辑,并把需要PullToRefresh的View(如:ListView、ScrollView等)提供接口给独立出来,让实现者提供,并把在哪里需要上拉和下拉也提供出接口、提供支持上下滚动或左右滚动的接口。
(2)LoadingLayout抽象类,实现的功能:基本Loading界面框架,但是把界面的元素(子View)独立出来,提供成接口,让实现者提供。
overscroll(弹簧阻尼效果)是通过ViewGroup的onInterceptTouchEvent()和onTouchEvent()函数通过过滤手势、捕捉手势,并且使用View的scrollTo()函数来实现的,当手势向下滑动或向上滑动的时候,使用scrollTo()函数做overscroll运动,当松开手势的时候,利用一个独立线程来scrollTo()滚动动画(采用View的post(Runnable)函数实现)到header,当refresh完成后,scrollTo()到0(不显示header/footer);上拉/下拉刷新的footer/header是通过ViewGroup的addView根据是否是下拉/上拉或双向来加入的,header或footer是在PullToRefreshBase初始化的时候一开始就被addView()到要PollToRefresh的View(ListView或GradView)之前和之后的,也就是说header/footer刚开始时是塞在了ListView或GridView之前的顶部栏/之后的底部栏后隐藏的,所以下拉/上拉刷新的时候直接把隐藏的header/footer从顶部栏/底部栏之后拉出来了。
这3个子目录分别是com.handmark.pulltorefresh.library是客户端开发人员直接使用的类,./extras也是客户端开发人员直接使用的类,是对library的补充,./interal是library和./extras内部使用的自定义View类。其中,在library包中,PullToRefreshBase是基类(抽象类),PullToRefreshListView等是客户端类,ILoadingLayout接口是针对Loading界面的回调接口,策略模式用于设置Loading界面的图像和文字,是被LoadingLayout继承实现的;IPullToRefresh是刷新动作回调接口,只在PullToRefreshBase中实现,IPullToRefresh类写成接口的目的应该是为了更加通用,目的是即使再重写一个PullToRefreshBase类都是可以的,并且PullToRefreshBase抽象类中提供了几个抽象方法:
public abstract Orientation getPullToRefreshScrollDirection(); // 子类实现scroll方向 protected abstract T createRefreshableView(Context context, AttributeSet attrs); // 子类实现可刷新View,例如ListView、GridView等 protected abstract boolean isReadyForPullEnd(); // 子类实现上拉刷新的条件,例如ListView实现上拉刷新的条件是上拉到底部 protected abstract boolean isReadyForPullStart(); // 子类实现下拉刷新的条件,例如ListView实现下拉刷新的条件是下拉到最上面第一行处
(其实,对于继承AbsListView的ListView和GridView来说,该项目又封装了一层PullToRefreshAdapterViewBase)。
LoadingLayout是Loading界面的基类,也是抽象类,该抽象类根据x拉刷新方向,加载生成了一个基本Loading界面,可以让FlipLoadingLayout和RotateLoadingLayout继承,这2个类主要是指定了Loading界面中的图片动画,并实现以下抽象方法,指定各个状态时的图片:
protected abstract int getDefaultDrawableResId(); // If we don't have a user defined drawable, load the default protected abstract void onLoadingDrawableSet(Drawable imageDrawable); // 设置Loading图片时候的回调 protected abstract void onPullImpl(float scaleOfLayout); // 在手势下/上拉时候回调 protected abstract void pullToRefreshImpl(); // 下/上拉手势正在执行时的:刷新后的回调 protected abstract void refreshingImpl(); // 正在下/上拉刷新中...的回调 protected abstract void releaseToRefreshImpl(); // 手势释放后的回调 protected abstract void resetImpl(); // Loading View重置后的回调
3. PullToRefreshListFragment目录是pulltoRefresh在ListFragment中使用的例子,供用户参考使用。
4. PullToRefreshViewPager目录是pulltoRefresh在ViewPager中使用的例子,供用户参考使用。
核心类PullToRefreshBase,代码分析如下:
/******************************************************************************* * Copyright 2011, 2012 Chris Banes. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package com.handmark.pulltorefresh.library; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.LinearLayout; import com.handmark.pulltorefresh.library.internal.FlipLoadingLayout; import com.handmark.pulltorefresh.library.internal.LoadingLayout; import com.handmark.pulltorefresh.library.internal.RotateLoadingLayout; import com.handmark.pulltorefresh.library.internal.Utils; import com.handmark.pulltorefresh.library.internal.ViewCompat; /** * 这个类是基类,实现了下拉/上拉刷新,并且提供抽象方法给继承类实现:提供Refreshable View、 * 提供上拉/下拉刷新的时机、支持纵向或横向刷新。 * (1)此抽象类初始化的界面是:加入refreshable view(如:ListView、ScrollView等),加入loading header * 或footer到界面中,然后把header或footer高度或宽度设置为整个界面高度或宽度的一半,之后把整个header或 * footer塞到顶部栏或底部栏之后,使整个界面只看到了refreshable view,当下拉或上拉的时候才能看到header * 或footer,这也是overscroll(弹簧阻尼效果)实现方案。 * (2)重写onInterceptTouchEvent()和onTouchEvent实现下拉/上拉刷新,当移动手势的时,使用scrollTo() * 函数滚动界面,当放开手势的时候,开启一个独立线程执行scrollTo()函数来滚动界面回滚,然后等刷新完成的时候, * 界面滚动到初始状态。【我们自定义的View及其子类的滚动动画基本上都是使用scrollTo()配合Animation及其子类来完成的】。 * * 这个类可用于一切需要overscroll效果的view。 * * @param <T> Refreshable View,比如:ListView ScrollView等 */ public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T> { // =========================================================== // Constants // =========================================================== static final String LOG_TAG = "PullToRefresh"; static final boolean DEBUG = true; static final boolean USE_HW_LAYERS = false; static final float FRICTION = 2.0f; public static final int SMOOTH_SCROLL_DURATION_MS = 200; public static final int SMOOTH_SCROLL_LONG_DURATION_MS = 325; static final int DEMO_SCROLL_INTERVAL = 225; static final String STATE_STATE = "ptr_state"; static final String STATE_MODE = "ptr_mode"; static final String STATE_CURRENT_MODE = "ptr_current_mode"; static final String STATE_SCROLLING_REFRESHING_ENABLED = "ptr_disable_scrolling"; static final String STATE_SHOW_REFRESHING_VIEW = "ptr_show_refreshing_view"; static final String STATE_SUPER = "ptr_super"; // =========================================================== // Fields // =========================================================== private int mTouchSlop; // TouchSlop private float mLastMotionX, mLastMotionY; // 上一次手势坐标 private float mInitialMotionX, mInitialMotionY; // 初始手势坐标 private boolean mIsBeingDragged = false; // 标记正在手势拖动下拉刷新中... private State mState = State.RESET; // 默认是重启状态 private Mode mMode = Mode.getDefault(); // 默认是下拉刷新 private Mode mCurrentMode; // 当前模式 T mRefreshableView; // 当前被刷新的View: ListView private FrameLayout mRefreshableViewWrapper; // 可刷新View(例如ListView)的包装器 private boolean mShowViewWhileRefreshing = true; // 当刷新的时候是否允许展示View private boolean mScrollingWhileRefreshingEnabled = false; // 当刷新的时候是否允许滚动 private boolean mFilterTouchEvents = true; // 是否过滤触摸事件 private boolean mOverScrollEnabled = true; // 是否允许过滚动(弹簧效果) private boolean mLayoutVisibilityChangesEnabled = true; private Interpolator mScrollAnimationInterpolator; private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault(); private LoadingLayout mHeaderLayout; // Loading头部界面 private LoadingLayout mFooterLayout; // Loading尾部界面 private OnRefreshListener<T> mOnRefreshListener; private OnRefreshListener2<T> mOnRefreshListener2; private OnPullEventListener<T> mOnPullEventListener; private SmoothScrollRunnable mCurrentSmoothScrollRunnable; // x拉刷新滚动子线程 // =========================================================== // Constructors // =========================================================== public PullToRefreshBase(Context context) { super(context); init(context, null); } public PullToRefreshBase(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public PullToRefreshBase(Context context, Mode mode) { super(context); mMode = mode; init(context, null); } public PullToRefreshBase(Context context, Mode mode, AnimationStyle animStyle) { super(context); mMode = mode; mLoadingAnimationStyle = animStyle; init(context, null); } /** * 这个重写函数非常重要:当该类用于ListView、ScrollView的时候,这些控件组一定会包含子控件, * 因此一定需要重写这个方法,让其孩子加入到真正的控件中。 */ @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (DEBUG) { Log.d(LOG_TAG, "addView: " + child.getClass().getSimpleName()); } final T refreshableView = getRefreshableView(); if (refreshableView instanceof ViewGroup) { ((ViewGroup) refreshableView).addView(child, index, params); } else { throw new UnsupportedOperationException("Refreshable View is not a " + "ViewGroup so can't addView"); } } @Override public final boolean demo() { if (mMode.showHeaderLoadingLayout() && isReadyForPullStart()) { smoothScrollToAndBack(-getHeaderSize() * 2); return true; } else if (mMode.showFooterLoadingLayout() && isReadyForPullEnd()) { smoothScrollToAndBack(getFooterSize() * 2); return true; } return false; } /** * 获得当前Pull模式 */ @Override public final Mode getCurrentMode() { return mCurrentMode; } @Override public final boolean getFilterTouchEvents() { return mFilterTouchEvents; } /** * 获得下拉/上拉Loading布局代理,主要是关于下拉/上拉Loading header or footer * 相关的文字/View等 */ @Override public final ILoadingLayout getLoadingLayoutProxy() { return getLoadingLayoutProxy(true, true); } @Override public final ILoadingLayout getLoadingLayoutProxy(boolean includeStart, boolean includeEnd) { return createLoadingLayoutProxy(includeStart, includeEnd); } @Override public final Mode getMode() { return mMode; } /** * 获得可刷新的View,比如ListView、GridView等 */ @Override public final T getRefreshableView() { return mRefreshableView; } @Override public final boolean getShowViewWhileRefreshing() { return mShowViewWhileRefreshing; } @Override public final State getState() { return mState; } /** * @deprecated See {@link #isScrollingWhileRefreshingEnabled()}. */ public final boolean isDisableScrollingWhileRefreshing() { return !isScrollingWhileRefreshingEnabled(); } /** * 判断是否Pull刷新使能 */ @Override public final boolean isPullToRefreshEnabled() { return mMode.permitsPullToRefresh(); } /** * 是否允许过滚动效果(弹簧效果) */ @Override public final boolean isPullToRefreshOverScrollEnabled() { return VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD && mOverScrollEnabled && OverscrollHelper.isAndroidOverScrollEnabled(mRefreshableView); } /** * 返回是否正在刷新 */ @Override public final boolean isRefreshing() { return mState == State.REFRESHING || mState == State.MANUAL_REFRESHING; } @Override public final boolean isScrollingWhileRefreshingEnabled() { return mScrollingWhileRefreshingEnabled; } /** * 重写这个函数的目的就是,拦截上拉/下拉事件给此PullToRefresh的onTouchEvent()函数处理, * 其余事件仍旧传递给子View处理,不改变子View原本的手势处理方式。 * * 重写该方法处理手势事件,实现下拉刷新。在这里过滤下手势事件,该PullToRefresh本身 * 只处理满足条件的下拉/上拉刷新事件,其余事件仍旧需要传递给子View(如ListView等)处理。 * * 当手势时拖动中,并且满足下拉刷新或上拉刷新条件的时候,返回true拦截向子View传递 * 手势事件,让PullToRefresh自己在onTouchEvent()中处理上拉或下拉刷新事件。 */ @Override public final boolean onInterceptTouchEvent(MotionEvent event) { if (!isPullToRefreshEnabled()) { // 如果不支持上拉/下拉刷新功能 return false; // 不处理手势事件,向下传递 } final int action = event.getAction(); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mIsBeingDragged = false; return false; } // 如果手势是正在拖动下拉刷新手势,则处理手势(让onTouchEvent()处理) if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) { return true; } switch (action) { case MotionEvent.ACTION_MOVE: /* * If we're refreshing, and the flag is set. Eat all MOVE events * 如果正在刷新中...并且不支持刷新过程中滚动事件,则处理 */ if (!mScrollingWhileRefreshingEnabled && isRefreshing()) { return true; } if (isReadyForPull()) { // 准备好可以下拉刷新或上拉刷新 final float y = event.getY(), x = event.getX(); final float diff, oppositeDiff, absDiff; /* * We need to use the correct values, based on scroll direction */ switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: diff = x - mLastMotionX; oppositeDiff = y - mLastMotionY; break; case VERTICAL: default: diff = y - mLastMotionY; oppositeDiff = x - mLastMotionX; break; } absDiff = Math.abs(diff); if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) { // 允许下拉刷新,且条件准备好可以下拉刷新了 if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) { mLastMotionY = y; mLastMotionX = x; mIsBeingDragged = true; if (mMode == Mode.BOTH) { mCurrentMode = Mode.PULL_FROM_START; } } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) { mLastMotionY = y; mLastMotionX = x; mIsBeingDragged = true; if (mMode == Mode.BOTH) { mCurrentMode = Mode.PULL_FROM_END; } } } } break; case MotionEvent.ACTION_DOWN: // 按下手势不处理,让子View处理 if (isReadyForPull()) { // 准备好可以上拉刷新或下拉刷新 mLastMotionY = mInitialMotionY = event.getY(); mLastMotionX = mInitialMotionX = event.getX(); mIsBeingDragged = false; } break; } return mIsBeingDragged; } /** * 仅处理上拉/下拉刷新手势事件,其余手势事件在onInterceptTouchEvent()函数中 * 已经传递给子View(如ListView等)处理了。这里剩下的就是onInterceptTouchEvent() * 函数过滤过来的上拉/下拉刷新事件。 */ @Override public final boolean onTouchEvent(MotionEvent event) { if (!isPullToRefreshEnabled()) { return false; } /* * If we're refreshing, and the flag is set. Eat the event * 如果正在刷新中...并且是刷新过程中不允许滚动事件,则进行空处理。 */ if (!mScrollingWhileRefreshingEnabled && isRefreshing()) { return true; } if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_MOVE: if (mIsBeingDragged) { mLastMotionY = event.getY(); mLastMotionX = event.getX(); pullEvent(); // 执行Loading header or footer的滚动事件 return true; } break; case MotionEvent.ACTION_DOWN: if (isReadyForPull()) { mLastMotionY = mInitialMotionY = event.getY(); mLastMotionX = mInitialMotionX = event.getX(); return true; } break; /* * 因为在onInterceptTouchEvent()函数中手势移动事件被处理了,所以之后的抬起事件仍旧在 * 此onTouchEvent()事件中处理。 * 开启独立线程执行滚动回滚。 */ case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (mIsBeingDragged) { mIsBeingDragged = false; if (mState == State.RELEASE_TO_REFRESH && (null != mOnRefreshListener || null != mOnRefreshListener2)) { setState(State.REFRESHING, true); return true; } // If we're already refreshing, just scroll back to the top if (isRefreshing()) { smoothScrollTo(0); return true; } // If we haven't returned by here, then we're not in a state // to pull, so just reset setState(State.RESET); return true; } break; } return false; } /** * 每次刷新完数据后都调用这个方法重置PullToRefresh */ @Override public final void onRefreshComplete() { if (isRefreshing()) { setState(State.RESET); } } public final void setScrollingWhileRefreshingEnabled(boolean allowScrollingWhileRefreshing) { mScrollingWhileRefreshingEnabled = allowScrollingWhileRefreshing; } /** * @deprecated See {@link #setScrollingWhileRefreshingEnabled(boolean)} */ public void setDisableScrollingWhileRefreshing(boolean disableScrollingWhileRefreshing) { setScrollingWhileRefreshingEnabled(!disableScrollingWhileRefreshing); } @Override public final void setFilterTouchEvents(boolean filterEvents) { mFilterTouchEvents = filterEvents; } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy()}. */ public void setLastUpdatedLabel(CharSequence label) { getLoadingLayoutProxy().setLastUpdatedLabel(label); } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy()}. */ public void setLoadingDrawable(Drawable drawable) { getLoadingLayoutProxy().setLoadingDrawable(drawable); } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy(boolean, boolean)}. */ public void setLoadingDrawable(Drawable drawable, Mode mode) { getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()) .setLoadingDrawable(drawable); } @Override public void setLongClickable(boolean longClickable) { getRefreshableView().setLongClickable(longClickable); } @Override public final void setMode(Mode mode) { if (mode != mMode) { if (DEBUG) { Log.d(LOG_TAG, "Setting mode to: " + mode); } mMode = mode; updateUIForMode(); // 更新PullToRefresh界面 } } public void setOnPullEventListener(OnPullEventListener<T> listener) { mOnPullEventListener = listener; } @Override public final void setOnRefreshListener(OnRefreshListener<T> listener) { mOnRefreshListener = listener; mOnRefreshListener2 = null; } @Override public final void setOnRefreshListener(OnRefreshListener2<T> listener) { mOnRefreshListener2 = listener; mOnRefreshListener = null; } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy()}. */ public void setPullLabel(CharSequence pullLabel) { getLoadingLayoutProxy().setPullLabel(pullLabel); } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy(boolean, boolean)}. */ public void setPullLabel(CharSequence pullLabel, Mode mode) { getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()) .setPullLabel(pullLabel); } /** * @param enable Whether Pull-To-Refresh should be used * @deprecated This simple calls setMode with an appropriate mode based on * the passed value. */ public final void setPullToRefreshEnabled(boolean enable) { setMode(enable ? Mode.getDefault() : Mode.DISABLED); } @Override public final void setPullToRefreshOverScrollEnabled(boolean enabled) { mOverScrollEnabled = enabled; } /** * 设置正在刷新 */ @Override public final void setRefreshing() { setRefreshing(true); } /** * 设置正在刷新 */ @Override public final void setRefreshing(boolean doScroll) { if (!isRefreshing()) { setState(State.MANUAL_REFRESHING, doScroll); } } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy()}. */ public void setRefreshingLabel(CharSequence refreshingLabel) { getLoadingLayoutProxy().setRefreshingLabel(refreshingLabel); } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy(boolean, boolean)}. */ public void setRefreshingLabel(CharSequence refreshingLabel, Mode mode) { getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()) .setRefreshingLabel(refreshingLabel); } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy()}. */ public void setReleaseLabel(CharSequence releaseLabel) { setReleaseLabel(releaseLabel, Mode.BOTH); } /** * @deprecated You should now call this method on the result of * {@link #getLoadingLayoutProxy(boolean, boolean)}. */ public void setReleaseLabel(CharSequence releaseLabel, Mode mode) { getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()) .setReleaseLabel(releaseLabel); } public void setScrollAnimationInterpolator(Interpolator interpolator) { mScrollAnimationInterpolator = interpolator; } @Override public final void setShowViewWhileRefreshing(boolean showView) { mShowViewWhileRefreshing = showView; } /** * @return Either {@link Orientation#VERTICAL} or * {@link Orientation#HORIZONTAL} depending on the scroll direction. */ public abstract Orientation getPullToRefreshScrollDirection(); // TODO final void setState(State state, final boolean... params) { mState = state; if (DEBUG) { Log.d(LOG_TAG, "State: " + mState.name()); } switch (mState) { case RESET: onReset(); break; case PULL_TO_REFRESH: // 手势拖动更新 onPullToRefresh(); break; case RELEASE_TO_REFRESH: // 释放手势拖动更新 onReleaseToRefresh(); break; case REFRESHING: // 正在刷新中... case MANUAL_REFRESHING: onRefreshing(params[0]); break; case OVERSCROLLING: // NO-OP break; } // Call OnPullEventListener if (null != mOnPullEventListener) { mOnPullEventListener.onPullEvent(this, mState, mCurrentMode); } } /** * Used internally for adding view. Need because we override addView to * pass-through to the Refreshable View * 用于把Loading header或loading footer加入到PullToRefresh中 */ protected final void addViewInternal(View child, int index, ViewGroup.LayoutParams params) { super.addView(child, index, params); } /** * Used internally for adding view. Need because we override addView to * pass-through to the Refreshable View * 用于把Refreshable加入到PullToRefresh中,加入到孩子之后 */ protected final void addViewInternal(View child, ViewGroup.LayoutParams params) { super.addView(child, -1, params); } /** * 创建loading header and loading footer * @param context * @param mode 根据mode创建flip样式或rotate样式的loading界面 * @param attrs * @return */ protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) { LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode, getPullToRefreshScrollDirection(), attrs); layout.setVisibility(View.INVISIBLE); return layout; } /** * Used internally for {@link #getLoadingLayoutProxy(boolean, boolean)}. * Allows derivative classes to include any extra LoadingLayouts. * 创建Loading界面代理 * 获得真正的Loading界面(继承LoadingLayout) */ protected LoadingLayoutProxy createLoadingLayoutProxy(final boolean includeStart, final boolean includeEnd) { LoadingLayoutProxy proxy = new LoadingLayoutProxy(); if (includeStart && mMode.showHeaderLoadingLayout()) { proxy.addLayout(mHeaderLayout); // 把loading header加入Loading界面代理 } if (includeEnd && mMode.showFooterLoadingLayout()) { proxy.addLayout(mFooterLayout); // 把loading footer加入Loading界面代理 } return proxy; } /** * This is implemented by derived classes to return the created View. If you * need to use a custom View (such as a custom ListView), override this * method and return an instance of your custom class. * <p/> * Be sure to set the ID of the view in this method, especially if you're * using a ListActivity or ListFragment. * * @param context Context to create view with * @param attrs AttributeSet from wrapped class. Means that anything you * include in the XML layout declaration will be routed to the * created View * @return New instance of the Refreshable View */ protected abstract T createRefreshableView(Context context, AttributeSet attrs); // TODO protected final void disableLoadingLayoutVisibilityChanges() { mLayoutVisibilityChangesEnabled = false; } protected final LoadingLayout getFooterLayout() { return mFooterLayout; } protected final int getFooterSize() { return mFooterLayout.getContentSize(); } protected final LoadingLayout getHeaderLayout() { return mHeaderLayout; } protected final int getHeaderSize() { return mHeaderLayout.getContentSize(); } protected int getPullToRefreshScrollDuration() { return SMOOTH_SCROLL_DURATION_MS; } protected int getPullToRefreshScrollDurationLonger() { return SMOOTH_SCROLL_LONG_DURATION_MS; } protected FrameLayout getRefreshableViewWrapper() { return mRefreshableViewWrapper; } /** * Allows Derivative classes to handle the XML Attrs without creating a * TypedArray themsevles * * @param a - TypedArray of PullToRefresh Attributes */ protected void handleStyledAttributes(TypedArray a) { } /** * Implemented by derived class to return whether the View is in a state * where the user can Pull to Refresh by scrolling from the end. * * @return true if the View is currently in the correct state (for example, * bottom of a ListView) */ protected abstract boolean isReadyForPullEnd(); /** * Implemented by derived class to return whether the View is in a state * where the user can Pull to Refresh by scrolling from the start. * * @return true if the View is currently the correct state (for example, top * of a ListView) */ protected abstract boolean isReadyForPullStart(); /** * Called by {@link #onRestoreInstanceState(Parcelable)} so that derivative * classes can handle their saved instance state. * * @param savedInstanceState - Bundle which contains saved instance state. */ protected void onPtrRestoreInstanceState(Bundle savedInstanceState) { } /** * Called by {@link #onSaveInstanceState()} so that derivative classes can * save their instance state. * * @param saveState - Bundle to be updated with saved state. */ protected void onPtrSaveInstanceState(Bundle saveState) { } /** * Called when the UI has been to be updated to be in the * {@link State#PULL_TO_REFRESH} state. */ protected void onPullToRefresh() { switch (mCurrentMode) { case PULL_FROM_END: mFooterLayout.pullToRefresh(); break; case PULL_FROM_START: mHeaderLayout.pullToRefresh(); break; default: // NO-OP break; } } /** * Called when the UI has been to be updated to be in the * {@link State#REFRESHING} or {@link State#MANUAL_REFRESHING} state. * 正在刷新的回调函数 * * @param doScroll - Whether the UI should scroll for this event. */ protected void onRefreshing(final boolean doScroll) { if (mMode.showHeaderLoadingLayout()) { // 允许下拉刷新 mHeaderLayout.refreshing(); } if (mMode.showFooterLoadingLayout()) { // 允许上拉刷新 mFooterLayout.refreshing(); } if (doScroll) { // 上/下拉刷新的时候允许UI滚动事件 if (mShowViewWhileRefreshing) { // Call Refresh Listener when the Scroll has finished OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() { @Override public void onSmoothScrollFinished() { callRefreshListener(); } }; switch (mCurrentMode) { case MANUAL_REFRESH_ONLY: case PULL_FROM_END: // 子线程中的滚动动画 smoothScrollTo(getFooterSize(), listener); break; default: case PULL_FROM_START: smoothScrollTo(-getHeaderSize(), listener); break; } } else { smoothScrollTo(0); } } else { // We're not scrolling, so just call Refresh Listener now callRefreshListener(); } } /** * Called when the UI has been to be updated to be in the * {@link State#RELEASE_TO_REFRESH} state. */ protected void onReleaseToRefresh() { switch (mCurrentMode) { case PULL_FROM_END: mFooterLayout.releaseToRefresh(); break; case PULL_FROM_START: mHeaderLayout.releaseToRefresh(); break; default: // NO-OP break; } } /** * Called when the UI has been to be updated to be in the * {@link State#RESET} state. */ protected void onReset() { mIsBeingDragged = false; mLayoutVisibilityChangesEnabled = true; // Always reset both layouts, just in case... mHeaderLayout.reset(); mFooterLayout.reset(); smoothScrollTo(0); } @Override protected final void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; setMode(Mode.mapIntToValue(bundle.getInt(STATE_MODE, 0))); mCurrentMode = Mode.mapIntToValue(bundle.getInt(STATE_CURRENT_MODE, 0)); mScrollingWhileRefreshingEnabled = bundle.getBoolean(STATE_SCROLLING_REFRESHING_ENABLED, false); mShowViewWhileRefreshing = bundle.getBoolean(STATE_SHOW_REFRESHING_VIEW, true); // Let super Restore Itself super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER)); State viewState = State.mapIntToValue(bundle.getInt(STATE_STATE, 0)); if (viewState == State.REFRESHING || viewState == State.MANUAL_REFRESHING) { setState(viewState, true); } // Now let derivative classes restore their state onPtrRestoreInstanceState(bundle); return; } super.onRestoreInstanceState(state); } @Override protected final Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); // Let derivative classes get a chance to save state first, that way we // can make sure they don't overrite any of our values onPtrSaveInstanceState(bundle); bundle.putInt(STATE_STATE, mState.getIntValue()); bundle.putInt(STATE_MODE, mMode.getIntValue()); bundle.putInt(STATE_CURRENT_MODE, mCurrentMode.getIntValue()); bundle.putBoolean(STATE_SCROLLING_REFRESHING_ENABLED, mScrollingWhileRefreshingEnabled); bundle.putBoolean(STATE_SHOW_REFRESHING_VIEW, mShowViewWhileRefreshing); bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState()); return bundle; } @Override protected final void onSizeChanged(int w, int h, int oldw, int oldh) { if (DEBUG) { Log.d(LOG_TAG, String.format("onSizeChanged. W: %d, H: %d", w, h)); } super.onSizeChanged(w, h, oldw, oldh); // We need to update the header/footer when our size changes refreshLoadingViewsSize(); // Update the Refreshable View layout refreshRefreshableViewSize(w, h); /** * As we're currently in a Layout Pass, we need to schedule another one * to layout any changes we've made here */ post(new Runnable() { @Override public void run() { requestLayout(); } }); } /** * Re-measure the Loading Views height, and adjust internal padding as * necessary * 设置loading header and footer视图大小、隐藏它们到顶部栏or底部栏后面 */ protected final void refreshLoadingViewsSize() { final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f); int pLeft = getPaddingLeft(); int pTop = getPaddingTop(); int pRight = getPaddingRight(); int pBottom = getPaddingBottom(); switch (getPullToRefreshScrollDirection()) { // TODO case HORIZONTAL: if (mMode.showHeaderLoadingLayout()) { mHeaderLayout.setWidth(maximumPullScroll); pLeft = -maximumPullScroll; } else { pLeft = 0; } if (mMode.showFooterLoadingLayout()) { mFooterLayout.setWidth(maximumPullScroll); pRight = -maximumPullScroll; } else { pRight = 0; } break; case VERTICAL: if (mMode.showHeaderLoadingLayout()) { mHeaderLayout.setHeight(maximumPullScroll); // 设置loading header高度 pTop = -maximumPullScroll; // 把loading header塞到顶部栏之后的距离参数 } else { pTop = 0; } if (mMode.showFooterLoadingLayout()) { mFooterLayout.setHeight(maximumPullScroll); // 设置loading footer高度 pBottom = -maximumPullScroll; } else { pBottom = 0; } break; } if (DEBUG) { Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d", pLeft, pTop, pRight, pBottom)); } setPadding(pLeft, pTop, pRight, pBottom); // 把loading header or footer塞到顶部栏or底部栏之后 } protected final void refreshRefreshableViewSize(int width, int height) { // We need to set the Height of the Refreshable View to the same as // this layout LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mRefreshableViewWrapper.getLayoutParams(); switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: if (lp.width != width) { lp.width = width; mRefreshableViewWrapper.requestLayout(); } break; case VERTICAL: if (lp.height != height) { lp.height = height; mRefreshableViewWrapper.requestLayout(); } break; } } /** * Helper method which just calls scrollTo() in the correct scrolling * direction. * 设置界面滚动到合适的位置,当手势拖动时直接调用这个函数;当释放拖动手势时把 * 该函数放在一个独立线程中执行。 * @param value - New Scroll value */ protected final void setHeaderScroll(int value) { if (DEBUG) { Log.d(LOG_TAG, "setHeaderScroll: " + value); } // Clamp value to with pull scroll range final int maximumPullScroll = getMaximumPullScroll(); value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value)); if (mLayoutVisibilityChangesEnabled) { if (value < 0) { mHeaderLayout.setVisibility(View.VISIBLE); } else if (value > 0) { mFooterLayout.setVisibility(View.VISIBLE); } else { mHeaderLayout.setVisibility(View.INVISIBLE); mFooterLayout.setVisibility(View.INVISIBLE); } } if (USE_HW_LAYERS) { /** * Use a Hardware Layer on the Refreshable View if we've scrolled at * all. We don't use them on the Header/Footer Views as they change * often, which would negate any HW layer performance boost. */ ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE); } switch (getPullToRefreshScrollDirection()) { case VERTICAL: scrollTo(0, value); break; case HORIZONTAL: scrollTo(value, 0); break; } } /** * Smooth Scroll to position using the default duration of * {@value #SMOOTH_SCROLL_DURATION_MS} ms. * * @param scrollValue - Position to scroll to */ protected final void smoothScrollTo(int scrollValue) { smoothScrollTo(scrollValue, getPullToRefreshScrollDuration()); } /** * Smooth Scroll to position using the default duration of * {@value #SMOOTH_SCROLL_DURATION_MS} ms. * * @param scrollValue - Position to scroll to * @param listener - Listener for scroll */ protected final void smoothScrollTo(int scrollValue, OnSmoothScrollFinishedListener listener) { smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0, listener); } /** * Smooth Scroll to position using the longer default duration of * {@value #SMOOTH_SCROLL_LONG_DURATION_MS} ms. * * @param scrollValue - Position to scroll to */ protected final void smoothScrollToLonger(int scrollValue) { smoothScrollTo(scrollValue, getPullToRefreshScrollDurationLonger()); } /** * Updates the View State when the mode has been set. This does not do any * checking that the mode is different to current state so always updates. * 更新PullToRefresh界面 */ protected void updateUIForMode() { // We need to use the correct LayoutParam values, based on scroll // direction final LinearLayout.LayoutParams lp = getLoadingLayoutLayoutParams(); // Remove Header, and then add Header Loading View again if needed if (this == mHeaderLayout.getParent()) { removeView(mHeaderLayout); } if (mMode.showHeaderLoadingLayout()) { addViewInternal(mHeaderLayout, 0, lp); // 把loading header加入到PullToRefresh } // Remove Footer, and then add Footer Loading View again if needed if (this == mFooterLayout.getParent()) { removeView(mFooterLayout); } if (mMode.showFooterLoadingLayout()) { addViewInternal(mFooterLayout, lp); // 把loading footer加入到PullToRefresh } // Hide Loading Views 把loading header or footer隐藏到顶部栏or底部栏之后 refreshLoadingViewsSize(); // If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise // set it to pull down mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START; } /** * 把Refresh View(ListView/GridView等)加入到PullToRefresh中 * @param context * @param refreshableView */ private void addRefreshableView(Context context, T refreshableView) { mRefreshableViewWrapper = new FrameLayout(context); mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); addViewInternal(mRefreshableViewWrapper, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } private void callRefreshListener() { if (null != mOnRefreshListener) { mOnRefreshListener.onRefresh(this); } else if (null != mOnRefreshListener2) { if (mCurrentMode == Mode.PULL_FROM_START) { mOnRefreshListener2.onPullDownToRefresh(this); } else if (mCurrentMode == Mode.PULL_FROM_END) { mOnRefreshListener2.onPullUpToRefresh(this); } } } /* * 初始化PullToRefresh界面基本框架 */ @SuppressWarnings("deprecation") private void init(Context context, AttributeSet attrs) { switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: setOrientation(LinearLayout.HORIZONTAL); break; case VERTICAL: default: setOrientation(LinearLayout.VERTICAL); break; } setGravity(Gravity.CENTER); // 拖动手势的最小识别距离 ViewConfiguration config = ViewConfiguration.get(context); mTouchSlop = config.getScaledTouchSlop(); // Styleables from XML TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh); if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) { mMode = Mode.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0)); } if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle)) { mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger( R.styleable.PullToRefresh_ptrAnimationStyle, 0)); } /* * Refreshable View,把Refreshable View加入到PullToRefresh界面(LinearLayout)中, * By passing the attrs, we can add ListView/GridView params via XML */ mRefreshableView = createRefreshableView(context, attrs); // TODO addRefreshableView(context, mRefreshableView); /* * We need to create now layouts now,这两个在刚开始创建PullToRefresh的时,就已经 * 根据Mode方向将其加入到了PullToRefresh中了,只是被隐藏在了顶部栏或底部栏之后了。 * 因此上拉或下拉的时,只是把loading header或loading footer从其中显示出来而已。 */ mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a); mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a); /** * Styleables from XML */ if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground)) { Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground); if (null != background) { mRefreshableView.setBackgroundDrawable(background); } } else if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) { Utils.warnDeprecation("ptrAdapterViewBackground", "ptrRefreshableViewBackground"); Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground); if (null != background) { mRefreshableView.setBackgroundDrawable(background); } } if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) { mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true); } if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) { mScrollingWhileRefreshingEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false); } // Let the derivative classes have a go at handling attributes, then // recycle them... handleStyledAttributes(a); a.recycle(); // Finally update the UI for the modes,上面是获得相关属性和子界面,这里是更新PullToRefresh界面 updateUIForMode(); } /* * 控件是否可以下拉刷新或上拉刷新 */ private boolean isReadyForPull() { switch (mMode) { case PULL_FROM_START: return isReadyForPullStart(); // TODO: 让子类来实现下拉刷新的时机 case PULL_FROM_END: return isReadyForPullEnd(); // TODO: 让子类来实现上拉刷新的时机 case BOTH: return isReadyForPullEnd() || isReadyForPullStart(); default: return false; } } /* * Actions a Pull Event 手势下/上拉事件 * @return true if the Event has been handled, false if there has been no * change * 执行滚动事件 */ private void pullEvent() { final int newScrollValue; final int itemDimension; final float initialMotionValue, lastMotionValue; switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: initialMotionValue = mInitialMotionX; lastMotionValue = mLastMotionX; break; case VERTICAL: default: initialMotionValue = mInitialMotionY; lastMotionValue = mLastMotionY; break; } switch (mCurrentMode) { case PULL_FROM_END: newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION); itemDimension = getFooterSize(); break; case PULL_FROM_START: default: newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION); itemDimension = getHeaderSize(); break; } setHeaderScroll(newScrollValue); // 设置laoding header滚动到指定位置 if (newScrollValue != 0 && !isRefreshing()) { float scale = Math.abs(newScrollValue) / (float) itemDimension; switch (mCurrentMode) { case PULL_FROM_END: mFooterLayout.onPull(scale); break; case PULL_FROM_START: default: mHeaderLayout.onPull(scale); break; } if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) { setState(State.PULL_TO_REFRESH); } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) { setState(State.RELEASE_TO_REFRESH); } } } /* * 获得loading header or footer布局参数 */ private LinearLayout.LayoutParams getLoadingLayoutLayoutParams() { switch (getPullToRefreshScrollDirection()) { // TODO case HORIZONTAL: return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT); case VERTICAL: default: return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); } } /* * 获取上拉和下拉最大滚动距离 */ private int getMaximumPullScroll() { switch (getPullToRefreshScrollDirection()) { //TODO case HORIZONTAL: return Math.round(getWidth() / FRICTION); // 最大滚动距离是宽度的一半 case VERTICAL: default: return Math.round(getHeight() / FRICTION); // 最大滚动距离是高度的一半 } } /** * Smooth Scroll to position using the specific duration * * @param scrollValue - Position to scroll to * @param duration - Duration of animation in milliseconds */ private final void smoothScrollTo(int scrollValue, long duration) { smoothScrollTo(scrollValue, duration, 0, null); } /* * 平滑滚动,在独立线程中执行 */ private final void smoothScrollTo(int newScrollValue, long duration, long delayMillis, OnSmoothScrollFinishedListener listener) { if (null != mCurrentSmoothScrollRunnable) { mCurrentSmoothScrollRunnable.stop(); } final int oldScrollValue; switch (getPullToRefreshScrollDirection()) { // TODO case HORIZONTAL: oldScrollValue = getScrollX(); break; case VERTICAL: default: oldScrollValue = getScrollY(); break; } if (oldScrollValue != newScrollValue) { if (null == mScrollAnimationInterpolator) { // Default interpolator is a Decelerate Interpolator mScrollAnimationInterpolator = new DecelerateInterpolator(); } mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener); if (delayMillis > 0) { postDelayed(mCurrentSmoothScrollRunnable, delayMillis); } else { post(mCurrentSmoothScrollRunnable); } } } private final void smoothScrollToAndBack(int y) { smoothScrollTo(y, SMOOTH_SCROLL_DURATION_MS, 0, new OnSmoothScrollFinishedListener() { @Override public void onSmoothScrollFinished() { smoothScrollTo(0, SMOOTH_SCROLL_DURATION_MS, DEMO_SCROLL_INTERVAL, null); } }); } /** * 动画样式 */ public static enum AnimationStyle { /** * This is the default for Android-PullToRefresh. Allows you to use any * drawable, which is automatically rotated and used as a Progress Bar. */ ROTATE, /** * This is the old default, and what is commonly used on iOS. Uses an * arrow image which flips depending on where the user has scrolled. */ FLIP; static AnimationStyle getDefault() { return ROTATE; } /** * Maps an int to a specific mode. This is needed when saving state, or * inflating the view from XML where the mode is given through a attr * int. * * @param modeInt - int to map a Mode to * @return Mode that modeInt maps to, or ROTATE by default. */ static AnimationStyle mapIntToValue(int modeInt) { switch (modeInt) { case 0x0: default: return ROTATE; case 0x1: return FLIP; } } /** * 创建旋转Loading界面、flip Loding界面 * @param context * @param mode * @param scrollDirection * @param attrs * @return */ LoadingLayout createLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) { switch (this) { case ROTATE: default: return new RotateLoadingLayout(context, mode, scrollDirection, attrs); case FLIP: return new FlipLoadingLayout(context, mode, scrollDirection, attrs); } } } /** * 下拉刷新模式、上拉刷新模式、双向刷新模式 */ public static enum Mode { /** * Disable all Pull-to-Refresh gesture and Refreshing handling * 关闭上/下拉刷新和正在刷新的处理 */ DISABLED(0x0), /** * Only allow the user to Pull from the start of the Refreshable View to * refresh. The start is either the Top or Left, depending on the * scrolling direction. * 只允许从上/左边下拉刷新 */ PULL_FROM_START(0x1), /** * Only allow the user to Pull from the end of the Refreshable View to * refresh. The start is either the Bottom or Right, depending on the * scrolling direction. * 只允许从下/又边上拉刷新 */ PULL_FROM_END(0x2), /** * Allow the user to both Pull from the start, from the end to refresh. * 允许两边上/下拉刷新 */ BOTH(0x3), /** * Disables Pull-to-Refresh gesture handling, but allows manually * setting the Refresh state via * {@link PullToRefreshBase#setRefreshing() setRefreshing()}. * 关闭上/下拉刷新手势处理,但是允许通过代码执行下/上拉刷新。 */ MANUAL_REFRESH_ONLY(0x4); /** * @deprecated Use {@link #PULL_FROM_START} from now on. */ public static Mode PULL_DOWN_TO_REFRESH = Mode.PULL_FROM_START; /** * @deprecated Use {@link #PULL_FROM_END} from now on. */ public static Mode PULL_UP_TO_REFRESH = Mode.PULL_FROM_END; /** * Maps an int to a specific mode. This is needed when saving state, or * inflating the view from XML where the mode is given through a attr * int. * * @param modeInt - int to map a Mode to * @return Mode that modeInt maps to, or PULL_FROM_START by default. */ static Mode mapIntToValue(final int modeInt) { for (Mode value : Mode.values()) { if (modeInt == value.getIntValue()) { return value; } } // If not, return default return getDefault(); } static Mode getDefault() { return PULL_FROM_START; } private int mIntValue; // The modeInt values need to match those from attrs.xml Mode(int modeInt) { mIntValue = modeInt; } /** * @return true if the mode permits Pull-to-Refresh */ boolean permitsPullToRefresh() { return !(this == DISABLED || this == MANUAL_REFRESH_ONLY); } /** * @return true if this mode wants the Loading Layout Header to be shown * 返回是否允许下拉刷新 */ public boolean showHeaderLoadingLayout() { return this == PULL_FROM_START || this == BOTH; } /** * @return true if this mode wants the Loading Layout Footer to be shown * 返回是否允许上拉刷新 */ public boolean showFooterLoadingLayout() { return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY; } int getIntValue() { return mIntValue; } } // =========================================================== // Inner, Anonymous Classes, and Enumerations // =========================================================== /** * Simple Listener that allows you to be notified when the user has scrolled * to the end of the AdapterView. See ( * {@link PullToRefreshAdapterViewBase#setOnLastItemVisibleListener}. * * @author Chris Banes */ public static interface OnLastItemVisibleListener { /** * Called when the user has scrolled to the end of the list */ public void onLastItemVisible(); } /** * Listener that allows you to be notified when the user has started or * finished a touch event. Useful when you want to append extra UI events * (such as sounds). See ( * {@link PullToRefreshAdapterViewBase#setOnPullEventListener}. * * @author Chris Banes */ public static interface OnPullEventListener<V extends View> { /** * Called when the internal state has been changed, usually by the user * pulling. * * @param refreshView - View which has had it's state change. * @param state - The new state of View. * @param direction - One of {@link Mode#PULL_FROM_START} or * {@link Mode#PULL_FROM_END} depending on which direction * the user is pulling. Only useful when <var>state</var> is * {@link State#PULL_TO_REFRESH} or * {@link State#RELEASE_TO_REFRESH}. */ public void onPullEvent(final PullToRefreshBase<V> refreshView, State state, Mode direction); } /** * Simple Listener to listen for any callbacks to Refresh. * * @author Chris Banes */ public static interface OnRefreshListener<V extends View> { /** * onRefresh will be called for both a Pull from start, and Pull from * end */ public void onRefresh(final PullToRefreshBase<V> refreshView); } /** * An advanced version of the Listener to listen for callbacks to Refresh. * This listener is different as it allows you to differentiate between Pull * Ups, and Pull Downs. * * @author Chris Banes */ public static interface OnRefreshListener2<V extends View> { // TODO These methods need renaming to START/END rather than DOWN/UP /** * onPullDownToRefresh will be called only when the user has Pulled from * the start, and released. */ public void onPullDownToRefresh(final PullToRefreshBase<V> refreshView); /** * onPullUpToRefresh will be called only when the user has Pulled from * the end, and released. */ public void onPullUpToRefresh(final PullToRefreshBase<V> refreshView); } public static enum Orientation { VERTICAL, HORIZONTAL; } /** * 状态:重置状态、拖动手势状态、释放拖动手势状态、正在刷新模式等 */ public static enum State { /** * When the UI is in a state which means that user is not interacting * with the Pull-to-Refresh function. * 用户还没有和PullToRefresh交互 */ RESET(0x0), /** * When the UI is being pulled by the user, but has not been pulled far * enough so that it refreshes when released. * 用户正在拖动刷新操作 */ PULL_TO_REFRESH(0x1), /** * When the UI is being pulled by the user, and <strong>has</strong> * been pulled far enough so that it will refresh when released. * 用户拖动释放后 */ RELEASE_TO_REFRESH(0x2), /** * When the UI is currently refreshing, caused by a pull gesture. * 正在刷新 */ REFRESHING(0x8), /** * When the UI is currently refreshing, caused by a call to * {@link PullToRefreshBase#setRefreshing() setRefreshing()}. * 代码中指定正在刷新中... */ MANUAL_REFRESHING(0x9), /** * When the UI is currently overscrolling, caused by a fling on the * Refreshable View. * 正在弹簧滚动中... */ OVERSCROLLING(0x10); /** * Maps an int to a specific state. This is needed when saving state. * * @param stateInt - int to map a State to * @return State that stateInt maps to */ static State mapIntToValue(final int stateInt) { for (State value : State.values()) { if (stateInt == value.getIntValue()) { return value; } } // If not, return default return RESET; } private int mIntValue; State(int intValue) { mIntValue = intValue; } int getIntValue() { return mIntValue; } } /** * 在独立线程中overscroll头部或尾部 */ final class SmoothScrollRunnable implements Runnable { private final Interpolator mInterpolator; private final int mScrollToY; private final int mScrollFromY; private final long mDuration; private OnSmoothScrollFinishedListener mListener; private boolean mContinueRunning = true; private long mStartTime = -1; private int mCurrentY = -1; public SmoothScrollRunnable(int fromY, int toY, long duration, OnSmoothScrollFinishedListener listener) { mScrollFromY = fromY; mScrollToY = toY; mInterpolator = mScrollAnimationInterpolator; mDuration = duration; mListener = listener; } @Override public void run() { /** * Only set mStartTime if this is the first time we're starting, * else actually calculate the Y delta */ if (mStartTime == -1) { mStartTime = System.currentTimeMillis(); } else { /** * We do do all calculations in long to reduce software float * calculations. We use 1000 as it gives us good accuracy and * small rounding errors */ long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration; normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0); final int deltaY = Math.round((mScrollFromY - mScrollToY) * mInterpolator.getInterpolation(normalizedTime / 1000f)); mCurrentY = mScrollFromY - deltaY; setHeaderScroll(mCurrentY); } // If we're not at the target Y, keep going... if (mContinueRunning && mScrollToY != mCurrentY) { ViewCompat.postOnAnimation(PullToRefreshBase.this, this); } else { if (null != mListener) { mListener.onSmoothScrollFinished(); } } } public void stop() { mContinueRunning = false; removeCallbacks(this); } } static interface OnSmoothScrollFinishedListener { void onSmoothScrollFinished(); } }
核心类LoadingLayout代码分析:
/******************************************************************************* * Copyright 2011, 2012 Chris Banes. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package com.handmark.pulltorefresh.library.internal; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.handmark.pulltorefresh.library.ILoadingLayout; import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode; import com.handmark.pulltorefresh.library.PullToRefreshBase.Orientation; import com.handmark.pulltorefresh.library.R; /** * 自定义Loading控件,这个是用户自定义Loading控件的基类,用户自定义的Loading控件 * 必须继承这个类,比如:FlipLoadingLayout、RotateLoadingLayout。这个类完成了Loading控件 * 的界面框架,其子类中还可以指定图片动画方式等。 */ @SuppressLint("ViewConstructor") public abstract class LoadingLayout extends FrameLayout implements ILoadingLayout { static final String LOG_TAG = "PullToRefresh-LoadingLayout"; static final Interpolator ANIMATION_INTERPOLATOR = new LinearInterpolator(); private FrameLayout mInnerLayout; // 该界面布局包含的布局器 protected final ImageView mHeaderImage; // 左边用于Rotate/Flip动画的图片 protected final ProgressBar mHeaderProgress; // 左边用于动画的圆形进度条 private boolean mUseIntrinsicAnimation; // 使用内置动画的标志 private final TextView mHeaderText; // 右边主标题 private final TextView mSubHeaderText; // 右边副标题 protected final Mode mMode; // 下拉/上拉刷新模式 protected final Orientation mScrollDirection; // Pull方向 private CharSequence mPullLabel; // 拖动手势时上拉/下拉刷新文本 private CharSequence mRefreshingLabel; // 正在刷新的文本 private CharSequence mReleaseLabel; // 手势上/下拉释放后的文本 /** * 构造函数:加载Loading header或footer,并根据xml属性设置Loading header或footer, * 最后根据客户端代码中指定的Loading header或footer样式(通过ILoadingLayout),再次 * 设置Loading View。 * * @param context * @param mode * @param scrollDirection 滚动方向 * @param attrs 从PullToRefresh View传递过来的xml属性集合 */ public LoadingLayout(Context context, final Mode mode, final Orientation scrollDirection, TypedArray attrs) { super(context); mMode = mode; mScrollDirection = scrollDirection; switch (scrollDirection) { case HORIZONTAL: LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_horizontal, this); break; case VERTICAL: default: // 从layou的xml中获取自定义界面,并加入到当前界面中(这个this参数就是layou的父类) LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_vertical, this); break; } mInnerLayout = (FrameLayout) findViewById(R.id.fl_inner); mHeaderText = (TextView) mInnerLayout.findViewById(R.id.pull_to_refresh_text); mHeaderProgress = (ProgressBar) mInnerLayout.findViewById(R.id.pull_to_refresh_progress); mSubHeaderText = (TextView) mInnerLayout.findViewById(R.id.pull_to_refresh_sub_text); mHeaderImage = (ImageView) mInnerLayout.findViewById(R.id.pull_to_refresh_image); FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mInnerLayout.getLayoutParams(); switch (mode) { case PULL_FROM_END: // 上拉 layoutParams.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.TOP : Gravity.LEFT; // Load in labels mPullLabel = context.getString(R.string.pull_to_refresh_from_bottom_pull_label); mRefreshingLabel = context.getString(R.string.pull_to_refresh_from_bottom_refreshing_label); mReleaseLabel = context.getString(R.string.pull_to_refresh_from_bottom_release_label); break; case PULL_FROM_START: // 下拉 default: layoutParams.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.BOTTOM : Gravity.RIGHT; // Load in labels mPullLabel = context.getString(R.string.pull_to_refresh_pull_label); mRefreshingLabel = context.getString(R.string.pull_to_refresh_refreshing_label); mReleaseLabel = context.getString(R.string.pull_to_refresh_release_label); break; } // 设置属性 if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderBackground)) { Drawable background = attrs.getDrawable(R.styleable.PullToRefresh_ptrHeaderBackground); if (null != background) { ViewCompat.setBackground(this, background); } } if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderTextAppearance)) { TypedValue styleID = new TypedValue(); attrs.getValue(R.styleable.PullToRefresh_ptrHeaderTextAppearance, styleID); setTextAppearance(styleID.data); } if (attrs.hasValue(R.styleable.PullToRefresh_ptrSubHeaderTextAppearance)) { TypedValue styleID = new TypedValue(); attrs.getValue(R.styleable.PullToRefresh_ptrSubHeaderTextAppearance, styleID); setSubTextAppearance(styleID.data); } // Text Color attrs need to be set after TextAppearance attrs if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderTextColor)) { ColorStateList colors = attrs.getColorStateList(R.styleable.PullToRefresh_ptrHeaderTextColor); if (null != colors) { setTextColor(colors); } } if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderSubTextColor)) { ColorStateList colors = attrs.getColorStateList(R.styleable.PullToRefresh_ptrHeaderSubTextColor); if (null != colors) { setSubTextColor(colors); } } // Try and get defined drawable from Attrs Drawable imageDrawable = null; if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawable)) { imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawable); } // Check Specific Drawable from Attrs, these overrite the generic // drawable attr above switch (mode) { case PULL_FROM_START: default: if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableStart)) { imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableStart); } else if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableTop)) { Utils.warnDeprecation("ptrDrawableTop", "ptrDrawableStart"); imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableTop); } break; case PULL_FROM_END: if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableEnd)) { imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableEnd); } else if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableBottom)) { Utils.warnDeprecation("ptrDrawableBottom", "ptrDrawableEnd"); imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableBottom); } break; } // If we don't have a user defined drawable, load the default if (null == imageDrawable) { imageDrawable = context.getResources().getDrawable(getDefaultDrawableResId()); } // Set Drawable, and save width/height setLoadingDrawable(imageDrawable); reset(); } public final void setHeight(int height) { ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams(); lp.height = height; requestLayout(); } public final void setWidth(int width) { ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams(); lp.width = width; requestLayout(); } /** * 获得Loading View的宽度或高度 * @return */ public final int getContentSize() { switch (mScrollDirection) { case HORIZONTAL: return mInnerLayout.getWidth(); case VERTICAL: default: return mInnerLayout.getHeight(); } } /** * 隐藏Loading View */ public final void hideAllViews() { if (View.VISIBLE == mHeaderText.getVisibility()) { mHeaderText.setVisibility(View.INVISIBLE); } if (View.VISIBLE == mHeaderProgress.getVisibility()) { mHeaderProgress.setVisibility(View.INVISIBLE); } if (View.VISIBLE == mHeaderImage.getVisibility()) { mHeaderImage.setVisibility(View.INVISIBLE); } if (View.VISIBLE == mSubHeaderText.getVisibility()) { mSubHeaderText.setVisibility(View.INVISIBLE); } } /** * 在手势下/上拉时候回调 * @param scaleOfLayout */ public final void onPull(float scaleOfLayout) { if (!mUseIntrinsicAnimation) { onPullImpl(scaleOfLayout); } } /** * 下/上拉手势正在执行时的:刷新后的回调 */ public final void pullToRefresh() { if (null != mHeaderText) { mHeaderText.setText(mPullLabel); } // Now call the callback pullToRefreshImpl(); } /** * 正在下/上拉刷新中... */ public final void refreshing() { if (null != mHeaderText) { mHeaderText.setText(mRefreshingLabel); } if (mUseIntrinsicAnimation) { ((AnimationDrawable) mHeaderImage.getDrawable()).start(); } else { // Now call the callback refreshingImpl(); } if (null != mSubHeaderText) { mSubHeaderText.setVisibility(View.GONE); } } public final void releaseToRefresh() { if (null != mHeaderText) { mHeaderText.setText(mReleaseLabel); } // Now call the callback releaseToRefreshImpl(); } /** * 重新设置Loading View */ public final void reset() { if (null != mHeaderText) { mHeaderText.setText(mPullLabel); } mHeaderImage.setVisibility(View.VISIBLE); if (mUseIntrinsicAnimation) { ((AnimationDrawable) mHeaderImage.getDrawable()).stop(); } else { // Now call the callback resetImpl(); } if (null != mSubHeaderText) { if (TextUtils.isEmpty(mSubHeaderText.getText())) { mSubHeaderText.setVisibility(View.GONE); } else { mSubHeaderText.setVisibility(View.VISIBLE); } } } /** * 设置最近更新文本 */ @Override public void setLastUpdatedLabel(CharSequence label) { setSubHeaderText(label); } /** * 设置Loading图片 */ @Override public final void setLoadingDrawable(Drawable imageDrawable) { mHeaderImage.setImageDrawable(imageDrawable); // Set Drawable mUseIntrinsicAnimation = (imageDrawable instanceof AnimationDrawable); // Now call the callback onLoadingDrawableSet(imageDrawable); } /** * 设置下/上拉时候的文本 */ @Override public void setPullLabel(CharSequence pullLabel) { mPullLabel = pullLabel; } /** * 设置正在刷新时的文本 */ @Override public void setRefreshingLabel(CharSequence refreshingLabel) { mRefreshingLabel = refreshingLabel; } /** * 设置正在释放刷新时的文本 */ @Override public void setReleaseLabel(CharSequence releaseLabel) { mReleaseLabel = releaseLabel; } /** * 设置文本字体 */ @Override public void setTextTypeface(Typeface tf) { mHeaderText.setTypeface(tf); } public final void showInvisibleViews() { if (View.INVISIBLE == mHeaderText.getVisibility()) { mHeaderText.setVisibility(View.VISIBLE); } if (View.INVISIBLE == mHeaderProgress.getVisibility()) { mHeaderProgress.setVisibility(View.VISIBLE); } if (View.INVISIBLE == mHeaderImage.getVisibility()) { mHeaderImage.setVisibility(View.VISIBLE); } if (View.INVISIBLE == mSubHeaderText.getVisibility()) { mSubHeaderText.setVisibility(View.VISIBLE); } } /** * Callbacks for derivative Layouts */ // If we don't have a user defined drawable, load the default protected abstract int getDefaultDrawableResId(); // 设置Loading图片时候的回调 protected abstract void onLoadingDrawableSet(Drawable imageDrawable); // 在手势下/上拉时候回调 protected abstract void onPullImpl(float scaleOfLayout); // 下/上拉手势正在执行时的:刷新后的回调 protected abstract void pullToRefreshImpl(); // 正在下/上拉刷新中...的回调 protected abstract void refreshingImpl(); // 手势释放后的回调 protected abstract void releaseToRefreshImpl(); // Loading View重置后的回调 protected abstract void resetImpl(); /* * 设置头部副标题 */ private void setSubHeaderText(CharSequence label) { if (null != mSubHeaderText) { if (TextUtils.isEmpty(label)) { mSubHeaderText.setVisibility(View.GONE); } else { mSubHeaderText.setText(label); // Only set it to Visible if we're GONE, otherwise VISIBLE will // be set soon if (View.GONE == mSubHeaderText.getVisibility()) { mSubHeaderText.setVisibility(View.VISIBLE); } } } } private void setSubTextAppearance(int value) { if (null != mSubHeaderText) { mSubHeaderText.setTextAppearance(getContext(), value); } } private void setSubTextColor(ColorStateList color) { if (null != mSubHeaderText) { mSubHeaderText.setTextColor(color); } } private void setTextAppearance(int value) { if (null != mHeaderText) { mHeaderText.setTextAppearance(getContext(), value); } if (null != mSubHeaderText) { mSubHeaderText.setTextAppearance(getContext(), value); } } private void setTextColor(ColorStateList color) { if (null != mHeaderText) { mHeaderText.setTextColor(color); } if (null != mSubHeaderText) { mSubHeaderText.setTextColor(color); } } }
相关文章推荐
- Android开源项目PullToRefresh下拉刷新功能详解2
- Android项目:使用pulltorefresh开源项目扩展为下拉刷新上拉加载更多的处理方法,监听listview滚动方向 推荐
- Android 学习之 开源项目PullToRefresh的使用
- Android开源项目pulltorefresh分析与简单使用
- 4.5.2 非常好的 Good Android 学习之开源项目PullToRefresh的使用
- Android 学习之 开源项目PullToRefresh的使用
- Android项目:使用pulltorefresh开源项目扩展为下拉刷新上拉加载更多的处理方法,监听listview滚动方向
- Android 学习之 开源项目PullToRefresh的使用
- 【Android 开源项目】下拉刷新Android-PullToRefresh介绍
- Android开源项目直接用之下拉刷新(Pull To Refresh)
- 【Android 开源项目】下拉刷新Android-PullToRefresh介绍
- Android学习之开源项目PullToRefresh的使用
- 采用github上的开源项目Android-PullToRefresh实现ListView的下拉刷新和上拉加载
- Android开源项目解析:PullToRefresh
- Android 开源项目源码解析 中 android-Ultra-Pull-To-Refresh 部分
- Android开源项目pulltorefresh分析与简单使用
- Android开源项目PullToRefresh下拉刷新功能详解
- 实现上拉、下拉刷新的开源项目Android-PullToRefresh 的使用
- Android项目:使用pulltorefresh开源项目扩展为下拉刷新上拉加载更多的处理方法,监听listview滚动方向
- android开源下拉刷新控件PullToRefreshLayout修正BUG