Ultra-Pull-To-Refresh源码解析

项目地址:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh
该项目作者已经不维护了,由于公司开发使用的该框架,本着追求“知其然知其所以然”的心态,对这个框架的源码进行解读一下。

目录:

1.基于PtrClassicFrameLayout、PtrClassicDefaultHeader查看如何使用
2.onMeasure、onLayout的分析
3.事件分发的分析

这个项目的拓展性非常好,按照PtrClassicFrameLayout、PtrClassicDefaultHeader上的代码写法,就可以定制自己想要的下拉刷新的效果

如何使用

先来查看PtrClassicFrameLayout

public class PtrClassicFrameLayout extends PtrFrameLayout {

private PtrClassicDefaultHeader mPtrClassicHeader;

public PtrClassicFrameLayout(Context context) {
    super(context);
    initViews();
}

public PtrClassicFrameLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    initViews();
}

public PtrClassicFrameLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initViews();
}

private void initViews() {
    mPtrClassicHeader = new PtrClassicDefaultHeader(getContext());
    setHeaderView(mPtrClassicHeader);
    addPtrUIHandler(mPtrClassicHeader);
}

代码比较简单,继承 PtrClassicFrameLayout ,创建一个PtrClassicDefaultHeader,然后添加进去,再来看PtrClassicDefaultHeader

public class PtrClassicDefaultHeader extends FrameLayout implements 
PtrUIHandler{

...
...
...

 public PtrClassicDefaultHeader(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initViews(attrs);
}

protected void initViews(AttributeSet attrs) {
    TypedArray arr = getContext().obtainStyledAttributes(attrs, R.styleable.PtrClassicHeader, 0, 0);
    if (arr != null) {
        mRotateAniTime = arr.getInt(R.styleable.PtrClassicHeader_ptr_rotate_ani_time, mRotateAniTime);
    }
    buildAnimation();
    View header = LayoutInflater.from(getContext()).inflate(R.layout.cube_ptr_classic_default_header, this);

    mRotateView = header.findViewById(R.id.ptr_classic_header_rotate_view);

    mTitleTextView = (TextView) header.findViewById(R.id.ptr_classic_header_rotate_view_header_title);
    mLastUpdateTextView = (TextView) header.findViewById(R.id.ptr_classic_header_rotate_view_header_last_update);
    mProgressBar = header.findViewById(R.id.ptr_classic_header_rotate_view_progressbar);

    resetView();
}

@Override
public void onUIReset(PtrFrameLayout frame) {
    resetView();
    mShouldShowLastUpdate = true;
    tryUpdateLastUpdateTime();
}

@Override
public void onUIRefreshPrepare(PtrFrameLayout frame) {

    mShouldShowLastUpdate = true;
    tryUpdateLastUpdateTime();
    mLastUpdateTimeUpdater.start();

    mProgressBar.setVisibility(INVISIBLE);

    mRotateView.setVisibility(VISIBLE);
    mTitleTextView.setVisibility(VISIBLE);
    if (frame.isPullToRefresh()) {
        mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down_to_refresh));
    } else {
        mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down));
    }
}

@Override
public void onUIRefreshBegin(PtrFrameLayout frame) {
    mShouldShowLastUpdate = false;
    hideRotateView();
    mProgressBar.setVisibility(VISIBLE);
    mTitleTextView.setVisibility(VISIBLE);
    mTitleTextView.setText(R.string.cube_ptr_refreshing);

    tryUpdateLastUpdateTime();
    mLastUpdateTimeUpdater.stop();
}

@Override
public void onUIRefreshComplete(PtrFrameLayout frame) {

    hideRotateView();
    mProgressBar.setVisibility(INVISIBLE);

    mTitleTextView.setVisibility(VISIBLE);
    mTitleTextView.setText(getResources().getString(R.string.cube_ptr_refresh_complete));

    // update last update time
    SharedPreferences sharedPreferences = getContext().getSharedPreferences(KEY_SharedPreferences, 0);
    if (!TextUtils.isEmpty(mLastUpdateTimeKey)) {
        mLastUpdateTime = new Date().getTime();
        sharedPreferences.edit().putLong(mLastUpdateTimeKey, mLastUpdateTime).commit();
    }
}  
 @Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {

    final int mOffsetToRefresh = frame.getOffsetToRefresh();
    final int currentPos = ptrIndicator.getCurrentPosY();
    final int lastPos = ptrIndicator.getLastPosY();

    if (currentPos < mOffsetToRefresh && lastPos >= mOffsetToRefresh) {
        if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) {
            crossRotateLineFromBottomUnderTouch(frame);
            if (mRotateView != null) {
                mRotateView.clearAnimation();
                mRotateView.startAnimation(mReverseFlipAnimation);
            }
        }
    } else if (currentPos > mOffsetToRefresh && lastPos <= mOffsetToRefresh) {
        if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) {
            crossRotateLineFromTopUnderTouch(frame);
            if (mRotateView != null) {
                mRotateView.clearAnimation();
                mRotateView.startAnimation(mFlipAnimation);
            }
        }
    }
}

实现了PtrUIHandler,然后initViews()也很简单,加载header布局跟初始化动画,接着看PtrUIHandler,

public interface PtrUIHandler {

/**
 * When the content view has reached top and refresh has been completed, view will be reset.
 *
 * @param frame
 */
public void onUIReset(PtrFrameLayout frame);

/**
 * prepare for loading
 *
 * @param frame
 */
public void onUIRefreshPrepare(PtrFrameLayout frame);

/**
 * perform refreshing UI
 */
public void onUIRefreshBegin(PtrFrameLayout frame);

/**
 * perform UI after refresh
 */
public void onUIRefreshComplete(PtrFrameLayout frame);

public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator);

}
在PtrClassicDefaultHeader中主要就是实现了PtrUIHandler 接口,在onUIReset、onUIRefreshPrepare、onUIRefreshBegin、onUIRefreshComplete进行下拉刷新效果的编写,那么我们现在就清楚了,PtrUIHandler 这个接口就是提供给大家去定制自己的下拉刷新效果的。

mPtrFrame.setPtrHandler(new PtrHandler() {
        @Override
        public void onRefreshBegin(PtrFrameLayout frame) {
            updateData();
        }

        @Override
        public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
            return PtrDefaultHandler.checkContentCanBePulledDown(frame, content, header);
        }
    });

接下来,我们在代码中设置PtrHandler,在onRefreshBegin中进行网络请求、在checkCanDoRefresh中进行自定义是否阻拦滑动

那么我们继续查看主类PtrFrameLayout 中的代码,这里主要贴有用的代码

public class PtrFrameLayout extends ViewGroup {
protected View mContent;
private View mHeaderView;
// optional config for define header and content in xml file
private int mHeaderId = 0;
private int mContainerId = 0;
// config
private int mDurationToClose = 200;
private int mDurationToCloseHeader = 1000;

private PtrUIHandlerHolder mPtrUIHandlerHolder = PtrUIHandlerHolder.create();
private PtrHandler mPtrHandler;
// working parameters
private ScrollChecker mScrollChecker;
private int mPagingTouchSlop;
private int mHeaderHeight;
private PtrIndicator mPtrIndicator;

public PtrFrameLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    mPtrIndicator = new PtrIndicator();

    TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.PtrFrameLayout, 0, 0);
    if (arr != null) {

        mHeaderId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_header, mHeaderId);
        mContainerId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_content, mContainerId);

        mPtrIndicator.setResistance(
                arr.getFloat(R.styleable.PtrFrameLayout_ptr_resistance, mPtrIndicator.getResistance()));

        mDurationToClose = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close, mDurationToClose);
        mDurationToCloseHeader = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close_header, mDurationToCloseHeader);

        float ratio = mPtrIndicator.getRatioOfHeaderToHeightRefresh();
        ratio = arr.getFloat(R.styleable.PtrFrameLayout_ptr_ratio_of_header_height_to_refresh, ratio);
        mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio);

        mKeepHeaderWhenRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_keep_header_when_refresh, mKeepHeaderWhenRefresh);

        mPullToRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_pull_to_fresh, mPullToRefresh);
        arr.recycle();
    }

    mScrollChecker = new ScrollChecker();

    final ViewConfiguration conf = ViewConfiguration.get(getContext());
    mPagingTouchSlop = conf.getScaledTouchSlop() * 2;
}

@Override
protected void onFinishInflate() {
    final int childCount = getChildCount();
    if (childCount > 2) {
        throw new IllegalStateException("PtrFrameLayout only can host 2 elements");
    } else if (childCount == 2) {
        if (mHeaderId != 0 && mHeaderView == null) {
            mHeaderView = findViewById(mHeaderId);
        }
        if (mContainerId != 0 && mContent == null) {
            mContent = findViewById(mContainerId);
        }
     .....
     .....
}

在构造方法中,主要就是读取了一些自定义属性进行配置和初始化PtrIndicator 、ScrollChecker,然后onFinishflate中,如果没有初始化mHeaderView 和mContent,就进行初始化

onMeasure onLayout

继续查看onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (mHeaderView != null) {
        measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0);
        MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
        mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        mPtrIndicator.setHeaderHeight(mHeaderHeight);
    }

    if (mContent != null) {
        measureContentView(mContent, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

测量mHeaderView与mContent的大小,并把mHeaderHeight 赋值给mPtrIndicator

继续查看onLayout

@Override
protected void onLayout(boolean flag, int i, int j, int k, int l) {
    layoutChildren();
}

private void layoutChildren() {
    int offsetX = mPtrIndicator.getCurrentPosY();
    int paddingLeft = getPaddingLeft();
    int paddingTop = getPaddingTop();

    if (mHeaderView != null) {
        MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
        final int left = paddingLeft + lp.leftMargin;
        final int top = paddingTop + lp.topMargin + offsetX - mHeaderHeight;
        final int right = left + mHeaderView.getMeasuredWidth();
        final int bottom = top + mHeaderView.getMeasuredHeight();
        mHeaderView.layout(left, top, right, bottom);
        }
    }
    if (mContent != null) {
        if (isPinContent()) {
            offsetX = 0;
        }
        MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams();
        final int left = paddingLeft + lp.leftMargin;
        final int top = paddingTop + lp.topMargin + offsetX;
        final int right = left + mContent.getMeasuredWidth();
        final int bottom = top + mContent.getMeasuredHeight();
        }
        mContent.layout(left, top, right, bottom);
    }
}

主要查看mHeaderView.layout,top=paddingTop + lp.topMargin + offsetX - mHeaderHeight;,向上偏移的一个 mHeaderHeight的高,这样mHeaderView初始化时就会隐藏
以下是 http://a.codekk.com/detail/Android/Grumoon/android-Ultra-Pull-To-Refresh%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90的解析
ViewGroup 的事件处理,通常重写 onInterceptTouchEvent 方法或者 dispatchTouchEvent 方法,PtrFrameLayout 重写了 dispatchTouchEvent 方法。
事件处理流程图 如下:

UltraPTR-dispatchTouchEvent-flow-chart

以上有两点需要分析下

  1. ACTION_UP 或者 ACTION_CANCEL 时候执行的 onRelease 方法。
    功能上,通过执行 tryToPerformRefresh 方法,如果向下拉动的位移已经超过了触发下拉刷新的偏移量 mOffsetToRefresh,并且当前状态是 PTR_STATUS_PREPARE,执行刷新功能回调。
    行为上,如果没有达到触发刷新的偏移量,或者当前状态为 PTR_STATUS_COMPLETE,或者刷新过程中不保持头部位置,则执行向上的位置回复动作。
  2. ACTION_MOVE 中判断是否可以纵向 move。
    ACTION_MOVE 的方向向下,如果 mPtrHandler 不为空,并且 mPtrHandler.checkCanDoRefresh 返回值为 true,则可以移动, Header 和 Content 向下移动,否则,事件交由父类处理。
    ACTION_MOVE 的方向向上,如果当前位置大于起始位置,则可以移动,Header 和 Content 向上移动,否则,事件交由父类处理。

你可能感兴趣的:(Ultra-Pull-To-Refresh源码解析)