使用 Android ViewPager 製作 Gallery

ViewPager

很久很久以前,當我第一次要做一系列圖片的橫向滑動展示時,使用的是 Gallery 這個類別,之後過了很久幸福快樂日子,某一天!Eclipse 上竟然出現了「This class was deprecated.」之類的訊息,嚇得我立即關閉專案,馬上逃走並灌了三瓶啤酒壓壓驚。無視了一陣子之後,想說這樣下去也不是辦法,決定來找找新的替代方法,就找到了親愛的 ViewPager。
Gallery 在 API 的第一行就寫道「This class was deprecated in API level 16.」並同時提供兩個 Gallery 的替代類別:HorizontalScrollViewViewPager

HorizontalScrollView 是 FrameLayout 的子類別,意謂者只允許擁有一個子類別,並且它只支援水平捲動,如果要同時做垂直及水平的捲動,應該用 ScrollView。看起來並不符合我的需求,所以不考慮。

ViewPager 介紹

ViewPager 允許使用者左右翻動頁面,你必須實作 PagerAdapter 來產生被顯示的頁面。ViewPager 通常搭配 Fragment 一起使用,便於管理每個頁面的生命週期。

PagerAdapter 有兩種類型可以使用:FragmentPagerAdapterFragmentStatePagerAdapter

FragmentPagerAdapter 適合頁面數量少的情況,因為每個 Fragment 頁面會持續的被保留在記憶體中,在頁面不可見(not visible)的情況下,view 會被銷毀,但整個 Fragment 仍然存在。

如果頁面數量很多的情況,必須改用 FragmentStatePagerAdapter,唯一不同的是,在頁面不可見(not visible)的情況下,整個 Fragment 都會被銷毀,只保留狀態 (state),因此不會佔用大量的記憶體。

ViewPagerDemo 範例程式碼

主要有三個檔案:
  • MainActivity.java:主要的 Activity
  • PageFragment.java:頁面內容的 Fragment
  • PagerAdapter.java:用來產生頁面

MainActivity.java

public class MainActivity extends ActionBarActivity {

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

        initToolbar();
        initViewPager();
    }

    private void initToolbar() {
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        ActionBar actionbar = getSupportActionBar();
        actionbar.setTitle("ViewPager Demo");
    }

    private void initViewPager(){
        List<Fragment> fragments = getFragments();
        PagerAdapter adapter = new PagerAdapter(getSupportFragmentManager(), fragments);

        ViewPager pager = (ViewPager) findViewById(R.id.viewpager);
        pager.setAdapter(adapter);
    }

    private List<Fragment> getFragments() {
        List<Fragment> fragments = new ArrayList<Fragment>();
        fragments.add(PageFragment.newInstance("Fragment 1", R.drawable.wallpaper_1));
        fragments.add(PageFragment.newInstance("Fragment 2", R.drawable.wallpaper_2));
        fragments.add(PageFragment.newInstance("Fragment 3", R.drawable.wallpaper_3));
        fragments.add(PageFragment.newInstance("Fragment 4", R.drawable.wallpaper_4));
        fragments.add(PageFragment.newInstance("Fragment 5", R.drawable.wallpaper_5));
        fragments.add(PageFragment.newInstance("Fragment 6", R.drawable.wallpaper_6));
        return fragments;
    }
}
說明:
整個流程很簡單,先建立要顯示的 Fragment 的實體,送給 PagerAdapter,再把 PagerAdapter 送給 ViewPager 來顯示。

PageFragment.java

public class PageFragment extends Fragment {

    private final String TAG = "PageFragment";
    public static final String TITLE = "TITLE";
    public static final String IMAGE = "IMAGE";

    private String title = "";
    private int resImageId = 0;

    public static final PageFragment newInstance(String title, int resImageId){
        PageFragment f = new PageFragment();
        Bundle bdl = new Bundle(1);
        bdl.putString(TITLE, title);
        bdl.putInt(IMAGE, resImageId);
        f.setArguments(bdl);
        return f;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        Log.d(TAG, "onAttach");
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        title = getArguments().getString(TITLE);
        resImageId = getArguments().getInt(IMAGE);
        Log.d(TAG, title + " - onCreate");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Log.d(TAG, title + " - onCreateView");
        return inflater.inflate(R.layout.myfragment, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Log.d(TAG, title + " - onViewCreated");
        TextView txtTitle = (TextView) view.findViewById(R.id.txtTitle);
        ImageView img = (ImageView) view.findViewById(R.id.imageView1);
        txtTitle.setText(title);
        img.setImageResource(resImageId);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, title + " - onActivityCreated");
        Toast.makeText(getActivity(), "Create View " + title, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDestroyView() {
        Log.d(TAG, title + " - onDestroyView");
        super.onDestroyView();
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, title + " - onDestroy");
        super.onDestroy();
    }
}
說明:
如果有要傳值給 Fragment 的話,必須在 newInstance() 方法中,利用 Bundle 來包裹值,並透過 setArguments() 方法指定給 Fragment,當它被建立(onCreate)時,則可以利用 getArguments() 來取得值,即 Bundle。

在 onCreate() 中 getArguments();在 onCreateView() 中指定 layout;在 onViewCreated() 中可以對 view 做動作;如果需要 Activity 的資源,則在 onActivityCreated() 方法中取用。

你可以在 LogCat 中觀查 Fragment 的生命週期,Adapter 會預先載入下一個 page 的 fragment,並且會把超過目前頁面往前/後加1之外的頁面移除,也就是保持目前所在頁面的前後各一個頁面。但它並沒有被 onDestroy(),而只是被 onDestroyView()。onDestroy() 只會發生在所屬的 Activity 被 destroy 時。

PagerAdapter.java

public class PagerAdapter extends FragmentPagerAdapter {

    private List<Fragment> fragments;

    public PagerAdapter(FragmentManager fm, List<Fragment> fragments){
        super(fm);
        this.fragments = fragments;
    }

    @Override
    public Fragment getItem(int position) {
        return fragments.get(position);
    }

    @Override
    public int getCount() {
        return fragments.size();
    }
}
說明:
沒什麼特別的,繼承 FragmentPagerAdapter,並且覆寫 getItem() 及 getCount() 方法。

main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <include
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        layout="@layout/toolbar" />

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager" 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/toolbar" />

</RelativeLayout>
說明:
MainActivity 的 layout,使用 ViewPager。

fragment_page.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:src="@drawable/wallpaper_1" />

    <TextView
        android:id="@+id/txtTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:gravity="center_horizontal"
        android:text="Title"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</RelativeLayout>
說明:
每個頁面都顯示一張圖及一行字。

最後完成的結果: 範例程式碼:github - tony915/ViewPagerDemo
!備註:範例專案的 support library 部份,是用 library 的方法加入,所以記得調整你的 librayr 目錄。(專案按 右鍵 -> properties -> Android,如果你的 library 路徑位置和我的不同,會打叉)。或是你也可以直接複製 v4,v7 的 jar 到 libs 目錄下,只是這個做法不太建議。
本文網址:http://blog.tonycube.com/2015/06/android-viewpager.html
Tony Blog 撰寫,轉載時請註明出處及文章連結,謝謝 😀

6 則留言

  1. 樓主您好,我將您的範例下載下來後

    照著您的方法更改了Library,但是卻發生你專案中的.java檔內的R檔錯誤,顯示R cannot be resolved to a variable

    還有values資料夾中的theme_material.xml item錯誤,顯示error: Error: No resource found that matches the given name: attr 'colorAccent'.

    請問有什麼方式可以成功編譯專案呢?

    回覆刪除
    回覆
    1. 先確定 Androidmanifest.xml 中的 targetSDKVersion是不是設定大於21,
      再確定support library是否有設定正確

      刪除
  2. 老師您好,我匯入專案之後出現project sdk is not defind,有出現setup sdk,所以我設置android api platform,每個版本都是過了還是出現http://ppt.cc/Sj4Uu,請問有什麼方法解決嗎?謝謝您

    回覆刪除
    回覆
    1. 抱歉!沒有說明問題是沒辦法執行該app

      刪除
    2. 這個範例是用舊的Eclipse寫的,可能是沒有 gradle 檔的關係,
      你用 AS 匯入的時候要選擇匯入 eclipse 專案的選項,
      不確定會不會自動建立 gradle 檔,
      如果沒有的話要自行加入,這樣應該就能執行。

      刪除
  3. 補充一下..如果想用Android.app.fragment 可以下載android.support.v13 ,謝謝樓主..

    回覆刪除

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