18.在檢視之間滑動
問題
需要在應用程式的UI中通過手勢滑動來實現頁面切換,例如檢視之間或Fragment之間的切換。
解決方案
(API level 4)
實現ViewPager小部件以提供手勢滑動時頁面切換的功能。ViewPager是AdapterView模式修改後的實現,ListView和GridView小部件也使用了框架的這種模式。ViewPager需要一個繼承自PagerAdapter的子類介面卡實現,但從概念上講,該介面卡與BaseAdapter和ListAdapter中使用的模式非常類似。ViewPager本身並不能實現分頁控制元件的回收,但它每時每刻都提供了回撥方法來進行條目的建立和銷燬。所以在特定的時間內,記憶體中執行的內容檢視的數量是固定的。
要點:
ViewPager是當前只可以通過Android支援庫使用的控制元件。無論在哪個級別的Android平臺中,原生的SDK都不包含ViewPager。不過,所有目標版本為API Level 4或以上級別的應用程式都可以通過包括支援庫來使用該小部件。關於在專案中包括支援庫的更多資訊,請參考 ofollow,noindex">https://developer.android.com/tools/support-library/index.html 。
實現機制
使用ViewPager最大的工作就是PagerAdapter的實現。讓我們開始一個簡單的示例,參見以下清單程式碼,它實現了一系列圖片的分頁顯示。
自定義影象PagerAdapter
public class ImagePagerAdapter extends PagerAdapter { private Context mContext; private static final int[] IMAGES = { android.R.drawable.ic_menu_camera, android.R.drawable.ic_menu_add, android.R.drawable.ic_menu_delete, android.R.drawable.ic_menu_share, android.R.drawable.ic_menu_edit }; private static final int[] COLORS = { Color.RED, Color.BLUE, Color.GREEN, Color.GRAY, Color.MAGENTA }; public ImagePagerAdapter(Context context) { super(); mContext = context; } /* * 提供頁面的總數 */ @Override public int getCount() { return 5; } /* * 如果要在Viewpager內一次顯示超過一頁的內容,那麼需要重寫該方法 */ @Override public float getPageWidth(int position) { return 0.333f; } @Override public Object instantiateItem(ViewGroup container, int position) { // 建立一個新的ImageView並把它新增到提供的容器中 ImageView iv = new ImageView(mContext); // 設定此位置的內容 iv.setImageResource(IMAGES[position]); iv.setBackgroundColor(COLORS[position]); // 這裡你必須自己新增檢視,Android框架是不會為你新增的 container.addView(iv); //將這個檢視作為這個位置的鍵物件返回 return iv; } @Override public void destroyItem(ViewGroup container, int position, Object object) { //此處從容器中刪除檢視 container.removeView((View) object); } @Override public boolean isViewFromObject(View view, Object object) { // 檢查從instantiateItem() 返回的物件與新增到容器相應位置的檢視是否 //是同個物件。 我們的示例在這兩個地方使用的是同一個物件。 return (view == object); } }
在這個示例中,我們實現了一個PagerAdapter,它提供了很多的ImageView例項供使用者翻看。和AdapterView的介面卡一樣,PagerAdapter中第一個需要重寫的就是getCount()方法,它會返回要顯示條目的總數。
ViewPager是基於跟蹤每個條目的鍵物件以及顯示該物件的檢視進行工作的,這樣會將介面卡條目和它們的檢視(開發人員在使用AdapterView時經常會用到)分離開來。但是它們的實現方式略有不同。如果使用AdapterView,介面卡的getView()方法會構建和返回條目上顯示的檢視。而使用ViewPager,當需要建立一個新檢視,或者某個檢視滾動超出了頁數限制的範圍後需要刪除該檢視時,就會分別呼叫instantiateItem()和destoryItem()回撥方法,通過setOffscreenPageLimit()方法來設定每個ViewPager可持有條目的數量。
注意:
螢幕以外的頁數預設限制值為3,這意味著ViewPager將會跟蹤當前可見頁面、當前頁面左側的頁面以及當前頁面右側的頁面。跟蹤頁面的編號總是圍繞當前可見的頁面居中進行的。
在我們的示例中,我們使用instantiateItem()來建立一個新的ImageView並設定該ImageView的相關屬性。和AdapterView不同的是,PagerAdapter不同的是,PageAdapter除了通過返回唯一的鍵物件來表示某個條目外,還必須把要顯示的View關聯到給定的ViewGroup上。這兩個操作不一定需要相同,但可以像本例中這樣簡單處理。需要重寫PagerAdapter的isViewFromObject()回撥方法,這樣應用程式就可以將鍵物件和檢視關聯起來。在我們的示例中,將ImageView新增到給定的父檢視上並將該ImageView作為instantiateItem()的鍵物件返回值。如此一來,isViewFromObject()中的程式碼就變得簡單了,如果兩個引數的例項是相同的,就返回true。
作為例項化過程的補充,PagerAdapter同樣需要在destoryItem()方法中將指定的檢視從父容器移除。如果頁面上顯示的是重量級檢視,同時你想實現可以在介面卡中迴圈利用的基本檢視,這個檢視被刪除後可以儲存它,這樣它就可以在instantiateItem()中附加在另一個鍵物件上。以下程式碼清單展示了一個Activity示例,在Viewpager中使用我們自定義的介面卡。
使用了Viewpager和ImagePagerAdapter的Activity
public class PagerActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewPager pager = new ViewPager(this); pager.setAdapter(new ImagePagerAdapter(this)); setContentView(pager); } }
執行這個應用程式後,使用者可以水平滑動手指來分頁瀏覽自定義介面卡提供的所有圖片,而且每張圖片都是全屏顯示。本例中有一個定義的方法我們沒有提到:getPageWidth()。這個方法允許在每個位置設定圖片頁面大小相對於ViewPager頁面大小的百分比。預設值設定為1,前面的示例也沒有改變該預設值。但如果要一次顯示幾個頁面,可以通過調整這個方法的返回值來實現。
如果按照下面的程式碼片段修改getPageWidth(),那麼我們一次可以顯示三個頁面:
/** * 如果要在ViewPager內一次顯示超過一頁的內容,那麼需要重寫該方法。 */ @Override public float getPageWidth(int position){ //每個頁面的寬應該是檢視的1/3 return 0.333f; }
如下圖所示展示了應用程式的修改結果。

每次顯示三個頁面的ViewPager
1.新增和刪除頁面
以下程式碼清單演示了一個用於ViewPager的稍微複雜的介面卡。它使用框架中的FragmentPagerAdapter作為父類,FragmentPagerAdapter的每個頁面條目都是Fragment而不是簡單的檢視。
顯示了一個列表的FragmentPagerAdapter
public class ListPagerAdapter extends FragmentPagerAdapter { private static final int ITEMS_PER_PAGE = 3; private List<String> mItems; public ListPagerAdapter(FragmentManager manager, List<String> items) { super(manager); mItems = items; } /* * This method will only get called the first time a Fragment is needed for this position. */ @Override public Fragment getItem(int position) { int start = position * ITEMS_PER_PAGE; return ArrayListFragment.newInstance(getPageList(position), start); } @Override public int getCount() { //Get whole number int pages = mItems.size() / ITEMS_PER_PAGE; // Add one more page for any remaining values if list size is not divisible by page size int excess = mItems.size() % ITEMS_PER_PAGE; if (excess > 0) { pages++; } return pages; } /* * This will get called after getItem() for new Fragments, but also when Fragments * beyond the off-screen page limit are added back; we need to make sure to update the * list for these elements. */ @Override public Object instantiateItem(ViewGroup container, int position) { ArrayListFragment fragment = (ArrayListFragment) super.instantiateItem(container, position); fragment.updateListItems(getPageList(position)); return fragment; } /* * Called by the framework when notifyDataSetChanged() is called, we must decide how * each Fragment has changed for the new data set.We also return POSITION_NONE if * a Fragment at a particular position is no longer needed so the adapter can * remove it. */ @Override public int getItemPosition(Object object) { ArrayListFragment fragment = (ArrayListFragment)object; int position = fragment.getBaseIndex() / ITEMS_PER_PAGE; if(position >= getCount()) { //This page no longer needed return POSITION_NONE; } else { //Refresh fragment data display fragment.updateListItems(getPageList(position)); return position; } } /* * Helper method to obtain the piece of the overall list that should be * applied to a given Fragment */ private List<String> getPageList(int position) { int start = position * ITEMS_PER_PAGE; int end = Math.min(start + ITEMS_PER_PAGE, mItems.size()); List<String> itemPage = mItems.subList(start, end); return itemPage; } /* * Internal custom Fragment that displays a list section inside * of a ListView, and provides external methods for updating the list */ public static class ArrayListFragment extends Fragment { private ArrayList<String> mItems; private ArrayAdapter<String> mAdapter; private int mBaseIndex; //Fragments are created by convention using a Factory pattern static ArrayListFragment newInstance(List<String> page, int baseIndex) { ArrayListFragment fragment = new ArrayListFragment(); fragment.updateListItems(page); fragment.setBaseIndex(baseIndex); return fragment; } public ArrayListFragment() { super(); mItems = new ArrayList<String>(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Make a new adapter for the list items mAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, mItems); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { //Construct and return a Listview with our adapter attached ListView list = new ListView(getActivity()); list.setAdapter(mAdapter); return list; } //Save the index in the global list where this page starts public void setBaseIndex(int index) { mBaseIndex = index; } //Retrieve the index in the global list where this page starts public int getBaseIndex() { return mBaseIndex; } public void updateListItems(List<String> items) { mItems.clear(); for (String piece : items) { mItems.add(piece); } if (mAdapter != null) { mAdapter.notifyDataSetChanged(); } } } }
這個示例使用一個很長的資料列表並將其分解成小段顯示在每個頁面上。這個介面卡顯示的Fragment是一個自定義內部實現,它會接收條目的一個列表並將這些條目顯示在ListView中。
FragmentPagerAdapter幫助我們實現了PagerAdapter底層的很多功能。不必再實現instantiateItem()、destroyItem()和isViewFromObject()方法,只需要重寫onItem()來為每個頁面位置提供相應的Fragment。本例為每個頁面應該顯示的列表條目的數量定義了一個常量。在getItem()內建立Fragment時,會傳入列表中的一部分資料,而這些資料是根據索引偏移和之前定義的常量來計算的。分頁的數量由getCount()方法返回,這個值是通過列表條目總量除以每頁顯示的條目常量計算得到的。
提示:
FragmentPagerAdapter將所有Fragment例項保持為活動狀態,無論它們在螢幕外頁數限制內是否被啟用。如果ViewPager需要容納更多數量的Fragment,或者一些ViewPager更加重量級,則可以改為使用FragmentStatePagerAdapter。FragmentStatePagerAdapter會銷燬超出螢幕外頁數限制的Fragment,同時保留其已儲存的狀態,這一點類似於旋轉操作。
這個介面卡還覆寫了前面簡單示例中未曾見到過的另一個方法:getItemPosition()。當應用程式從外部呼叫notifyDataSetChanged()時,這個方法會被呼叫。它主要的作用是在頁面發生變化時判斷頁面中的條目應該被移動還是刪除。如果條目的位置發生改變,該實現就應該返回新位置的值。如果條目不應該被移動,該實現就會返回一個常量值PagerAdapter.POSITION_UNCHANGED。如果頁面應該被刪除,應用程式應該返回PagerAdapter.POSITION_NONE。
這個示例會比較檢查當前頁面的位置(我們需要從初始索引資料開始重新建立)和當前頁面數量的大小。如果當前頁面位置大於當前頁面數量,就需要從列表中刪除足夠的條目,如此一來就不再需要該頁面了,然後返回POSITION_NONE。而在其他情況下,我們會更新當前Fragment中顯示的列表條目,並返回重新計算得到的位置值。
每個ViewPager當前跟蹤的頁面都會呼叫getItemPosition(),呼叫的次數即為getOffscreenPageLimit()返回的頁面數量。然而,雖然ViewPager不會跟蹤滾動出限定值之外的Fragment,但FragmentManager會繼續追蹤。因此,當之前的Fragment回滾時,getItem()不會被再次呼叫,因為Fragment已經存在了。但是正因為如此,如果一個數據集在這期間發生改變,Fragment列表資料不會跟著更新。這就是要重寫instantiateItem()的原因。雖然這個介面卡不需要重寫instantiateItem(),但是當列表發生變化時,確實需要更新超出螢幕外頁數限制的Fragment。因為Fragment回滾到頁數限制內以後,每次都會呼叫instantiateItem(),所以這是重置顯示列表的好時機。
讓我們看一個使用該介面卡的示例應用程式。參見以下兩段程式碼清單:
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Add Item" android:onClick="onAddClick" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Remove Item" android:onClick="onRemoveClick" /> <android.support.v4.view.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
使用了ListPagerAdapter的Activity
public class FragmentPagerActivity extends ActionBarActivity { private ArrayList<String> mListItems; private ListPagerAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //Create the initial data set mListItems = new ArrayList<String>(); mListItems.add("Mom"); mListItems.add("Dad"); mListItems.add("Sister"); mListItems.add("Brother"); mListItems.add("Cousin"); mListItems.add("Niece"); mListItems.add("Nephew"); //Attach the data to the pager ViewPager pager = (ViewPager) findViewById(R.id.view_pager); mAdapter = new ListPagerAdapter(getSupportFragmentManager(), mListItems); pager.setAdapter(mAdapter); } public void onAddClick(View v) { //Add a new unique item to the end of the list mListItems.add("Crazy Uncle " + System.currentTimeMillis()); mAdapter.notifyDataSetChanged(); } public void onRemoveClick(View v) { //Remove an item from the head of the list if (!mListItems.isEmpty()) { mListItems.remove(0); } mAdapter.notifyDataSetChanged(); } }
就像ViewPager的效果一樣,這個示例中有兩個按鈕用來新增和刪除資料集中的條目,注意ViewPager必須在XML檔案中定義並使用完全限定的包名,因為它僅是支援庫中的類,在android.widget或android.view包中並沒有這個類。該Activity構建了一個預設的條目列表並把它傳入我們自定義的介面卡中,然後再把該介面卡關聯到ViewPager上。
每次單擊Add按鈕都在列表末尾新增一個新的條目並通過呼叫notofyDataSetChanged()來觸發ListPagerAdapter進行更新。每次單擊Remove按鈕都會在列表頂部刪除一個條目,然後再次通知介面卡。每次變化期間,介面卡都會調整當前可用頁數並更新Viewpager。如果當前可見頁的所有條目都被刪除,那麼該頁也會被刪除並顯示上一頁。
2.其他有用的方法
ViewPager中有幾個其他的方法,它們會對你的應用程式很有幫助:
- setPagerMargin()和setPageMarginDrawable()允許在頁面之間設定一些額外的間隔,並且使用一個Drawable(可選)來填充間隔的內容。
- setCurrentItem()允許你以程式設計的方式設定要顯示的頁面,並提供了一個選項來禁用頁面切換時的滾動動畫。
- OnPageChangeListener用於將滾動和變更動作通知給應用程式。
onPageSelected()會在顯示一個新頁面時被呼叫。
當發生滾動操作時會連續呼叫onPageScrolled()。
onPageScrollStateChanged()在ViewPager處於以下狀態時會被呼叫:閒置時、使用者主動滾動ViewPager時、自動滾動對齊到最近的頁面時。