Android 使用 SwipeRefreshLayout 製作下拉更新 (Pull to Refresh)

Pull to refresh

下拉更新已被很多 App 使用,在 iOS 較新的版中已是內建的套件,使用上很方便。那 Android 是否也有呢?有,被新增在 support v4 的支援套件裡面,類別名稱是 SwipeRefreshLayout

因為最近有用到下拉更新,所以找了一下怎麼製作,基本上不難。它的更新顯示方式不是一般常見的圓形旋轉圖示,而是在最上方以動態的橫條來顯示。在不指定顏色的方式下,預設是黑色的,看起來不太明顯,指定顏色後,還滿繽紛的。

SwipeRefreshLayout 使用說明

SwipeRefreshLayout (之後以 SRL 簡稱) 被設計為執行下拉手勢時更新內容之用。Activity 透過實作 OnRefreshListener 來接收更新通知。當收到更新通知時,使用 setRefreshing(boolean refreshing) 來顯示或取消更新狀態,在"正在更新"狀態下,setRefreshing(true),畫面上方會出現動態的橫條提示,當資料更新完成,使用 setRefreshing(false) 取消後則消失。

若要暫時取消"下拉更新手勢"的偵聽,只要呼叫 setEnabled(false) 即可,開啟則呼叫 setEnabled(true),在某些時候必須使用,後面提到的範例中會說明。

重要!這個 SRL 必須是整個 layout 的父元件,也就是在 layout xml 中,必須是最上層的,而且只能有一個子元件,也就是說如果要有多個子元作,就要先加入一個 layout,然後才能在其中加入子元件。
2016/9/19 修正如下:
官方文件的說明是
This layout should be made the parent of the view that will be refreshed as a result of the gesture and can only support one direct child.
意思是,SRL 必須是「你想要使用下滑手勢來更新內容的 view 的父層,而且 SRL 只支援一個直接子元件」。

實作 Layout xml

通常我們會用 Graphical Layout 界面拖拉元件來設計畫面,但當你使用 SRL 時,在屬性清單中有些屬性會無法顯示,以拖拉的方式新增元件也會看不到,這在設計複雜的畫面時會很麻煩。

我的解決方法是,先建一個 Layout,當成 SRL 的暫時替代品,佔滿全畫面,它之下只會有一個 Layout,也是佔滿全畫面,我們設計的所有元件都是放在這個 Layout 底下。當全部設計完成了,在切換到 xml 文字模式,把最外層的 Layout 的標籤名稱換成 android.support.v4.widget.SwipeRefreshLayout 記得開始及結尾都要換掉,這樣就完成了。

activity_main.xml

<android.support.v4.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/laySwipe"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <TextView
            android:id="@+id/txtTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:background="#95ea53"
            android:padding="10dp"
            android:text="Pull to Refresh Demo"
            android:textAppearance="?android:attr/textAppearanceMedium" />

        <ListView
            android:id="@+id/lstData"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@+id/txtTitle" />

    </RelativeLayout>
</android.support.v4.widget.SwipeRefreshLayout>
接下來在 Activity 中偵聽下拉手勢。

MainActivity.java

public class MainActivity extends Activity {

    private SwipeRefreshLayout laySwipe;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
    }

    private void initView() {
        laySwipe = (SwipeRefreshLayout) findViewById(R.id.laySwipe);
        laySwipe.setOnRefreshListener(onSwipeToRefresh);
        laySwipe.setColorSchemeResources(
            android.R.color.holo_red_light, 
            android.R.color.holo_blue_light,
            android.R.color.holo_green_light,
            android.R.color.holo_orange_light);

        ListView lstData = (ListView) findViewById(R.id.lstData);
        lstData.setAdapter(getAdapter());
        lstData.setOnScrollListener(onListScroll);
    }

    private OnRefreshListener onSwipeToRefresh = new OnRefreshListener() {
        @Override
        public void onRefresh() {
            laySwipe.setRefreshing(true);
            new Handler().postDelayed(new Runnable() {

                @Override
                public void run() {
                    laySwipe.setRefreshing(false);
                    Toast.makeText(getApplicationContext(), "Refresh done!", Toast.LENGTH_SHORT).show();
                }
            }, 3000);
        }
    };

    private ArrayAdapter<String> getAdapter(){
        //fake data
        String[] data = new String[20];
        int len = data.length;
        for (int i = 0; i < len; i++) {
            data[i] = Double.toString(Math.random() * 1000);
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1 , data);
        return adapter;
    }

    private OnScrollListener onListScroll = new OnScrollListener() {

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

        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {
            if (firstVisibleItem == 0) {
                laySwipe.setEnabled(true);
            }else{
                laySwipe.setEnabled(false);
            }
        }
    };
}
程式碼說明:
首先,建立一個 SRL 成員變數:
private SwipeRefreshLayout laySwipe;
對 laySwipe 設定更新手勢(即下拉)的偵測:
laySwipe.setOnRefreshListener(onSwipeToRefresh);

private OnRefreshListener onSwipeToRefresh = new OnRefreshListener() {
    @Override
    public void onRefresh() {
        laySwipe.setRefreshing(true);
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                laySwipe.setRefreshing(false);
                Toast.makeText(getApplicationContext(), "Refresh done!", Toast.LENGTH_SHORT).show();
            }
        }, 3000);
    }
};
當你在螢幕上按住往下滑動,到達一定的距離,就會完成下拉手勢,此時就會觸發 OnRefreshListener 偵聽器的 onRefresh() 方法。

在方法的一開始,我們要讓使用者看到現在要開始更新的動作了,因此設定:
laySwipe.setRefreshing(true);
這時螢幕上方會出現更新的橫條動畫。這裡使用 Handler() 來模擬更新,時間為 3 秒。完成後會顯示 Toast 訊息。這裡必須把橫條動畫結束掉:
laySwipe.setRefreshing(false);
到這裡,你已經可以在模擬器中執行,體驗看看下拉更新的效果。

為了讓 demo 看起來符合實際需求,範例建立了一些假資料到 ListView 中。另外還有一個可能會遇到的問題,當你的畫面中,不是只有一個 ListView 的時候,例如,此範例故意在 ListView 上面加了一個 TextView。這時候會發生當你只是要捲動 ListView 中的內容,卻觸發了更新。

為了解決這個問題,你必須去偵聽 ListView 的捲動事件,判斷只有當第一個可視項目 (列) 為 0 的時候,表示已經捲到頂了,這時才去偵聽下拉更新手勢。開啟及關閉偵聽下拉手勢的方法:
if (firstVisibleItem == 0) {
    laySwipe.setEnabled(true);
}else{
    laySwipe.setEnabled(false);
}
到頂時才開啟,不然就關閉。

更新時顯示的動態橫條顏色

laySwipe.setColorSchemeResources(
    android.R.color.holo_red_light, 
    android.R.color.holo_blue_light, 
    android.R.color.holo_green_light, 
    android.R.color.holo_orange_light);
如果你沒有指定顏色,預色會是以黑色顯示。可以同時指定 4 種顏色,更新橫條會自動以動畫顯示。這裡取用 Android 內建的顏色。

備註

範例使用的 SDK 版本 (AndroidManifest.xml)
<uses-sdk
    android:minSdkVersion="16"
    android:targetSdkVersion="21" />

參考資料

本文網址:http://blog.tonycube.com/2014/09/android-swiperefreshlayout-pull-to.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

7 則留言

  1. 建議可以加上這些,提醒大家先加在 Java 檔裡面
    import android.support.v4.widget.SwipeRefreshLayout;
    import android.widget.ListView;
    import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
    import android.os.Handler;
    import android.widget.Toast;
    import android.widget.ArrayAdapter;
    import android.widget.AbsListView.OnScrollListener;
    import android.widget.AbsListView;

    回覆刪除
  2. "重要!這個 SRL 必須是整個 layout 的父元件,也就是在 layout xml 中,必須是最上層的,而且只能有一個子元件,也就是說如果要有多個子元作,就要先加入一個 layout,然後才能在其中加入子元件。"這句話是錯的,可以包在其他layout裡面,google應該沒傻到設計出只能包在最外層的layout,太沒彈性了

    回覆刪除
    回覆
    1. 內容已修改,感謝指正。

      刪除
  3. 請問一下 !要怎麼判斷到頂部才重新整理
    條件要怎麼寫

    if (firstVisibleItem == 0) {
    laySwipe.setEnabled(true);
    }else{
    laySwipe.setEnabled(false);
    }

    這個部分不是很懂

    回覆刪除
    回覆
    1. 你可以把這幾行註解掉試看看,就應該知道它是做什麼的。

      原本的 ListView 是可以往上滑或往下滑,
      而 laySwipe 是它的父層,所以它也會收到滑動的事件,
      而 laySwipe 在收到往下滑的事件時,就會執行「下滑更新」的判斷,
      那這樣這個 ListView 將永逺收不到往下滑的事件,因為被 laySwipe 吃掉了,
      所以我們把它限制成,當我滑到 ListView 的最上面,
      已經沒有項目了,
      也就是 firstVisibleItem == 0 (當第一個可見的項目它的索引是 0) 的時候,
      我才啟動 laySwipe 的「下滑更新」判斷。

      刪除
  4. 我手機裡面沒有SwipeRefreshLayout, https://www.youtube.com/watch?v=6gNiyhU7h5k ,那白色圓形圖案要怎麼製作呢?觸碰一下就出現。

    回覆刪除
    回覆
    1. 去手機的設定,開發人員選項中找一下觸碰相關的設定

      刪除

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。