1 認識一下ViewPager?
ViewPager最早出自4.0版本,那么低版本如何能使用ViewPager呢?為了兼容低版本安卓設備,谷歌官方給我們提供了一個的軟件包android.support.v4.view。這個V4包囊了只有在安卓3.0以上可以使用的api,而viewpager就是其中之一。利用它,我們可以做很多事情,從最簡單的引導頁導航,到輪轉廣告,到頁面菜單等等,無不出現ViewPager的身影。應用廣泛,簡單好用,更好的交互性,這也是ViewPager一出現便大受程序員歡迎的原因。如此好用的控件,你是不是已經蠢蠢欲動了呢?不廢話,我們將以項目為向導,由淺入深的講解ViewPager,開始ViewPager的學習之旅吧。
2 什么時候可以使用ViewPager?
任何新的技術,最難的不是學習如何使用它,而是明白什么時候使用它最合適。正所謂物盡其用,只有正確的技術用在了正確的地方,那么才能發揮該技術最大的功效,做出好的應用。下面結合一些典型場景來讓不了解ViewPager的你了解在什么情況下使用ViewPager才是最好的。ViewPager最典型的應用場景主要包括引導頁導航,輪轉廣告,和頁面菜單。可以這么說,但凡遇到界面切換的需求,都可以考慮ViewPager。拋磚引玉,剩下的就看讀者發揮想象力了。
3 ViewPager的基本入門(和ListView對比學習)
那如何使用它呢,與ListView類似,我們也需要一個適配器,他就是PagerAdapter。ViewPager采用MVC模式將前段顯示與后端數據進行分離,也就是說器裝載數據並不是直接添加數據,而是,需要使用PagerAdapter。PagerAdapter相當於,MVC模式中的C(Controller,控制器),ViewPager相當MVC模式中的V(View,視圖),為ViewPager提供的數據List,數組或者數據庫,就相當於MVC中的M(Mode,模型)。
學習ViewPager不僅僅是學習ViewPager單一個控件那么簡單,我們需要圍繞MVC模式,把ViewPager用到的數據(M),視圖(V),控制器(C)都理一遍,明白如何把他們,組合在一起,達到ViewPager的切換效果。
我們通過一個簡單的項目來認識一下ViewPager的使用方式。
首先新建項目,引入ViewPager控件
ViewPager,它是google SDk中自帶的一個附加包的一個類,可以用來實現屏幕間的切換,在V4包中。
三步曲:
3.1 准備視圖 View
在主布局文件main.xml中添加ViewPager如下:
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" tools:context="com.example.testviewpage_1.MainActivity" > <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </RelativeLayout>
其中,其中 <android.support.v4.view.ViewPager /> 是ViewPager對應的組件,要將其放到想要滑動的位置,可以全屏顯示,也可以半屏,任意大小,由程序員按需求控制。
3.2 准備數據模型 ,Mode
① 新建三個layout,用於滑動切換的視圖:
我們的三個視圖都非常簡單,里面沒有任何的控件,大家當然可以往里添加各種控件,但這里是個DEMO,只詳解原理即可,所以我這里僅僅用背景來區別不用layout布局。
layout1.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:background="#ffffff" android:orientation="vertical" > </LinearLayout>
layout2.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:background="#ffff00" android:orientation="vertical" > </LinearLayout>
layout3.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:background="#ff00ff" android:orientation="vertical" > </LinearLayout>
② 聲明變量:
private View view1, view2, view3;
private List<View> viewList;//view數組
private ViewPager viewPager; //對應的viewPager
我們來看看上面的變量聲明:
首先viewPager對應 <android.support.v4.view.ViewPager/>控件。
view1, view2, view3對應我們的三個layout,即layout1.xml,layout2.xml,layout3.xml
viewList是一個View數組,盛裝上面的三個VIEW
③ 數據的初始化:
viewPager = (ViewPager) findViewById(R.id.viewpager); LayoutInflater inflater=getLayoutInflater(); view1 = inflater.inflate(R.layout.layout1, null); view2 = inflater.inflate(R.layout.layout2,null); view3 = inflater.inflate(R.layout.layout3, null); viewList = new ArrayList<View>();// 將要分頁顯示的View裝入數組中 viewList.add(view1); viewList.add(view2); viewList.add(view3);
獲取到找到ViewPager ,賦值給變量,最后將實例化的view1,view2,view3添加到viewList中。
3.3 准備控制器(Controller)—— PagerAdapter
PagerAdapter 是ViewPager的適配器。
適配器我們在ListView里面早就使用過,listView通過重寫GetView()函數來獲取當前要加載的Item。而PageAdapter不太相同,畢竟PageAdapter是單個VIew的合集。PagerAdapter在instantiateItem()里面給布局容器添加了將要顯示的視圖。
PageAdapter 必須重寫的四個函數:
boolean isViewFromObject(View arg0, Object arg1)
int getCount()
void destroyItem(ViewGroup container, int position,Object object)
Object instantiateItem(ViewGroup container, int position)
下面,我們就看看四個主要方法改如何重寫,都分別做了什么吧
@Override public int getCount() { // TODO Auto-generated method stub return viewList.size(); } getCount(),返回滑動的View的個數。 @Override public void destroyItem(ViewGroup container, int position, Object object) { // TODO Auto-generated method stub container.removeView(viewList.get(position)); } destroyItem,從容器中刪除指定position的View @Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return viewList.get(position); } };
instantiateItem()方法中,我先講指定position位置的View添加到容器中,末了,將本View返回。
@Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; }
這里為什么這么寫暫不做講解,知道這樣寫即可,后面我們會單獨講解清楚。
這么簡單,我們就實現了三個view間的相互滑動。
第一個界面想第二個界面滑動 第二個界面想第三個界面滑動
以下是全部核心代碼:
package com.example.testviewpage_1; import java.util.ArrayList; import java.util.List; import java.util.zip.Inflater; import android.app.Activity; import android.os.Bundle; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class MainActivity extends Activity { private View view1, view2, view3; private ViewPager viewPager; //對應的viewPager private List<View> viewList;//view數組 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewPager = (ViewPager) findViewById(R.id.viewpager); LayoutInflater inflater=getLayoutInflater(); view1 = inflater.inflate(R.layout.layout1, null); view2 = inflater.inflate(R.layout.layout2,null); view3 = inflater.inflate(R.layout.layout3, null); viewList = new ArrayList<View>();// 將要分頁顯示的View裝入數組中 viewList.add(view1); viewList.add(view2); viewList.add(view3); PagerAdapter pagerAdapter = new PagerAdapter() { @Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; } @Override public int getCount() { // TODO Auto-generated method stub return viewList.size(); } @Override public void destroyItem(ViewGroup container, int position, Object object) { // TODO Auto-generated method stub container.removeView(viewList.get(position)); } @Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return viewList.get(position); } }; viewPager.setAdapter(pagerAdapter); } }
至此我們已經基本了解了ViewPager,學會了基本用法,接下來,我們就來詳細學習ViewPager的核心PagerAdapter。
4 從PagerAdapter說開去——解讀PagerAdapter的四大函數
4.1 且看官方文檔怎么說?
最權威的講解是官方文檔,都是英文的,不好排版,我就不貼出來了,以下是我根據文檔翻譯出來的。有不明白的,可以自己看官方文檔:
http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html(加紅)
安卓提供一個適配器用於填充ViewPager頁面. 你很可能想要使用一個更加具體的實現, 例如:
FragmentPagerAdapter or FragmentStatePagerAdapter.
當你實現一個PagerAdapter時,至少需要覆蓋以下幾個方法:
instantiateItem(ViewGroup, int)
destroyItem(ViewGroup, int, Object)
isViewFromObject(View, Object)
PagerAdapter比AdapterView的使用更加普通.ViewPager使用回調函數來表示一個更新的步驟,而不是使用一個視圖回收機制。在需要的時候pageradapter也可以實現視圖的回收或者使用一種更為巧妙的方法來管理視圖,比如采用可以管理自身視圖的fragment。
① viewpager不直接處理每一個視圖而是將各個視圖與一個鍵聯系起來。這個鍵用來跟蹤且唯一代表一個頁面,不僅如此,該鍵還獨立於這個頁面所在adapter的位置。當pageradapter將要改變的時候他會調用startUpdate函數, 接下來會調用一次或多次的instantiateItem或者destroyItem。最后在更新的后期會調用finishUpdate。當finishUpdate返回時 instantiateItem返回的對象應該添加到父ViewGroup destroyItem返回的對象應該被ViewGroup刪除。methodisViewFromObject(View, Object)代表了當前的頁面是否與給定的鍵相關聯。
② 對於非常簡單的pageradapter或許你可以選擇用page本身作為鍵,在創建並且添加到viewgroup后instantiateItem方法里返回該page本身即可
destroyItem將會將該page從viewgroup里面移除。isViewFromObject方法里面直接可以返回view == object。
pageradapter支持數據集合的改變,數據集合的改變必須要在主線程里面執行,然后還要調用notifyDataSetChanged方法。和baseadapter非常相似。數據集合的改變包括頁面的添加刪除和修改位置。viewpager要維持當前頁面是活動的,所以你必須提供getItemPosition方法。
上面的FragmentPagerAdapter 和FragmentStatePagerAdapter非常常用,我們放到后面來講。
上面的話,重點只有兩段:① ,②
針對上面兩段,集中理解兩點:
(1)第一段說明了,鍵(Key)的概念,首先這里要清楚的一點是,每個滑動頁面都對應一個Key,而且這個Key值是用來唯一追蹤這個頁面的,也就是說每個滑動頁面都與一個唯一的Key一一對應。大家先有這個概念就好,關於這個Key是怎么來的,下面再講。
(2)當前page本身可以作為鍵,直接在destroyItem()返回,用來標示自己。下面,我們來講講Key
4.2 ViewPager的 key
① destroyItem(ViewGroup, int, Object)
該方法把給定位置的界面叢容器中移除,負責從容器中刪除視圖,確保在finishUpdate(viewGroup)返回時視圖能夠被移除。
來看看我們前面的項目是怎么重寫這個方法的:
@Override public void destroyItem(ViewGroup container, int position, Object object) { // TODO Auto-generated method stub container.removeView(viewList.get(position)); }
將給定位置的視圖從container中移除了…… 這個方法必須被實現,而且不能調用父類,否則拋出異常。(說該方法沒有被覆蓋)
返回當前有效視圖的個數。
@Override public int getCount() { // TODO Auto-generated method stub return viewList.size(); }
返回了當前需要顯示的視圖的個數。
接下來的兩個方法是重點。
③ instantiateItem(ViewGroup, int)
這個方法實現的功能是創建指定位置的視圖,同時肩負着添加該創建的視圖到指定容器container中,而這一步,要確保在finishUpdate(viewGroup)返回之后完成。
該方法返回一個代表該視圖的鍵(key),沒必要非是視圖本身,也可以是這個頁面的其他容器,我的理解是沒必要視圖本身,只要這個返回值能代表當前視圖,並與視圖一意對應即可,比如返回和該視圖對應的position可以嗎?(接下來我們做個例子試試)
總結:
給container添加一個視圖。
返回代表該視圖的Key
該方法和destroyItem(ViewGroup, int, Object)一樣,在finishUpdate(ViewGroup)這句話執行完之后執行。
我們來看看我們是怎么做的:
@Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return viewList.get(position); } };
沒有錯,這里我們給container添加了一個View viewList.get(position),,並將該視圖作為key返回了。
回過頭來,我們看看第四章的官方文檔翻譯:
② 對於非常簡單的pageradapter或許你可以選擇用page本身作為鍵,在創建並且添加到viewgroup后instantiateItem方法里返回該page本身即可
destroyItem將會將該page從viewgroup里面移除。isViewFromObject方法里面直接可以返回view == object。
就是這里,把當前的View作為key傳出去,那么這個key在哪里被使用呢?就得來看看下面的方法了。
④ isViewFromObject(View, Object)
功能:該函數用來判斷instantiateItem(ViewGroup, int)函數所返回來的Key與一個頁面視圖是否是代表的同一個視圖(即它倆是否是對應的,對應的表示同一個View)
返回值:如果對應的是同一個View,返回True,否則返回False。
在上面的項目中,我們這樣做的:
@Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; }
由於在instantiateItem()中,我們作為Key返回來的是當前的View,所以在這里判斷時,我們直接將Key與View看是否相等來判斷是否是同一個View。
發散思維:如果我們在instantiateItem()返回的是代表當前視圖的position而非本身呢?這里該怎么做?接下來我們就解答你的疑問。
4.3 自定義key
上面我們想必對key有個初步認識,下面我們舉個例子來說明一下key和View的關系,由於key要和View一一對應,這里我把和View一一對應的position作為key返回,然后在上面的項目的基礎上修改。這里只展示需要修改的代碼。
我們更改了兩個地方:
(1)instantiateItem()
@Override public Object instantiateItem(ViewGroup container, int position) { // TODO Auto-generated method stub container.addView(viewList.get(position)); return position ; } };
(2)2、isViewFromObject ()
@Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub //根據傳來的key(arg1),找到view,判斷與傳來的參數View arg0是不是同一個視圖 return arg0 == viewList.get((int)Integer.parseInt(arg1.toString())); }
判斷instantiateItem()返回的key與視圖是否對應,這里我們返回的是position,我們需要根據position找到對應的View,與傳過來的View對比,看看是否對應。注意:這里,我們要先將obect對應轉換為int類型:(int)Integer.parseInt(arg1.toString());然后再根據position找到對應的View;
5 ViewPager的進階,添加標題欄
5.1 PagerTitleStrip
View可以添加標題欄,用來指示當前滑動到哪一頁。先來一張效果圖:
PagerTabStrip是ViewPager的一個關於當前頁面、上一個頁面和下一個頁面的一個非交互的指示器。它經常作為ViewPager控件的一個子控件被被添加在XML布局文件中。在你的布局文件中,將它作為子控件添加在ViewPager中。而且要將它的 android:layout_gravity 屬性設置為TOP或BOTTOM來將它顯示在ViewPager的頂部或底部。每個頁面的標題是通過適配器的getPageTitle(int)函數提供給ViewPager的。
主要是兩點:
① PagerTabStrip可以作為控件直接添加到xml布局文件中。
② 重寫getPageTitle(int)來給PagerTabStrip提供標題。
你也許會發現上面只有上部分一部分的地方才有滑動切換,是因為我更改了布局文件。
(1) 先來看看布局文件:
<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="com.example.testviewpage_2.MainActivity" > <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="wrap_content" android:layout_height="200dip" android:layout_gravity="center"> <android.support.v4.view.PagerTitleStrip android:id="@+id/pagertitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="top" /> </android.support.v4.view.ViewPager> </RelativeLayout>
這里將layout_height更改為200dip,只所以這么做,是為了告訴大家,只要在想要實現滑動切換的地方添加上<android.support.v4.view.ViewPager />就可以實現切換,無所謂位置和大小,跟普通控件一樣!!!!!!
重點是我們將PagerTabStrip作為子控件直接鑲嵌在ViewPager中,設置layout_gravity="top" 或者 buttom。
(2) 重寫適配器的getPageTitle()函數
在元項目基礎上我們做了如下更改:
1、定義變量:
private List<String> titleList; //標題列表數組
申請了一個標題數組,來存儲三個頁面所對應的標題、
2、初始化
titleList = new ArrayList<String>();// 每個頁面的Title數據
titleList.add("王鵬");
titleList.add("姜語");
titleList.add("結婚");
添加了標題數據
3、重寫CharSequence getPageTitle(int )函數
@Override public CharSequence getPageTitle(int position) { // TODO Auto-generated method stub return titleList.get(position); }
根據位置返回當前所對應的標題。
5.2 PagerTabStrip
PagerTabStrip使用方法和上面類似。
先來看看效果:
效果和PagerTitleStrip差不多,但是有微小差別:
PagerTabStrip在當前頁面下,標題的下方有一個橫線作為導航。
PagerTabStrip的Tab是可以點擊的,點擊標題可以跳轉到對應的頁面。
PagerTabStrip是ViewPager的一個關於當前頁面、上一個頁面和下一個頁面的一個可交互的指示器。它經常作為ViewPager控件的一個子控件被被添加在XML布局文件中。在你的布局文件中,將它作為子控件添加在ViewPager中。而且要將它的 android:layout_gravity 屬性設置為TOP或BOTTOM來將它顯示在ViewPager的頂部或底部。每個頁面的標題是通過適配器的getPageTitle(int)函數提供給ViewPager的。
注意:可交互的,這就是PagerTabStrip和PagerTitleStrip最大的不一樣。PagerTabStrip是可交互的,PagerTitleStrip是不可交互的。
用法與PagerTitleStrip完全相同,即:
1、首先,文中提到:在你的布局文件中,將它作為子控件添加在ViewPager中。
2、第二,標題的獲取,是重寫適配器的getPageTitle(int)函數來獲取的。
看看實例:
1、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="com.example.testviewpage_2.MainActivity" > <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"> <android.support.v4.view.PagerTabStrip android:id="@+id/pagertab" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top"/> </android.support.v4.view.ViewPager> </RelativeLayout>
可以看到,同樣,是將PagerTabStrip作為ViewPager的一個子控件直接插入其中,當然android:layout_gravity=""的值一樣要設置為top或bottom。
2、重寫適配器的getPageTitle()函數
代碼里面不用改
@Override public CharSequence getPageTitle(int position) { // TODO Auto-generated method stub return titleList.get(position); }
根據位置返回當前所對應的標題。
6 Fragment 和 ViewPager的完美結合—— FragmentPagerAdapter
前面講解了ViewPager的普通實現方法,但android官方最推薦的一種實現方法卻是使用fragment,Fragment的碎片化功能大大的豐富了ViewPager的功能和表現形式。先前我們實現ViewPager使用的是ViewPagerAdapter。而對於fragment,使用的是FragmentPagerAdapter和FragmentStatePagerAdapter。下面我們來學習一下。
6.1 FragmentPagerAdapter
FragmentPagerAdapter是PagerAdapter的子類,專門用來呈現fragment頁面的,這些Fragment會一直保存在FragmentManager,以方便用戶隨時取用。
FragmentPagerAdapter適用於有限個fragment的頁面管理,因為你所訪問過的fragment都會保存在內存中。由於,fragment保存着大量的各種狀態,這樣就造成了比較大的內存開銷。故,當遇到大量的頁面切換的時候,建議采用FragmentStatePagerAdapter,這個我們會在下面的章節講到。
FragmentPagerAdapter使用過程:
6.1.1 適配器的實現:
public class FragAdapter extends FragmentPagerAdapter { private List<Fragment> mFragments; public FragAdapter(FragmentManager fm,List<Fragment> fragments) { super(fm); // TODO Auto-generated constructor stub mFragments=fragments; } @Override public Fragment getItem(int arg0) { // TODO Auto-generated method stub return mFragments.get(arg0); } @Override public int getCount() { // TODO Auto-generated method stub return mFragments.size(); } }
很簡單吧,只需要繼承FragmentPagerAdapter實現兩個方法getItem(int arg)和 getCount(),就可以了。
這里,我們定義了一個fragment的List對象,在構造方法里面初始化了。如下
public FragAdapter(FragmentManager fm,List<Fragment> fragments) { super(fm); // TODO Auto-generated constructor stub mFragments=fragments; }
接下來我們實現了getCount(),和前面一樣返回了頁面的個數。這里我們返回了List對象的大小。List就是fragment的集合,有多少個fragment就展示多少個頁面,這點很容易理解。如下:
@Override public int getCount() { // TODO Auto-generated method stub return mFragments.size(); }
最后,根據傳過來的鍵Key參數,返回該當前要顯示的fragment,如下:
@Override public Fragment getItem(int arg0) { // TODO Auto-generated method stub return mFragments.get(arg0); }
6.1.2 構造Fragment類。
下面我們要分別構造3個Fragment,這里,我們第一個fragment1有一個可以點擊的按鈕,第二個和第三個fragment2,fragment3分別用不同的背景代替。
第一個Fragment類:
XML:(layout1.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:background="#ffffff" android:orientation="vertical" > <Button android:id="@+id/fragment1_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="show toast" /> </LinearLayout>
Fragment1的java代碼:
public class Fragment1 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO Auto-generated method stub View view= inflater.inflate(R.layout.layout1, container, false); //對View中控件的操作方法 Button btn = (Button)view.findViewById(R.id.fragment1_btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Toast.makeText(getActivity(), "點擊了第一個fragment的BTN", Toast.LENGTH_SHORT).show(); } }); return view; } }
這里我加入了一個按鈕,在onCreateView()方法里面加載了layout1,返回了要顯示的View,同時,給按鈕添加了一個監聽事件,這里為了向讀者說明利用了fragment我們可以實現各種各樣的交互,ViewPager能做到的不僅僅是動態的圖片,而是動態的交互。
第二個Fragment類:
XML代碼:(layout2.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:background="#ffff00" android:orientation="vertical" > </LinearLayout>
java代碼:
public class Fragment2 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO Auto-generated method stub View view=inflater.inflate(R.layout.layout2, container, false); return view; } }
第三個Fragment類:
XML代碼:(layout3.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:background="#ff00ff" android:orientation="vertical" > </LinearLayout>
Java代碼
public class Fragment3 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO Auto-generated method stub View view=inflater.inflate(R.layout.layout3, container, false); return view; } }
6.1.3 主Activity我繼承了FragmentActivity,只有FragmentActivity內部才能內嵌Fragment普通Activity是不行的。
public class MainActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //構造適配器 List<Fragment> fragments=new ArrayList<Fragment>(); fragments.add(new Fragment1()); fragments.add(new Fragment2()); fragments.add(new Fragment3()); FragAdapter adapter = new FragAdapter(getSupportFragmentManager(), fragments); //設定適配器 ViewPager vp = (ViewPager)findViewById(R.id.viewpager); vp.setAdapter(adapter); } }
很簡單,我們構造了一個適配器,然后為ViewPager設置的適配器,和前面幾乎一樣。而適配器里面傳入的,就是那三個我們准備好的fragment。
看看效果:
在第一個頁面加一個按鈕 第一頁面向第二頁面滑動
第二頁面向第三個頁面滑動
6.2 FragmentStatePagerAdapter
和FragmentPagerAdapter相比,它更適用於大量頁面的展示,當整個fragment不再被訪問,則會被銷毀(由於預加載,默認最多保存3個fragment),只保存其狀態,這相對於FragmentPagerAdapter占有了更少的內存,為什么大量頁面用FragmentStatePagerAdapter?不言而喻了吧。
FragmentStatePagerAdapter的用法和FragmentPagerAdapter一樣,這里就不再贅述。
注意:
在初次使用的FragmentPagerAdapter的時候,曾爆出類型轉換的異常。這是為什么呢?跟蹤代碼才發現出錯的地方發生在fragments.add(new Fragment1()); 錯誤提示無法將Fragment1(Fragment的子類)強制轉換成Fragment,當時真是莫名其妙,明明Fragment1就是Fragment,為什么說不是呢?經過仔細排查才發現在Fragment1里面導入的是android.app.Fragment,而在Activity類導入的是為android.support.v4.app.Fragment。統一導入之后才消除異常,不細心造成的錯誤往往難以排查,讓人糾結,這里我們在使用FragmentPagerAdapter必須注意導入正確的包android.support.v4.app.Fragment。
7 ViewPager的預加載機制。
ViewPager能夠如此流暢的切換頁面得益於其預加載的機制,那么什么是ViewPager的預加載呢?
歸納掌握兩點:
① ViewPager會預先加載左右兩邊的圖片,預加載的個數最多3個。前方超出當個數由4個的時候,最前方的會被銷毀。預加載和銷毀分別回調以下兩個方法:
instantiateItem(ViewGroup, int)
destroyItem(ViewGroup, int, Object)
② 限制:當左邊圖片的position小於0的時候,不會預加載;
當右邊的圖片的position大於或者等於item總數的時候,也不會預加載。
我畫了一張示意圖:如下左邊0位置的被銷毀
8 學以致用,用ViewPager做個選項卡。
至此,我們已經基本學完了ViewPager的常用特性。學貴於致用,接下來我們通過一個涵蓋面全的例子,來把我們所學的知識用一遍。
做一個選項卡效果,我們立刻想到ViewPagerIndicator,利用我們以上學過的知識,就可以輕易實現這個功能。
先來一張效果圖,激發激發熱血吧:
上圖 左右滑動,或者點擊文字,界面會切換,同時,頁卡文字下方的滑塊也會滑動,指示當前顯示的頁面。
8.1 准備布局
回憶一下,我們在使用ViewPagerIndicator的時候,會在ViewPager的上面添加ViewPagerIndicator,然后通過ViewPagerIndicator的setViewPager(ViewPager mPager)設置ViewPager,使得ViewPagerIndicator指示器與ViewPager相關聯。
這里,我們用一個包含幾個 TextView的LinearLayout,下邊一個ImageView 替換,如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="60dip" android:background="#FFFFFF" > <TextView android:id="@+id/text1" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="頁卡1" android:textColor="#000000" android:textSize="22.0dip" /> <TextView android:id="@+id/text2" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="頁卡2" android:textColor="#000000" android:textSize="22.0dip" /> <TextView android:id="@+id/text3" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="頁卡3" android:textColor="#000000" android:textSize="22.0dip" /> </LinearLayout> <ImageView android:id="@+id/cursor" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scaleType="matrix" android:src="@drawable/a" /> <android.support.v4.view.ViewPager android:id="@+id/vPager" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_weight="1.0" android:background="#000000" android:flipInterval="30" android:persistentDrawingCache="animation" /> </LinearLayout>
下面是ViewPager。
接下來准備3個切換的布局:(3個布局都是一個RelativeLayout,只是,背景顏色不同而已)
fragment_main_1.xml,fragment_main_2.xml,fragment_main_3.xml
fragment_main_1.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" android:background="#000000" > </RelativeLayout>
fragment_main_2.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" android:background="#ffffff" > </RelativeLayout>
fragment_main_3.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" android:background="#ff0000" > </RelativeLayout>
下面由淺入深,一步步把功能做完:
8.2 先完成ViewPager界面切換,和3 基本入門那一章節一致。
① 初始化 ViewPager布局
private void initViewPager() { vPager = (ViewPager) findViewById(R.id.vPager); List<View> listViews = new ArrayList<View>(); listViews.add(View.inflate(this, R.layout.fragment_main_1, null)); listViews.add(View.inflate(this, R.layout.fragment_main_2, null)); listViews.add(View.inflate(this, R.layout.fragment_main_3, null)); MyPagerAdapter adapter = new MyPagerAdapter(listViews); vPager.setAdapter(adapter); // 給ViewPager設置監聽 MyOnPagerChangeListener listener = new MyOnPagerChangeListener(); vPager.setOnPageChangeListener(listener); }
這一部分相信大家很熟悉了,無非做了2步:
(1)給ViewPager設置適配器。(加載了3個我們已經准備好的布局)
(2)給ViewPager設置滑動頁面監聽。
在此就不再多講,下面是適配器的實現:
class MyPagerAdapter extends PagerAdapter{ List<View> listViews; public MyPagerAdapter(List<View> listViews) { super(); this.listViews = listViews; } @Override public int getCount() { // TODO Auto-generated method stub return listViews.size(); } @Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub return arg0 == arg1; } @Override public Object instantiateItem(View container, int position) { // TODO Auto-generated method stub ((ViewPager)container).addView(listViews.get(position)); return listViews.get(position); } @Override public void destroyItem(View container, int position, Object object) { ((ViewPager)container).removeView(listViews.get(position)); } }
至此,我們已經可以切換界面了。可是,我們發現選項卡下面的指示滑動條並不能隨着頁面的切換而移動,從而標識當前頁面。這就是我們下一步要做的。
8.3 這里,我們完成指示滑動條的移動。
思路:
通過對ViewPager頁面切換的監聽,用唯一動畫相應的距離實現標識滑塊的移動。
① 滑塊相關數據初始化
這一段是很重要的,先貼出核心代碼,隨后詳細講解。
private void initImageView() { cursor = (ImageView) findViewById(R.id.cursor); bmpw = BitmapFactory.decodeResource(getResources(), R.drawable.a).getWidth(); //滑塊的寬度 DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); //給DisplayMetrics賦值 screenW = dm.widthPixels; offset = (screenW/3 - bmpw)/2; //滑塊動畫初始位置 //設置動畫初始位置 Matrix matrix = new Matrix(); matrix.postTranslate(offset, 0); cursor.setImageMatrix(matrix); }
通過上面的代碼主要做了這幾個事兒:
(1)
cursor = (ImageView) findViewById(R.id.cursor);
//滑塊的寬度
bmpw = BitmapFactory.decodeResource(getResources(), R.drawable.a).getWidth();
加載了滑塊,獲得了滑塊的寬度。
(2)獲得了屏幕的寬度
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm); //給DisplayMetrics賦值
screenW = dm.widthPixels; //獲得屏幕寬度
上面的代碼通過一個類WindowManager獲得屏幕的相關信息,保存在 DisplayMetrics 對象里面,然后獲取其屏幕寬度。
(3)計算滑塊的初始位置
offset = (screenW/3 - bmpw)/2; //滑塊動畫初始位置
滑塊的初始位置的計算,請看如下示意圖
(4)
//設置動畫初始位置
Matrix matrix = new Matrix();
matrix.postTranslate(offset, 0);
cursor.setImageMatrix(matrix);
這段代碼做的事情也很簡單,給滑塊設置了初始位置,即當選項頁面為第0頁的時候滑塊的位置。這里是通過Matrix 對象來設置滑塊的位置信息。
② 實現頁面監聽類的方法
class MyOnPagerChangeListener implements OnPageChangeListener{ int one = offset * 2 + bmpw; //選項卡0 -> 1 的偏移量 int two = one * 2; // //選項卡1 -> 2 的偏移量 @Override public void onPageScrollStateChanged(int arg0) { // TODO Auto-generated method stub } @Override public void onPageScrolled(int arg0, float arg1, int arg2) { // TODO Auto-generated method stub } @Override public void onPageSelected(int position) { Animation animation = null; Toast.makeText(MainActivity.this, "position:"+position, Toast.LENGTH_SHORT).show(); Log.i("hql-->", "one=="+one+" two=="+two); Log.i("hql-->", "offset=="+offset+" bmpw=="+bmpw+"screenW"+screenW); switch (position) { case 0: if(currentIndex == 1){ animation = new TranslateAnimation(one, 0, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, 0, 0, 0); } break; case 1: if(currentIndex == 0){ animation = new TranslateAnimation(offset, one, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, one, 0, 0); } break; case 2: if(currentIndex == 0){ animation = new TranslateAnimation(offset, two, 0, 0); }else if(currentIndex == 1){ animation = new TranslateAnimation(one, two, 0, 0); } break; } currentIndex = position; //記錄當前的頁面號 animation.setDuration(300); //設置動畫時間 animation.setFillAfter(true); //設置停留在動畫后 cursor.startAnimation(animation); } }
上面主要做了兩件事:
① 計算由第0頁到第1頁,滑塊移動的距離。
int one = offset * 2 + bmpw; //選項卡0 -> 1 的偏移量
int two = one * 2; // //選項卡1 -> 2 的偏移量
計算方法無非就是數學題,我畫了張示意圖。
② 實現onPageSelected(int Position)方法
@Override public void onPageSelected(int position) { Animation animation = null; Toast.makeText(MainActivity.this, "position:"+position, Toast.LENGTH_SHORT).show(); Log.i("hql-->", "one=="+one+" two=="+two); Log.i("hql-->", "offset=="+offset+" bmpw=="+bmpw+"screenW"+screenW); switch (position) { case 0: if(currentIndex == 1){ animation = new TranslateAnimation(one, 0, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, 0, 0, 0); } break; case 1: if(currentIndex == 0){ animation = new TranslateAnimation(offset, one, 0, 0); }else if(currentIndex == 2){ animation = new TranslateAnimation(two, one, 0, 0); } break; case 2: if(currentIndex == 0){ animation = new TranslateAnimation(offset, two, 0, 0); }else if(currentIndex == 1){ animation = new TranslateAnimation(one, two, 0, 0); } break; } currentIndex = position; //記錄當前的頁面號 animation.setDuration(300); //設置動畫時間 animation.setFillAfter(true); //設置停留在動畫后 cursor.startAnimation(animation); }
通過該方法,我們可以很清晰的看到,這個方法里根據傳入的代表當前頁面的鍵,這里是Position,來在滑動的時候使滑塊做相應的移動。
做完這一步,我們的滑塊已經可以隨着頁面切換而移動起來了。
8.4 為了更好的交互,完成點擊選項卡切換頁面。
思路:給3個選項卡(這里是3個TextView)設置點擊事件,在點擊事件里面通過ViewPager.setCurrentItem(int num)設置當前頁面的鍵(KEY).
private void initTextView() { TextView text1 = (TextView) findViewById(R.id.text1); TextView text2 = (TextView) findViewById(R.id.text2); TextView text3 = (TextView) findViewById(R.id.text3); text1.setOnClickListener(this); text2.setOnClickListener(this); text3.setOnClickListener(this); }
初始化選項卡文字,這些文字做成控件的時候可以設置,同時給他們設置監聽。
處理點擊事件:
@Override public void onClick(View v) { switch (v.getId()) { case R.id.text1: vPager.setCurrentItem(0); break; case R.id.text2: vPager.setCurrentItem(1); break; case R.id.text3: vPager.setCurrentItem(2); break; }
代碼很簡單,至此,點擊選項卡也可以實現頁面切換,實現了雙向互動。
這里我再把變量申明和OnCreate()方法里面的調用代碼貼出。
private int offset; //滑塊動畫初始位置 private int bmpw; //滑塊的寬度 private int currentIndex = 0; //默認當前也卡號為0 private ImageView cursor; //滑塊 private int screenW; //屏幕寬度 private ViewPager vPager; //ViewPager @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initImageView(); //初始化滑塊 initTextView(); //初始化文字 initViewPager(); //初始化ViewPager布局 }
好了,一個雙向互動的選項卡就完成了,怎么樣,是不是很簡單?其實ViewPagerIndicator實現的思路和我們做的選項卡差不多,親愛讀者們,花點時間把這個Demo封裝一下,提供一些方便的設置方法,比如設置任意長度的title,就是一個精簡的選顯卡控件了。
由此我們可以逆推,當然我們要掌握一個開源的控件時並不難,都是由使用到熟悉。通常都是在布局里面引用該控件,然后在java代碼通過findViewById()方法找到該空間,之后通過該控件的一些方法,設置相應參數即可使用。當然,熟悉的基礎上,下一步,就是深入了解,畢竟市面上的開源控件再怎么樣都是人寫的,那就很可能不是你想當然那樣,所以通過一些方法去了解開源控件很重要。我了解一個控件的特性一般先閱讀說明,然后通過調試,打log日志和假設驗證的方式,來了解一個控件,這些方式都很普通,也很簡單,卻多用幾次就得心應手了,但是卻很實用,重要的是要有求真精神。
一路來,從認識到靈活運用,由淺入深,我們好像挺順利,其實不然,一個好的應用都是從bug中產出的,好的程序也是錯誤中不斷優化出來。我們在計算滑塊的移動距離的時候,會發生計算結果為0的情況:如下
int one = offset * 2 + bmpw; //選項卡0 -> 1 的偏移量
int two = one * 2; // //選項卡1 -> 2 的偏移量
這是為什么呢?
經過代碼跟蹤,最后才發現我們先初始化ViewPager的監聽類,在初始化ViewPager的過程中,我們計算了移動的偏移量,滑塊動畫初始位置offset 和screenW都是還沒有初始化,都是0,此時我們再計算滑塊長度和初始值,導致移動距離one和two都為0.所以當我們遇到問題,調試是一個很好的辦法。
前面的代碼太簡單了,就不上傳了,這里分享最后的自定義選項卡的Demo大家可以下載來看看: