Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 打造android萬能上拉下拉刷新框架——XRefreshView (二)

打造android萬能上拉下拉刷新框架——XRefreshView (二)

編輯:關於Android編程

一、前言

自從上次發表了打造android萬能上拉下拉刷新框架——XRefreshView (一)之後,期間的大半個月一直都很忙,但是我每天晚上下班以後都有在更新和維護XRefreshView,也根據一些朋友的意見解決了一些問題,這次之所以寫這篇文章,是因為XRefreshView已經到了一個功能相對可靠和穩定的一個階段。下面我會介紹下XrefreshView的最新功能和用法,以及實現的主要思路。

二、更新

2.1判斷下拉上拉刷新時機方式的修改

之前是通過 refreshView.setRefreshViewType(XRefreshViewType.ABSLISTVIEW);這樣來預先設置view的類型來選擇對應判斷時機的方法,現在已經不用這樣做了,改成了下面這樣。

/**
	 * @return Whether it is possible for the child view of this layout to
	 *         scroll up. Override this if the child view is a custom view.
	 */
	public boolean canChildPullDown() {
		if (child instanceof AbsListView) {
			final AbsListView absListView = (AbsListView) child;
			return canScrollVertically(child, -1)
					|| absListView.getChildCount() > 0
					&& (absListView.getFirstVisiblePosition() > 0 || absListView
							.getChildAt(0).getTop() < absListView
							.getPaddingTop());
		} else {
			return canScrollVertically(child, -1) || child.getScrollY() > 0;
		}
	}

	public boolean canChildPullUp() {
		if (child instanceof AbsListView) {
			AbsListView absListView = (AbsListView) child;
			return canScrollVertically(child, 1)
					|| absListView.getLastVisiblePosition() != mTotalItemCount - 1;
		} else if (child instanceof WebView) {
			WebView webview = (WebView) child;
			return canScrollVertically(child, 1)
					|| webview.getContentHeight() * webview.getScale() != webview
							.getHeight() + webview.getScrollY();
		} else if (child instanceof ScrollView) {
			ScrollView scrollView = (ScrollView) child;
			View childView = scrollView.getChildAt(0);
			if (childView != null) {
				return canScrollVertically(child, 1)
						|| scrollView.getScrollY() != childView.getHeight()
								- scrollView.getHeight();
			}
		}else{
			return canScrollVertically(child, 1);
		}
		return true;
	}

	/**
	 * 用來判斷view在豎直方向上能不能向上或者向下滑動
	 * @param view v
	 * @param direction 方向    負數代表向上滑動 ,正數則反之
	 * @return
	 */
	public boolean canScrollVertically(View view, int direction) {
		return ViewCompat.canScrollVertically(view, direction);
	}
正如你所見,ViewCompat.canScrollVertically(view, direction)這個方法可以用來判斷view能不能向上或者向下滑動,從而可以判斷view有沒有到達頂部或者底部,在4.0以後在個方法通常是很管用的,但是2.3.3以前則不是這樣,為了兼容2.3.3我又做了一些view類型的判斷,通過view的類型來提供特別的判斷到達頂部或者底部的方法。一般情況下,常用的view通過上述的方法都可以准確的判斷出有沒有到達頂部或者底部,但是如果你要刷新的是一個復雜的或者自定義的view,也可以通過以下的方式來做
refreshView.setOnTopRefreshTime(new OnTopRefreshTime() {

			@Override
			public boolean isTop() {
				return stickyLv.getFirstVisiblePosition() == 0;
			}
		});
		refreshView.setOnBottomLoadMoreTime(new OnBottomLoadMoreTime() {

			@Override
			public boolean isBottom() {
				return stickyLv.getLastVisiblePosition() == mTotalItemCount - 1;
			}
		});

XRefreshView把判斷view到達頂部和底部的工作交給你去做了,你只要告訴XRefreshView什麼時候是正確的刷新時機就行了,與上次博客中提到的方法不同的是,XRefreshView這次提供了兩個接口,把頂部和底部的判斷時機給分開了,主要是考慮到下拉刷新和上拉加載有的時候並不是都需要的。

2.2headview和footview上下移動時的方式的修改

一開始,移動headview和footview我是通過屬性動畫來移動的

 

public static void moveChildAndAddedView(View child, View addView,
			float childY, float addY, int during, AnimatorListener... listener) {
		// 屬性動畫移動
		ObjectAnimator y = ObjectAnimator.ofFloat(child, y, child.getY(),
				childY);
		ObjectAnimator y2 = ObjectAnimator.ofFloat(addView, y,
				addView.getY(), addY);

		AnimatorSet animatorSet = new AnimatorSet();
		animatorSet.playTogether(y, y2);
		animatorSet.setDuration(during);
		if (listener.length > 0)
			animatorSet.addListener(listener[0]);
		animatorSet.start();
	}
後來為了兼容2.3.3我還專門下載了動畫開源庫NineOldAndroidsNineOldAndroids,這個庫究竟是干嘛的呢?在API3.0(Honeycomb), SDK新增了一個android.animation包,裡面的類是實現動畫效果相關的類,通過Honeycomb API,能夠實現非常復雜的動畫效果,但是如果開發者想在3.0以下使用這一套API, 則需要使用開源框架Nine Old Androids,在這個庫中會根據我們運行的機器判斷其SDK版本,如果是API3.0以上則使用Android自帶的動畫類,否則就使用Nine Old Androids庫中,這是一個兼容庫。 (注:紅色部分的字我是直接引用夏安明大神的博客原文,一直都在看他的博客,所以一直很佩服他,他的博客的質量都很不錯。)之後兼容性的問題就算處理好了,但後來Xutils 4群的大炮告訴我,XRefreshView在下拉的時候會有抖動的情況,我知道了這個情況以後就開始找問題,後來發現是因為用屬性動畫來移動header的問題,不用屬性動畫就好了,仔細想一想,屬性動畫其實是通過反射來屬性對應的get/set方法來執行的,畢竟是反射,而在手指移動的時候會觸發大量的action_move,每個action_move都會做一次反射,那麼就會做大量的反射工作,大量的密集的反射就會導致性能方面有所降低,所以出現了抖動的情況。放棄反射以後,我用的是view.offsetTopAndBottom(deltaY)這個方法,看方法的注釋
    /**
     * Offset this view's vertical location by the specified number of pixels.
     *
     * @param offset the number of pixels to offset the view by
     */
翻譯過來就是在豎直方向上以像素為單位來移動view。沒什麼好說的,用起來很簡單,你值得擁有。

2.3demo用了流式布局

很簡單,感興趣的可以看看

\

2.4點擊按鈕刷新和支持回彈

現在有支持點擊按鈕刷新,

	protected void onResume() {
		super.onResume();
		xRefreshView.startRefresh();
	}
還有就是可以支持設置是否下拉刷新和上拉加載
// 設置是否可以下拉刷新
		refreshView.setPullRefreshEnable(false);
		// 設置是否可以上拉加載
		refreshView.setPullLoadEnable(false);
大炮說如果可以在不可以下拉刷新和上拉加載的情況下也可以有回彈的效果就好了,於是現在的版本就支持了。

三、實現相關
3.1前後變化

之前我是把headview,被刷新的childview和footview當成了三個部分來看待,並且分別記錄了一開始的各個view的位置

/**
	 * 在開始上拉加載更多的時候,記錄下childView一開始的Y軸坐標
	 */
	private float mChildY = -1;
	/**
	 * 在開始上拉加載更多的時候,記錄下FootView一開始的Y軸坐標
	 */
	private float mFootY = -1;
	/**
	 * 在開始上拉加載更多的時候,記錄下HeadView一開始的Y軸坐標
	 */
	private float mHeadY = -1;
然後在手指移動的時候不斷更新當前各個view的y軸坐標,最後再來逐個移動各個view,這樣做無意中就加大了工作量以及工作的復雜度,後來我想到了把三個部分當成一個整體,這樣以來就簡單很多了,也就不再需要那麼多的變量。


3.2實現過程

3.2.1測量

/*
	 * 丈量視圖的寬、高。寬度為用戶設置的寬度,高度則為header, content view, footer這三個子控件的高度之和。
	 * 
	 * @see android.view.View#onMeasure(int, int)
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		int width = MeasureSpec.getSize(widthMeasureSpec);
		int childCount = getChildCount();
		int finalHeight = 0;
		for (int i = 0; i < childCount; i++) {
			View child = getChildAt(i);
			measureChild(child, widthMeasureSpec, heightMeasureSpec);
			finalHeight += child.getMeasuredHeight();
		}
		setMeasuredDimension(width, finalHeight);
	}
3.2.2布局
@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		LogUtils.d(onLayout mHolder.mOffsetY= + mHolder.mOffsetY);
		mFootHeight = mFooterView.getMeasuredHeight();
		int childCount = getChildCount();
		int top = getPaddingTop() + mHolder.mOffsetY;
		for (int i = 0; i < childCount; i++) {
			View child = getChildAt(i);
			if (child == mHeaderView) {
				// 通過把headerview向上移動一個headerview高度的距離來達到隱藏headerview的效果
				child.layout(0, top - mHeaderViewHeight,
						child.getMeasuredWidth(), top);
			} else {
				child.layout(0, top, child.getMeasuredWidth(),
						child.getMeasuredHeight() + top);
				top += child.getMeasuredHeight();
			}
		}
	}

其中

int top = getPaddingTop() + mHolder.mOffsetY;
mHolder.mOffsetY是用來記錄整個view在y軸方向上的偏移量的。這裡之所以加上mHolder.mOffsetY,是因為在拖動刷新的過程中view的改變會引起系統重新測量和布局,加上這個偏移量以後,可以在系統重新布局的時候保住view當前的位置,不恢復到初始位置。

3.2.3 事件處理並移動view

	public boolean dispatchTouchEvent(MotionEvent ev) {
		final int action = MotionEventCompat.getActionMasked(ev);
		int deltaY = 0;
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mHasSendCancelEvent = false;
			mHasSendDownEvent = false;
			mLastY = (int) ev.getRawY();
			mInitialMotionY = mLastY;

			if (!mScroller.isFinished() && !mPullRefreshing && !mPullLoading) {
				mScroller.forceFinished(true);
			}
			break;
		case MotionEvent.ACTION_MOVE:
			if (mPullLoading || mPullRefreshing || !isEnabled()) {
				return super.dispatchTouchEvent(ev);
			}
			mLastMoveEvent = ev;
			int currentY = (int) ev.getRawY();
			deltaY = currentY - mLastY;
			mLastY = currentY;
			// intercept the MotionEvent only when user is not scrolling
			if (!isIntercepted && Math.abs(deltaY) < mTouchSlop) {
				isIntercepted = true;
				return super.dispatchTouchEvent(ev);
			}
			LogUtils.d(isTop= + mContentView.isTop() + ;isBottom=
					+ mContentView.isBottom());
			deltaY = (int) (deltaY / OFFSET_RADIO);
			if (mContentView.isTop()
					&& (deltaY > 0 || (deltaY < 0 && mHolder
							.hasHeaderPullDown()))) {
				sendCancelEvent();
				updateHeaderHeight(currentY, deltaY);
			} else if (mContentView.isBottom()
					&& (deltaY < 0 || deltaY > 0 && mHolder.hasFooterPullUp())) {
				sendCancelEvent();
				updateFooterHeight(deltaY);
			} else if (mContentView.isTop() && !mHolder.hasHeaderPullDown()
					|| mContentView.isBottom() && !mHolder.hasFooterPullUp()) {
				if (deltaY > 0)
					sendDownEvent();
			}
			break;
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			// if (mHolder.mOffsetY != 0 && mRefreshViewListener != null
			// && !mPullRefreshing && !mPullLoading) {
			// mRefreshViewListener.onRelease(mHolder.mOffsetY);
			// }
			if (mContentView.isTop() && mHolder.hasHeaderPullDown()) {
				// invoke refresh
				if (mEnablePullRefresh && mHolder.mOffsetY > mHeaderViewHeight) {
					mPullRefreshing = true;
					mHeaderView.setState(XRefreshViewState.STATE_REFRESHING);
					if (mRefreshViewListener != null) {
						mRefreshViewListener.onRefresh();
					}
				}
				resetHeaderHeight();
			} else if (mContentView.isBottom() && mHolder.hasFooterPullUp()) {
				if (mEnablePullLoad) {
					int offset = 0 - mHolder.mOffsetY - mFootHeight;
					startScroll(offset, SCROLL_DURATION);
					startLoadMore();
				} else {
					int offset = 0 - mHolder.mOffsetY;
					startScroll(offset, SCROLL_DURATION);
				}
			}
			mLastY = -1; // reset
			mInitialMotionY = 0;
			isIntercepted = true;
			break;
		}
		return super.dispatchTouchEvent(ev);
	}
首先可以看到,所以的事件處理都在dispatchTouchEvent(MotionEvent ev)方法裡進行,而之前則是分成兩部分進行的,在onInterceptTouchEvent(MotionEvent ev)方法中進行攔截,事件處理則在onTouchEvent(MotionEvent ev)中進行。這樣做是因為大炮說他下拉刷新的時候,由於子view非常復雜,子view有時候會搶占事件,造成卡住不刷新了。我們都知道子view是可以通過requestDisallowInterceptTouchEvent來請求父類不要攔截事件,那麼onInterceptTouchEvent方法就不會執行,那我們下拉刷新也就不可靠了,所以為了解決這個問題,我把所有的處理都丟到dispatchTouchEvent方法中做。

再來看看sendCancelEvent()和sendDownEvent()這兩個方法

private void sendCancelEvent() {
		if (!mHasSendCancelEvent) {
			setRefreshTime();
			mHasSendCancelEvent = true;
			mHasSendDownEvent = false;
			MotionEvent last = mLastMoveEvent;
			MotionEvent e = MotionEvent.obtain(
					last.getDownTime(),
					last.getEventTime()
							+ ViewConfiguration.getLongPressTimeout(),
					MotionEvent.ACTION_CANCEL, last.getX(), last.getY(),
					last.getMetaState());
			dispatchTouchEventSupper(e);
		}
	}

	private void sendDownEvent() {
		if (!mHasSendDownEvent) {
			LogUtils.d(sendDownEvent);
			mHasSendCancelEvent = false;
			mHasSendDownEvent = true;
			isIntercepted = false;
			final MotionEvent last = mLastMoveEvent;
			if (last == null)
				return;
			MotionEvent e = MotionEvent.obtain(last.getDownTime(),
					last.getEventTime(), MotionEvent.ACTION_DOWN, last.getX(),
					last.getY(), last.getMetaState());
			dispatchTouchEventSupper(e);
		}
	}
觸摸事件一開始肯定會被子view接收到的,如果是listview的話,就會有item的點擊效果出現,這很正常,但是如果此時觸發下拉刷新的話,同時又有item的點擊效果,那麼看起來就不是很自然,所有此時可以通過sendCancelEvent()來給子view發送一個cancel事件,這樣item的點擊效果就會消失。還有當我們拉下headerview以後沒有達到刷新條件,並且接著有往上推把headerview又完全隱藏了,此時就應該i把事件交還給子view,讓子view接收到事件並移動,可以通過sendDownEvent來達到效果。

最後說下移動view的處理

當手指在拖動的時候,

public void moveView(int deltaY) {
		mHolder.move(deltaY);
		mChild.offsetTopAndBottom(deltaY);
		mHeaderView.offsetTopAndBottom(deltaY);
		mFooterView.offsetTopAndBottom(deltaY);
		invalidate();
	}
public int mOffsetY;

	public void move(int deltaY) {
		mOffsetY += deltaY;
	}
通過moveView方法來移動view,並把偏移量存了下來。

當手指離開以後,通過scroller來移動view

mScroller = new Scroller(getContext(), new LinearInterpolator());
這裡用了線性的插值器,表示移動的時候是勻速變動的
/**
	 * 
	 * @param offsetY
	 *            滑動偏移量,負數向上滑,正數反之
	 * @param duration
	 *            滑動持續時間
	 */
	public void startScroll(int offsetY, int duration) {
		mScroller.startScroll(0, mHolder.mOffsetY, 0, offsetY, duration);
		invalidate();
	}
	public void computeScroll() {
		super.computeScroll();
		if (mScroller.computeScrollOffset()) {
			int lastScrollY = mHolder.mOffsetY;
			int currentY = mScroller.getCurrY();
			int offsetY = currentY - lastScrollY;
			lastScrollY = currentY;
			moveView(offsetY);

			LogUtils.d(currentY= + currentY + ;mHolder.mOffsetY=
					+ mHolder.mOffsetY);
		} else {
			LogUtils.d(scroll end mOffsetY= + mHolder.mOffsetY);
		}
	}
從上面可以看出,整個移動過程中只用到了一個mOffsetY變量來儲存偏移量,代碼相較於之前瞬間變得很簡單。

四、最後的說明

如果你對XRefreshView感興趣,可以在github上關注XRefreshView

 

 

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved