Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 深度剖析:Android_PullToRefresh

深度剖析:Android_PullToRefresh

編輯:關於Android編程

上拉加載更多,下拉刷新,網上比較強大比較全的一個開源庫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 has
         * 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
     * setLoadingDrawable(drawable, Mode.BOTH)
     * 設置使用的可拉的加載布局的drawable
     * @param drawable - Drawable to display
     */
    public void setLoadingDrawable(Drawable drawable);

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

    /**
     * Set Text to show when the Widget is refreshing
     * setRefreshingLabel(releaseLabel, Mode.BOTH)
     * 設置下拉刷新顯示文字
     * @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
     * setReleaseLabel(releaseLabel, Mode.BOTH)
     * 設置釋放顯示文字
     * @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 {

        /**
         * onRefresh will be called for both a Pull from start, and Pull from
         * end
         * 下拉結束後能夠刷新(滑動距離>=阈值)調用onRefresh回掉函數
         */
        public void onRefresh(final PullToRefreshBase 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 {
        // 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 refreshView);

        /**
         * onPullUpToRefresh will be called only when the user has Pulled from
         * the end, and released.
         * 上啦加載更多
         */
        public void onPullUpToRefresh(final PullToRefreshBase 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 {

        /**
         * 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 state is
         *            {@link State#PULL_TO_REFRESH} or
         *            {@link State#RELEASE_TO_REFRESH}.
         */
        public void onPullEvent(final PullToRefreshBase 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() {

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

        setLastItemVisibleListener();

    }

GridView、ViewPager、ExpandListView、WebView等相關控件的關於刷新加載更多的這塊的調用實例都大同小異,不一一列舉,如果實在搞不定可以參考官方simple,這裡有個實踐:PullToRefreshScrollView+NoScrollListView 實現不能滑動的ListView嵌套到PullToRefreshScrollView裡面,隨便添加header 或者其他任意布局,PullToRefreshScrollView 的刷新和加載更多監聽加載數據從而調用NoScrollView的adapter.notifyChangeData();已達到無縫銜接的滑動。


  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved