您的位置:首页 > 移动开发 > Android开发

开源项目:Android-PullToRefresh

2014-02-17 15:12 281 查看
项目链接:https://github.com/chrisbanes/Android-PullToRefresh

此项目的文件目录是:



核心库是library,其余3个目录是使用pulltorefresh库的例子。



分别写了10个实例供用户开发参考:ListView、GridView、ScrollView、ExpandListView、ListFragment、ViewPager、HorizontalScrollView、ListView in ViewPager、WebView、WebView2。

2. 核心库library:



功能实现总览:

整个项目采用策略模式,把公共的部分实现为抽象类,并提供抽象接口(策略)让具体类来实现:

(1)PullToRefreshBase抽象类,实现的功能:scrollview、下拉/上拉逻辑,并把需要PullToRefresh的View(如:ListView、ScrollView等)提供接口给独立出来,让实现者提供,并把在哪里需要上拉和下拉也提供出接口、提供支持上下滚动或左右滚动的接口。

(2)LoadingLayout抽象类,实现的功能:基本Loading界面框架,但是把界面的元素(子View)独立出来,提供成接口,让实现者提供。

overscroll(弹簧阻尼效果)是通过ViewGroup的onInterceptTouchEvent()和onTouchEvent()函数通过过滤手势、捕捉手势,并且使用View的scrollTo()函数来实现的,当手势向下滑动或向上滑动的时候,使用scrollTo()函数做overscroll运动,当松开手势的时候,利用一个独立线程来scrollTo()滚动动画(采用View的post(Runnable)函数实现)到header,当refresh完成后,scrollTo()到0(不显示header/footer);上拉/下拉刷新的footer/header是通过ViewGroup的addView根据是否是下拉/上拉或双向来加入的,header或footer是在PullToRefreshBase初始化的时候一开始就被addView()到要PollToRefresh的View(ListView或GradView)之前和之后的,也就是说header/footer刚开始时是塞在了ListView或GridView之前的顶部栏/之后的底部栏后隐藏的,所以下拉/上拉刷新的时候直接把隐藏的header/footer从顶部栏/底部栏之后拉出来了。

这3个子目录分别是com.handmark.pulltorefresh.library是客户端开发人员直接使用的类,./extras也是客户端开发人员直接使用的类,是对library的补充,./interal是library和./extras内部使用的自定义View类。其中,在library包中,PullToRefreshBase是基类(抽象类),PullToRefreshListView等是客户端类,ILoadingLayout接口是针对Loading界面的回调接口,策略模式用于设置Loading界面的图像和文字,是被LoadingLayout继承实现的;IPullToRefresh是刷新动作回调接口,只在PullToRefreshBase中实现,IPullToRefresh类写成接口的目的应该是为了更加通用,目的是即使再重写一个PullToRefreshBase类都是可以的,并且PullToRefreshBase抽象类中提供了几个抽象方法:

public abstract Orientation getPullToRefreshScrollDirection(); // 子类实现scroll方向
protected abstract T createRefreshableView(Context context, AttributeSet attrs); // 子类实现可刷新View,例如ListView、GridView等
protected abstract boolean isReadyForPullEnd(); // 子类实现上拉刷新的条件,例如ListView实现上拉刷新的条件是上拉到底部
protected abstract boolean isReadyForPullStart(); // 子类实现下拉刷新的条件,例如ListView实现下拉刷新的条件是下拉到最上面第一行处


(其实,对于继承AbsListView的ListView和GridView来说,该项目又封装了一层PullToRefreshAdapterViewBase)。

LoadingLayout是Loading界面的基类,也是抽象类,该抽象类根据x拉刷新方向,加载生成了一个基本Loading界面,可以让FlipLoadingLayout和RotateLoadingLayout继承,这2个类主要是指定了Loading界面中的图片动画,并实现以下抽象方法,指定各个状态时的图片:

protected abstract int getDefaultDrawableResId(); // If we don't have a user defined drawable, load the default
protected abstract void onLoadingDrawableSet(Drawable imageDrawable); // 设置Loading图片时候的回调
protected abstract void onPullImpl(float scaleOfLayout); // 在手势下/上拉时候回调
protected abstract void pullToRefreshImpl(); // 下/上拉手势正在执行时的:刷新后的回调
protected abstract void refreshingImpl(); // 正在下/上拉刷新中...的回调
protected abstract void releaseToRefreshImpl(); // 手势释放后的回调
protected abstract void resetImpl(); // Loading View重置后的回调


3. PullToRefreshListFragment目录是pulltoRefresh在ListFragment中使用的例子,供用户参考使用。

4. PullToRefreshViewPager目录是pulltoRefresh在ViewPager中使用的例子,供用户参考使用。

核心类PullToRefreshBase,代码分析如下:

/*******************************************************************************
* Copyright 2011, 2012 Chris Banes.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.handmark.pulltorefresh.library;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import com.handmark.pulltorefresh.library.internal.FlipLoadingLayout;
import com.handmark.pulltorefresh.library.internal.LoadingLayout;
import com.handmark.pulltorefresh.library.internal.RotateLoadingLayout;
import com.handmark.pulltorefresh.library.internal.Utils;
import com.handmark.pulltorefresh.library.internal.ViewCompat;

/**
* 这个类是基类,实现了下拉/上拉刷新,并且提供抽象方法给继承类实现:提供Refreshable View、
* 提供上拉/下拉刷新的时机、支持纵向或横向刷新。
* (1)此抽象类初始化的界面是:加入refreshable view(如:ListView、ScrollView等),加入loading header
* 或footer到界面中,然后把header或footer高度或宽度设置为整个界面高度或宽度的一半,之后把整个header或
* footer塞到顶部栏或底部栏之后,使整个界面只看到了refreshable view,当下拉或上拉的时候才能看到header
* 或footer,这也是overscroll(弹簧阻尼效果)实现方案。
* (2)重写onInterceptTouchEvent()和onTouchEvent实现下拉/上拉刷新,当移动手势的时,使用scrollTo()
* 函数滚动界面,当放开手势的时候,开启一个独立线程执行scrollTo()函数来滚动界面回滚,然后等刷新完成的时候,
* 界面滚动到初始状态。【我们自定义的View及其子类的滚动动画基本上都是使用scrollTo()配合Animation及其子类来完成的】。
*
* 这个类可用于一切需要overscroll效果的view。
*
* @param <T> Refreshable View,比如:ListView ScrollView等
*/
public abstract class PullToRefreshBase<T extends View> extends LinearLayout
implements IPullToRefresh<T> {

// ===========================================================
// Constants
// ===========================================================
static final String LOG_TAG = "PullToRefresh";
static final boolean DEBUG = true;
static final boolean USE_HW_LAYERS = false;
static final float FRICTION = 2.0f;

public static final int SMOOTH_SCROLL_DURATION_MS = 200;
public static final int SMOOTH_SCROLL_LONG_DURATION_MS = 325;
static final int DEMO_SCROLL_INTERVAL = 225;

static final String STATE_STATE = "ptr_state";
static final String STATE_MODE = "ptr_mode";
static final String STATE_CURRENT_MODE = "ptr_current_mode";
static final String STATE_SCROLLING_REFRESHING_ENABLED = "ptr_disable_scrolling";
static final String STATE_SHOW_REFRESHING_VIEW = "ptr_show_refreshing_view";
static final String STATE_SUPER = "ptr_super";

// ===========================================================
// Fields
// ===========================================================

private int mTouchSlop; // TouchSlop
private float mLastMotionX, mLastMotionY; // 上一次手势坐标
private float mInitialMotionX, mInitialMotionY; // 初始手势坐标

private boolean mIsBeingDragged = false; // 标记正在手势拖动下拉刷新中...
private State mState = State.RESET; // 默认是重启状态
private Mode mMode = Mode.getDefault(); // 默认是下拉刷新

private Mode mCurrentMode; // 当前模式
T mRefreshableView; // 当前被刷新的View: ListView
private FrameLayout mRefreshableViewWrapper; // 可刷新View(例如ListView)的包装器

private boolean mShowViewWhileRefreshing = true; // 当刷新的时候是否允许展示View
private boolean mScrollingWhileRefreshingEnabled = false; // 当刷新的时候是否允许滚动
private boolean mFilterTouchEvents = true; // 是否过滤触摸事件
private boolean mOverScrollEnabled = true; // 是否允许过滚动(弹簧效果)
private boolean mLayoutVisibilityChangesEnabled = true;

private Interpolator mScrollAnimationInterpolator;
private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();

private LoadingLayout mHeaderLayout; // Loading头部界面
private LoadingLayout mFooterLayout; // Loading尾部界面

private OnRefreshListener<T> mOnRefreshListener;
private OnRefreshListener2<T> mOnRefreshListener2;
private OnPullEventListener<T> mOnPullEventListener;

private SmoothScrollRunnable mCurrentSmoothScrollRunnable;	// x拉刷新滚动子线程

// ===========================================================
// Constructors
// ===========================================================

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);
}

/**
* 这个重写函数非常重要:当该类用于ListView、ScrollView的时候,这些控件组一定会包含子控件,
* 因此一定需要重写这个方法,让其孩子加入到真正的控件中。
*/
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (DEBUG) {
Log.d(LOG_TAG, "addView: " + child.getClass().getSimpleName());
}

final T refreshableView = getRefreshableView();

if (refreshableView instanceof ViewGroup) {
((ViewGroup) refreshableView).addView(child, index, params);
} else {
throw new UnsupportedOperationException("Refreshable View is not a " +
"ViewGroup so can't addView");
}
}

@Override
public final boolean demo() {
if (mMode.showHeaderLoadingLayout() && isReadyForPullStart()) {
smoothScrollToAndBack(-getHeaderSize() * 2);
return true;
} else if (mMode.showFooterLoadingLayout() && isReadyForPullEnd()) {
smoothScrollToAndBack(getFooterSize() * 2);
return true;
}

return false;
}

/**
* 获得当前Pull模式
*/
@Override
public final Mode getCurrentMode() {
return mCurrentMode;
}

@Override
public final boolean getFilterTouchEvents() {
return mFilterTouchEvents;
}

/**
* 获得下拉/上拉Loading布局代理,主要是关于下拉/上拉Loading header or footer
* 相关的文字/View等
*/
@Override
public final ILoadingLayout getLoadingLayoutProxy() {
return getLoadingLayoutProxy(true, true);
}

@Override
public final ILoadingLayout getLoadingLayoutProxy(boolean includeStart, boolean includeEnd) {
return createLoadingLayoutProxy(includeStart, includeEnd);
}

@Override
public final Mode getMode() {
return mMode;
}

/**
* 获得可刷新的View,比如ListView、GridView等
*/
@Override
public final T getRefreshableView() {
return mRefreshableView;
}

@Override
public final boolean getShowViewWhileRefreshing() {
return mShowViewWhileRefreshing;
}

@Override
public final State getState() {
return mState;
}

/**
* @deprecated See {@link #isScrollingWhileRefreshingEnabled()}.
*/
public final boolean isDisableScrollingWhileRefreshing() {
return !isScrollingWhileRefreshingEnabled();
}

/**
* 判断是否Pull刷新使能
*/
@Override
public final boolean isPullToRefreshEnabled() {
return mMode.permitsPullToRefresh();
}

/**
* 是否允许过滚动效果(弹簧效果)
*/
@Override
public final boolean isPullToRefreshOverScrollEnabled() {
return VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD && mOverScrollEnabled
&& OverscrollHelper.isAndroidOverScrollEnabled(mRefreshableView);
}

/**
* 返回是否正在刷新
*/
@Override
public final boolean isRefreshing() {
return mState == State.REFRESHING || mState == State.MANUAL_REFRESHING;
}

@Override
public final boolean isScrollingWhileRefreshingEnabled() {
return mScrollingWhileRefreshingEnabled;
}

/**
* 重写这个函数的目的就是,拦截上拉/下拉事件给此PullToRefresh的onTouchEvent()函数处理,
* 其余事件仍旧传递给子View处理,不改变子View原本的手势处理方式。
*
* 重写该方法处理手势事件,实现下拉刷新。在这里过滤下手势事件,该PullToRefresh本身
* 只处理满足条件的下拉/上拉刷新事件,其余事件仍旧需要传递给子View(如ListView等)处理。
*
* 当手势时拖动中,并且满足下拉刷新或上拉刷新条件的时候,返回true拦截向子View传递
* 手势事件,让PullToRefresh自己在onTouchEvent()中处理上拉或下拉刷新事件。
*/
@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;
}

// 如果手势是正在拖动下拉刷新手势,则处理手势(让onTouchEvent()处理)
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: // 按下手势不处理,让子View处理
if (isReadyForPull()) { // 准备好可以上拉刷新或下拉刷新
mLastMotionY = mInitialMotionY = event.getY();
mLastMotionX = mInitialMotionX = event.getX();
mIsBeingDragged = false;
}
break;
}

return mIsBeingDragged;
}

/**
* 仅处理上拉/下拉刷新手势事件,其余手势事件在onInterceptTouchEvent()函数中
* 已经传递给子View(如ListView等)处理了。这里剩下的就是onInterceptTouchEvent()
* 函数过滤过来的上拉/下拉刷新事件。
*/
@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(); // 执行Loading header or footer的滚动事件
return true;
}
break;

case MotionEvent.ACTION_DOWN:
if (isReadyForPull()) {
mLastMotionY = mInitialMotionY = event.getY();
mLastMotionX = mInitialMotionX = event.getX();
return true;
}
break;

/*
* 因为在onInterceptTouchEvent()函数中手势移动事件被处理了,所以之后的抬起事件仍旧在
* 此onTouchEvent()事件中处理。
* 开启独立线程执行滚动回滚。
*/
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;
}

/**
* 每次刷新完数据后都调用这个方法重置PullToRefresh
*/
@Override
public final void onRefreshComplete() {
if (isRefreshing()) {
setState(State.RESET);
}
}

public final void setScrollingWhileRefreshingEnabled(boolean allowScrollingWhileRefreshing) {
mScrollingWhileRefreshingEnabled = allowScrollingWhileRefreshing;
}

/**
* @deprecated See {@link #setScrollingWhileRefreshingEnabled(boolean)}
*/
public void setDisableScrollingWhileRefreshing(boolean disableScrollingWhileRefreshing) {
setScrollingWhileRefreshingEnabled(!disableScrollingWhileRefreshing);
}

@Override
public final void setFilterTouchEvents(boolean filterEvents) {
mFilterTouchEvents = filterEvents;
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy()}.
*/
public void setLastUpdatedLabel(CharSequence label) {
getLoadingLayoutProxy().setLastUpdatedLabel(label);
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy()}.
*/
public void setLoadingDrawable(Drawable drawable) {
getLoadingLayoutProxy().setLoadingDrawable(drawable);
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy(boolean, boolean)}.
*/
public void setLoadingDrawable(Drawable drawable, Mode mode) {
getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout())
.setLoadingDrawable(drawable);
}

@Override
public void setLongClickable(boolean longClickable) {
getRefreshableView().setLongClickable(longClickable);
}

@Override
public final void setMode(Mode mode) {
if (mode != mMode) {
if (DEBUG) {
Log.d(LOG_TAG, "Setting mode to: " + mode);
}
mMode = mode;
updateUIForMode(); // 更新PullToRefresh界面
}
}

public void setOnPullEventListener(OnPullEventListener<T> listener) {
mOnPullEventListener = listener;
}

@Override
public final void setOnRefreshListener(OnRefreshListener<T> listener) {
mOnRefreshListener = listener;
mOnRefreshListener2 = null;
}

@Override
public final void setOnRefreshListener(OnRefreshListener2<T> listener) {
mOnRefreshListener2 = listener;
mOnRefreshListener = null;
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy()}.
*/
public void setPullLabel(CharSequence pullLabel) {
getLoadingLayoutProxy().setPullLabel(pullLabel);
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy(boolean, boolean)}.
*/
public void setPullLabel(CharSequence pullLabel, Mode mode) {
getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout())
.setPullLabel(pullLabel);
}

/**
* @param enable Whether Pull-To-Refresh should be used
* @deprecated This simple calls setMode with an appropriate mode based on
*             the passed value.
*/
public final void setPullToRefreshEnabled(boolean enable) {
setMode(enable ? Mode.getDefault() : Mode.DISABLED);
}

@Override
public final void setPullToRefreshOverScrollEnabled(boolean enabled) {
mOverScrollEnabled = enabled;
}

/**
* 设置正在刷新
*/
@Override
public final void setRefreshing() {
setRefreshing(true);
}

/**
* 设置正在刷新
*/
@Override
public final void setRefreshing(boolean doScroll) {
if (!isRefreshing()) {
setState(State.MANUAL_REFRESHING, doScroll);
}
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy()}.
*/
public void setRefreshingLabel(CharSequence refreshingLabel) {
getLoadingLayoutProxy().setRefreshingLabel(refreshingLabel);
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy(boolean, boolean)}.
*/
public void setRefreshingLabel(CharSequence refreshingLabel, Mode mode) {
getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout())
.setRefreshingLabel(refreshingLabel);
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy()}.
*/
public void setReleaseLabel(CharSequence releaseLabel) {
setReleaseLabel(releaseLabel, Mode.BOTH);
}

/**
* @deprecated You should now call this method on the result of
*             {@link #getLoadingLayoutProxy(boolean, boolean)}.
*/
public void setReleaseLabel(CharSequence releaseLabel, Mode mode) {
getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout())
.setReleaseLabel(releaseLabel);
}

public void setScrollAnimationInterpolator(Interpolator interpolator) {
mScrollAnimationInterpolator = interpolator;
}

@Override
public final void setShowViewWhileRefreshing(boolean showView) {
mShowViewWhileRefreshing = showView;
}

/**
* @return Either {@link Orientation#VERTICAL} or
*         {@link Orientation#HORIZONTAL} depending on the scroll direction.
*/
public abstract Orientation getPullToRefreshScrollDirection(); // TODO

final void setState(State state, final boolean... params) {
mState = state;
if (DEBUG) {
Log.d(LOG_TAG, "State: " + mState.name());
}

switch (mState) {
case RESET:
onReset();
break;

case PULL_TO_REFRESH: // 手势拖动更新
onPullToRefresh();
break;

case RELEASE_TO_REFRESH: // 释放手势拖动更新
onReleaseToRefresh();
break;

case REFRESHING: // 正在刷新中...
case MANUAL_REFRESHING:
onRefreshing(params[0]);
break;

case OVERSCROLLING: // NO-OP
break;
}

// Call OnPullEventListener
if (null != mOnPullEventListener) {
mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
}
}

/**
* Used internally for adding view. Need because we override addView to
* pass-through to the Refreshable View
* 用于把Loading header或loading footer加入到PullToRefresh中
*/
protected final void addViewInternal(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
}

/**
* Used internally for adding view. Need because we override addView to
* pass-through to the Refreshable View
* 用于把Refreshable加入到PullToRefresh中,加入到孩子之后
*/
protected final void addViewInternal(View child, ViewGroup.LayoutParams params) {
super.addView(child, -1, params);
}

/**
* 创建loading header and loading footer
* @param context
* @param mode 根据mode创建flip样式或rotate样式的loading界面
* @param attrs
* @return
*/
protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) {
LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode,
getPullToRefreshScrollDirection(), attrs);

layout.setVisibility(View.INVISIBLE);
return layout;
}

/**
* Used internally for {@link #getLoadingLayoutProxy(boolean, boolean)}.
* Allows derivative classes to include any extra LoadingLayouts.
* 创建Loading界面代理
* 获得真正的Loading界面(继承LoadingLayout)
*/
protected LoadingLayoutProxy createLoadingLayoutProxy(final boolean includeStart,
final boolean includeEnd) {

LoadingLayoutProxy proxy = new LoadingLayoutProxy();

if (includeStart && mMode.showHeaderLoadingLayout()) {
proxy.addLayout(mHeaderLayout); // 把loading header加入Loading界面代理
}
if (includeEnd && mMode.showFooterLoadingLayout()) {
proxy.addLayout(mFooterLayout); // 把loading footer加入Loading界面代理
}

return proxy;
}

/**
* This is implemented by derived classes to return the created View. If you
* need to use a custom View (such as a custom ListView), override this
* method and return an instance of your custom class.
* <p/>
* Be sure to set the ID of the view in this method, especially if you're
* using a ListActivity or ListFragment.
*
* @param context Context to create view with
* @param attrs AttributeSet from wrapped class. Means that anything you
*            include in the XML layout declaration will be routed to the
*            created View
* @return New instance of the Refreshable View
*/
protected abstract T createRefreshableView(Context context, AttributeSet attrs); // TODO

protected final void disableLoadingLayoutVisibilityChanges() {
mLayoutVisibilityChangesEnabled = false;
}

protected final LoadingLayout getFooterLayout() {
return mFooterLayout;
}

protected final int getFooterSize() {
return mFooterLayout.getContentSize();
}

protected final LoadingLayout getHeaderLayout() {
return mHeaderLayout;
}

protected final int getHeaderSize() {
return mHeaderLayout.getContentSize();
}

protected int getPullToRefreshScrollDuration() {
return SMOOTH_SCROLL_DURATION_MS;
}

protected int getPullToRefreshScrollDurationLonger() {
return SMOOTH_SCROLL_LONG_DURATION_MS;
}

protected FrameLayout getRefreshableViewWrapper() {
return mRefreshableViewWrapper;
}

/**
* Allows Derivative classes to handle the XML Attrs without creating a
* TypedArray themsevles
*
* @param a - TypedArray of PullToRefresh Attributes
*/
protected void handleStyledAttributes(TypedArray a) {
}

/**
* Implemented by derived class to return whether the View is in a state
* where the user can Pull to Refresh by scrolling from the end.
*
* @return true if the View is currently in the correct state (for example,
*         bottom of a ListView)
*/
protected abstract boolean isReadyForPullEnd();

/**
* Implemented by derived class to return whether the View is in a state
* where the user can Pull to Refresh by scrolling from the start.
*
* @return true if the View is currently the correct state (for example, top
*         of a ListView)
*/
protected abstract boolean isReadyForPullStart();

/**
* Called by {@link #onRestoreInstanceState(Parcelable)} so that derivative
* classes can handle their saved instance state.
*
* @param savedInstanceState - Bundle which contains saved instance state.
*/
protected void onPtrRestoreInstanceState(Bundle savedInstanceState) {
}

/**
* Called by {@link #onSaveInstanceState()} so that derivative classes can
* save their instance state.
*
* @param saveState - Bundle to be updated with saved state.
*/
protected void onPtrSaveInstanceState(Bundle saveState) {
}

/**
* 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;
}
}

/**
* Called when the UI has been to be updated to be in the
* {@link State#REFRESHING} or {@link State#MANUAL_REFRESHING} state.
* 正在刷新的回调函数
*
* @param doScroll - Whether the UI should scroll for this event.
*/
protected void onRefreshing(final boolean doScroll) {
if (mMode.showHeaderLoadingLayout()) { // 允许下拉刷新
mHeaderLayout.refreshing();
}
if (mMode.showFooterLoadingLayout()) { // 允许上拉刷新
mFooterLayout.refreshing();
}

if (doScroll) { // 上/下拉刷新的时候允许UI滚动事件
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();
}
}

/**
* Called when the UI has been to be updated to be in the
* {@link State#RELEASE_TO_REFRESH} state.
*/
protected void onReleaseToRefresh() {
switch (mCurrentMode) {
case PULL_FROM_END:
mFooterLayout.releaseToRefresh();
break;

case PULL_FROM_START:
mHeaderLayout.releaseToRefresh();
break;

default:
// NO-OP
break;
}
}

/**
* 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);
}

@Override
protected final void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;

setMode(Mode.mapIntToValue(bundle.getInt(STATE_MODE, 0)));
mCurrentMode = Mode.mapIntToValue(bundle.getInt(STATE_CURRENT_MODE, 0));

mScrollingWhileRefreshingEnabled = bundle.getBoolean(STATE_SCROLLING_REFRESHING_ENABLED, false);
mShowViewWhileRefreshing = bundle.getBoolean(STATE_SHOW_REFRESHING_VIEW, true);

// Let super Restore Itself
super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER));

State viewState = State.mapIntToValue(bundle.getInt(STATE_STATE, 0));
if (viewState == State.REFRESHING || viewState == State.MANUAL_REFRESHING) {
setState(viewState, true);
}

// Now let derivative classes restore their state
onPtrRestoreInstanceState(bundle);
return;
}

super.onRestoreInstanceState(state);
}

@Override
protected final Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();

// Let derivative classes get a chance to save state first, that way we
// can make sure they don't overrite any of our values
onPtrSaveInstanceState(bundle);

bundle.putInt(STATE_STATE, mState.getIntValue());
bundle.putInt(STATE_MODE, mMode.getIntValue());
bundle.putInt(STATE_CURRENT_MODE, mCurrentMode.getIntValue());
bundle.putBoolean(STATE_SCROLLING_REFRESHING_ENABLED, mScrollingWhileRefreshingEnabled);
bundle.putBoolean(STATE_SHOW_REFRESHING_VIEW, mShowViewWhileRefreshing);
bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState());

return bundle;
}

@Override
protected final void onSizeChanged(int w, int h, int oldw, int oldh) {
if (DEBUG) {
Log.d(LOG_TAG, String.format("onSizeChanged. W: %d, H: %d", w, h));
}

super.onSizeChanged(w, h, oldw, oldh);

// We need to update the header/footer when our size changes
refreshLoadingViewsSize();

// Update the Refreshable View layout
refreshRefreshableViewSize(w, h);

/**
* As we're currently in a Layout Pass, we need to schedule another one
* to layout any changes we've made here
*/
post(new Runnable() {
@Override
public void run() {
requestLayout();
}
});
}

/**
* Re-measure the Loading Views height, and adjust internal padding as
* necessary
* 设置loading header and footer视图大小、隐藏它们到顶部栏or底部栏后面
*/
protected final void refreshLoadingViewsSize() {
final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f);

int pLeft = getPaddingLeft();
int pTop = getPaddingTop();
int pRight = getPaddingRight();
int pBottom = getPaddingBottom();

switch (getPullToRefreshScrollDirection()) { // TODO
case HORIZONTAL:
if (mMode.showHeaderLoadingLayout()) {
mHeaderLayout.setWidth(maximumPullScroll);
pLeft = -maximumPullScroll;
} else {
pLeft = 0;
}

if (mMode.showFooterLoadingLayout()) {
mFooterLayout.setWidth(maximumPullScroll);
pRight = -maximumPullScroll;
} else {
pRight = 0;
}
break;

case VERTICAL:
if (mMode.showHeaderLoadingLayout()) {
mHeaderLayout.setHeight(maximumPullScroll); // 设置loading header高度
pTop = -maximumPullScroll; // 把loading header塞到顶部栏之后的距离参数
} else {
pTop = 0;
}

if (mMode.showFooterLoadingLayout()) {
mFooterLayout.setHeight(maximumPullScroll); // 设置loading footer高度
pBottom = -maximumPullScroll;
} else {
pBottom = 0;
}
break;
}

if (DEBUG) {
Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d",
pLeft, pTop, pRight, pBottom));
}
setPadding(pLeft, pTop, pRight, pBottom); // 把loading header or footer塞到顶部栏or底部栏之后
}

protected final void refreshRefreshableViewSize(int width, int height) {

// We need to set the Height of the Refreshable View to the same as
// this layout
LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) mRefreshableViewWrapper.getLayoutParams();

switch (getPullToRefreshScrollDirection()) {
case HORIZONTAL:
if (lp.width != width) {
lp.width = width;
mRefreshableViewWrapper.requestLayout();
}
break;

case VERTICAL:
if (lp.height != height) {
lp.height = height;
mRefreshableViewWrapper.requestLayout();
}
break;
}
}

/**
* Helper method which just calls scrollTo() in the correct scrolling
* direction.
* 设置界面滚动到合适的位置,当手势拖动时直接调用这个函数;当释放拖动手势时把
* 该函数放在一个独立线程中执行。
* @param value - New Scroll value
*/
protected final void setHeaderScroll(int value) {
if (DEBUG) {
Log.d(LOG_TAG, "setHeaderScroll: " + value);
}

// Clamp value to with pull scroll range
final int maximumPullScroll = getMaximumPullScroll();
value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value));

if (mLayoutVisibilityChangesEnabled) {
if (value < 0) {
mHeaderLayout.setVisibility(View.VISIBLE);
} else if (value > 0) {
mFooterLayout.setVisibility(View.VISIBLE);
} else {
mHeaderLayout.setVisibility(View.INVISIBLE);
mFooterLayout.setVisibility(View.INVISIBLE);
}
}

if (USE_HW_LAYERS) {
/**
* Use a Hardware Layer on the Refreshable View if we've scrolled at
* all. We don't use them on the Header/Footer Views as they change
* often, which would negate any HW layer performance boost.
*/
ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ?
View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE);
}

switch (getPullToRefreshScrollDirection()) {
case VERTICAL:
scrollTo(0, value);
break;

case HORIZONTAL:
scrollTo(value, 0);
break;
}
}

/**
* Smooth Scroll to position using the default duration of
* {@value #SMOOTH_SCROLL_DURATION_MS} ms.
*
* @param scrollValue - Position to scroll to
*/
protected final void smoothScrollTo(int scrollValue) {
smoothScrollTo(scrollValue, getPullToRefreshScrollDuration());
}

/**
* Smooth Scroll to position using the default duration of
* {@value #SMOOTH_SCROLL_DURATION_MS} ms.
*
* @param scrollValue - Position to scroll to
* @param listener - Listener for scroll
*/
protected final void smoothScrollTo(int scrollValue, OnSmoothScrollFinishedListener listener) {
smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0, listener);
}

/**
* Smooth Scroll to position using the longer default duration of
* {@value #SMOOTH_SCROLL_LONG_DURATION_MS} ms.
*
* @param scrollValue - Position to scroll to
*/
protected final void smoothScrollToLonger(int scrollValue) {
smoothScrollTo(scrollValue, getPullToRefreshScrollDurationLonger());
}

/**
* Updates the View State when the mode has been set. This does not do any
* checking that the mode is different to current state so always updates.
* 更新PullToRefresh界面
*/
protected void updateUIForMode() {

// We need to use the correct LayoutParam values, based on scroll
// direction
final LinearLayout.LayoutParams lp = getLoadingLayoutLayoutParams();

// Remove Header, and then add Header Loading View again if needed
if (this == mHeaderLayout.getParent()) {
removeView(mHeaderLayout);
}
if (mMode.showHeaderLoadingLayout()) {
addViewInternal(mHeaderLayout, 0, lp); // 把loading header加入到PullToRefresh
}

// Remove Footer, and then add Footer Loading View again if needed
if (this == mFooterLayout.getParent()) {
removeView(mFooterLayout);
}
if (mMode.showFooterLoadingLayout()) {
addViewInternal(mFooterLayout, lp); // 把loading footer加入到PullToRefresh
}

// Hide Loading Views 把loading header or footer隐藏到顶部栏or底部栏之后
refreshLoadingViewsSize();

// If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise
// set it to pull down
mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
}

/**
* 把Refresh View(ListView/GridView等)加入到PullToRefresh中
* @param context
* @param refreshableView
*/
private void addRefreshableView(Context context, T refreshableView) {
mRefreshableViewWrapper = new FrameLayout(context);
mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);

addViewInternal(mRefreshableViewWrapper,
new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}

private void callRefreshListener() {
if (null != mOnRefreshListener) {
mOnRefreshListener.onRefresh(this);
} else if (null != mOnRefreshListener2) {
if (mCurrentMode == Mode.PULL_FROM_START) {
mOnRefreshListener2.onPullDownToRefresh(this);
} else if (mCurrentMode == Mode.PULL_FROM_END) {
mOnRefreshListener2.onPullUpToRefresh(this);
}
}
}

/*
* 初始化PullToRefresh界面基本框架
*/
@SuppressWarnings("deprecation")
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);

if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) {
mMode = Mode.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0));
}

if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle)) {
mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger(
R.styleable.PullToRefresh_ptrAnimationStyle, 0));
}

/*
* Refreshable View,把Refreshable View加入到PullToRefresh界面(LinearLayout)中,
* By passing the attrs, we can add ListView/GridView params via XML
*/
mRefreshableView = createRefreshableView(context, attrs); // TODO
addRefreshableView(context, mRefreshableView);

/*
* We need to create now layouts now,这两个在刚开始创建PullToRefresh的时,就已经
* 根据Mode方向将其加入到了PullToRefresh中了,只是被隐藏在了顶部栏或底部栏之后了。
* 因此上拉或下拉的时,只是把loading header或loading footer从其中显示出来而已。
*/
mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);

/**
* Styleables from XML
*/
if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground)) {
Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground);
if (null != background) {
mRefreshableView.setBackgroundDrawable(background);
}
} else if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) {
Utils.warnDeprecation("ptrAdapterViewBackground", "ptrRefreshableViewBackground");
Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground);
if (null != background) {
mRefreshableView.setBackgroundDrawable(background);
}
}

if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) {
mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true);
}

if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
mScrollingWhileRefreshingEnabled =
a.getBoolean(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false);
}

// Let the derivative classes have a go at handling attributes, then
// recycle them...
handleStyledAttributes(a);
a.recycle();

// Finally update the UI for the modes,上面是获得相关属性和子界面,这里是更新PullToRefresh界面
updateUIForMode();
}

/*
* 控件是否可以下拉刷新或上拉刷新
*/
private boolean isReadyForPull() {
switch (mMode) {
case PULL_FROM_START:
return isReadyForPullStart(); // TODO: 让子类来实现下拉刷新的时机
case PULL_FROM_END:
return isReadyForPullEnd(); // TODO: 让子类来实现上拉刷新的时机
case BOTH:
return isReadyForPullEnd() || isReadyForPullStart();
default:
return false;
}
}

/*
* Actions a Pull Event 手势下/上拉事件
* @return true if the Event has been handled, false if there has been no
* change
* 执行滚动事件
*/
private void pullEvent() {
final int newScrollValue;
final int itemDimension;
final float initialMotionValue, lastMotionValue;

switch (getPullToRefreshScrollDirection()) {
case HORIZONTAL:
initialMotionValue = mInitialMotionX;
lastMotionValue = mLastMotionX;
break;

case VERTICAL:
default:
initialMotionValue = mInitialMotionY;
lastMotionValue = mLastMotionY;
break;
}

switch (mCurrentMode) {
case PULL_FROM_END:
newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
itemDimension = getFooterSize();
break;

case PULL_FROM_START:
default:
newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
itemDimension = getHeaderSize();
break;
}

setHeaderScroll(newScrollValue); // 设置laoding header滚动到指定位置

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);
}
}
}

/*
* 获得loading header or footer布局参数
*/
private LinearLayout.LayoutParams getLoadingLayoutLayoutParams() {
switch (getPullToRefreshScrollDirection()) { // TODO
case HORIZONTAL:
return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT);

case VERTICAL:
default:
return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
}
}

/*
* 获取上拉和下拉最大滚动距离
*/
private int getMaximumPullScroll() {
switch (getPullToRefreshScrollDirection()) { //TODO
case HORIZONTAL:
return Math.round(getWidth() / FRICTION); // 最大滚动距离是宽度的一半

case VERTICAL:
default:
return Math.round(getHeight() / FRICTION); // 最大滚动距离是高度的一半
}
}

/**
* Smooth Scroll to position using the specific duration
*
* @param scrollValue - Position to scroll to
* @param duration - Duration of animation in milliseconds
*/
private final void smoothScrollTo(int scrollValue, long duration) {
smoothScrollTo(scrollValue, duration, 0, null);
}

/*
* 平滑滚动,在独立线程中执行
*/
private final void smoothScrollTo(int newScrollValue, long duration, long delayMillis,
OnSmoothScrollFinishedListener listener) {

if (null != mCurrentSmoothScrollRunnable) {
mCurrentSmoothScrollRunnable.stop();
}

final int oldScrollValue;
switch (getPullToRefreshScrollDirection()) { // TODO
case HORIZONTAL:
oldScrollValue = getScrollX();
break;

case VERTICAL:
default:
oldScrollValue = getScrollY();
break;
}

if (oldScrollValue != newScrollValue) {
if (null == mScrollAnimationInterpolator) {

// Default interpolator is a Decelerate Interpolator
mScrollAnimationInterpolator = new DecelerateInterpolator();
}
mCurrentSmoothScrollRunnable =
new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener);

if (delayMillis > 0) {
postDelayed(mCurrentSmoothScrollRunnable, delayMillis);
} else {
post(mCurrentSmoothScrollRunnable);
}
}
}

private final void smoothScrollToAndBack(int y) {
smoothScrollTo(y, SMOOTH_SCROLL_DURATION_MS, 0, new OnSmoothScrollFinishedListener() {

@Override
public void onSmoothScrollFinished() {
smoothScrollTo(0, SMOOTH_SCROLL_DURATION_MS, DEMO_SCROLL_INTERVAL, null);
}
});
}

/**
* 动画样式
*/
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.
*/
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;
}
}

/**
* 创建旋转Loading界面、flip Loding界面
* @param context
* @param mode
* @param scrollDirection
* @param attrs
* @return
*/
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);
}
}
}

/**
* 下拉刷新模式、上拉刷新模式、双向刷新模式
*/
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(int modeInt) {
mIntValue = modeInt;
}

/**
* @return true if the mode permits Pull-to-Refresh
*/
boolean permitsPullToRefresh() {
return !(this == DISABLED || this == MANUAL_REFRESH_ONLY);
}

/**
* @return true if this mode wants the Loading Layout Header to be shown
* 返回是否允许下拉刷新
*/
public boolean showHeaderLoadingLayout() {
return this == PULL_FROM_START || this == BOTH;
}

/**
* @return true if this mode wants the Loading Layout Footer to be shown
* 返回是否允许上拉刷新
*/
public boolean showFooterLoadingLayout() {
return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY;
}

int getIntValue() {
return mIntValue;
}

}

// ===========================================================
// Inner, Anonymous Classes, and Enumerations
// ===========================================================

/**
* Simple Listener that allows you to be notified when the user has scrolled
* to the end of the AdapterView. See (
* {@link PullToRefreshAdapterViewBase#setOnLastItemVisibleListener}.
*
* @author Chris Banes
*/
public static interface OnLastItemVisibleListener {

/**
* Called when the user has scrolled to the end of the list
*/
public void onLastItemVisible();

}

/**
* 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);

}

/**
* 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
*/
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);

}

public static enum Orientation {
VERTICAL, HORIZONTAL;
}

/**
* 状态:重置状态、拖动手势状态、释放拖动手势状态、正在刷新模式等
*/
public static enum State {

/**
* When the UI is in a state which means that user is not interacting
* with the Pull-to-Refresh function.
* 用户还没有和PullToRefresh交互
*/
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.
* 正在刷新
*/
REFRESHING(0x8),

/**
* When the UI is currently refreshing, caused by a call to
* {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
* 代码中指定正在刷新中...
*/
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.
*
* @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;
}
}

/**
* 在独立线程中overscroll头部或尾部
*/
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;
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();
}
}


核心类LoadingLayout代码分析:

/*******************************************************************************
* Copyright 2011, 2012 Chris Banes.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.handmark.pulltorefresh.library.internal;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.handmark.pulltorefresh.library.ILoadingLayout;
import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
import com.handmark.pulltorefresh.library.PullToRefreshBase.Orientation;
import com.handmark.pulltorefresh.library.R;

/**
* 自定义Loading控件,这个是用户自定义Loading控件的基类,用户自定义的Loading控件
* 必须继承这个类,比如:FlipLoadingLayout、RotateLoadingLayout。这个类完成了Loading控件
* 的界面框架,其子类中还可以指定图片动画方式等。
*/
@SuppressLint("ViewConstructor")
public abstract class LoadingLayout extends FrameLayout implements ILoadingLayout {
static final String LOG_TAG = "PullToRefresh-LoadingLayout";

static final Interpolator ANIMATION_INTERPOLATOR = new LinearInterpolator();
private FrameLayout mInnerLayout; // 该界面布局包含的布局器

protected final ImageView mHeaderImage; // 左边用于Rotate/Flip动画的图片
protected final ProgressBar mHeaderProgress; // 左边用于动画的圆形进度条

private boolean mUseIntrinsicAnimation;	// 使用内置动画的标志

private final TextView mHeaderText; // 右边主标题
private final TextView mSubHeaderText; // 右边副标题

protected final Mode mMode; // 下拉/上拉刷新模式
protected final Orientation mScrollDirection; // Pull方向

private CharSequence mPullLabel;		// 拖动手势时上拉/下拉刷新文本
private CharSequence mRefreshingLabel;	// 正在刷新的文本
private CharSequence mReleaseLabel;		// 手势上/下拉释放后的文本

/**
* 构造函数:加载Loading header或footer,并根据xml属性设置Loading header或footer,
* 最后根据客户端代码中指定的Loading header或footer样式(通过ILoadingLayout),再次
* 设置Loading View。
*
* @param context
* @param mode
* @param scrollDirection 滚动方向
* @param attrs 从PullToRefresh View传递过来的xml属性集合
*/
public LoadingLayout(Context context, final Mode mode,
final Orientation scrollDirection, TypedArray attrs) {

super(context);

mMode = mode;
mScrollDirection = scrollDirection;

switch (scrollDirection) {
case HORIZONTAL:
LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_horizontal, this);
break;

case VERTICAL:
default: // 从layou的xml中获取自定义界面,并加入到当前界面中(这个this参数就是layou的父类)
LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_vertical, this);
break;
}

mInnerLayout = (FrameLayout) findViewById(R.id.fl_inner);
mHeaderText = (TextView) mInnerLayout.findViewById(R.id.pull_to_refresh_text);
mHeaderProgress = (ProgressBar) mInnerLayout.findViewById(R.id.pull_to_refresh_progress);
mSubHeaderText = (TextView) mInnerLayout.findViewById(R.id.pull_to_refresh_sub_text);
mHeaderImage = (ImageView) mInnerLayout.findViewById(R.id.pull_to_refresh_image);

FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mInnerLayout.getLayoutParams();

switch (mode) {
case PULL_FROM_END:	// 上拉
layoutParams.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.TOP : Gravity.LEFT;

// Load in labels
mPullLabel = context.getString(R.string.pull_to_refresh_from_bottom_pull_label);
mRefreshingLabel = context.getString(R.string.pull_to_refresh_from_bottom_refreshing_label);
mReleaseLabel = context.getString(R.string.pull_to_refresh_from_bottom_release_label);
break;

case PULL_FROM_START:	// 下拉
default:
layoutParams.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.BOTTOM : Gravity.RIGHT;

// Load in labels
mPullLabel = context.getString(R.string.pull_to_refresh_pull_label);
mRefreshingLabel = context.getString(R.string.pull_to_refresh_refreshing_label);
mReleaseLabel = context.getString(R.string.pull_to_refresh_release_label);
break;
}

// 设置属性
if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderBackground)) {
Drawable background = attrs.getDrawable(R.styleable.PullToRefresh_ptrHeaderBackground);
if (null != background) {
ViewCompat.setBackground(this, background);
}
}

if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderTextAppearance)) {
TypedValue styleID = new TypedValue();
attrs.getValue(R.styleable.PullToRefresh_ptrHeaderTextAppearance, styleID);
setTextAppearance(styleID.data);
}
if (attrs.hasValue(R.styleable.PullToRefresh_ptrSubHeaderTextAppearance)) {
TypedValue styleID = new TypedValue();
attrs.getValue(R.styleable.PullToRefresh_ptrSubHeaderTextAppearance, styleID);
setSubTextAppearance(styleID.data);
}

// Text Color attrs need to be set after TextAppearance attrs
if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderTextColor)) {
ColorStateList colors = attrs.getColorStateList(R.styleable.PullToRefresh_ptrHeaderTextColor);
if (null != colors) {
setTextColor(colors);
}
}
if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderSubTextColor)) {
ColorStateList colors = attrs.getColorStateList(R.styleable.PullToRefresh_ptrHeaderSubTextColor);
if (null != colors) {
setSubTextColor(colors);
}
}

// Try and get defined drawable from Attrs
Drawable imageDrawable = null;
if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawable)) {
imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawable);
}

// Check Specific Drawable from Attrs, these overrite the generic
// drawable attr above
switch (mode) {
case PULL_FROM_START:
default:
if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableStart)) {
imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableStart);
} else if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableTop)) {
Utils.warnDeprecation("ptrDrawableTop", "ptrDrawableStart");
imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableTop);
}
break;

case PULL_FROM_END:
if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableEnd)) {
imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableEnd);
} else if (attrs.hasValue(R.styleable.PullToRefresh_ptrDrawableBottom)) {
Utils.warnDeprecation("ptrDrawableBottom", "ptrDrawableEnd");
imageDrawable = attrs.getDrawable(R.styleable.PullToRefresh_ptrDrawableBottom);
}
break;
}

// If we don't have a user defined drawable, load the default
if (null == imageDrawable) {
imageDrawable = context.getResources().getDrawable(getDefaultDrawableResId());
}

// Set Drawable, and save width/height
setLoadingDrawable(imageDrawable);

reset();
}

public final void setHeight(int height) {
ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
lp.height = height;
requestLayout();
}

public final void setWidth(int width) {
ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
lp.width = width;
requestLayout();
}

/**
* 获得Loading View的宽度或高度
* @return
*/
public final int getContentSize() {
switch (mScrollDirection) {
case HORIZONTAL:
return mInnerLayout.getWidth();

case VERTICAL:
default:
return mInnerLayout.getHeight();
}
}

/**
* 隐藏Loading View
*/
public final void hideAllViews() {
if (View.VISIBLE == mHeaderText.getVisibility()) {
mHeaderText.setVisibility(View.INVISIBLE);
}
if (View.VISIBLE == mHeaderProgress.getVisibility()) {
mHeaderProgress.setVisibility(View.INVISIBLE);
}
if (View.VISIBLE == mHeaderImage.getVisibility()) {
mHeaderImage.setVisibility(View.INVISIBLE);
}
if (View.VISIBLE == mSubHeaderText.getVisibility()) {
mSubHeaderText.setVisibility(View.INVISIBLE);
}
}

/**
* 在手势下/上拉时候回调
* @param scaleOfLayout
*/
public final void onPull(float scaleOfLayout) {
if (!mUseIntrinsicAnimation) {
onPullImpl(scaleOfLayout);
}
}

/**
* 下/上拉手势正在执行时的:刷新后的回调
*/
public final void pullToRefresh() {
if (null != mHeaderText) {
mHeaderText.setText(mPullLabel);
}

// Now call the callback
pullToRefreshImpl();
}

/**
* 正在下/上拉刷新中...
*/
public final void refreshing() {
if (null != mHeaderText) {
mHeaderText.setText(mRefreshingLabel);
}

if (mUseIntrinsicAnimation) {
((AnimationDrawable) mHeaderImage.getDrawable()).start();
} else { // Now call the callback
refreshingImpl();
}

if (null != mSubHeaderText) {
mSubHeaderText.setVisibility(View.GONE);
}
}

public final void releaseToRefresh() {
if (null != mHeaderText) {
mHeaderText.setText(mReleaseLabel);
}

// Now call the callback
releaseToRefreshImpl();
}

/**
* 重新设置Loading View
*/
public final void reset() {
if (null != mHeaderText) {
mHeaderText.setText(mPullLabel);
}
mHeaderImage.setVisibility(View.VISIBLE);

if (mUseIntrinsicAnimation) {
((AnimationDrawable) mHeaderImage.getDrawable()).stop();
} else { // Now call the callback
resetImpl();
}

if (null != mSubHeaderText) {
if (TextUtils.isEmpty(mSubHeaderText.getText())) {
mSubHeaderText.setVisibility(View.GONE);
} else {
mSubHeaderText.setVisibility(View.VISIBLE);
}
}
}

/**
* 设置最近更新文本
*/
@Override
public void setLastUpdatedLabel(CharSequence label) {
setSubHeaderText(label);
}

/**
* 设置Loading图片
*/
@Override
public final void setLoadingDrawable(Drawable imageDrawable) {
mHeaderImage.setImageDrawable(imageDrawable);	// Set Drawable
mUseIntrinsicAnimation = (imageDrawable instanceof AnimationDrawable);

// Now call the callback
onLoadingDrawableSet(imageDrawable);
}

/**
* 设置下/上拉时候的文本
*/
@Override
public void setPullLabel(CharSequence pullLabel) {
mPullLabel = pullLabel;
}

/**
* 设置正在刷新时的文本
*/
@Override
public void setRefreshingLabel(CharSequence refreshingLabel) {
mRefreshingLabel = refreshingLabel;
}

/**
* 设置正在释放刷新时的文本
*/
@Override
public void setReleaseLabel(CharSequence releaseLabel) {
mReleaseLabel = releaseLabel;
}

/**
* 设置文本字体
*/
@Override
public void setTextTypeface(Typeface tf) {
mHeaderText.setTypeface(tf);
}

public final void showInvisibleViews() {
if (View.INVISIBLE == mHeaderText.getVisibility()) {
mHeaderText.setVisibility(View.VISIBLE);
}
if (View.INVISIBLE == mHeaderProgress.getVisibility()) {
mHeaderProgress.setVisibility(View.VISIBLE);
}
if (View.INVISIBLE == mHeaderImage.getVisibility()) {
mHeaderImage.setVisibility(View.VISIBLE);
}
if (View.INVISIBLE == mSubHeaderText.getVisibility()) {
mSubHeaderText.setVisibility(View.VISIBLE);
}
}

/**
* Callbacks for derivative Layouts
*/
// If we don't have a user defined drawable, load the default
protected abstract int getDefaultDrawableResId();
// 设置Loading图片时候的回调
protected abstract void onLoadingDrawableSet(Drawable imageDrawable);
// 在手势下/上拉时候回调
protected abstract void onPullImpl(float scaleOfLayout);
// 下/上拉手势正在执行时的:刷新后的回调
protected abstract void pullToRefreshImpl();
// 正在下/上拉刷新中...的回调
protected abstract void refreshingImpl();
// 手势释放后的回调
protected abstract void releaseToRefreshImpl();
// Loading View重置后的回调
protected abstract void resetImpl();

/*
* 设置头部副标题
*/
private void setSubHeaderText(CharSequence label) {
if (null != mSubHeaderText) {
if (TextUtils.isEmpty(label)) {
mSubHeaderText.setVisibility(View.GONE);
} else {
mSubHeaderText.setText(label);

// Only set it to Visible if we're GONE, otherwise VISIBLE will
// be set soon
if (View.GONE == mSubHeaderText.getVisibility()) {
mSubHeaderText.setVisibility(View.VISIBLE);
}
}
}
}

private void setSubTextAppearance(int value) {
if (null != mSubHeaderText) {
mSubHeaderText.setTextAppearance(getContext(), value);
}
}

private void setSubTextColor(ColorStateList color) {
if (null != mSubHeaderText) {
mSubHeaderText.setTextColor(color);
}
}

private void setTextAppearance(int value) {
if (null != mHeaderText) {
mHeaderText.setTextAppearance(getContext(), value);
}
if (null != mSubHeaderText) {
mSubHeaderText.setTextAppearance(getContext(), value);
}
}

private void setTextColor(ColorStateList color) {
if (null != mHeaderText) {
mHeaderText.setTextColor(color);
}
if (null != mSubHeaderText) {
mSubHeaderText.setTextColor(color);
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: