Android 自定義 ViewGroup

一、前言

自定義 ViewGroup 是另一種重要的自定義 View 形式,當我們需要自定義子視圖的排列方式時,通常需要通過這種形式實現(xiàn)。例如:最常用的下拉刷新組件,實現(xiàn)下拉刷新、上拉加載更多的原理就是自定義一個 ViewGroup,將 Header View、Content View、Footer View 從上到下依次布局。然后在初始時通過 Scroller 滾動使得該組件在 y 軸方向上滾動 Header View 的高度,這樣當依賴該 ViewGroup 顯示在用戶眼前時 Header View 就被隱藏掉了。而 Content View 的寬度和高度都是 match_parent 的,因此,此時屏幕上只顯示 Content View,Header View 和 Footer View 都被隱藏在屏幕之外。

當 Content View 被滾動到頂部,此時如果用戶繼續(xù)下拉,那么該下拉刷新組件將攔截觸摸事件,然后根據(jù)用戶的觸摸事件獲取到手指滑動的 y 軸距離,并通過 Scroller 將該下拉刷新組件在 y 軸上滾動手指滑動的距離,實現(xiàn) Header View 顯示與隱藏,從而達到下拉的效果。

當用戶滑動到最底部時會觸發(fā)加載更多的操作,此時會通過 Scroller 滾動該下拉刷新組件,將 Footer View 顯示出來,實現(xiàn)加載更多的效果。

通過使用 Scroller 使得整個滾動效果更加平滑,而使用 Margin 來實現(xiàn)則需要自己來計算滾動時間和 margin 值,滾動效果不是很流暢,且頻繁地修改布局參數(shù)效率也不高。使用 Scroller 只是滾動位置,并沒有修改布局參數(shù),因此,使用 Scroller 是最好的選擇。

為了更好地理解下拉刷新的實現(xiàn),可以先看看以下這篇博文了解 Scroller 的作用以及如何使用。

Android Scroller 介紹與使用說明

二、下拉刷新實現(xiàn)

了解了 Scroller 原理后,我們來看看通用的下拉刷新組件的實現(xiàn)吧。

以下是重要的代碼段:

// 下拉刷新組件抽象基類,泛型參數(shù)T為中間內(nèi)容視圖的類型
public abstract class RefreshLayoutBase<T extends View>
        extends ViewGroup implements AbsListView.OnScrollListener {

    /**
     * 滾動控制器
     */
    protected Scroller mScroller;

    /**
     * 下拉刷新時顯示的header view
     */
    protected View mHeaderView;

    /**
     * 上拉加載更多時顯示的footer view
     */
    protected View mFooterView;

    /**
     * 本次觸摸滑動y坐標上的偏移量
     */
    protected int mYOffset;

    /**
     * 內(nèi)容視圖, 即用戶觸摸導致下拉刷新、上拉加載的主視圖. 比如ListView, GridView等.
     */
    protected T mContentView;

    /**
     * 最初的滾動位置.第一次布局時滾動header的高度的距離
     */
    protected int mInitScrollY = 0;

    /**
     * 最后一次觸摸事件的y軸坐標
     */
    protected int mLastY = 0;

    /**
     * 空閑狀態(tài)
     */
    public static final int STATUS_IDLE = 0;

    /**
     * 下拉或者上拉狀態(tài), 還沒有到達可刷新的狀態(tài)
     */
    public static final int STATUS_PULL_TO_REFRESH = 1;

    /**
     * 下拉或者上拉狀態(tài)
     */
    public static final int STATUS_RELEASE_TO_REFRESH = 2;

    /**
     * 刷新中
     */
    public static final int STATUS_REFRESHING = 3;

    /**
     * Loading中
     */
    public static final int STATUS_LOADING = 4;

    /**
     * 當前狀態(tài)
     */
    protected int mCurrentStatus = STATUS_IDLE;

    /**
     * header中的箭頭圖標
     */
    private ImageView mArrowImageView;

    /**
     * 箭頭是否向上
     */
    private boolean isArrowUp;

    /**
     * header 中的文本標簽
     */
    private TextView mTipsTextView;

    /**
     * header中的時間標簽
     */
    private TextView mTimeTextView;

    /**
     * header中的進度條
     */
    private ProgressBar mProgressBar;

    /**
     * 屏幕高度
     */
    private int mScreenHeight;

    /**
     * Header 高度
     */
    private int mHeaderHeight;

    /**
     * 下拉刷新監(jiān)聽器
     */
    protected OnRefreshListener mOnRefreshListener;

    /**
     * 加載更多回調(diào)
     */
    protected OnLoadListener mLoadListener;


    public RefreshLayoutBase(Context context) {
        this(context, null);
    }

    public RefreshLayoutBase(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 初始化Scroller對象
        mScroller = new Scroller(context);

        // 獲取屏幕高度
        mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;

        // header 的高度為屏幕高度的 1/4
        mHeaderHeight = mScreenHeight / 4;

        // 初始化整個布局
        initLayout(context);
    }


    /**
     * 初始化整個布局,從上到下分別為 header、內(nèi)容視圖、footer
     *
     * @param context
     */
    private final void initLayout(Context context) {
        // 設置header view
        setupHeaderView(context);

        // 設置內(nèi)容視圖
        setupContentView(context);

        // 設置布局參數(shù)
        setDefaultContentLayoutParams();

        // 添加內(nèi)容視圖,如ListView、GridView等
        addView(mContentView);

        // 設置footer view
        setupFooterView(context);
    }

    //代碼省略
}

在構(gòu)造函數(shù)中首先調(diào)用 initLayout() 函數(shù)初始化整個布局,從上到下分別為 Header View、內(nèi)容視圖、Footer View,我們先看看這 3 部分的相關(guān)函數(shù):

    /**
     * 初始化 header view
     */
    protected void setupHeaderView(Context context) {
        mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this,
                false);
        mHeaderView
                .setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
                        mHeaderHeight));
        mHeaderView.setBackgroundColor(Color.RED);
        // header 的高度為1/4的屏幕高度,但是,它只有100px是有效的顯示區(qū)域
        // 其余為paddingTop,這樣是為了達到下拉的效果
        mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);
        addView(mHeaderView);

        // 初始化 header view 中的子視圖
        mArrowImageView = (ImageView) mHeaderView.findViewById(R.id.pull_to_arrow_image);
        mTipsTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_text);
        mTimeTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
        mProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);
    }
    

    /**
     * 初始化 Content View, 子類覆寫.
     */
    protected abstract void setupContentView(Context context);


    /**
     * 初始化 footer view
     */
    protected void setupFooterView(Context context) {
        mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_footer,
                this, false);
        addView(mFooterView);
    }

其中 Header View 和 Footer View 都是從默認的布局中加載,因此它們是固定的。但是,最中間的內(nèi)容視圖是可變的,例如,我們顯示內(nèi)容的控件可能是 ListView、GridView、TextView 等,因此 setContentView() 留給子類去具體化。還有另外兩個抽象函數(shù),分別為判斷是否下拉到頂部以及上拉到底部的函數(shù),因為不同內(nèi)容視圖判斷是否滾動到頂部、底部的實現(xiàn)代碼也是不一樣的,因此也需要抽象化。函數(shù)定義如下:

    /**
     * 是否已經(jīng)到了最頂部,子類需覆寫該方法,使得mContentView滑動到最頂端時返回true, 
     * 如果到達最頂端用戶繼續(xù)下拉則攔截事件;
     *
     * @return
     */
    protected abstract boolean isTop();

    /**
     * 是否已經(jīng)到了最底部,子類需覆寫該方法,使得mContentView滑動到最底端時返回true;
     * 從而觸發(fā)自動加載更多的操作
     *
     * @return
     */
    protected abstract boolean isBottom();

初始化這 3 部分視圖之后,接下來的第一個關(guān)鍵步驟就是視圖測量與布局,也就是我們自定義 ViewGroup 中必備的兩個步驟。Header View 、內(nèi)容視圖、Footer View 是縱向布局,因此,需要將它們從上到下布局。在布局之前還需要測量各個子視圖的尺寸以及該下拉刷新組件自身的尺寸,代碼如下:

    /*
     * 丈量視圖的寬、高。寬度為用戶設置的寬度,
     * 高度則為header, content view, footer這三個子控件的高度之和。
     * @see android.view.View#onMeasure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // MeasureSpec 中的寬度值
        int width = MeasureSpec.getSize(widthMeasureSpec);
        // 子視圖的個數(shù)
        int childCount = getChildCount();
        // 最終的高度
        int finalHeight = 0;

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 測量每個子視圖的尺寸
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 所有子視圖的高度和就是該下拉刷新組件的總高度
            finalHeight += child.getMeasuredHeight();
        }
        // 設置該下拉刷新組件的尺寸
        setMeasuredDimension(width, finalHeight);
    }


    /*
     * 布局函數(shù),將header, content view,
     * footer view 這三個view從上到下布局。
     * 布局完成后通過Scroller滾動到header的底部,即滾動距離為header的高度 +
     * 本視圖的paddingTop,從而達到隱藏header的效果.
     * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int)
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int childCount = getChildCount();
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            child.layout(left, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
            top += child.getMeasuredHeight();
        }

        // 計算初始化滑動的y軸距離
        mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
        // 滑動到header view高度的位置, 從而達到隱藏header view的效果
        scrollTo(0, mInitScrollY);
    }

onMeasure() 中我們測量了該組件自身的大小以及所有子視圖的大小,并且將該控件的高度設置為所有子視圖的高度之和,這樣在布局時我們才有足夠的空間豎向放置子視圖。

onLayout() 時,會將 Header View,內(nèi)容視圖、Footer View 從上到下布局。而在 onLayout() 的最后,我們通過 Scroller 將該 ViewGroup 向上滾動了 Header View 的高度,使得 Header View 變得不可見。當用戶向下拉時,該組件判斷內(nèi)容視圖是否滑到了頂部,此時又通過 Scroller 將該組件向下滾動,使得 Header View 慢慢顯示出來。實現(xiàn)這些功能就需要我們處理該控件的觸摸事件,通過內(nèi)容視圖滾動到了頂部或者底部來判斷是否需要攔截觸摸事件。代碼如下:

    /*
     * 在適當?shù)臅r候攔截觸摸事件,這里指的適當?shù)臅r候是當mContentView滑動到頂部,
     * 并且是下拉時攔截觸摸事件,否則不攔截,交給其child view 來處理。
     * @see
     * android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        //獲取觸摸事件的類型
        final int action = MotionEventCompat.getActionMasked(ev);
        // 取消事件和抬起事件則直接返回 false
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Do not intercept touch event, let the child handle it
            return false;
        }

        switch (action) {

            case MotionEvent.ACTION_DOWN:
                mLastY = (int) ev.getRawY();
                break;

            case MotionEvent.ACTION_MOVE:
                mYOffset = (int) ev.getRawY() - mLastY;
                // 如果拉到了頂部, 并且是下拉,則攔截觸摸事件
                // 從而轉(zhuǎn)到onTouchEvent來處理下拉刷新事件
                if (isTop() && mYOffset > 0) {
                    return true;
                }
                break;
        }
        // 默認不攔截觸摸事件,使得該控件的子視圖能夠得到處理機會
        return false;
    }

onInterceptTouchEvent() 是 ViewGroup 中對觸摸事件進行攔截的函數(shù),當返回 true 時后續(xù)的觸摸事件就會被該 ViewGroup 攔截,此時子視圖將不會再獲得觸摸事件。相應地,返回 false 則表示不進行攔截。例如在上述 onInterceptTouchEvent() 函數(shù)中,我們在 ACTION_DOWN 事件(手指第一次按下)時記錄了 y 軸的坐標,當用戶的手指在屏幕上滑動時就會產(chǎn)生 ACTION_MOVE 事件,此時我們獲取了 y 軸坐標,并且與最初的 ACTION_DOWN 事件的 y 軸坐標相減。如果 mYOffset 大于 0,那么表示用戶的手指是從上到下滑動,如果此時內(nèi)容視圖已經(jīng)是到了頂部,例如:ListView 的第一次可見元素就是第一項,那么則返回 true,也就是將后續(xù)的觸摸事件攔截。此時,后續(xù)的 ACTION_MOVE、ACTION_UP 等事件就會由該組件進行處理,處理函數(shù)為 onTouchEvent() 函數(shù),代碼如下:

    /*
     * 在這里處理觸摸事件以達到下拉刷新或者上拉自動加載的問題
     * @see android.view.View#onTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(VIEW_LOG_TAG, "@@@ onTouchEvent : action = " + event.getAction());
        switch (event.getAction()) {
            // 滑動事件
            case MotionEvent.ACTION_MOVE:
                // 獲取手指觸摸的當前 y 坐標
                int currentY = (int) event.getRawY();
                // 當前坐標減去按下時的 y 坐標得到 y 軸上的偏移量
                mYOffset = currentY - mLastY;
                if (mCurrentStatus != STATUS_LOADING) {
                    // 在 y 軸方向上滾動該控件
                    changeScrollY(mYOffset);
                }

                // 旋轉(zhuǎn) Header View 中的箭頭圖標
                rotateHeaderArrow();

                // 修改 Header View 中的文本信息
                changeTips();

                // mLastY 設置為這次的 y 軸坐標
                mLastY = currentY;
                break;

            case MotionEvent.ACTION_UP:
                // 下拉刷新的具體操作
                doRefresh();
                break;
            default:
                break;
        }

        // 返回 true 表示消耗該事件,不再傳遞 
        return true;
    }

onTouchEvent() 函數(shù)中,我們會判斷觸摸事件的類型,如果還是 ACTION_MOVE 事件,那么計算當前觸摸事件的 y 坐標與 ACTION_DOWN 時的 y 坐標的差值,然后調(diào)用 changeScrollY() 函數(shù)在 y 軸上滾動該控件。如果用戶一直向下滑動手指,那么 mYOffset 值將不斷增大,那么此時該控件將不斷地往上滾動,Header View 的可見高度也就越來越大。我們看看 changeScrollY() 函數(shù)的實現(xiàn):

    /**
     * 修改 y 軸上的滾動值,從而實現(xiàn) Header View 被下拉的效果
     * @param distance 這次觸摸事件的 y 軸與上一次的 y 軸的差值
     * @return
     */
    protected void changeScrollY(int distance) {
        // 最大值為 mInitScrollY(header 隱藏), 最小值為0 ( header 完全顯示).
        int curY = getScrollY();
        // 下拉
        if (distance > 0 && curY - distance > getPaddingTop()) {
            scrollBy(0, -distance);
        } else if (distance < 0 && curY - distance <= mInitScrollY) {
            // 上拉過程
            scrollBy(0, -distance);
        }

        curY = getScrollY();
        int slop = mInitScrollY / 2;
        if (curY > 0 && curY < slop) {
            mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
        } else if (curY > 0 && curY > slop) {
            mCurrentStatus = STATUS_PULL_TO_REFRESH;
        }
    }

從上述代碼中可以看到,changeScrollY() 函數(shù)實際上就是根據(jù)這一次與上一次 y 軸的差值來滾動當前控件,由于兩次觸摸事件的差值較小,因此,滾動起來相對比較流暢。當 distance 小于 0 時,則是向上滾動,此時 Header View 的可見范圍越來越小,最后完全隱蔽;當 distance 大于 0 時則是向下滾動,此時 Header View 的可見范圍越來越大,這樣一來也就實現(xiàn)了下拉時顯示 Header View 的效果。當然在下拉過程中,我們也會修改 Header View 布局中的一些控件狀態(tài),例如箭頭、文本信息等。

Header View 顯示之后,當我們的手指離開屏幕時,如果在 y 軸上的滾動高度大于 Header View 有效區(qū)域高度的 1/2,那么就會觸發(fā)刷新操作,否則就會通過 Scroller 將 Header View 再次隱藏起來。相關(guān)代碼為 ACTION_UP 觸摸事件中調(diào)用的 doRefresh() 函數(shù):

    /**
     * 執(zhí)行下拉刷新
     */
    protected void doRefresh() {
        changeHeaderViewStaus();
        // 執(zhí)行刷新操作
        if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
            mOnRefreshListener.onRefresh();
        }
    }


    /**
     * 手指抬起時,根據(jù)用戶下拉的高度來判斷是否是有效的下拉刷新操作。
     * 如果下拉的距離超過header view 的 1/2
     * 那么則認為是有效的下拉刷新操作,否則恢復原來的視圖狀態(tài).
     */
    private void changeHeaderViewStaus() {
        int curScrollY = getScrollY();
        // 超過1/2則認為是有效的下拉刷新, 否則還原
        if (curScrollY < mInitScrollY / 2) {
            mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop()
                    - curScrollY);
            mCurrentStatus = STATUS_REFRESHING;
            mTipsTextView.setText(R.string.pull_to_refresh_refreshing_label);
            mArrowImageView.clearAnimation();
            mArrowImageView.setVisibility(View.GONE);
            mProgressBar.setVisibility(View.VISIBLE);
        } else {
            mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);
            mCurrentStatus = STATUS_IDLE;
        }
        
        invalidate();
    }

changeHeaderViewStaus() 函數(shù)中,當判斷為滿足下拉刷新的條件時,就會設置當前組件的狀態(tài)為 STATUS_REFRESHING 狀態(tài),并且設置正好顯示 Header View 區(qū)域,最后調(diào)用 OnRefreshListener 實現(xiàn)用戶設定的下拉刷新操作。刷新操作執(zhí)行完成之后,用戶需要調(diào)用 refreshComplete() 函數(shù)告知當前控件刷新完畢,此時當前控件會將 Header View 隱藏,代碼如下:

    /**
     * 刷新結(jié)束,恢復狀態(tài)
     */
    public void refreshComplete() {
        mCurrentStatus = STATUS_IDLE;
        mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());
        invalidate();
        updateHeaderTimeStamp();
        
        // 200毫秒后處理arrow和progressbar,免得太突兀
        this.postDelayed(new Runnable() {

            @Override
            public void run() {
                mArrowImageView.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);
            }
        }, 200);
    }

refreshComplete() 中將重置控件的狀態(tài),并且將 Header View 滾動到屏幕之外。此時,整個下拉刷新操作就完成了。滾動到底部時加載更多比下拉刷新要簡單一些,只需要判斷是否滾動到底部,如果已經(jīng)到底部那么直接觸發(fā)加載更多,因此,當前控件需要監(jiān)聽內(nèi)容視圖的滾動事件:

    /*
     * 滾動監(jiān)聽,當滾動到最底部,且用戶設置了加載更多的監(jiān)聽器時觸發(fā)加載更多操作.
     * @see android.widget.AbsListView.OnScrollListener#onScroll(android.widget.
     * AbsListView, int, int, int)
     */
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                         int totalItemCount) {
        // 用戶設置了加載更多監(jiān)聽器,且到了最底部,并且是上拉操作,那么執(zhí)行加載更多.
        if (mLoadListener != null && isBottom() && mScroller.getCurrY() <= mInitScrollY
                && mYOffset <= 0
                && mCurrentStatus == STATUS_IDLE) {
            showFooterView();
            // 調(diào)用加載更多
            doLoadMore();
        }
    }


    /**
     * 顯示footer view
     */
    private void showFooterView() {
        startScroll(mFooterView.getMeasuredHeight());
        mCurrentStatus = STATUS_LOADING;
    }


    /**
     * 執(zhí)行下拉(自動)加載更多的操作
     */
    protected void doLoadMore() {
        if (mLoadListener != null) {
            mLoadListener.onLoadMore();
        }
    }

onScroll() 中監(jiān)聽內(nèi)容視圖的滾動事件,當內(nèi)容視圖滾動到底部時顯示 Footer View,并且調(diào)用 OnLoadListener 回調(diào)執(zhí)行加載更多的操作。當操作執(zhí)行完畢后用戶需要調(diào)用 loadComplete() 函數(shù)告知當前控件加載完畢,下拉刷新組件此時隱藏 Footer View 并且設置為 STATUS_IDLE 狀態(tài)。

這就是整個 RefreshLayoutBase 類的核心邏輯。構(gòu)成完整的 RefreshLayoutBase 類還需要添加以下代碼:

    /**
     * 設置Content View的默認布局參數(shù)
     */
    protected void setDefaultContentLayoutParams() {
        ViewGroup.LayoutParams params =
                new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
                        LayoutParams.MATCH_PARENT);
        mContentView.setLayoutParams(params);
    }


    /**
     * 旋轉(zhuǎn)箭頭圖標
     */
    protected void rotateHeaderArrow() {

        if (mCurrentStatus == STATUS_REFRESHING) {
            return;
        } else if (mCurrentStatus == STATUS_PULL_TO_REFRESH && !isArrowUp) {
            return;
        } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH && isArrowUp) {
            return;
        }

        mProgressBar.setVisibility(View.GONE);
        mArrowImageView.setVisibility(View.VISIBLE);
        float pivotX = mArrowImageView.getWidth() / 2f;
        float pivotY = mArrowImageView.getHeight() / 2f;
        float fromDegrees = 0f;
        float toDegrees = 0f;
        if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
            fromDegrees = 180f;
            toDegrees = 360f;
        } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
            fromDegrees = 0f;
            toDegrees = 180f;
        }

        RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
        animation.setDuration(100);
        animation.setFillAfter(true);
        mArrowImageView.startAnimation(animation);

        if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
            isArrowUp = true;
        } else {
            isArrowUp = false;
        }
    }


    /**
     * 根據(jù)當前狀態(tài)修改header view中的文本標簽
     */
    protected void changeTips() {
        if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
            mTipsTextView.setText(R.string.pull_to_refresh_pull_label);
        } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
            mTipsTextView.setText(R.string.pull_to_refresh_release_label);
        }
    }


    /**
     * 修改header上的最近更新時間
     */
    private void updateHeaderTimeStamp() {
        // 設置更新時間
        mTimeTextView.setText(R.string.pull_to_refresh_update_time_label);
        SimpleDateFormat sdf = (SimpleDateFormat) SimpleDateFormat.getInstance();
        sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
        mTimeTextView.append(sdf.format(new Date()));
    }


    /**
     * 設置滾動的參數(shù)
     *
     * @param yOffset
     */
    private void startScroll(int yOffset) {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
        invalidate();
    }


    /**
     * 設置下拉刷新監(jiān)聽器
     *
     * @param listener
     */
    public void setOnRefreshListener(OnRefreshListener listener) {
        mOnRefreshListener = listener;
    }


    /**
     * 設置滑動到底部時自動加載更多的監(jiān)聽器
     *
     * @param listener
     */
    public void setOnLoadListener(OnLoadListener listener) {
        mLoadListener = listener;
    }


    /**
     * 加載結(jié)束,恢復狀態(tài)
     */
    public void loadCompelte() {
        // 隱藏footer
        startScroll(mInitScrollY - getScrollY());
        mCurrentStatus = STATUS_IDLE;
    }

下面我們來看看具體實現(xiàn)類,例如內(nèi)容視圖是 ListView 的實現(xiàn):

public abstract class RefreshAdapterView<T extends AbsListView> extends RefreshLayoutBase<T> {

    public RefreshAdapterView(Context context) {
        this(context, null);
    }

    public RefreshAdapterView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    public void setAdapter(ListAdapter adapter) {
        mContentView.setAdapter(adapter);
    }

    public ListAdapter getAdapter() {
        return mContentView.getAdapter();
    }

}
public class RefreshListView extends RefreshAdapterView<ListView> {


    public RefreshListView(Context context) {
        this(context, null);
    }

    public RefreshListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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


    // 設置內(nèi)容視圖為 ListView,并且設置 mContentView 的滾動監(jiān)聽器為當前對象
    @Override
    protected void setupContentView(Context context) {
        mContentView = new ListView(context);
        // 設置滾動監(jiān)聽器
        mContentView.setOnScrollListener(this);
    }

    @Override
    protected boolean isTop() {
        // 當?shù)谝粋€可見項是第一項表示到了頂部
        return mContentView.getFirstVisiblePosition() == 0
                && getScrollY() <= mHeaderView.getMeasuredHeight();
    }

    @Override
    protected boolean isBottom() {
        // 最后一個可見項是最后一項時表示滾動到了底部
        return mContentView != null && mContentView.getAdapter() != null
                && mContentView.getLastVisiblePosition() == mContentView.getAdapter().getCount() - 1;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }
}

RefreshListView 覆寫了 RefreshLayoutBase 的 3 個函數(shù),分別為設置內(nèi)容視圖、判斷是否滾動到頂部以及判斷是否滾動到底部。需要注意的是,在 setContentView() 函數(shù)中,我們將 mContentView(在這里也就是 ListView)的 onScrollListener 設置為 this,這是因為需要監(jiān)聽 ListView 的滾動狀態(tài),當滾動到最后一項時觸發(fā)加載更多操作。因為 RefreshLayoutBase 實現(xiàn)了 onScrollListener() 接口,而判斷是否調(diào)用加載更多的代碼被封裝在了 RefreshLayoutBase 中,因此,在這里直接調(diào)用 mContentView 對象的 setOnScrollListener(this) 即可。使用示例代碼如下:

    private void setListView() {
        final RefreshListView refreshLayout = new RefreshListView(this);

        List<String> dataStrings = new ArrayList<String>();

        // 準備數(shù)據(jù)
        for (int i = 0; i < 20; i++) {
            dataStrings.add("item - " + i);
        }

        // 獲取ListView, 這里的listview就是Content View
        refreshLayout.setAdapter(new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_1, dataStrings));

        // 設置下拉刷新監(jiān)聽器
        refreshLayout.setOnRefreshListener(new OnRefreshListener() {

            @Override
            public void onRefresh() {
                Toast.makeText(getApplicationContext(), "refreshing", Toast.LENGTH_SHORT)
                        .show();

                refreshLayout.postDelayed(new Runnable() {

                    @Override
                    public void run() {
                        refreshLayout.refreshComplete();
                    }
                }, 1500);
            }
        });

        // 不設置的話到底部不會自動加載
        refreshLayout.setOnLoadListener(new OnLoadListener() {

            @Override
            public void onLoadMore() {
                Toast.makeText(getApplicationContext(), "loading", Toast.LENGTH_SHORT)
                        .show();

                refreshLayout.postDelayed(new Runnable() {

                    @Override
                    public void run() {
                        refreshLayout.loadCompelte();
                    }
                }, 1500);
            }
        });

        //
        setContentView(refreshLayout);
    }

擴展一個支持下拉刷新的控件也很簡單,只需要繼承自 RefreshLayoutBase 類并且覆寫 setContentView()isTop()、isBottom() 函數(shù)即可。通過這種形式,使得下拉刷新組件具有良好的可擴展性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,228評論 3 119
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 14,103評論 2 59
  • 今年年初,無意中看到這本書,對于向來重視時間而又在一天天消逝的時光中悔恨不已的我來說,如同遇到了時間的指南
    蘇醒的公主閱讀 606評論 0 0
  • 或許也只有你,能讓他哭成這個樣子。 前段時間,奶奶重病住院,自己工作上也出現(xiàn)了挫折,忽然感覺好運離自己遠去。請假回...
    Rumlena閱讀 270評論 0 0

友情鏈接更多精彩內(nèi)容