Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android打造(ListView、GridView等)通用的下拉刷新、上拉自動加載的組件

Android打造(ListView、GridView等)通用的下拉刷新、上拉自動加載的組件

編輯:關於Android編程

前言

下拉刷新組件在開發中使用率是非常高的,基本上聯網的APP都會采用這種方式。對於開發效率而言,使用獲得大家認可的開源庫必然是效率最高的,但是不重復發明輪子的前提是你得自己知道輪子是怎麼發明出來的,並且自己能夠實現這些功能。否則只是知道其原理,並沒有去實踐那也就是紙上談兵了。做程序猿,動手做才會遇到真正的問題,否則就只是自以為是的認為自己懂了。今天這篇文章就是以自己重復發明輪子這個出發點而來的,通過實現經典、使用率較高的組件來提高自己的認識。下面我們就一起來學習吧。

整體布局結構

\ \

圖1 圖2

該組件整體以豎直方向的LinearLayout為根視圖,分別是Header、ContentView、Foooter, 從上到下依次排列下來,其中ContentView的寬高都為match_parent,footer和header的寬、高分別為match_parent、wrap_content,原始效果如圖1;在Header、Foooter初始時都會通過設置padding隱藏掉,如圖2中的Header設置paddingTop為負的Header的高度值,同理Footer也通過設置paddingBottom為Footer的負的Footer高度來達到隱藏的效果,所以只有 ContentView區域顯示出來。當用戶下拉到頂端,並且繼續下拉時觸發下拉刷新操作;當用戶上拉到底部, 並且繼續上拉時觸發加載更多的操作。

原理都雖然簡單,但是實現起來卻也是會有很多小麻煩。這裡沒有采用通過設置onTouchListener的方法,因此使用這個方式在下拉的時候依然會出現ListView的最頂部的"HOLD"視圖,不太爽。這種實現方法也蠻簡單的,具體看郭神的博客 Android下拉刷新完全解析,教你如何一分鐘實現下拉刷新功能。

下拉刷新基本原理

基本原理就是在用戶滑動屏幕上的組件時,在onInterceptTouchEvent方法中判斷是否到了ContentView (這裡我們以ListView為例來說明)的最頂端,如果到了最頂端且用戶還繼續向下滑,那麼會攔截觸摸事件避免它分發到ListView,即在onInterceptTouchEvent中返回true ( 不太清楚的可以參考資料如下 : Android Touch事件分發過程。 ),這樣就將觸摸事件分發到了onTouchEvent函數中,我們對於用戶觸摸事件的處理邏輯主要都在這個函數中。如果該函數返回false,那麼觸摸事件則會分發給其Child View,這裡的這個Child View就是ListView了,當返回false時用戶滑動屏幕時就會滾動ListView。
    /*
     * 在適當的時候攔截觸摸事件,這裡指的適當的時候是當mContentView滑動到頂部,並且是下拉時攔截觸摸事件,否則不攔截,交給其child
     * view 來處理。
     * @see
     * android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onTouchEvent will be called and we do the actual
         * scrolling there.
         */
        final int action = MotionEventCompat.getActionMasked(ev);
        // Always handle the case of the touch gesture being complete.
        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:
                mYDown = (int) ev.getRawY();
                break;

            case MotionEvent.ACTION_MOVE:
                // int yDistance = (int) ev.getRawY() - mYDown;
                mYDistance = (int) ev.getRawY() - mYDown;
                showStatus(mCurrentStatus);
                Log.d(VIEW_LOG_TAG, "%%% isBottom : " + isBottom() + ", isTop : " + isTop()
                        + ", mYDistance : " + mYDistance);
                // 如果拉到了頂部, 並且是下拉,則攔截觸摸事件,從而轉到onTouchEvent來處理下拉刷新事件
                if ((isTop() && mYDistance > 0)
                        || (mYDistance > 0 && mCurrentStatus == STATUS_REFRESHING)) {
                    return true;
                }
                break;

        }

        // Do not intercept touch event, let the child handle it
        return false;
    }
首先我們在ACTION_DOWN事件中記錄用戶按下的觸摸點的Y軸坐標mYDown,然後在ACTION_MOVE中再次獲取Y軸的坐標,計算出兩者之間的差值。如果滑動的差值大於mTouchSlop則繼續進行處理,mTouchSlop為判斷一個觸摸滑動事件是否有效的的最小閥值,如果小於這個閥值我們認為這個觸摸滑動事件無效,例如手抖了一下,距離很短,因此我們忽略類似的事件。 在有效的滑動距離之內,我們判斷當前組件的狀態,如果不是正在刷新的狀態,那麼我們根據當前ListView的paddingTop的高度來設置不同的值,paddingTop如果高度大於ListView高度的70%,那麼我們將當前狀態設置為“釋放可刷新”狀態,即STATUS_RELEASE_TO_REFRESH狀態;反之,我們設置狀態為“繼續下拉”狀態,即“STATUS_PULL_TO_REFRESH”。通過這個paddingTop高度我們來規定當用戶下拉距離到一定的距離後才出發刷新操作,否則視為無效下拉。然而不管這個時候是什麼狀態,我們都會修改Header的padding Top屬性,從而達到拉伸header的效果。 當狀態為“釋放可刷新”時,我們抬起手指,會出發ACTION_UP事件,此時我們在該事件中進行下拉刷新操作。onTouchEvent代碼如下 :
    /*
     * 在這裡處理觸摸事件以達到下拉刷新或者上拉自動加載的問題
     * @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_DOWN:
                mYDown = (int) event.getRawY();
                Log.d(VIEW_LOG_TAG, "#### ACTION_DOWN");
                break;

            case MotionEvent.ACTION_MOVE:
                Log.d(VIEW_LOG_TAG, "#### ACTION_MOVE");
                int currentY = (int) event.getRawY();
                mYDistance = currentY - mYDown;
                // 高度大於header view的高度才可以刷新
                if (mYDistance >= mTouchSlop) {
                    if (mCurrentStatus != STATUS_REFRESHING) {
                        //
                        if (mHeaderView.getPaddingTop() > mHeaderViewHeight * 0.7f) {
                            mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
                            mTipsTextView.setText(R.string.pull_to_refresh_release_label);
                        } else {
                            mCurrentStatus = STATUS_PULL_TO_REFRESH;
                            mTipsTextView.setText(R.string.pull_to_refresh_pull_label);
                        }
                    }

                    rotateHeaderArrow();
                    int scaleHeight = (int) (mYDistance * 0.8f);// 去了滑動距離的80%,減小靈敏度而已
                    // Y軸的滑動距離小於屏幕高度4分之一時才會拉伸header,反之保持不變
                    if (scaleHeight <= mScrHeight / 4) {
                        adjustHeaderPadding(scaleHeight);
                    }
                }

                break;

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

        }
        return true;
    }
抬起手指時出發的刷新操作,代碼如下:
    /**
     * 執行刷新操作
     */
    private final void doRefresh() {
        if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
            // 設置狀態為正在刷新狀態
            mCurrentStatus = STATUS_REFRESHING;
            mArrowImageView.clearAnimation();
            // 隱藏header中的箭頭圖標
            mArrowImageView.setVisibility(View.GONE);
            // 設置header中的進度條可見
            mHeaderProgressBar.setVisibility(View.VISIBLE);
            // 設置一些文本
            mTimeTextView.setText(R.string.pull_to_refresh_update_time_label);
            SimpleDateFormat sdf = new SimpleDateFormat();
            mTimeTextView.append(sdf.format(new Date()));
            //
            mTipsTextView.setText(R.string.pull_to_refresh_refreshing_label);

            // 執行回調
            if (mPullRefreshListener != null) {
                mPullRefreshListener.onRefresh();
            }
            // 使headview 正常顯示, 直到調用了refreshComplete後再隱藏
            new HeaderViewHideTask().execute(0);

        } else {
            // 隱藏header view
            adjustHeaderPadding(-mHeaderViewHeight);
        }
    }
在刷新狀態時,header正常顯示,即此時的padding top需要設置為0,我們使用一個異步任務來逐步修改padding top的值,使得header從拉伸效果逐步、平滑的恢復原始的大小。用戶調用refreshComplete()函數後,即刷新完成後,再逐步調整listview的padding top將其隱藏。至此,整個下拉刷新過程結束。

滑動到底部自動加載

滑動到底部自動加載相對來說要簡單得多,我們也是以ContentView是ListView的情況來說明。原理就是監聽ListView ( 即 ContentView )的的滾動事件,因此如果ContentView的類型不支持滾動事件,則不能實現該功能。listview符合要求,因此其能實現自動加載。我們在onScroll函數中判斷listview是否到了最後一項,如果到了最後一項,那麼顯示出footer,並且開始加載。當用戶調用loadMoreComplete函數時代表加載結束。此時隱藏footer,整個過程結束。
    /*
     * 滾動事件,實現滑動到底部時上拉加載更多
     * @see android.widget.AbsListView.OnScrollListener#onScroll(android.widget.
     * AbsListView, int, int, int)
     */
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
            int totalItemCount) {

        Log.d(VIEW_LOG_TAG, "&&& mYDistance = " + mYDistance);
        if (mFooterView == null || mYDistance >= 0 || mCurrentStatus == STATUS_LOADING
                || mCurrentStatus == STATUS_REFRESHING) {
            return;
        }

        loadmore();
    }

    /**
     * 下拉到底部時加載更多
     */
    private void loadmore() {
        if (isShowFooterView() && mLoadMoreListener != null) {
            mFooterTextView.setText(R.string.pull_to_refresh_refreshing_label);
            mFooterProgressBar.setVisibility(View.VISIBLE);
            adjustFooterPadding(0);
            mCurrentStatus = STATUS_LOADING;
            mLoadMoreListener.onLoadMore();
        }
    }
其中loadmore函數中調用的isShowFooterView函數就是用來判斷是否到了最底部的,代碼如下 :
 /*
     * 下拉到listview 最後一項時則返回true, 將出發自動加載
     * @see com.uit.pullrefresh.base.PullRefreshBase#isShowFooterView()
     */
    @Override
    protected boolean isShowFooterView() {
        if (mContentView == null || mContentView.getAdapter() == null) {
            return false;
        }

        return mContentView.getLastVisiblePosition() == mContentView.getAdapter().getCount() - 1;
    }
OK,至此整個核心的過程介紹完畢了。

效果圖

ListView

\


TextView下拉刷新


github地址

猛擊這裡!

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