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 向上移动,否则,事件交由父类处理。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容