打造Android萬能上拉下拉刷新框架--XRefreshView(一)
打造Android萬能上拉下拉刷新框架--XRefreshView(三)
一、前言
自從上次發表了打造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。
沒什么好說的。用起來非常easy,你值得擁有。
2.3demo用了流式布局
非常easy,感興趣的能夠看看

2.4點擊button刷新和支持回彈
如今有支持點擊button刷新,
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變量來儲存偏移量,代碼相較於之前瞬間變得非常easy。
四、最后的說明
假設你對XRefreshView感興趣。能夠在github上關注XRefreshView
當然你也能夠點此直接下載
