編輯:關於Android編程
在了解ViewPager的工作原理之前,先回顧ListView的工作原理:
ListView只有在需要顯示某些列表項時,它才會去申請可用的視圖對象;如果為所有的列表項數據創建視圖對象,會浪費內存;
ListView找誰去申請視圖對象呢? 答案是adapter。adapter是一個控制器對象,負責從模型層獲取數據,創建並填充必要的視圖對象,將准備好的視圖對象返回給ListView;
首先,通過調用adapter的getCount()方法,ListView詢問數組列表中包含多少個對象(為避免出現數組越界的錯誤);緊接著ListView就調用adapter的getView(int, View, ViewGroup)方法。
ViewPager某種程度上類似於ListView,區別在於:ListView通過ArrayAdapter.getView(int position, View convertView, ViewGroup parent)填充視圖;ViewPager通過FragmentPagerAdapter.getItem(int position)生成指定位置的fragment.
而我們需要關注的是:
ViewPager和它的adapter是如何配合工作的?
聲明:本文內容針對android.support.v4.app.*
繼承自android.support.v4.view.PagerAdapter,每頁都是一個Fragment,並且所有的Fragment實例一直保存在Fragment manager中。所以它適用於少量固定的fragment,比如一組用於分頁顯示的標簽。除了當Fragment不可見時,它的視圖層(view hierarchy)有可能被銷毀外,每頁的Fragment都會被保存在內存中。(翻譯自代碼文件的注釋部分)
繼承自android.support.v4.view.PagerAdapter,每頁都是一個Fragment,當Fragment不被需要時(比如不可見),整個Fragment都會被銷毀,除了saved state被保存外(保存下來的bundle用於恢復Fragment實例)。所以它適用於很多頁的情況。(翻譯自代碼文件的注釋部分)
它倆的子類,需要實現getItem(int) 和 android.support.v4.view.PagerAdapter.getCount().
先通過一段代碼了解ViewPager和FragmentPagerAdapter的典型用法
稍後做詳細分析:
// Set a PagerAdapter to supply views for this pager. ViewPager viewPager = (ViewPager) findViewById(R.id.my_viewpager_id); viewPager.setAdapter(mMyFragmentPagerAdapter); private FragmentPagerAdapter mMyFragmentPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public int getCount() { return 2; // Return the number of views available. } @Override public Fragment getItem(int position) { return new MyFragment(); // Return the Fragment associated with a specified position. } // Called when the host view is attempting to determine if an item's position has changed. @Override public int getItemPosition(Object object) { if (object instanceof MyFragment) { ((MyFragment)object).updateView(); } return super.getItemPosition(object); } }; private class MyFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // do something such as init data } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_my, container, false); // init view in the fragment return view; } public void updateView() { // do something to update the fragment } }
FragmentPagerAdapter和FragmentStatePagerAdapter對Fragment的管理略有不同,在詳細考察二者區別之前,我們通過兩種較為直觀的方式先感受下:
通過兩張圖片直觀的對比FragmentPagerAdapter和FragmentStatePagerAdapter的區別
說明:這兩張圖片來自於《Android權威編程指南》,原圖有3個Fragment,我增加了1個Fragment,以及被調到的方法。
FragmentPagerAdapter的Fragment管理:
FragmentStatePageAdapter的Fragment管理:
詳細分析 adapter method和fragment lifecycle method 的調用情況
好啦,感受完畢,我們需要探究其詳情,梳理adapter創建、銷毀Fragment的過程,過程中adapter method和fragment lifecycle method哪些被調到,有哪些一樣,有哪些不一樣。
最開始處於第0頁時,adapter不僅為第0頁創建Fragment實例,還為相鄰的第1頁創建了Fragment實例:
// 剛開始處在page0 D/Adapter (25946): getItem(0) D/Fragment0(25946): newInstance(2015-09-10) // 注釋:newInstance()調用了Fragment的構造器方法,下同。 D/Adapter (25946): getItem(1) D/Fragment1(25946): newInstance(Hello World, I'm li2.) D/Fragment0(25946): onAttach() D/Fragment0(25946): onCreate() D/Fragment0(25946): onCreateView() D/Fragment1(25946): onAttach() D/Fragment1(25946): onCreate() D/Fragment1(25946): onCreateView()
第1次從第0頁滑到第1頁,adapter同樣會為相鄰的第2頁創建Fragment實例;
// 第1次滑到page1 D/Adapter (25946): onPageSelected(1) D/Adapter (25946): getItem(2) D/Fragment2(25946): newInstance(true) D/Fragment2(25946): onAttach() D/Fragment2(25946): onCreate() D/Fragment2(25946): onCreateView()
FragmentPagerAdapter和FragmentStatePagerAdapter齊聲說:吶,請主公貳放心,屬下定會為您准備好相鄰的下一頁視圖哒!麼麼哒!
它倆對待下一頁的態度是相同的,但對於上上頁,它倆做出了不一樣的事情:
FragmentPagerAdapter說:上上頁的實例還保留著,只是銷毀了它的視圖:
// 第N次(N不等於1)向右滑動選中page2 D/Adapter (25946): onPageSelected(2) D/Adapter (25946): destroyItem(0) // 銷毀page0的視圖 D/Fragment0(25946): onDestroyView() D/Fragment3(25946): onCreateView() // page3的Fragment實例仍保存在FragmentManager中,所以只需創建它的視圖 FragmentStatePagerAdapter說:上上頁的實例和視圖都被俺銷毀啦: // 第N次(N不等於1)向右滑選中page2 D/Adapter (27880): onPageSelected(2) D/Adapter (27880): destroyItem(0) // 銷毀page0的實例和視圖 D/Adapter (27880): getItem(3) // 創建page3的Fragment D/Fragment3(27880): newInstance() D/Fragment0(27880): onDestroyView() D/Fragment0(27880): onDestroy() D/Fragment0(27880): onDetach() D/Fragment3(27880): onAttach() D/Fragment3(27880): onCreate() D/Fragment3(27880): onCreateView() Fragment getItem(int position)
// Return the Fragment associated with a specified position. public abstract Fragment getItem(int position);
當adapter需要一個指定位置的Fragment,並且這個Fragment不存在時,getItem就被調到,返回一個Fragment實例給adapter。
所以,有必要再次強調,getItem是創建一個新的Fragment,但是這個方法名可能會被誤認為是返回一個已經存在的Fragment。
對於FragmentPagerAdapter,當每頁的Fragment被創建後,這個函數就不會被調到了。對於FragmentStatePagerAdapter,由於Fragment會被銷毀,所以它仍會被調到。
由於我們必須在getItem中實例化一個Fragment,所以當getItem()被調用後,Fragment相應的生命周期函數也就被調到了:
D/Adapter (25946): getItem(1) D/Fragment1(25946): newInstance(Hello World, I'm li2.) // newInstance()調用了Fragment的構造器方法; D/Fragment1(25946): onAttach() D/Fragment1(25946): onCreate() D/Fragment1(25946): onCreateView()
void destroyItem(ViewGroup container, int position, Object object) // Remove a page for the given position. public void FragmentPagerAdapter.destroyItem(ViewGroup container, int position, Object object) { mCurTransaction.detach((Fragment)object); } public void FragmentStatePagerAdapter.destroyItem(ViewGroup container, int position, Object object) { mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); mFragments.set(position, null); mCurTransaction.remove(fragment); }
銷毀指定位置的Fragment。從源碼中可以看出二者的區別,一個detach,一個remove,這將調用到不同的Fragment生命周期函數:
// 對於FragmentPagerAdapter D/Adapter (25946): onPageSelected(2) D/Adapter (25946): destroyItem(0) D/Fragment0(25946): onDestroyView() // 銷毀視圖 // 對於FragmentStatePagerAdapter D/Adapter (27880): onPageSelected(2) D/Adapter (27880): destroyItem(0) D/Fragment0(27880): onDestroyView() // 銷毀視圖 D/Fragment0(27880): onDestroy() // 銷毀實例 D/Fragment0(27880): onDetach() FragmentPagerAdapter和FragmentStatePagerAdapter對比總結
二者使用方法基本相同,唯一的區別就在卸載不再需要的fragment時,采用的處理方式不同:
使用FragmentStatePagerAdapter會銷毀掉不需要的fragment。事務提交後,可將fragment從activity的FragmentManager中徹底移除。類名中的“state”表明:在銷毀fragment時,它會將其onSaveInstanceState(Bundle) 方法中的Bundle信息保存下來。用戶切換回原來的頁面後,保存的實例狀態可用於恢復生成新的fragment.
FragmentPagerAdapter的做法大不相同。對於不再需要的fragment,FragmentPagerAdapter則選擇調用事務的detach(Fragment) 方法,而非remove(Fragment)方法來處理它。也就是說,FragmentPagerAdapter只是銷毀了fragment的視圖,但仍將fragment實例保留在FragmentManager中。因此, FragmentPagerAdapter創建的fragment永遠不會被銷毀。
更新ViewPager中的Fragment
調用notifyDataSetChanged()時,2個adapter的方法的調用情況相同,當前頁和相鄰的兩頁的getItemPosition都會被調用到。
// Called when the host view is attempting to determine if an item's position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed or POSITION_NONE if the item is no longer present in the adapter. public int getItemPosition(Object object) { return POSITION_UNCHANGED; }
從網上找到的解決辦法是,覆寫getItemPosition使其返POSITION_NONE,以觸發Fragment的銷毀和重建。可是這將導致Fragment頻繁的銷毀和重建,並不是最佳的方法。
後來我把注意力放在了入口參數object上,"representing an item", 實際上就是Fragment,只需要為Fragment提供一個更新view的public方法:
@Override // To update fragment in ViewPager, we should override getItemPosition() method, // in this method, we call the fragment's public updating method. public int getItemPosition(Object object) { Log.d(TAG, "getItemPosition(" + object.getClass().getSimpleName() + ")"); if (object instanceof Page0Fragment) { ((Page0Fragment) object).updateDate(mDate); } else if (object instanceof Page1Fragment) { ((Page1Fragment) object).updateContent(mContent); } else if (object instanceof Page2Fragment) { ((Page2Fragment) object).updateCheckedStatus(mChecked); } else if (...) { } return super.getItemPosition(object); }; // 更新界面時方法的調用情況 // 當前頁為0時 D/Adapter (21517): notifyDataSetChanged(+0) D/Adapter (21517): getItemPosition(Page0Fragment) D/Fragment0(21517): updateDate(2015-09-12) D/Adapter (21517): getItemPosition(Page1Fragment) D/Fragment1(21517): updateContent(Hello World, I am li2.) // 當前頁為1時 D/Adapter (21517): notifyDataSetChanged(+1) D/Adapter (21517): getItemPosition(Page0Fragment) D/Fragment0(21517): updateDate(2015-09-13) D/Adapter (21517): getItemPosition(Page1Fragment) D/Fragment1(21517): updateContent(Hello World, I am li2.) D/Adapter (21517): getItemPosition(Page2Fragment) D/Fragment2(21517): updateCheckedStatus(true)
在最開始調用notifyDataSetChanged試圖更新Fragment時,我是這樣做的:用arraylist保存所有的Fragment,當需要更新時,就從arraylist中取出Fragment,然後調用該Fragment的update方法。這種做法非常魚唇,當時完全不懂得adapter的Fragment manager在替我管理所有的Fragment。而我只需要:
只需要覆寫這幾個adapter的方法,adapter會為你完成所有的管理工作,不需要自己保存、維護Fragment。
替換ViewPager中的Fragment
應用場景可能是這樣,比如有一組按鈕,Day/Month/Year,有一個包含幾個Fragment的ViewPager。點擊不同的按鈕,需要秀出不同的Fragment。
具體怎麼實現,請參考下面的代碼:
github.com/li2/Update_Replace_Fragment_In_ViewPager/ContainerFragment.java
一些誤區
ViewPager.getChildCount() 返回的是當前ViewPager所管理的沒有被銷毀視圖的Fragment,並不是所有的Fragment。想要獲取所有的Fragment數量,應該調用ViewPager.getAdapter().getCount().
ViewPager中使用Fragment+ListView,多次切換後造成ListView沒有數據顯示?
ViewPager+Fragment動態增刪緩存問題
產生原因:
我們在開發中會常常用到ViewPager+Fragment,有時候可能會有這樣的需求,需要對ViewPager中的內容進行動態的增刪管理,但是我們都知道ViewPager為了保證滑動的流暢性,viewpager在加載當前頁的時候已經將pager頁左右頁的內容加載進內存裡了,所以此時我們不進行任何處理的話,是我發達到我們預期的效果的。
解決方案:
一、將FragmentPagerAdapter 替換成FragmentStatePagerAdapter, 因為前者只要加載過,fragment中的視圖就一直在內存中,在這個過 程中無論你怎麼刷新,清除都是無用的,直至程序退出; 後者可以滿足我們的需求。 2.我們可以重寫Adapter的方法–getItemPosition(),讓其返回PagerAdapter.POSITION_NONE即可。 以下為引用內容:
@Override public int getItemPosition(Object object) { return PagerAdapter.POSITION_NONE; }
到這一步我們就可以真正的實現隨意、徹底刪除viewpager中的fragment,隨意增刪。
二、善用Dialog 一些交互簡單、或者只是展示功能的頁面,如果使用一個Activity來顯示的話,過於繁瑣,開銷也很大,使用Fragment的話,蛋疼的生命周期也不好處理,此時使用一個全屏的Dialog來模擬一個Activity就是一個不錯的選擇。
三、Splash頁面那點事 :幾乎每個頁面都會有一個Spalsh頁,通常我們會用一個Activity加載一張全屏的背景圖,或者放一個app的logo,展示2秒之後,跳轉到登錄或者主頁面,期間可能會做一些數據初始化,檢查更新等操作。相信大多數小伙伴也是這麼干的,但是,你不覺得一個Activity只顯示2秒就殺掉有點浪費?個人覺得這樣的開銷是非常之不劃算的,我們可以借用上面一條,利用一個Dialog模擬一個Splash頁面,2秒之後dismiss掉這個Dialog,而檢查更新,初始化數據等操作就放到MainActivity中。或者使用Fragment替代SplashActivity等等方法, 都可以達到Splash頁的相同效果。
四、善待內部類 在開發中,我們會經常用到內部類,內部類的出現,解決了Java只能單繼承的局限性,使得開發能更加靈活。但如果內部類用的不好,就會出現Android Developer的噩夢,OOM!。為什麼呢?底子稍微好點的同學,應該都知道內部類可以訪問外部類的成員變量和方法,因為內部類持有了外部類的引用,當你在一個Activity中使用的內部類,當Activity銷毀時,你的內部類沒有釋放,就會造成這個Activity無法被GC回收,因為內部類中持有了Activity的應用。
五、library那些事 library中的switch中不能使用id來case,這個在我的上一篇博文中已經講過。這裡我們再講一個library的坑,當我們引入一個依賴庫時,依賴庫中一般都會自帶一個support v4的包,這個v4包的版本,和我們創建工程時的版本一般情況下是一致的,但是一旦我們自己工程的v4包的依賴庫中的v4包中的版本不一致時,一大推莫名其妙的錯誤日志就會接踵而來。此時的處理方法也很簡單,由於v4包都是向下兼容的,只需要保持依賴庫的版本和我們自身項目的版本一致即可。 今天暫時先總結到這裡,如果上述言論有錯誤的地方,希望各位小伙伴們及時指出。
啟動或結束一個Activity,我們就要跟它的生命周期打交道,安卓程序猿根據Activity的生命周期中各回調方法的觸發時機處理對應的業務邏輯,以保證用戶與頁面之間能正常
主要實現兩個步驟:1、實現打開和關閉閃光燈;而實現操作閃光燈主要通過Camera類Camera camera = Camera.open(); Parameters mP
1.Device tree設備樹概述設備樹包含DTC(device treecompiler),DTS(device treesource和DTB(device tree
效果圖: 如何解析以下的xml: (#大笑) (#微笑) (#親親) (#抱抱) (#色色) (#好失望喲) 這樣來解析: public class