Android PullToRefresh 详解
2015-12-15 22:41
197 查看
转载请注明出处 /article/3713289.html 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); } } ......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>整个刷新过程完成.
相关文章推荐
- Android Studio——android中的文件操作详解以及内部存储和外部存储
- 回忆:2014进入android三个月时的那种初心!纯属个人分享初学的心态。
- Android 动态添加Spinner(.java文件内实现) 实现 改变spinner 内文字属性
- [Android]代码实现ColorStateList及StateListDrawable
- Android 动态添加线性布局(.java文件内) 实现控件按比例分割空间
- Android四大组件之ConentProvider.
- Android studio使用与设置
- android中Service组件总结
- Android Studio系列教程四--Gradle基础
- Android开发之ListView-SimpleAdapter的使用
- 解决Android使用第三方库.so自动删除问题
- Android Studio 教程(干货)
- android studio 更换包名
- Android内核学习之Binder
- Android IntentService的使用和源码分析
- Android Studio 开发效率提高常用的快捷键
- Android ViewGroup 触摸事件传递机制
- Android初体验之Monkey和MonkeyRunner
- Android编程容易忽略细点
- Android View触摸事件传递机制