深度剖析:Android_PullToRefresh
Posted AnalyzeSystem
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度剖析:Android_PullToRefresh相关的知识,希望对你有一定的参考价值。
上拉加载更多,下拉刷新,网上比较强大比较全的一个开源库PullToRefresh,支持Listview、GridView、ScrollView等众多控件。下载地址:
git clone https://github.com/chrisbanes/android-PullToRefresh.git
噢,伙计,当然你也可以这样
https://github.com/chrisbanes/Android-PullToRefresh
源码剖析
整个库先从地基入手PullToRefreshBase,我们必须先了解这个类关联的类别,先看State枚举类
public static enum State {
/**
* When the UI is in a state which means that user is not interacting
* with the Pull-to-Refresh function.
* 重置初始化状态
*/
RESET(0x0),
/**
* When the UI is being pulled by the user, but has not been pulled far
* enough so that it refreshes when released.
* 拉动距离不足指定阈值,进行释放
*/
PULL_TO_REFRESH(0x1),
/**
* When the UI is being pulled by the user, and <strong>has</strong>
* been pulled far enough so that it will refresh when released.
* 拉动距离大于等于指定阈值,进行释放
*/
RELEASE_TO_REFRESH(0x2),
/**
* When the UI is currently refreshing, caused by a pull gesture.
* 由于用户手势操作,引起当前UI刷新
*/
REFRESHING(0x8),
/**
* When the UI is currently refreshing, caused by a call to
* {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
* 由于代码调用setRefreshing引起刷新UI
*/
MANUAL_REFRESHING(0x9),
/**
* When the UI is currently overscrolling, caused by a fling on the
* Refreshable View.
* 由于结束滑动,可以刷新视图
*/
OVERSCROLLING(0x10);
/**
* Maps an int to a specific state. This is needed when saving state.
* int 映射到状态,需要保存这个状态,直白的说:根据index 获取枚举类型
* @param stateInt - int to map a State to
* @return State that stateInt maps to
*/
static State mapIntToValue(final int stateInt) {
for (State value : State.values()) {
if (stateInt == value.getIntValue()) {
return value;
}
}
// If not, return default
return RESET;
}
private int mIntValue;
State(int intValue) {
mIntValue = intValue;
}
int getIntValue() {
return mIntValue;
}
}
再来看Mode的枚举类
public static enum Mode {
/**
* Disable all Pull-to-Refresh gesture and Refreshing handling
* 禁用刷新加载
*/
DISABLED(0x0),
/**
* Only allow the user to Pull from the start of the Refreshable View to
* refresh. The start is either the Top or Left, depending on the
* scrolling direction.
* 仅仅支持下动刷新
*/
PULL_FROM_START(0x1),
/**
* Only allow the user to Pull from the end of the Refreshable View to
* refresh. The start is either the Bottom or Right, depending on the
* scrolling direction.
* 仅仅支持上啦加载更多
*/
PULL_FROM_END(0x2),
/**
* Allow the user to both Pull from the start, from the end to refresh.
* 上啦下拉都支持
*/
BOTH(0x3),
/**
* Disables Pull-to-Refresh gesture handling, but allows manually
* setting the Refresh state via
* {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
* 只允许手动触发
*/
MANUAL_REFRESH_ONLY(0x4);
/**
* @deprecated Use {@link #PULL_FROM_START} from now on.
* 不赞成使用,过时了
*/
public static Mode PULL_DOWN_TO_REFRESH = Mode.PULL_FROM_START;
/**
* @deprecated Use {@link #PULL_FROM_END} from now on.
*/
public static Mode PULL_UP_TO_REFRESH = Mode.PULL_FROM_END;
/**
* Maps an int to a specific mode. This is needed when saving state, or
* inflating the view from XML where the mode is given through a attr
* int.
*
* @param modeInt - int to map a Mode to
* @return Mode that modeInt maps to, or PULL_FROM_START by default.
*/
static Mode mapIntToValue(final int modeInt) {
for (Mode value : Mode.values()) {
if (modeInt == value.getIntValue()) {
return value;
}
}
// If not, return default
return getDefault();
}
//默认状态只支持刷新
static Mode getDefault() {
return PULL_FROM_START;
}
private int mIntValue;
// The modeInt values need to match those from attrs.xml
//mode的值要与自定义属性的值相匹配
Mode(int modeInt) {
mIntValue = modeInt;
}
/**
* @return true if the mode permits Pull-to-Refresh
* 如果当前模式允许刷新则返回true
*/
boolean permitsPullToRefresh() {
return !(this == DISABLED || this == MANUAL_REFRESH_ONLY);
}
/**
* @return true if this mode wants the Loading Layout Header to be shown
* 如果该模式下能加载显示header部分,则返回true
*/
public boolean showHeaderLoadingLayout() {
return this == PULL_FROM_START || this == BOTH;
}
/**
* @return true if this mode wants the Loading Layout Footer to be shown
* 如果该模式下能加载显示footer部分,则返回true
*/
public boolean showFooterLoadingLayout() {
return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY;
}
int getIntValue() {
return mIntValue;
}
}
动画相关枚举类型AnimationStyle
public static enum AnimationStyle {
/**
* This is the default for Android-PullToRefresh. Allows you to use any
* drawable, which is automatically rotated and used as a Progress Bar.
* 默认使用旋转的进度条 ProgressBar
*/
ROTATE,
/**
* This is the old default, and what is commonly used on ios. Uses an
* arrow image which flips depending on where the user has scrolled.
* 箭头图像翻转根据用户手势
*/
FLIP;
static AnimationStyle getDefault() {
return ROTATE;
}
/**
* Maps an int to a specific mode. This is needed when saving state, or
* inflating the view from XML where the mode is given through a attr
* int.
*
* @param modeInt - int to map a Mode to
* @return Mode that modeInt maps to, or ROTATE by default.
*/
static AnimationStyle mapIntToValue(int modeInt) {
switch (modeInt) {
case 0x0:
default:
return ROTATE;
case 0x1:
return FLIP;
}
}
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);
}
}
}
HeaderLayout 、FooterLayout对应的接口ILoadingLayout
public interface ILoadingLayout {
/**
* Set the Last Updated Text. This displayed under the main label when
* Pulling
* 最后更新时间
* @param label - Label to set
*/
public void setLastUpdatedLabel(CharSequence label);
/**
* Set the drawable used in the loading layout. This is the same as calling
* <code>setLoadingDrawable(drawable, Mode.BOTH)</code>
* 设置使用的可拉的加载布局的drawable
* @param drawable - Drawable to display
*/
public void setLoadingDrawable(Drawable drawable);
/**
* Set Text to show when the Widget is being Pulled
* <code>setPullLabel(releaseLabel, Mode.BOTH)</code>
* 设置上拉显示文字
* @param pullLabel - CharSequence to display
*/
public void setPullLabel(CharSequence pullLabel);
/**
* Set Text to show when the Widget is refreshing
* <code>setRefreshingLabel(releaseLabel, Mode.BOTH)</code>
* 设置下拉刷新显示文字
* @param refreshingLabel - CharSequence to display
*/
public void setRefreshingLabel(CharSequence refreshingLabel);
/**
* Set Text to show when the Widget is being pulled, and will refresh when
* released. This is the same as calling
* <code>setReleaseLabel(releaseLabel, Mode.BOTH)</code>
* 设置释放显示文字
* @param releaseLabel - CharSequence to display
*/
public void setReleaseLabel(CharSequence releaseLabel);
/**
* Set's the Sets the typeface and style in which the text should be
* displayed. Please see
* {@link android.widget.TextView#setTypeface(Typeface)
* TextView#setTypeface(Typeface)}.
* 设置字体
*/
public void setTextTypeface(Typeface tf);
}
进入LoadingLayout构造函数
public LoadingLayout(Context context, final Mode mode, final Orientation scrollDirection, TypedArray attrs) {
super(context);
//根据不同方向选择加载不同布局
mMode = mode;
mScrollDirection = scrollDirection;
switch (scrollDirection) {
case HORIZONTAL:
//Inflater这种用法第一次见到,比较新颖,get..
LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_horizontal, this);
break;
case VERTICAL:
default:
LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_vertical, this);
break;
}
mInnerLayout = (FrameLayout) findViewById(R.id.fl_inner);
//自定义属性的另一种取法
if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderBackground)) {
Drawable background = attrs.getDrawable(R.styleable.PullToRefresh_ptrHeaderBackground);
if (null != background) {
//版本分支设置背景
ViewCompat.setBackground(this, background);
}
}
//**************************此处略*******************************
reset();
}
该类内部定义了一系列抽象方法,具体稍后再说,下面接着了解刷新和加载更多的监听接口OnRefreshListener、OnRefreshListener2
/**
* Simple Listener to listen for any callbacks to Refresh.
* 这个接口只是用与仅仅支持刷新模式
* @author Chris Banes
*/
public static interface OnRefreshListener<V extends View> {
/**
* onRefresh will be called for both a Pull from start, and Pull from
* end
* 下拉结束后能够刷新(滑动距离>=阈值)调用onRefresh回掉函数
*/
public void onRefresh(final PullToRefreshBase<V> refreshView);
}
/**
* An advanced version of the Listener to listen for callbacks to Refresh.
* This listener is different as it allows you to differentiate between Pull
* Ups, and Pull Downs.
* 当前模式支持刷新和加载更多
* @author Chris Banes
*/
public static interface OnRefreshListener2<V extends View> {
// TODO These methods need renaming to START/END rather than DOWN/UP
/**
* onPullDownToRefresh will be called only when the user has Pulled from
* the start, and released.
* 下拉刷新
*/
public void onPullDownToRefresh(final PullToRefreshBase<V> refreshView);
/**
* onPullUpToRefresh will be called only when the user has Pulled from
* the end, and released.
* 上啦加载更多
*/
public void onPullUpToRefresh(final PullToRefreshBase<V> refreshView);
}
上啦和下拉的Event事件回调接口类OnPullEventListener
/**
* Listener that allows you to be notified when the user has started or
* finished a touch event. Useful when you want to append extra UI events
* (such as sounds). See (
* {@link PullToRefreshAdapterViewBase#setOnPullEventListener}.
*
* @author Chris Banes
*/
public static interface OnPullEventListener<V extends View> {
/**
* Called when the internal state has been changed, usually by the user
* pulling.
* 通过用户上下拉引起状态改变,把触摸事件回调
* @param refreshView - View which has had it's state change.
* @param state - The new state of View.
* @param direction - One of {@link Mode#PULL_FROM_START} or
* {@link Mode#PULL_FROM_END} depending on which direction
* the user is pulling. Only useful when <var>state</var> is
* {@link State#PULL_TO_REFRESH} or
* {@link State#RELEASE_TO_REFRESH}.
*/
public void onPullEvent(final PullToRefreshBase<V> refreshView, State state, Mode direction);
}
一个滑动相关联的Runnable 实现类SmoothScrollRunnable以及一个滑动结束的监听接口OnSmoothScrollFinishedListener
final class SmoothScrollRunnable implements Runnable {
private final Interpolator mInterpolator;
private final int mScrollToY;
private final int mScrollFromY;
private final long mDuration;
private OnSmoothScrollFinishedListener mListener;
private boolean mContinueRunning = true;
private long mStartTime = -1;
private int mCurrentY = -1;
public SmoothScrollRunnable(int fromY, int toY, long duration, OnSmoothScrollFinishedListener listener) {
mScrollFromY = fromY;
mScrollToY = toY;
mInterpolator = mScrollAnimationInterpolator;
mDuration = duration;
mListener = listener;
}
@Override
public void run() {
/**
* Only set mStartTime if this is the first time we're starting,
* else actually calculate the Y delta
*/
if (mStartTime == -1) {
mStartTime = System.currentTimeMillis();
} else {
/**
* We do do all calculations in long to reduce software float
* calculations. We use 1000 as it gives us good accuracy and
* small rounding errors
*/
long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration;
normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);
final int deltaY = Math.round((mScrollFromY - mScrollToY)
* mInterpolator.getInterpolation(normalizedTime / 1000f));
mCurrentY = mScrollFromY - deltaY;
//根据计算的距离设置Hearlayout的滑动,该方法控制HeaderLayout、FooterLayout的显示与否,同时还根据参数控制硬件加速渲染相关,最终目的调用了scrollTo方法。
setHeaderScroll(mCurrentY);
}
// If we're not at the target Y, keep going...
if (mContinueRunning && mScrollToY != mCurrentY) {
ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
} else {
if (null != mListener) {
//滑动结束了回调
mListener.onSmoothScrollFinished();
}
}
}
public void stop() {
//停止滑动,并移除监听
mContinueRunning = false;
removeCallbacks(this);
}
}
static interface OnSmoothScrollFinishedListener {
void onSmoothScrollFinished();
}
PullToRefreshBase类构造函数初始化了触摸敏感系数mTouchSlop,并创建添加HeaderLayout、FooterLayout, 再调用updateUIForMode方法更具Mode修改调整UI,refreshLoadingViewsSize方法调整LoadingLayout相关大小,而影响其本质的因素,先看下面这个方法
private int getMaximumPullScroll() {
switch (getPullToRefreshScrollDirection()) {
case HORIZONTAL:
return Math.round(getWidth() / FRICTION);
case VERTICAL:
default:
return Math.round(getHeight() / FRICTION);
}
}
FRICTION这个参数固定值2.0,根据父控件宽高/固定系数得到(左右上下方向)上拉下拉对应的HeaderLayout 、FooterLayout的宽高,如果我们想缩小HeaderLayout的高度只需要加大固定系数FRICTION,但是的注意,别改得太大了导致布局显示出问题。
onInterceptTouchEvent方法重写MotionEvent.ACTION_DOWN && mIsBeingDragged先拦截触摸事件,在action_move 时,根据设置刷新ing能否继续滑动的参数以及是否能刷新, 判断是否拦截触摸事件if mScrollingWhileRefreshingEnabled && isRefreshing(),以及根据触摸滑动距离和Mode判断拦截Touch事件。
当我们HeaderLayout 、FooterLayout视图弹出,请求完了数据需要隐藏掉它们,这时候就需要用到它
@Override
public final void onRefreshComplete() {
if (isRefreshing()) {
setState(State.RESET);
}
}
setStatue方法里面调用onReset,继续跟进发现LoadingLayout调用了reset方法,并且smoothScrollTo方法调用,间接的new 了SmoothScrollRunnable,一个定时长的减速scrollTo动画执行
/**
* Called when the UI has been to be updated to be in the
* {@link State#RESET} state.
*/
protected void onReset() {
mIsBeingDragged = false;
mLayoutVisibilityChangesEnabled = true;
// Always reset both layouts, just in case...
mHeaderLayout.reset();
mFooterLayout.reset();
smoothScrollTo(0);
}
而onTouchEvent方法 内部则是根据各种状态判断设置当前的状态枚举类型State
@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;
}
ILoadingLayout 接口的实现类LoadingLayoutProxy ,也是LoadingLayout的代理,通过HashSet存储LoadingLayout,设置LoadingLayout的属性则通过该代理来设置,实例如下:
/**
* @deprecated You should now call this method on the result of
* {@link #getLoadingLayoutProxy()}.
*/
public void setPullLabel(CharSequence pullLabel) {
getLoadingLayoutProxy().setPullLabel(pullLabel);
}
方法setRefreshing (boolean ) 原理是在改变State状态,从而改变ui
@Override
public final void setRefreshing(boolean doScroll) {
if (!isRefreshing()) {
setState(State.MANUAL_REFRESHING, doScroll);
}
}
onPullToRefresh方法根据mCurrentMode调用HeaderLayout、FooterLayout(LoadingLayout)各自的抽象方法具体实现稍后再说,诸如此类方法就不一一列举
/**
* Called when the UI has been to be updated to be in the
* {@link State#PULL_TO_REFRESH} state.
*/
protected void onPullToRefresh() {
switch (mCurrentMode) {
case PULL_FROM_END:
mFooterLayout.pullToRefresh();
break;
case PULL_FROM_START:
mHeaderLayout.pullToRefresh();
break;
default:
// NO-OP
break;
}
}
基本涉及到的类别粗略过了一遍,接着我们挨着来了解怎么用这些自定义控件,至于这些控件源码就不分析了,大同小异,代码量太大太累了
调用实例
ListView
运行效果图如下:
首先需要在xml引用控件,activity获取实例得到PullToRefreshListView,进行初始化
protected void initialPullToRefreshListView() {
adapter = new SimpleAdapter(this, null);
mListView = mPullToRefreshListView.getRefreshableView();
mListView.setAdapter(adapter);
//Adapter的List.size=0的时候用到的,建议工厂生产view以适应多种情景
mPullToRefreshListView.setEmptyView(getEmptyView());
//不能刷新
mPullToRefreshListView.setMode(Mode.DISABLED);
onRefresh();
}
调用方法刷新获取数据,改变Mode和监听,setOnLastItemVisibleListener是否滑动到底部的监听,而mPullToRefreshListView.getOnRefreshListener()相关方法在源码中不存在,自己添加的一个返回方法
public void onRefresh() {
mPullToRefreshListView.postDelayed(new Runnable() {
@Override
public void run() {
pageSize=0;
int [] arrays ={5,10};
int size = new Random().nextInt(2);
size = arrays[size];
adapter.onRefresh(getData(pageSize, size));
mPullToRefreshListView.onRefreshComplete();
if (size == 10) {
pageSize++;
setRefreshListener2();
mPullToRefreshListView
.setOnLastItemVisibleListener(null);
}else {
if(size==0){
mPullToRefreshListView.setMode(Mode.PULL_FROM_START);
Toast.makeText(getApplicationContext(), "暂无更多数据,请稍后再试", Toast.LENGTH_SHORT).show();
}
if(mPullToRefreshListView.getMode()!=Mode.PULL_FROM_START||mPullToRefreshListView.getOnRefreshListener()==null){
setRefreshListener1();
}
}
}
}, 3000);
}
setRefreshListener1 和setRefreshListener2方法分别是支持只刷新和刷新加载更多都支持的接口building,例如setRefreshLisenter1:
public void setRefreshListener1() {
mPullToRefreshListView.setMode(Mode.PULL_FROM_START);
mPullToRefreshListView
.setOnRefreshListener(new OnRefreshListener<ListView>() {
@Override
public void onRefresh(
PullToRefreshBase<ListView> refreshView) {
setLable(refreshView);
MainActivity.this.onRefresh();
}
});
setLastItemVisibleListener();
}
GridView、ViewPager、ExpandListView、WebView等相关控件的关于刷新加载更多的这块的调用实例都大同小异,不一一列举,如果实在搞不定可以参考官方simple,这里有个实践:PullToRefreshScrollView+NoScrollListView 实现不能滑动的ListView嵌套到PullToRefreshScrollView里面,随便添加header 或者其他任意布局,PullToRefreshScrollView 的刷新和加载更多监听加载数据从而调用NoScrollView的adapter.notifyChangeData();已达到无缝衔接的滑动。
源码以及修改后的Libary下载: 用力一戳
以上是关于深度剖析:Android_PullToRefresh的主要内容,如果未能解决你的问题,请参考以下文章