深度剖析:Android_PullToRefresh

Posted AnalyzeSystem

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度剖析:Android_PullToRefresh相关的知识,希望对你有一定的参考价值。

上拉加载更多,下拉刷新,网上比较强大比较全的一个开源库PullToRefresh,支持Listview、GridView、ScrollView等众多控件。下载地址:

git clone https://github.com/chrisbanes/android-PullToRefresh.git

噢,伙计,当然你也可以这样

https://github.com/chrisbanes/Android-PullToRefresh

源码剖析

整个库先从地基入手PullToRefreshBase,我们必须先了解这个类关联的类别,先看State枚举类

public static enum State {

        /**
         * When the UI is in a state which means that user is not interacting
         * with the Pull-to-Refresh function.
         * 重置初始化状态
         */
        RESET(0x0),

        /**
         * When the UI is being pulled by the user, but has not been pulled far
         * enough so that it refreshes when released.
         * 拉动距离不足指定阈值,进行释放
         */
        PULL_TO_REFRESH(0x1),

        /**
         * When the UI is being pulled by the user, and <strong>has</strong>
         * been pulled far enough so that it will refresh when released.
         * 拉动距离大于等于指定阈值,进行释放
         */
        RELEASE_TO_REFRESH(0x2),

        /**
         * When the UI is currently refreshing, caused by a pull gesture.
         * 由于用户手势操作,引起当前UI刷新
         */
        REFRESHING(0x8),

        /**
         * When the UI is currently refreshing, caused by a call to
         * {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
         * 由于代码调用setRefreshing引起刷新UI
         */
        MANUAL_REFRESHING(0x9),

        /**
         * When the UI is currently overscrolling, caused by a fling on the
         * Refreshable View.
         * 由于结束滑动,可以刷新视图
         */
        OVERSCROLLING(0x10);

        /**
         * Maps an int to a specific state. This is needed when saving state.
         * int 映射到状态,需要保存这个状态,直白的说:根据index 获取枚举类型
         * @param stateInt - int to map a State to
         * @return State that stateInt maps to
         */
        static State mapIntToValue(final int stateInt) {
            for (State value : State.values()) {
                if (stateInt == value.getIntValue()) {
                    return value;
                }
            }

            // If not, return default
            return RESET;
        }

        private int mIntValue;

        State(int intValue) {
            mIntValue = intValue;
        }

        int getIntValue() {
            return mIntValue;
        }
    }

再来看Mode的枚举类

public static enum Mode {

        /**
         * Disable all Pull-to-Refresh gesture and Refreshing handling
         * 禁用刷新加载
         */
        DISABLED(0x0),

        /**
         * Only allow the user to Pull from the start of the Refreshable View to
         * refresh. The start is either the Top or Left, depending on the
         * scrolling direction.
         * 仅仅支持下动刷新
         */
        PULL_FROM_START(0x1),

        /**
         * Only allow the user to Pull from the end of the Refreshable View to
         * refresh. The start is either the Bottom or Right, depending on the
         * scrolling direction.
         * 仅仅支持上啦加载更多
         */
        PULL_FROM_END(0x2),

        /**
         * Allow the user to both Pull from the start, from the end to refresh.
         * 上啦下拉都支持
         */
        BOTH(0x3),

        /**
         * Disables Pull-to-Refresh gesture handling, but allows manually
         * setting the Refresh state via
         * {@link PullToRefreshBase#setRefreshing() setRefreshing()}.
         * 只允许手动触发
         */
        MANUAL_REFRESH_ONLY(0x4);

        /**
         * @deprecated Use {@link #PULL_FROM_START} from now on.
         * 不赞成使用,过时了
         */
        public static Mode PULL_DOWN_TO_REFRESH = Mode.PULL_FROM_START;

        /**
         * @deprecated Use {@link #PULL_FROM_END} from now on.
         */
        public static Mode PULL_UP_TO_REFRESH = Mode.PULL_FROM_END;

        /**
         * Maps an int to a specific mode. This is needed when saving state, or
         * inflating the view from XML where the mode is given through a attr
         * int.
         * 
         * @param modeInt - int to map a Mode to
         * @return Mode that modeInt maps to, or PULL_FROM_START by default.
         */
        static Mode mapIntToValue(final int modeInt) {
            for (Mode value : Mode.values()) {
                if (modeInt == value.getIntValue()) {
                    return value;
                }
            }

            // If not, return default
            return getDefault();
        }
        //默认状态只支持刷新 
        static Mode getDefault() {
            return PULL_FROM_START;
        }

        private int mIntValue;

        // The modeInt values need to match those from attrs.xml
        //mode的值要与自定义属性的值相匹配
        Mode(int modeInt) {
            mIntValue = modeInt;
        }

        /**
         * @return true if the mode permits Pull-to-Refresh
         * 如果当前模式允许刷新则返回true
         */
        boolean permitsPullToRefresh() {
            return !(this == DISABLED || this == MANUAL_REFRESH_ONLY);
        }

        /**
         * @return true if this mode wants the Loading Layout Header to be shown
         * 如果该模式下能加载显示header部分,则返回true
         */
        public boolean showHeaderLoadingLayout() {
            return this == PULL_FROM_START || this == BOTH;
        }

        /**
         * @return true if this mode wants the Loading Layout Footer to be shown
         * 如果该模式下能加载显示footer部分,则返回true
         */
        public boolean showFooterLoadingLayout() {
            return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY;
        }

        int getIntValue() {
            return mIntValue;
        }

    }

动画相关枚举类型AnimationStyle

public static enum AnimationStyle {
        /**
         * This is the default for Android-PullToRefresh. Allows you to use any
         * drawable, which is automatically rotated and used as a Progress Bar.
         * 默认使用旋转的进度条 ProgressBar
         */
        ROTATE,

        /**
         * This is the old default, and what is commonly used on ios. Uses an
         * arrow image which flips depending on where the user has scrolled.
         * 箭头图像翻转根据用户手势
         */
        FLIP;

        static AnimationStyle getDefault() {
            return ROTATE;
        }

        /**
         * Maps an int to a specific mode. This is needed when saving state, or
         * inflating the view from XML where the mode is given through a attr
         * int.
         * 
         * @param modeInt - int to map a Mode to
         * @return Mode that modeInt maps to, or ROTATE by default.
         */
        static AnimationStyle mapIntToValue(int modeInt) {
            switch (modeInt) {
                case 0x0:
                default:
                    return ROTATE;
                case 0x1:
                    return FLIP;
            }
        }

        LoadingLayout createLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {
            switch (this) {
                case ROTATE:
                default:
                    return new RotateLoadingLayout(context, mode, scrollDirection, attrs);
                case FLIP:
                    return new FlipLoadingLayout(context, mode, scrollDirection, attrs);
            }
        }
    }

HeaderLayout 、FooterLayout对应的接口ILoadingLayout


public interface ILoadingLayout {

    /**
     * Set the Last Updated Text. This displayed under the main label when
     * Pulling
     * 最后更新时间
     * @param label - Label to set
     */
    public void setLastUpdatedLabel(CharSequence label);

    /**
     * Set the drawable used in the loading layout. This is the same as calling
     * <code>setLoadingDrawable(drawable, Mode.BOTH)</code>
     * 设置使用的可拉的加载布局的drawable
     * @param drawable - Drawable to display
     */
    public void setLoadingDrawable(Drawable drawable);

    /**
     * Set Text to show when the Widget is being Pulled
     * <code>setPullLabel(releaseLabel, Mode.BOTH)</code>
     * 设置上拉显示文字
     * @param pullLabel - CharSequence to display
     */
    public void setPullLabel(CharSequence pullLabel);

    /**
     * Set Text to show when the Widget is refreshing
     * <code>setRefreshingLabel(releaseLabel, Mode.BOTH)</code>
     * 设置下拉刷新显示文字
     * @param refreshingLabel - CharSequence to display
     */
    public void setRefreshingLabel(CharSequence refreshingLabel);

    /**
     * Set Text to show when the Widget is being pulled, and will refresh when
     * released. This is the same as calling
     * <code>setReleaseLabel(releaseLabel, Mode.BOTH)</code>
     * 设置释放显示文字
     * @param releaseLabel - CharSequence to display
     */
    public void setReleaseLabel(CharSequence releaseLabel);

    /**
     * Set's the Sets the typeface and style in which the text should be
     * displayed. Please see
     * {@link android.widget.TextView#setTypeface(Typeface)
     * TextView#setTypeface(Typeface)}.
     * 设置字体
     */
    public void setTextTypeface(Typeface tf);

}

进入LoadingLayout构造函数

public LoadingLayout(Context context, final Mode mode, final Orientation scrollDirection, TypedArray attrs) {
        super(context);
        //根据不同方向选择加载不同布局
        mMode = mode;
        mScrollDirection = scrollDirection;

        switch (scrollDirection) {
            case HORIZONTAL:
                //Inflater这种用法第一次见到,比较新颖,get..
    LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_horizontal, this);
                break;
            case VERTICAL:
            default:
                LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_vertical, this);
                break;
        }

        mInnerLayout = (FrameLayout) findViewById(R.id.fl_inner);


//自定义属性的另一种取法
if (attrs.hasValue(R.styleable.PullToRefresh_ptrHeaderBackground)) {
            Drawable background = attrs.getDrawable(R.styleable.PullToRefresh_ptrHeaderBackground);
            if (null != background) {
                //版本分支设置背景
                ViewCompat.setBackground(this, background);
            }
        }

    //**************************此处略*******************************

        reset();
    }

该类内部定义了一系列抽象方法,具体稍后再说,下面接着了解刷新和加载更多的监听接口OnRefreshListener、OnRefreshListener2

/**
     * Simple Listener to listen for any callbacks to Refresh.
     * 这个接口只是用与仅仅支持刷新模式
     * @author Chris Banes
     */
    public static interface OnRefreshListener<V extends View> {

        /**
         * onRefresh will be called for both a Pull from start, and Pull from
         * end
         * 下拉结束后能够刷新(滑动距离>=阈值)调用onRefresh回掉函数
         */
        public void onRefresh(final PullToRefreshBase<V> refreshView);

    }

    /**
     * An advanced version of the Listener to listen for callbacks to Refresh.
     * This listener is different as it allows you to differentiate between Pull
     * Ups, and Pull Downs.
     * 当前模式支持刷新和加载更多
     * @author Chris Banes
     */
    public static interface OnRefreshListener2<V extends View> {
        // TODO These methods need renaming to START/END rather than DOWN/UP

        /**
         * onPullDownToRefresh will be called only when the user has Pulled from
         * the start, and released.
         * 下拉刷新
         */
        public void onPullDownToRefresh(final PullToRefreshBase<V> refreshView);

        /**
         * onPullUpToRefresh will be called only when the user has Pulled from
         * the end, and released.
         * 上啦加载更多
         */
        public void onPullUpToRefresh(final PullToRefreshBase<V> refreshView);

    }

上啦和下拉的Event事件回调接口类OnPullEventListener

/**
     * Listener that allows you to be notified when the user has started or
     * finished a touch event. Useful when you want to append extra UI events
     * (such as sounds). See (
     * {@link PullToRefreshAdapterViewBase#setOnPullEventListener}.
     * 
     * @author Chris Banes
     */
    public static interface OnPullEventListener<V extends View> {

        /**
         * Called when the internal state has been changed, usually by the user
         * pulling.
         * 通过用户上下拉引起状态改变,把触摸事件回调
         * @param refreshView - View which has had it's state change.
         * @param state - The new state of View.
         * @param direction - One of {@link Mode#PULL_FROM_START} or
         *            {@link Mode#PULL_FROM_END} depending on which direction
         *            the user is pulling. Only useful when <var>state</var> is
         *            {@link State#PULL_TO_REFRESH} or
         *            {@link State#RELEASE_TO_REFRESH}.
         */
        public void onPullEvent(final PullToRefreshBase<V> refreshView, State state, Mode direction);

    }

一个滑动相关联的Runnable 实现类SmoothScrollRunnable以及一个滑动结束的监听接口OnSmoothScrollFinishedListener


    final class SmoothScrollRunnable implements Runnable {
        private final Interpolator mInterpolator;
        private final int mScrollToY;
        private final int mScrollFromY;
        private final long mDuration;
        private OnSmoothScrollFinishedListener mListener;

        private boolean mContinueRunning = true;
        private long mStartTime = -1;
        private int mCurrentY = -1;

        public SmoothScrollRunnable(int fromY, int toY, long duration, OnSmoothScrollFinishedListener listener) {
            mScrollFromY = fromY;
            mScrollToY = toY;
            mInterpolator = mScrollAnimationInterpolator;
            mDuration = duration;
            mListener = listener;
        }

        @Override
        public void run() {

            /**
             * Only set mStartTime if this is the first time we're starting,
             * else actually calculate the Y delta
             */
            if (mStartTime == -1) {
                mStartTime = System.currentTimeMillis();
            } else {

                /**
                 * We do do all calculations in long to reduce software float
                 * calculations. We use 1000 as it gives us good accuracy and
                 * small rounding errors
                 */
                long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration;
                normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);

                final int deltaY = Math.round((mScrollFromY - mScrollToY)
                        * mInterpolator.getInterpolation(normalizedTime / 1000f));
                mCurrentY = mScrollFromY - deltaY;
                //根据计算的距离设置Hearlayout的滑动,该方法控制HeaderLayout、FooterLayout的显示与否,同时还根据参数控制硬件加速渲染相关,最终目的调用了scrollTo方法。
                setHeaderScroll(mCurrentY);
            }

            // If we're not at the target Y, keep going...
            if (mContinueRunning && mScrollToY != mCurrentY) {
                ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
            } else {
                if (null != mListener) {
                    //滑动结束了回调
                    mListener.onSmoothScrollFinished();
                }
            }
        }

        public void stop() {
            //停止滑动,并移除监听
            mContinueRunning = false;
            removeCallbacks(this);
        }
    }
    static interface OnSmoothScrollFinishedListener {
        void onSmoothScrollFinished();
    }

PullToRefreshBase类构造函数初始化了触摸敏感系数mTouchSlop,并创建添加HeaderLayout、FooterLayout, 再调用updateUIForMode方法更具Mode修改调整UI,refreshLoadingViewsSize方法调整LoadingLayout相关大小,而影响其本质的因素,先看下面这个方法

private int getMaximumPullScroll() {
        switch (getPullToRefreshScrollDirection()) {
            case HORIZONTAL:
                return Math.round(getWidth() / FRICTION);
            case VERTICAL:
            default:
                return Math.round(getHeight() / FRICTION);
        }
    }

FRICTION这个参数固定值2.0,根据父控件宽高/固定系数得到(左右上下方向)上拉下拉对应的HeaderLayout 、FooterLayout的宽高,如果我们想缩小HeaderLayout的高度只需要加大固定系数FRICTION,但是的注意,别改得太大了导致布局显示出问题。

onInterceptTouchEvent方法重写MotionEvent.ACTION_DOWN && mIsBeingDragged先拦截触摸事件,在action_move 时,根据设置刷新ing能否继续滑动的参数以及是否能刷新, 判断是否拦截触摸事件if mScrollingWhileRefreshingEnabled && isRefreshing(),以及根据触摸滑动距离和Mode判断拦截Touch事件。

当我们HeaderLayout 、FooterLayout视图弹出,请求完了数据需要隐藏掉它们,这时候就需要用到它

    @Override
    public final void onRefreshComplete() {
        if (isRefreshing()) {
            setState(State.RESET);
        }
    }

setStatue方法里面调用onReset,继续跟进发现LoadingLayout调用了reset方法,并且smoothScrollTo方法调用,间接的new 了SmoothScrollRunnable,一个定时长的减速scrollTo动画执行


    /**
     * Called when the UI has been to be updated to be in the
     * {@link State#RESET} state.
     */
    protected void onReset() {
        mIsBeingDragged = false;
        mLayoutVisibilityChangesEnabled = true;

        // Always reset both layouts, just in case...
        mHeaderLayout.reset();
        mFooterLayout.reset();

        smoothScrollTo(0);
    }

而onTouchEvent方法 内部则是根据各种状态判断设置当前的状态枚举类型State


    @Override
    public final boolean onTouchEvent(MotionEvent event) {

        if (!isPullToRefreshEnabled()) {
            return false;
        }

        // If we're refreshing, and the flag is set. Eat the event
        if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
            return true;
        }

        if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
            return false;
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                if (mIsBeingDragged) {
                    mLastMotionY = event.getY();
                    mLastMotionX = event.getX();
                    pullEvent();
                    return true;
                }
                break;
            }

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

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                if (mIsBeingDragged) {
                    mIsBeingDragged = false;

                    if (mState == State.RELEASE_TO_REFRESH
                            && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
                        setState(State.REFRESHING, true);
                        return true;
                    }

                    // If we're already refreshing, just scroll back to the top
                    if (isRefreshing()) {
                        smoothScrollTo(0);
                        return true;
                    }

                    // If we haven't returned by here, then we're not in a state
                    // to pull, so just reset
                    setState(State.RESET);

                    return true;
                }
                break;
            }
        }

        return false;
    }

ILoadingLayout 接口的实现类LoadingLayoutProxy ,也是LoadingLayout的代理,通过HashSet存储LoadingLayout,设置LoadingLayout的属性则通过该代理来设置,实例如下:

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

方法setRefreshing (boolean ) 原理是在改变State状态,从而改变ui


    @Override
    public final void setRefreshing(boolean doScroll) {
        if (!isRefreshing()) {
            setState(State.MANUAL_REFRESHING, doScroll);
        }
    }

onPullToRefresh方法根据mCurrentMode调用HeaderLayout、FooterLayout(LoadingLayout)各自的抽象方法具体实现稍后再说,诸如此类方法就不一一列举

/**
     * Called when the UI has been to be updated to be in the
     * {@link State#PULL_TO_REFRESH} state.
     */
    protected void onPullToRefresh() {
        switch (mCurrentMode) {
            case PULL_FROM_END:
                mFooterLayout.pullToRefresh();
                break;
            case PULL_FROM_START:
                mHeaderLayout.pullToRefresh();
                break;
            default:
                // NO-OP
                break;
        }
    }

基本涉及到的类别粗略过了一遍,接着我们挨着来了解怎么用这些自定义控件,至于这些控件源码就不分析了,大同小异,代码量太大太累了

调用实例

ListView

运行效果图如下:

首先需要在xml引用控件,activity获取实例得到PullToRefreshListView,进行初始化

protected void initialPullToRefreshListView() {
        adapter = new SimpleAdapter(this, null);
        mListView = mPullToRefreshListView.getRefreshableView();
        mListView.setAdapter(adapter);

       //Adapter的List.size=0的时候用到的,建议工厂生产view以适应多种情景
        mPullToRefreshListView.setEmptyView(getEmptyView());
        //不能刷新
        mPullToRefreshListView.setMode(Mode.DISABLED);
        onRefresh();
    }

调用方法刷新获取数据,改变Mode和监听,setOnLastItemVisibleListener是否滑动到底部的监听,而mPullToRefreshListView.getOnRefreshListener()相关方法在源码中不存在,自己添加的一个返回方法

public void onRefresh() {
        mPullToRefreshListView.postDelayed(new Runnable() {

            @Override
            public void run() {
                pageSize=0;
                int [] arrays ={5,10};
                int size = new Random().nextInt(2);
                size = arrays[size];
                adapter.onRefresh(getData(pageSize, size));
                mPullToRefreshListView.onRefreshComplete();

                if (size == 10) {
                    pageSize++;
                    setRefreshListener2();
                    mPullToRefreshListView
                    .setOnLastItemVisibleListener(null);
                }else {
                     if(size==0){
                         mPullToRefreshListView.setMode(Mode.PULL_FROM_START);
                         Toast.makeText(getApplicationContext(), "暂无更多数据,请稍后再试", Toast.LENGTH_SHORT).show();
                     }
                    if(mPullToRefreshListView.getMode()!=Mode.PULL_FROM_START||mPullToRefreshListView.getOnRefreshListener()==null){
                        setRefreshListener1();
                    }
                } 
            }
        }, 3000);
    }

setRefreshListener1 和setRefreshListener2方法分别是支持只刷新和刷新加载更多都支持的接口building,例如setRefreshLisenter1:

public void setRefreshListener1() {
        mPullToRefreshListView.setMode(Mode.PULL_FROM_START);
        mPullToRefreshListView
        .setOnRefreshListener(new OnRefreshListener<ListView>() {

            @Override
            public void onRefresh(
                    PullToRefreshBase<ListView> refreshView) {
                setLable(refreshView);
                MainActivity.this.onRefresh();
            }
        });

        setLastItemVisibleListener();

    }

GridView、ViewPager、ExpandListView、WebView等相关控件的关于刷新加载更多的这块的调用实例都大同小异,不一一列举,如果实在搞不定可以参考官方simple,这里有个实践:PullToRefreshScrollView+NoScrollListView 实现不能滑动的ListView嵌套到PullToRefreshScrollView里面,随便添加header 或者其他任意布局,PullToRefreshScrollView 的刷新和加载更多监听加载数据从而调用NoScrollView的adapter.notifyChangeData();已达到无缝衔接的滑动。


源码以及修改后的Libary下载: 用力一戳

以上是关于深度剖析:Android_PullToRefresh的主要内容,如果未能解决你的问题,请参考以下文章

深度剖析,从普通时钟系统到各种授时方式

libevent源码深度剖析

深度剖析智能指针

mybatis源码级别深度剖析

Kubernetes 安全权限管理深度剖析

PyTorch 深度剖析:并行训练的 DP 和 DDP 分别在啥情况下使用及实例