打造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。没什么好说的,用起来很简单,你值得拥有。
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
当然你也可以点此直接下载
版权声明:本文为博主原创文章,未经博主允许不得转载。