Android-PullToRefresh 是一款非常出名的上拉加载和下拉刷新控件,相信同学们都使用过这个控件,Android-PullToRefresh 控件内部是如何实现的呢,我们通过阅读源码来一窥究竟。这是它的 github 的地址,下载下来后可以看到无论是 ListView、SrollView、WebView 等,都是继承自 PullToRefreshBase 这个抽象类,这个抽象类在整个实现过程中起着举足轻重的作用,并且该控件使用了模板方法模式,封装了上拉加载和下拉刷新的逻辑以及事件的回调,同时将触发这些操作的条件交由子类来实现(这里面很容易看到的是,我们在判断控件是否滑动到顶部或是底部时的具体逻辑,是通过子类实现父类的抽象方法来实现的),也就是说,我们可以很方便的实现一个新的刷新控件(只要实现模板定义的抽象方法或是重写相关方法)。
- public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T> {}
PullToRefreshBase 它是一个继承自 LinearLayout 的泛型抽象类,当我们继承 PullToRefreshBase 抽象类时,通过限定了刷新控件必须是 View 或是 View 的子类。并且 Android-PullToRefresh 控件实现了两种方式的刷新方式,分别是垂直的上拉加载下拉刷新和水平的右拉刷新左拉加载,也就是说我们需要识别当前控件到底是横向还是纵向,前面也说过了这个控件使用了模板方法模式,因此我们将识别横向还是纵向的判断交由子类来实现,比如 ListView 支持纵向滚动,而 HorizontalScrollView 支持横向滚动,基于这些,我们在 PullToRefreshBase 中定义了一个抽象方法 getPullToRefreshScrollDirection 方法。
- public abstract Orientation getPullToRefreshScrollDirection();
下面代码是 PullToRefreshBase 的初始化方法,第一步是设置控件的布局方向,前面也讲过,识别刷新控件的到底是垂直滚动还是水平滚动是交由具体的子类来实现的。
- private void init(Context context, AttributeSet attrs) {
- //(1)通过getPullToRefreshScrollDirection方法判断是垂直滑动还是水平滑动。
- switch (getPullToRefreshScrollDirection()) {
- case HORIZONTAL:
- setOrientation(LinearLayout.HORIZONTAL);
- break;
- case VERTICAL:
- default:
- setOrientation(LinearLayout.VERTICAL);
- break;
- }
- //(2)设置内容居中。
- setGravity(Gravity.CENTER);
- //(3)使用 ViewConfiguration 获取用户手指滑动距离,用于判定滑动依据。
- ViewConfiguration config = ViewConfiguration.get(context);
- mTouchSlop = config.getScaledTouchSlop();
- // (4)获取自定义属性
- 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));
- }
- //(5)创建真正刷新的控件
- mRefreshableView = createRefreshableView(context, attrs);
- //(6)添加刷新控件
- addRefreshableView(context, mRefreshableView);
- //(7)根据刷新和加载的模式分别创建头部刷新控件和底部加载控件
- mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
- mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);
- //(8)获取自定义属性
- 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);
- }
- //(9)自定义属性派生给子类
- handleStyledAttributes(a);
- a.recycle();
- //(10)根据mode刷新UI
- updateUIForMode();
- }
PullToRefresh 控件定义了很多自定义属性,比如控件执行的模式、背景、刷新状态下是否能滚动控件等,可以通过 xml 来设置,当然也可以通过 Java 代码来设置,除了在 PullToRefreshBase 获取自定义属性外,作者还定义了一个 handleStyledAttributes 空方法,使用者可以通过重写这个方法来获取自定义属性。上面第 5 步创建真正刷新的控件,其实就是获取我们的 ListView、SrollView、WebView 等相关刷新控件 View,获取这些控件的实现也是交由具体的子类来实现,因此定义了一个泛型的抽象方法 createRefreshableView,返回的类型是 T 也就是 View 或者继承 View 的子类。第 7 步通过 createLoadingLayout 方法创建头部和底部的刷新 View。
下面是 createLoadingLayout 方法的具体实现:
- private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();
- protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) {
- LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode,
- getPullToRefreshScrollDirection(), attrs);
- layout.setVisibility(View.INVISIBLE);
- return layout;
- }
AnimationStyle 是一个内部枚举类,定义了刷新 View 在执行刷新时的动画方式,目前定义了两种方式,同学们可以试着设置这两种方式试试,这个枚举类内部定义了 createLoadingLayout 方法用于创建这两种动画方式的刷新 View,根据这两种动画方式分别创建 RotateLoadingLayout 和 FlipLoadingLayout 实例,RotateLoadingLayout 和 FlipLoadingLayout 都是继承自 LoadingLayout 这个抽象类(HeaderLayout 和 FooterLayout 的布局)。既然顶部和底部的刷新控件已经创建完毕,接下来将它们设置为隐藏,毕竟在一开始进入页面时头部和底部的刷新控件应该不是处于显示状态的。
到了这里我们的头部刷新控件和底部刷新控件已经创建完毕,第 10 步调用方法 udateUIForMode,该方法就是将它们添加到我们的整个刷新控件的容器中去:
- protected void updateUIForMode() {
- final LayoutParams lp = getLoadingLayoutLayoutParams();
- if (this == mHeaderLayout.getParent()) {
- removeView(mHeaderLayout);
- }
- if (mMode.showHeaderLoadingLayout()) {
- addViewInternal(mHeaderLayout, 0, lp);
- }
- if (this == mFooterLayout.getParent()) {
- removeView(mFooterLayout);
- }
- if (mMode.showFooterLoadingLayout()) {
- addViewInternal(mFooterLayout, lp);
- }
- refreshLoadingViewsSize();
- mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
- }
上面代码中在添加头部刷新控件和底部刷新控件时都会检查之前刷新控件是否存在,如果存在就先移除,为什么这样做呢?这是因为我们在设置 PullToRefresh 控件支持的刷新模式时,需要按照指定刷新模式来添加相应的头部和底部刷新控件,这里的刷新模式是通过 Mode 这个内部枚举类来判别(默认是 PULL_FROM_START),比如说什么时候添加头部的刷新控件,在该枚举类提供了 showHeaderLoadingLayout 方法用于判断当前支持的模式是 PULL_FROM_START(用于下拉刷新) 还是 BOTH(支持下拉刷新和上拉加载两种模式);又比如说什么时候添加底部的刷新控件,相应的在该枚举类也提供了 showFooterLoadingLayout 方法用于判断当前支持的模式是 PULL_FROM_END(用于上拉加载) 或是 BOTH(支持下拉刷新和上拉加载两种模式) 亦或是 MANUAL_PEFRESH_ONLY(禁止上拉和下拉操作,但可以手动设置上拉加载操作)。
无论是下拉刷新(右拉)还是上拉加载(左拉),比如下拉刷新时,我们的手指在屏幕上能一直往下拖动一段距离,在上面的 refreshLoadingViewSize 方法中设置了这个拖动的一个距离:
- 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()) {
- 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);
- pTop = -maximumPullScroll;
- } else {
- pTop = 0;
- }
- if (mMode.showFooterLoadingLayout()) {
- mFooterLayout.setHeight(maximumPullScroll);
- pBottom = -maximumPullScroll;
- } else {
- pBottom = 0;
- }
- break;
- }
- setPadding(pLeft, pTop, pRight, pBottom);
- }
在上面方法中分别通过垂直滚动还是水平滚动来设置顶部 View(或左边) 和底部 View(或右边) 的宽度或高度,这里面的宽度和高度值是通过 getMaximumPullScroll 方法中获取,该方法根据垂直和水平两种方式,分别获取当前 PullToRefresh 控件的高度或宽度,再将得到的宽高度进行相应的计算,这里的值只是一个经验值(高度或宽度的一半再乘以 1.2)maximumPullScroll,最后将得到的这个经验值 maximumPullScroll 以及 PullToRefresh 的 padding 值进行处理,比如在水平滚动下设置左边和右边的刷新控件的宽度为 maximumPullScroll,同时在显示左边 View 的前提下设置 PullToRefresh 控件距离左边为 - maximumPullScroll(隐藏掉左边的刷新控件),右边 View 也是一样,设置 PullToRefresh 控件距离右边 - maximumPullScroll(隐藏掉右边的刷新控件),从文字说明上看,如果大家还是不怎么明白,可以看下面两幅图:
上面这幅图是我们在设置头部 View 的高度为 maximumPullScroll ,然后将 pTop 设置为 0 时展示的,可以看到,我们的 HeadView 并没有隐藏掉,那我们将 pTop 设置为 - maximumPullScroll,这时的展示图如下:
PullToRefresh 内部的布局已经讲解清楚,接下来具体分析一下触摸事件的处理,查看源码我们知道 PullToRefreshBase 继承自 LinearLayout,也就是说 PullToRefreshBase 本身就是一个容器,用于存放顶部和底部的 LoadingLayout 以及中间的 T(ListView、SrollView、WebView),那么问题来了,当我们手指触摸 PullToRefreshBase 容器时,整个容器的拖动何时处理,内部 View 的滚动何时处理,这就需要进行相应的事件传递。PullToRefreshBase 内部重写了 onIterceptTouchEvent 和 onTouchEvent 方法,下面我们先对这两个有事件相关的回调函数进行扫盲。
onIterceptTouchEvent 方法,我们称它为事件的拦截,默认返回 super.onInterceptTouchEvent(ev),事件默认会被拦截,并将拦截的事件交由当前 View 的 onTouchEvent 进行处理;如果返回 true,事件的处理和返回 super.onInterceptTouchEvent(ev) 的事件处理一样,交由当前 View 的 onTouchEvent 进行处理;如果返回 false,表示事件放行(不拦截),当前 View 上的事件会被传递到子 View 上,由子 View 的 onTouchEvent 进行处理。
onTouchEvent 方法,我们称它为事件的响应,默认返回 super.onTouchEvent(ev),同事事件会从当前 View 向上传递,由上层 View 的 onTouchEvent 来接收处理;如果返回 false,事件处理与返回 super.onTouchEvent(ev) 的事件处理一样,交由上层处理;如果返回 true,说明当前的事件被接收并被消费。
接下来我们从 onIterceptTouchEvent 方法也就是事件的拦截来说起。
- @Override public final boolean onInterceptTouchEvent(MotionEvent event) {
- /*
- T1:当不支持上拉加载和下拉刷新操作时,不进行事件的拦截,交由子View(我们的刷新控件,如:ListView、ScrollView等)来处理。
- */
- if (!isPullToRefreshEnabled()) {
- return false;
- }
- final int action = event.getAction();
- /*
- T2:当手指取消触摸或是离开屏幕时,说明一次刷新的操作结束(无论是否真正进行了刷新和加载的操作),
- 最后将事件的处理交由子View(T)来处理。
- */
- if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
- mIsBeingDragged = false;
- return false;
- }
- /*
- T3:当我们手指在屏幕上拖动时(mIsBeingDragged标志位为true,说明当前满足了刷新和加载操作),进行
- 事件的拦截,交由自身onTouchEvent来处理。
- */
- if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
- return true; //由自身onTouchEvent消费
- }
- /*
- T4:mIsBeingDragged标志位什么时候为true?需要手指触摸该控件时进行判断:当前的子View(T)是否到达了顶部以满足下拉刷新的条件,或是
- 当前的子View(T)是否滑动到了底部以满足上拉加载的条件。
- */
- switch (action) {
- case MotionEvent.ACTION_MOVE:
- {
- /*
- T5:mScrollingWhileRefreshingEnabled标志位用于说明在刷新过程中,手指再次滑动控件,这时控件是否能滑动,当mScrollingWhileRefreshingEnabled为
- false时,说明刷新状态下控件不能被滑动,这时就需要事件的拦截。这里的isRefreshing用于判断当前是否处于刷新状态。
- */
- if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
- return true;
- }
- /*
- T6:isReadyForPull方法的作用是检查当前控件是否满足刷新和加载操作条件,可以看的isReadyForPull方法中调用了isReadyForPullStart和isReadyForPullEnd
- 抽象方法,这两个方法交由实现类来实现。
- */
- if (isReadyForPull()) {
- final float y = event.getY(),
- x = event.getX();
- final float diff,
- oppositeDiff,
- absDiff;
- 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()) {
- //T7:下拉刷新
- mLastMotionY = y;
- mLastMotionX = x;
- mIsBeingDragged = true;
- if (mMode == Mode.BOTH) {
- mCurrentMode = Mode.PULL_FROM_START;
- }
- } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
- //T8:上拉加载
- mLastMotionY = y;
- mLastMotionX = x;
- mIsBeingDragged = true;
- if (mMode == Mode.BOTH) {
- mCurrentMode = Mode.PULL_FROM_END;
- }
- }
- }
- }
- break;
- }
- case MotionEvent.ACTION_DOWN:
- {
- if (isReadyForPull()) {
- /*
- T9:当我们手指按下屏幕时,试想下如果当前子View(T)位于顶部或是底部时,我们拦截事件会导致T无法上拉或下拉操作,因此
- 通过isReadyForPull方法检查当前T是否位于顶部或是滑动到了底部位置,这时将mIsBeingDragged设置为false,不进行事件的拦截,交由
- T来处理。
- */
- mLastMotionY = mInitialMotionY = event.getY();
- mLastMotionX = mInitialMotionX = event.getX();
- mIsBeingDragged = false;
- }
- break;
- }
- }
- return mIsBeingDragged;
- }
在事件不需要拦截的时候,事件的一系列响应交由子 View 来处理,比如 ListView 的滑动和点击事件,因此,总结在以下几种情况事件不需要拦截:
针对第一条:我们在上面的事件拦截方法的第一部分 T1 中可以看到一个条件判断语句,isPullToRefreshEnabled 方法用于判断当前控件是否支持手动拖动来触发上拉加载和下拉刷新操作,也就是说当控件不支持上拉加载和下拉刷新时,需要将事件传递给它的子 View(T) 来处理,这样我们的 T(ListView、SrollView…) 才能处理自身的触摸事件。针对第二条:T2 手指离开屏幕或是取消触摸事件交由 T 来处理。针对第三条:在手指触摸按下或是触摸时(前提在设置 Mode 为 FULL_FROM_START 或是 FULL_FROM_END 亦或是 BOTH), 依据 Mode 判断 T 是否需要刷新或加载,具体看以下代码:
- /**
- * 何时下拉刷新和上拉加载,交由子类来实现
- */
- private boolean isReadyForPull() {
- switch (mMode) {
- case PULL_FROM_START:
- return isReadyForPullStart();
- case PULL_FROM_END:
- return isReadyForPullEnd();
- case BOTH:
- return isReadyForPullEnd() || isReadyForPullStart();
- default:
- return false;
- }
- }
调用的抽象方法交由子类来实现,isReadyForPullStart 方法用于说明当前控件是否做好下拉刷新的准备,isReadyForPullEnd 方法用于说明当前控件是否做好上拉加载的准备。最后我们看在执行到 MotionEvent.ACTION_DOWN,也就是手指按下屏幕时,如果加载或刷新都已经准备好了,这里面的 mIsBeingDraggd 标志位设置为 false,最后 return mIsBeingDraggd,既然加载或刷新都准备好了,为什么还要进行事件的拦截呢?在这里举个例子:如果我们进入页面,此时 ListView 已经滑动到了顶部,这时我们想往下滑,如果在 onIterceptTouchEvent 方法中返回了 true,What? 页面怎么滑动不了了,这是因为我们将事件拦截了,所以在手指按下屏幕是并且满足刷新或加载条件时,事件还是交由 T 来处理。
以上的做法可以保证我们的 T 能接受到事件并得到处理(也就是满足了最基本的 T 的滑动点击等事件的处理),何时执行我们的刷新和加载操作,也就是在设置 Mode 为 FULL_FROM_START 或是 FULL_FROM_END 亦或是 BOTH 的条件下,并满足刷新或加载的条件成立。查看 onIterceptTouchEvent 方法中的 T6 段代码,手指在屏幕滑动时,并满足刷新和加载条件的成立,判断滑动的距离是否满足下拉或是上拉操作,只有当这些条件成立,接下来才会去处理刷新和加载的相关逻辑;在 T7 段代码处,当 Mode 为 PULL_FROM_START 或 BOTH 时,并且下拉刷新的条件成立,这时将 mCurrentMode 设置为 PULL_FROM_START,将标志位是否开始拖动 mIsBeingDragged 设置为 true;在 T8 段代码处,当 Mode 为 PULL_FROM_END 或 BOTH 时,并且上拉加载的条件成立,这时将 mCurrentMode 设置为 PULL_FROM_END ,将标志位是否开始拖动 mIsBeingDragged 设置为 true。当 mIsBeingDragged 为 ture 时,自身的 onTouchEvent 开始处理 LoadingLayout 显示以及相关操作。
事件的拦截已经讲解完毕,接下来聊聊事件的响应 onTouchEvent 方法的实现:
- @Override
- public final boolean onTouchEvent(MotionEvent event) {
- if (!isPullToRefreshEnabled()) {
- return false;
- }
- 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();
- return true;
- }
- break;
- }
- case MotionEvent.ACTION_DOWN: {
- if (isReadyForPull()) {
- mLastMotionY = mInitialMotionY = event.getY();
- mLastMotionX = mInitialMotionX = event.getX();
- return true;
- }
- break;
- }
- 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 (isRefreshing()) {
- smoothScrollTo(0);
- return true;
- }
- setState(State.RESET);
- return true;
- }
- break;
- }
- }
- return false;
- }
onTouchEvent 方法中,手指按下去的操作没有执行,这是因为在事件拦截方法中按下的动作并没有进行拦截,这里我们重点分析手指拖动以及手指抬起时的相关操作逻辑。在拖动时(ACTION_MOVE),通过不停的调用 pullEvent 方法来显示或隐藏 LoadingLayout,pullEvent 方法如下:
- 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);
- if (newScrollValue != 0 && !isRefreshing()) {
- /*
- 计算出下拉或上拉时 刷新View显示的占比
- */
- 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)) {
- /*
- 头部和底部刷新View显示的高度低于刷新View的高度,说明还没有到达刷新状态
- */
- setState(State.PULL_TO_REFRESH);
- } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
- /*
- 头部和底部刷新View显示的高度高于刷新View的高度,说明手指松开执行刷新
- */
- setState(State.RELEASE_TO_REFRESH);
- }
- }
- }
该方法主要负责 HeaderLayout 和 FooterLayout 的显示与隐藏、HeaderLayout 和 FooterLayout 显示的百分比以及刷新状态的更新,其中通过 setHeaderScroll 方法来实现 HeaderLayout 和 FooterLayout 的显示与隐藏:
- protected final void setHeaderScroll(int value) {
- 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) {
- 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;
- }
- }
通过 scrollTo 方法来实现 HeaderLayout 和 FooterLayout 的移动,scrollTo 通过拖动的距离移动整个控件的位置从而达到拖动 HeaderLayout 和 FooterLayout 的效果。在 pullEvent 方法中,通过 HeaderLayout 和 FooterLayout 显示的区域来判断当前是否处于刷新状态(也就是 HeaderLayout 和 FooterLayout 完全被显示出来),当 State 状态为 PULL_TO_REFRESH 时还没有到达刷新状态,当 State 状态为 RELEASE_TO_REFRESH 时说明已经处于刷新状态。手指离开屏幕时可以根据该状态做出相应的操作。
- 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 (isRefreshing()) {
- smoothScrollTo(0);
- return true;
- }
- setState(State.RESET);
- return true;
- }
- break;
- }
从上面的代码中可以看到当 State 状态为 RELEASE_TO_REFRESH 时说明已经处于刷新状态,这时调用 setState(State.REFRESHING ,true) 方法将 State 设置成 REFRESHING,说明当前处于刷新状态:
- final void setState(State state, final boolean... params) {
- mState = state;
- 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;
- }
- if (null != mOnPullEventListener) {
- mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
- }
- }
这时的 State 状态为 REFRESHING,我直接查看 REFRESHING 这条分支,调用了 onRefreshing 方法,继续往下查看:
- protected void onRefreshing(final boolean doScroll) {
- /*
- 通知给相应的Layout当前处于刷新状态。
- */
- if (mMode.showHeaderLoadingLayout()) {
- mHeaderLayout.refreshing();
- }
- if (mMode.showFooterLoadingLayout()) {
- mFooterLayout.refreshing();
- }
- if (doScroll) {
- if (mShowViewWhileRefreshing) {
- 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 {
- callRefreshListener();
- }
- }
在 onRefreshing 方法中,前两个条件判断语句用于通知 HeaderLayout 和 FooterLayout 当前处于刷新状态(可以进行更新文案、时间之类的操作),OnSmoothScrollFinishedListener 用于刷新和加载操作的监听,接着通过 smoothScrollTo 方法实现 OnSmoothScrollFinishedListener 接口中 onSmoothScrollFinished 方法的回调。
- protected final void smoothScrollTo(int scrollValue, OnSmoothScrollFinishedListener listener) {
- smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0, listener);
- }
- private final void smoothScrollTo(int newScrollValue, long duration, long delayMillis,
- OnSmoothScrollFinishedListener listener) {
- if (null != mCurrentSmoothScrollRunnable) {
- mCurrentSmoothScrollRunnable.stop();
- }
- final int oldScrollValue;
- switch (getPullToRefreshScrollDirection()) {
- 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);
- }
- }
- }
HeaderLayout 和 FooterLayout 的回弹实现和刷新的监听通过 View 的 postDelayed 和 post 方法来实现,通过 post 方法,可以获取当前线程 (UI 线程) 的 Handler。
- 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() {
- if (mStartTime == -1) {
- mStartTime = System.currentTimeMillis();
- } else {
- 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 (mContinueRunning && mScrollToY != mCurrentY) {
- ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
- } else {
- if (null != mListener) {
- mListener.onSmoothScrollFinished();
- }
- }
- }
- public void stop() {
- mContinueRunning = false;
- removeCallbacks(this);
- }
- }
从上面代码中可以看到,当 HeaderLayout 和 FooterLayout 回弹效果结束后就开始回调 OnSmoothScrollFinishedListener 接口中 onSmoothScrollFinished 方法。在 onSmoothScrollFinished 方法中调用了 callRefreshListener 方法。
- 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);
- }
- }
- }
将刷新事件回调给 UI 线程,交由 UI 线程处理相关操作。最后调用 onRefreshComplete 方法进行重置。
到这里事件的处理已经讲解完毕,总结来说,PullToRefreshBase 主要负责以下几件事:
PullToRefreshBase 这个抽象类的职责已经讲解了差不多了,现在我们就来自定义一个 ScrollView 的上拉加载和下拉刷新的控件,首先实现 PullToRefreshBase 抽线类定以的几个方法
- @Override
- public Orientation getPullToRefreshScrollDirection() {
- return null;
- }
- @Override
- protected ScrollView createRefreshableView(Context context, AttributeSet attrs) {
- return null;
- }
- @Override
- protected boolean isReadyForPullEnd() {
- return false;
- }
- @Override
- protected boolean isReadyForPullStart() {
- return false;
- }
这些方法的作用在上面都讲解过,这里我们只要实现这些方法就可以:
- public class PullToRefreshScrollView extends PullToRefreshBase<ScrollView> {
- public PullToRefreshScrollView(Context context) {
- super(context);
- }
- public PullToRefreshScrollView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public PullToRefreshScrollView(Context context, Mode mode) {
- super(context, mode);
- }
- public PullToRefreshScrollView(Context context, Mode mode, AnimationStyle style) {
- super(context, mode, style);
- }
- @Override
- public final Orientation getPullToRefreshScrollDirection() {
- return Orientation.VERTICAL;
- }
- @Override
- protected ScrollView createRefreshableView(Context context, AttributeSet attrs) {
- ScrollView scrollView;
- scrollView = new ScrollView(context, attrs);
- scrollView.setId(R.id.scrollview);
- return scrollView;
- }
- @Override
- protected boolean isReadyForPullStart() {
- return mRefreshableView.getScrollY() == 0;
- }
- @Override
- protected boolean isReadyForPullEnd() {
- View scrollViewChild = mRefreshableView.getChildAt(0);
- if (null != scrollViewChild) {
- return mRefreshableView.getScrollY() >= (scrollViewChild.getHeight() - getHeight());
- }
- return false;
- }
- }
到此整个 PullToRefresh 控件的大体实现思路已经讲解完毕。
来源: http://blog.csdn.net/hai_qing_xu_kong/article/details/73802453