转载请注明出处http://blog.csdn.net/u011510784/article/details/50321743

Android PullToRefresh是Android应用开发中常用到的下拉刷新框架(https://github.com/chrisbanes/Android-PullToRefresh),有时候我们需要修改此开源框架以适应于自己项目的特殊需求,接下来我们分析此框架的工作流程以便定制自己所需要的控件,本文以分析PullToRefreshListView为例,其它类似.

在分析具体实现流程之前先看几个类:

<span style="font-family:SimHei;font-size:18px;">public abstract class LoadingLayout extends FrameLayout implements ILoadingLayout {}</span>
这个类是实现刷新布局的基类,主要初始化了刷新布局中的控件,提供了更新刷新布局状态的方法和抽象方法,它有两个子类FlipLoadingLayout和RotateLoadingLayout,实现了两种不同的刷新布局:

<span style="font-family:SimHei;font-size:18px;">public class RotateLoadingLayout extends LoadingLayout {......(省略部分代码)public RotateLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {super(context, mode, scrollDirection, attrs);mRotateDrawableWhilePulling = attrs.getBoolean(R.styleable.PullToRefresh_ptrRotateDrawableWhilePulling, true);mHeaderImage.setScaleType(ScaleType.MATRIX);mHeaderImageMatrix = new Matrix();mHeaderImage.setImageMatrix(mHeaderImageMatrix);mRotateAnimation = new RotateAnimation(0, 720, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,0.5f);mRotateAnimation.setInterpolator(ANIMATION_INTERPOLATOR);mRotateAnimation.setDuration(ROTATION_ANIMATION_DURATION);mRotateAnimation.setRepeatCount(Animation.INFINITE);mRotateAnimation.setRepeatMode(Animation.RESTART);}......protected void onPullImpl(float scaleOfLayout) {float angle;if (mRotateDrawableWhilePulling) {angle = scaleOfLayout * 90f;} else {angle = Math.max(0f, Math.min(180f, scaleOfLayout * 360f - 180f));}mHeaderImageMatrix.setRotate(angle, mRotationPivotX, mRotationPivotY);mHeaderImage.setImageMatrix(mHeaderImageMatrix);}@Overrideprotected void refreshingImpl() {mHeaderImage.startAnimation(mRotateAnimation);}@Overrideprotected void resetImpl() {mHeaderImage.clearAnimation();resetImageRotation();}private void resetImageRotation() {if (null != mHeaderImageMatrix) {mHeaderImageMatrix.reset();mHeaderImage.setImageMatrix(mHeaderImageMatrix);}}......}</span>

初始化了刷新动画的属性,并实现了刷新布局中图片状态变化时所应该执行的方法,例如当用户下拉时,刷新布局里面的图片会相应的旋转.最后一个方法是返回刷新图片的默认ID.FlipLoadingLayout类似.

IPullToRefresh接口:

<span style="font-family:SimHei;font-size:18px;">public interface IPullToRefresh<T extends View> {......(省略部分代码)//获取当前模式public Mode getCurrentMode();//是否过滤掉当前的滑动事件public boolean getFilterTouchEvents();//获取当前的可刷新控件,如ListView,ScrollView,ViewPager等public T getRefreshableView();//得到当前状态,是正在刷新还是需要释放等public State getState();        //是否支持滑动刷新public boolean isPullToRefreshEnabled();//结束刷新public void onRefreshComplete();......}</span>

在IPullToRefresh接口中定义了实现下拉刷新所用到的公共方法,可以查看相应注释.

我们看一下PullToRefreshListView的集成结构:

<span style="font-family:SimHei;font-size:18px;">public class PullToRefreshListView extends PullToRefreshAdapterViewBase<ListView>public abstract class PullToRefreshAdapterViewBase<T extends AbsListView> extends PullToRefreshBase<T> implements OnScrollListener public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T> </span>

  我们先从分析PullToRefreshBase类开始,它继承于LinearLayout,并实现了IPullToRefresh接口,首先来看PullToRefreshBase的构造方法:  

<span style="font-family:SimHei;font-size:18px;">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);}</span>


后面两个构造方法中,Mode对象表示刷新的模式,是支持上拉还是下拉或者是都支持,AnimationStyle对象表示下拉时候的动画效果,最后都会进入到init()方法中.

<span style="font-family:SimHei;font-size:18px;">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 XMLTypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);......mRefreshableView = createRefreshableView(context, attrs);addRefreshableView(context, mRefreshableView);// We need to create now layouts nowmHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);......// Let the derivative classes have a go at handling attributes, then// recycle them...handleStyledAttributes(a);a.recycle();// Finally update the UI for the modesupdateUIForMode();}</span>

首先通过getPullToRefreshScrollDirection()方法判断当前布局的方向,该方法为抽象方法,在具体实现类中重写此方法.在PullToRefreshListView类中可以找到:

<span style="font-family:SimHei;font-size:18px;">        @Overridepublic final Orientation getPullToRefreshScrollDirection() {return Orientation.VERTICAL;}</span>
接下来的mTouchSlop = config.getScaledTouchSlop();当滑动的距离大于mTouchSlop 时则认为是有滑动的意图.

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);获取自定的属性,并将初始化默认属性.

<span style="font-family:SimHei;font-size:18px;">mRefreshableView = createRefreshableView(context, attrs);addRefreshableView(context, mRefreshableView);</span>

createRefreshableView是抽象方法,在PullToRefreshListView中实现为:

<span style="font-family:SimHei;font-size:18px;">@Overrideprotected ListView createRefreshableView(Context context, AttributeSet attrs) {ListView lv = createListView(context, attrs);// Set it to this so it can be used in ListActivity/ListFragmentlv.setId(android.R.id.list);return lv;}</span>

即返回一个Listview,并将这个listview添加到PullToRefreshBase这个LinearLayout中.接下来是创建刷新的头布局和尾部局:

<span style="font-family:SimHei;font-size:18px;">protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) {LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode,getPullToRefreshScrollDirection(), attrs);layout.setVisibility(View.INVISIBLE);return layout;}</span>

mLoadingAnimationStyle在初始化的时候已经被赋值,是一个内部类.进入到createLoadingLayout()方法中:

<span style="font-family:SimHei;font-size:18px;">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);}}</span>

通过类型来创建两种不同的刷新布局,默认的为ROTATE,即RotateLoadingLayout类型,这时支持的刷新的VIew(listView,scrollView等)和刷新头尾布局已经全部添加到此linearLayout布局中,等待刷新动作.

那么是如何实现下拉刷新的呢,PullToRefreshBase重写了onInterceptTouchEvent()和onTouchEvent()方法:

<span style="font-family:SimHei;font-size:18px;">@Overridepublic 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;}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 eventsif (!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// directionswitch (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: {if (isReadyForPull()) {mLastMotionY = mInitialMotionY = event.getY();mLastMotionX = mInitialMotionX = event.getX();mIsBeingDragged = false;}break;}}return mIsBeingDragged;}</span>

在事件拦截方法中判断是否需要拦截此次的滑动事件.重点看ACTION_MOVE里面的代码,这里有判断条件叫isReadyForPull(),顾名思义,当前view是否准备好要刷新了,也就是view是否滑到了顶部:

<span style="font-family:SimHei;font-size:18px;">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;}}</span>

根据不同的判断条件执行了相应的方法,执行的方法是抽象方法,我们看一下其子类是如何实现这些抽象方法的,在PullToRefreshAdapterViewBase中我们可以找到:

<span style="font-family:SimHei;font-size:18px;">protected boolean isReadyForPullStart() {return isFirstItemVisible();}protected boolean isReadyForPullEnd() {return isLastItemVisible();}</span>

和我们预想的一样,在sFirstItemVisible()方法中具体判断了是否此view滑到了顶部.接着回到ACTION_MOVE滑动事件中,当isReadyForPull()返回true时,计算一些滑动的数据后,mIsBeingDragged被赋值为true,即onInterceptTouchEvent()返回true,事件被拦截,交给onTouchEvent()方法:

<span style="font-family:SimHei;font-size:18px;">@Overridepublic final boolean onTouchEvent(MotionEvent event) {if (!isPullToRefreshEnabled()) {return false;}// If we're refreshing, and the flag is set. Eat the eventif (!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 we're already refreshing, just scroll back to the topif (isRefreshing()) {smoothScrollTo(0);return true;}// If we haven't returned by here, then we're not in a state// to pull, so just resetsetState(State.RESET);return true;}break;}}return false;}</span>

先看一下ACTION_MOVE事件,计算完一些滑动的数据后,进入了pullEvent()方法:

<pre name="code" class="html">......
                setHeaderScroll(newScrollValue);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);}}      ......
  
  newScrollValue为滑动的距离,首先调用setHeaderScroll()方法更新刷新布局的大小,接着根据情况调用mHeaderLayout.onPull(scale)或者mFooterLayout.onPull(scale)方法更新刷新布局里面的view的变化,如刷新图标的旋转

<span style="font-family:SimHei;font-size:18px;">protected final void setHeaderScroll(int value) {......switch (getPullToRefreshScrollDirection()) {case VERTICAL:scrollTo(0, value);break;case HORIZONTAL:<span style="font-family: Arial, Helvetica, sans-serif;">scrollTo</span><span style="font-family: Arial, Helvetica, sans-serif;">(value, 0);</span>break;}}</span>
不要忘了PullToRefreshBase是LinearLayout布局,刷新布局已经被加载到了此LinearLayout的头部和尾部,通过scrollTo()来实现刷新布局大小的动态变化.我们继续看mHeaderLayout.onPull(scale),

<span style="font-family:SimHei;font-size:18px;">public final void onPull(float scaleOfLayout) {if (!mUseIntrinsicAnimation) {onPullImpl(scaleOfLayout);}}</span>
调用了onPullImpl()这个抽象方法,我们看一下它的子类是如何实现的,在RotateLoadingLayout中:

<span style="font-family:SimHei;font-size:18px;">protected void onPullImpl(float scaleOfLayout) {float angle;if (mRotateDrawableWhilePulling) {angle = scaleOfLayout * 90f;} else {angle = Math.max(0f, Math.min(180f, scaleOfLayout * 360f - 180f));}mHeaderImageMatrix.setRotate(angle, mRotationPivotX, mRotationPivotY);mHeaderImage.setImageMatrix(mHeaderImageMatrix);}</span>
没错,在这里实现了刷新图片的旋转动作的刷新,回到onTouchEvent()的ACTION_UP事件:

<span style="font-family:SimHei;font-size:18px;">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 topif (isRefreshing()) {smoothScrollTo(0);return true;}// If we haven't returned by here, then we're not in a state// to pull, so just resetsetState(State.RESET);return true;}break;}</span>
根据相应的条件设置当前刷新布局所应有的状态,如果是需要调用用户自定义的刷新事件则进入setState(State.REFRESHING, true)状态并进入:

<span style="font-family:SimHei;font-size:18px;">protected void onRefreshing(final boolean doScroll) {if (mMode.showHeaderLoadingLayout()) {mHeaderLayout.refreshing();}if (mMode.showFooterLoadingLayout()) {mFooterLayout.refreshing();}if (doScroll) {if (mShowViewWhileRefreshing) {// Call Refresh Listener when the Scroll has finishedOnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() {@Overridepublic 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 nowcallRefreshListener();}}</span>

在这里设置正在刷新的状态,并执行用户的刷新方法,当用户的刷新方法执行完后调用mPullRefreshListView.onRefreshComplete();最终会调用:

<span style="font-family:SimHei;font-size:18px;">protected void onReset() {mIsBeingDragged = false;mLayoutVisibilityChangesEnabled = true;// Always reset both layouts, just in case...mHeaderLayout.reset();mFooterLayout.reset();smoothScrollTo(0);}</span>
整个刷新过程完成.

更多相关文章

  1. Android开发指南(35) —— Toast Notifications
  2. android GPS定位,基站定位,WIFI定位开关的控制 (转)
  3. 第一个变化———由support库到Androidx
  4. Robolectric使用(一)简单使用
  5. Android(安卓)组件系列-----Activity的传值和回传值
  6. Android艺术探索学习笔记:第2章 IPC机制
  7. 本科学习Android笔记之传智播客_快速掌握Android视频教程
  8. Android(安卓)Fragment 剖析 - 01
  9. 关于实现S5PV210同时跑wince 和android双系统实现的可能性讨方法

随机推荐

  1. 6 android 滑块和进度条
  2. Android在View中的动画
  3. Android 底部导航BottomNavigationView(非
  4. Android sqlite 基础实例
  5. android webview点击返回键返回上一个htm
  6. Android - 判断当前是否使用的是Wifi网络
  7. android intent uri 传参
  8. android http协议post请求方式
  9. Android学习之通过content provider获得
  10. Android下openGL操作矩阵的函数