转载请注明出处 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);
}
@Override
protected void refreshingImpl() {
mHeaderImage.startAnimation(mRotateAnimation);
}
@Override
protected 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 XML
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
......
mRefreshableView = createRefreshableView(context, attrs);
addRefreshableView(context, mRefreshableView);
// We need to create now layouts now
mHeaderLayout = 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 modes
updateUIForMode();
}</span>
首先通过getPullToRefreshScrollDirection()方法判断当前布局的方向,该方法为抽象方法,在具体实现类中重写此方法.在PullToRefreshListView类中可以找到:
<span style="font-family:SimHei;font-size:18px;"> @Override
public 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;">@Override
protected ListView createRefreshableView(Context context, AttributeSet attrs) {
ListView lv = createListView(context, attrs);
// Set it to this so it can be used in ListActivity/ListFragment
lv.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;">@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;
}
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: {
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;">@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();
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 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;
}</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);
}
}
......
</pre><pre>
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 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;
}</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 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();
}
}</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>
整个刷新过程完成.