android-Ultra-Pull-to-Refresh 深入理解及使用


下拉刷新,几乎是每个 Android 应用都会需要的功能。 android-Ultra-Pull-To-Refresh (以下简称 UltraPTR )便是一个强大的 Andriod 下拉刷新框架。
主要特点:
(1).继承于 ViewGroup, Content 可以包含任何 View。
(2).简洁完善的 Header 抽象,方便进行拓展,构建符合需求的头部。

项目地址:
https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh

竞品:
https://github.com/chrisbanes/Android-PullToRefresh
https://github.com/johannilsson/android-pulltorefresh
https://github.com/Demievil/SwipeRefreshLayout

参考文章:
android-Ultra-Pull-To-Refresh源码解析: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
公共技术点之 View 绘制流程:http://codekk.com/open-source-project-analysis/detail/Android/lightSky/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8B%20View%20%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B
公共技术点之 View 事件传递:http://codekk.com/open-source-project-analysis/detail/Android/Trinea/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8B%20View%20%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92

1.添加依赖

compile 'in.srain.cube:ultra-ptr:1.0.10'

2.XML界面配置

详细官方中文文档:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/blob/master/README-cn.md

<?xml version="1.0" encoding="utf-8"?>
<in.srain.cube.views.ptr.PtrClassicFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:cube_ptr="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f1f1f1"
    cube_ptr:ptr_duration_to_close="200"
    cube_ptr:ptr_duration_to_close_header="1000"
    cube_ptr:ptr_keep_header_when_refresh="true"
    cube_ptr:ptr_pull_to_fresh="false"
    cube_ptr:ptr_ratio_of_header_height_to_refresh="1.2"
    cube_ptr:ptr_resistance="1.7">

    <WebView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</in.srain.cube.views.ptr.PtrClassicFrameLayout>

自定义属性实现

//1.在attr里定义属性
<declare-styleable name="PtrFrameLayout"?
//2.在构造方法里获取
TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.PtrFrameLayout);
arr.recycle();

如何获取header及content

1.重写view的onFinishInflate方法
2.判断childCount
childCount > 2 throw excetpion
childCount = 2 header+content
childCount = 1 content
childCount = 0 errorView
3.mHeaderView.bringToFront(),不懂什么意思

3.Java代码配置

// the following are default settings
//1.设置阻尼系数
mPtrFrame.setResistance(1.7f);
//2.下拉刷新头部的比率
mPtrFrame.setRatioOfHeaderHeightToRefresh(1.2f);
//3.设置从松手的位置到头部所要的时间
mPtrFrame.setDurationToClose(200);
//4.设置从头部到顶部所要的时间
mPtrFrame.setDurationToCloseHeader(1000);
//5.default is false,false下拉就进行刷新,true松手到超过指定高度才进行刷新
mPtrFrame.setPullToRefresh(false);
//6.default is true,true刷新时有消息头,flase刷新时没有消息头
mPtrFrame.setKeepHeaderWhenRefresh(true);

1.mPtrFrame.setResistance(1.7f);

//setp 1.在PtrIndicator.class里面设置resistance
mPtrIndicator.setResistance(arr.getFloat(R.styleable.PtrFrameLayout_ptr_resistance, mPtrIndicator.getResistance()));

private float mResistance = 1.7f;

//setp 2.在MotionEvent.ACTION_MOVE中计算,新的offsetY。(即offsetY / mResistance)
mPtrIndicator.onMove(e.getX(), e.getY());
public final void onMove(float x, float y) {
    float offsetX = x - mPtLastMove.x;
    float offsetY = (y - mPtLastMove.y);
    processOnMove(x, y, offsetX, offsetY);
    mPtLastMove.set(x, y);
}
protected void processOnMove(float currentX, float currentY, float offsetX, float offsetY) {
    setOffset(offsetX, offsetY / mResistance);
}
protected void setOffset(float x, float y) {
    mOffsetX = x;
    mOffsetY = y;
}
private PointF mPtLastMove = new PointF();

//setp 3.在MotionEvent.ACTION_MOVE的时候使用新的offsetY
movePos(offsetY);
mContent.offsetTopAndBottom(change);

2.mPtrFrame.setRatioOfHeaderHeightToRefresh(1.2f);

//setp 1.在attr里设置header高度的比率
float ratio = mPtrIndicator.getRatioOfHeaderToHeightRefresh();
ratio = arr.getFloat(R.styleable.PtrFrameLayout_ptr_ratio_of_header_height_to_refresh, ratio);
mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio);

//setp 1'.在java代码里设置header高度的比率
 public void setRatioOfHeaderHeightToRefresh(float ratio) {
    mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio);
}
//以上两种方式都会调用PtrIndicator的方法
public void setRatioOfHeaderHeightToRefresh(float ratio) {
    mRatioOfHeaderHeightToRefresh = ratio;
    mOffsetToRefresh = (int) (mHeaderHeight * ratio);
}

//setp 2.在onMeasure的时候设置header的高度及栈值
mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
mPtrIndicator.setHeaderHeight(mHeaderHeight);

public void setHeaderHeight(int height) {
    mHeaderHeight = height;
    updateHeight();
}
protected void updateHeight() {
    mOffsetToRefresh = (int) (mRatioOfHeaderHeightToRefresh * mHeaderHeight);
}
public int getOffsetToRefresh() {
    return mOffsetToRefresh;
}


//setp 3.在MotionEvent.ACTION_UP,使用这个栈值
//3.1
onRelease(false);
//3.2
tryToPerformRefresh();
//3.3
mPtrIndicator.isOverOffsetToRefresh()
public boolean isOverOffsetToRefresh() {
    return mCurrentPos >= getOffsetToRefresh();
}
public int getOffsetToRefresh() {
    return mOffsetToRefresh;
}

//疑问:setRatioOfHeaderHeightToRefresh与onMeasure的调用顺序

3.mPtrFrame.setDurationToClose(200);

//step 1.设置方式省略
//step 2.在MotionEvent.ACTION_UP中使用这个值
private void onRelease(boolean stayForLoading)
//step 3.要满足正在加载中&当刷新的时候保持头部&当前的pos要大于offsetToKeepHeader.
//这三个条件都要满足,才会执行回滚头部。(也就是从当前位置回滚到offsetToKeepHeader)
if (mStatus == PTR_STATUS_LOADING) {
        // keep header for fresh
        if (mKeepHeaderWhenRefresh) {
            // scroll header back
            if (mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && !stayForLoading) {
                mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose);
            }
        }
}
//step 4.那什么是offsetToKeepHeader?可以看到它的默认值为mHeaderHeight,也有公开的方法可以设置,///但是,不知道应该在什么时候设置
private int mOffsetToKeepHeaderWhileLoading = -1;
public void setOffsetToKeepHeaderWhileLoading(int offset) {
    mOffsetToKeepHeaderWhileLoading = offset;
}
public int getOffsetToKeepHeaderWhileLoading() {
    return mOffsetToKeepHeaderWhileLoading >= 0 ? mOffsetToKeepHeaderWhileLoading : mHeaderHeight;
}

4.mPtrFrame.setDurationToCloseHeader(1000);

//step 1.当主动调用刷新完成的
private void performRefreshComplete() {
    mStatus = PTR_STATUS_COMPLETE;
    notifyUIRefreshComplete(false);
}
//step 2.通知UI刷新完成
private void notifyUIRefreshComplete(boolean ignoreHook) {
    tryScrollBackToTopAfterComplete();
    tryToNotifyReset();
}
private void tryScrollBackToTopAfterComplete() {
    tryScrollBackToTop();
}
//step 3.回滚到起点(即顶部)
private void tryScrollBackToTop() {
    if (!mPtrIndicator.isUnderTouch()) {
        mScrollChecker.tryToScrollTo(PtrIndicator.POS_START, mDurationToCloseHeader);
    }
}

//问题:mDurationToClose和mDurationToCloseHeader可能会有冲突?
//当网络请求的响应的时候.即小于mDurationToClose的时间。在UI就会有卡顿的问题
//根本原因:当执行mDurationToClose一半的时候,在调用refreshComplete()的,就会有一个回退的效果,然//后在执行mDurationToCloseHeader

5.mPtrFrame.setPullToRefresh(false);

//step 1.设置方法略
//step 2.在下拉的时候,调用movePos(即MotionEvent.ACTION_MOVE)
private void movePos(float deltaY) {
    updatePos(change);
}
private void updatePos(int change) {
// Pull to Refresh
    if (mStatus == PTR_STATUS_PREPARE) {
        // reach fresh height while moving from top to bottom
        if (isUnderTouch && !isAutoRefresh() && mPullToRefresh
                && mPtrIndicator.crossRefreshLineFromTopToBottom()) {
            tryToPerformRefresh();
        }
        // reach header height while auto refresh
        if (performAutoRefreshButLater() && mPtrIndicator.hasJustReachedHeaderHeightFromTopToBottom()) {
            tryToPerformRefresh();
        }
    }
}
//step 3.执行下拉刷新(就是没有滑动超过指定高度的时候,就执行下拉刷新。相当于一个提前执行网络请求的一个效果)
 private boolean tryToPerformRefresh() {
    performRefresh();
}
 private void performRefresh() {
    if (mPtrUIHandlerHolder.hasHandler()) {
        mPtrUIHandlerHolder.onUIRefreshBegin(this);
    }
    if (mPtrHandler != null) {
        mPtrHandler.onRefreshBegin(this);
    }
}

6.mPtrFrame.setKeepHeaderWhenRefresh(true);

//step 1.设置省略(xml,java两种方式)
//step 2.在MotionEvent.ACTION_UP时调用onRelease(false)
private void onRelease(boolean stayForLoading) {
    tryToPerformRefresh();
    if (mStatus == PTR_STATUS_LOADING) {
        // keep header for fresh
        if (mKeepHeaderWhenRefresh) {
            // scroll header back
            if (mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && !stayForLoading) {
                mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose);
            } else {
                // do nothing
            }
        } else {
            tryScrollBackToTopWhileLoading();
        }
    } else {
        if (mStatus == PTR_STATUS_COMPLETE) {
            notifyUIRefreshComplete(false);
        } else {
            tryScrollBackToTopAbortRefresh();
        }
    }
}
//step 3.与mDurationToClose相比,不再是回滚到mHeaderHeight(默认值),而是直接回滚到顶点(0)。
//这样做达到了。回滚的时候隐藏header的目的

4.设置下拉刷新监听

注意:如果有下拉问题,可以选择性重写checkCanDoRefresh

mPtrFrame.setPtrHandler(new PtrHandler() {
    //检查能否下刷新
    @Override
    public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
        return PtrDefaultHandler.checkContentCanBePulledDown(frame, mWebView, header);
    }

    //开始下拉刷新
    @Override
    public void onRefreshBegin(PtrFrameLayout frame) {
        updateData();
    }
});

需要自己实现checkContentCanBePulledDown

public static boolean canChildScrollUp(View view) {
    if (android.os.Build.VERSION.SDK_INT < 14) {
        if (view instanceof AbsListView) {
            final AbsListView absListView = (AbsListView) view;
            return absListView.getChildCount() > 0
                    && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                    .getTop() < absListView.getPaddingTop());
        } else {
            return view.getScrollY() > 0;
        }
    } else {
        return view.canScrollVertically(-1);
    }
}

5.手动回调完成下拉刷新

mPtrFrame.refreshComplete();

6.自定义下拉刷新的头部

5.1 自定义view,并实现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);
}

5.2 用过xml布局或者setHeaderView,设置头部

5.3 设置PtrUIHandler

ptrHome.addPtrUIHandler(ptrUIHandler);

7.自动下拉刷新

ptrFrame.postDelayed(new Runnable() {
    @Override
    public void run() {
        ptrFrame.autoRefresh(true);
    }
}, 150);

8.其他设置

1. setPinContent(false)

解释:设置ContentView是否顶住.(设置为true即content不动,只有header随着移动)

//step 1.在MotionEvent.ACTION_MOVE中调用movePos(offsetY)
private void movePos(float deltaY) {
    updatePos(change);
}
private void updatePos(int change) {
    mHeaderView.offsetTopAndBottom(change);
    if (!isPinContent()) {
        mContent.offsetTopAndBottom(change);
    }
}

2.setLoadingMinTime(500)

解释:设置网络加载时间最少为500毫秒

//step 1.在MotionEvent.ACTION_UP的时候调用
onRelease(false)
//step 2.执行下拉刷新,并保存mLoadingStartTime
 private void onRelease(boolean stayForLoading) {
    tryToPerformRefresh();
}
private boolean tryToPerformRefresh() {
    if (mStatus != PTR_STATUS_PREPARE) {
        return false;
    }

    //
    if ((mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && isAutoRefresh()) || mPtrIndicator.isOverOffsetToRefresh()) {
        mStatus = PTR_STATUS_LOADING;
        performRefresh();
    }
    return false;
}
private void performRefresh() {
    mLoadingStartTime = System.currentTimeMillis();
    if (mPtrUIHandlerHolder.hasHandler()) {
        mPtrUIHandlerHolder.onUIRefreshBegin(this);
        if (DEBUG) {
            PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshBegin");
        }
    }
    if (mPtrHandler != null) {
        mPtrHandler.onRefreshBegin(this);
    }
}
//step 3.在调用refreshComplete()时计算delay。并且postDelayed
final public void refreshComplete() {
    int delay = (int) (mLoadingMinTime - (System.currentTimeMillis() - mLoadingStartTime));
    if (delay <= 0) {
        performRefreshComplete();
    } else {
        postDelayed(new Runnable() {
            @Override
            public void run() {
                performRefreshComplete();
            }
        }, delay);
    }
}
//可处理mDurationToClose和mDurationToCloseHeader的冲突的问题

3.disableWhenHorizontalMove(false)

解释:为了解决与viewpager的手势冲突问题(即在x轴滑动的距离大于在y轴滑动的距离,则直接返回,UltraPtr不做处理)

//step 1.在MotionEvent.ACTION_MOVE
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
    if (mPtrIndicator.isInStartPosition()) {
            mPreventForHorizontal = true;
    }
}
if (mPreventForHorizontal) {
    return dispatchTouchEventSupper(e);
}

9.其他问题

1.UltraPtr如何实现滑动动画

解答:通过Scroller与ScrollChecker。
1.Scroller为滚动的封装类,通过偏移来描述从一个点到移动另一个点,坐标的变化情况。(mScroller.startScroll(startX, startY, dx, dy, duration)
2.ScrollChecker实现Runnable接口的线程类,但是其run放在主线程执行。根据其变更的坐标,去设置header
和content的offsetTopAndBottom(int offset),最后在刷新界面
3.使用view.post(action),不断执行ScrollChecker的run方法。这样到header和content位置不断变化,然后产生动画
4.最后mScroller.computeScrollOffset()或者mScroller.isFinished()来判断是否到了指定的间隔

2.如何测量header和content?

3.为什么UltraPtr没有实现加载更多

对比 Android-PullToRefresh 项目,UltraPTR 没有实现 加载更多 的功能,但我认为 下拉刷新 和 加载更多 不是同一层次的功能, 下拉刷新 有更广泛的需求,可以适用于任何页面。而 加载更多 的功能应该交由具体的 Content 自己去实现。这应该是和 Google 官方推出 SwipeRefreshLayout 是相同的设计思路,但对比 SwipeRefreshLayout, UltraPTR 更灵活,更容易拓展。

4.事件处理流程是怎么样

5.类的关系图是怎样的

9.PtrIndicator

字段

  1. mIsUnderTouch-在ACTION_DOWN为true,ACTION_UP为false
  2. mPtLastMove-在ACTION_DOWN和ACTION_MOVE都要更新这个坐标
  3. mOffsetX,mOffsetY-偏移量,当前坐标减去mPtLastMove
  4. mOffsetToRefresh-(int) (mHeaderHeight * ratio)
  5. mCurrentPos-ACTION_MOVE的时候更新这个坐标
  6. mLastPos-ACTION_MOVE保存上一个mCurrentPos坐标
  7. mPressedPos-ACTION_DOWN保存mCurrentPos坐标
  8. mRefreshCompleteY-刷新完成的时候保存mCurrentPos坐标

方法

//滑动的位置与Header的高度的百分比
public float getCurrentPercent()

//在顶部上面
public boolean willOverTop(int to)
//在顶部
public boolean isAlreadyHere(int to)
//顶部有空余
public boolean hasLeftStartPosition()

//在开始点
public boolean isInStartPosition()
//‘刚刚’离开开始点
public boolean hasJustLeftStartPosition() 
//‘刚刚’回到开始点
public boolean hasJustBackToStartPosition()

//超过设定的刷新高度
public boolean hasMovedAfterPressedDown()


public boolean goDownCrossFinishPosition()
public boolean crossRefreshLineFromTopToBottom()
public boolean hasJustReachedHeaderHeightFromTopToBottom()

可选

public void setOffsetToKeepHeaderWhileLoading(int offset) {
    mOffsetToKeepHeaderWhileLoading = offset;
}

public int getOffsetToKeepHeaderWhileLoading() {
    return mOffsetToKeepHeaderWhileLoading >= 0 ? mOffsetToKeepHeaderWhileLoading : mHeaderHeight;
}
public boolean isOverOffsetToKeepHeaderWhileLoading() {
    return mCurrentPos > getOffsetToKeepHeaderWhileLoading();
}