上拉加载更多,下拉刷新,网上比较强大比较全的一个开源库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;
}
}
基本涉及到的类别粗略过了一遍,接着我们挨着来了解怎么用这些自定义控件,至于这些控件源码就不分析了,大同小异,代码量太大太累了
首先需要在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下载: 用力一戳