打造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
当然你也能够点此直接下载