Android PullToRefresh 源碼分析

轉載請註明出處 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>
整個刷新過程完成.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章