**
參考文章:
**
https://www.jianshu.com/p/266861496508
http://blog.sina.com.cn/u/2017385987
https://blog.csdn.net/z13759561330/article/details/40737381
https://blog.csdn.net/happylishang/article/details/78961984
**
目錄
**
一.問題復現
1.從ViewPager的3種適配器說起
這裏我們先來盤一下ViewPager的三種適配器
PagerAdapter:
重寫:getCount()、isViewFromObject()、instantiateItem()、destroyItem()
對應的item爲View;
FragmentPagerAdapter:
重寫:getCount()、getItem()、
對應的item爲Fragment,適用於Fragment較少的情況;
會緩存所有Fragment,爲每個fragment添加Tag,需要的時候根據Tag查找;
首次顯示時候add,非首次根據Tag查找然後attach;
移除的時候detach,注意這裏並未remove;
FragmentStateAdapter:
重寫:getCount()、getItem()
對應item爲Fragment,適用於Fragment較多的情況;
只緩存最近顯示的Fragment ,不會爲fragment添加Tag ,需要的時候如果沒有緩存就重新創建;
顯示時候如果沒有緩存就add,有緩存直接顯示
移除的時候remove
2.問題來了,刷新dapter的數據源 Viewpager並不能及時正確地刷新UI頁面
操作步驟:
首先,對dapter進行增刪改的操作;
然後調用adapter.notifyDateSetChanged()觀察ViewPager的頁面
親測結果如下:
PagerAdapter | FragmentPagerAdapter | FragmentStateAdapter | |
---|---|---|---|
更新Item | 生效但不及時 | 不生效 | 生效但不及時 |
增加Item | 生效但不及時 | 不生效 | crash |
刪除Item | 頁面錯亂 | 不生效 | crash |
清空Item | 殘留當前頁面 | OK | 殘留當前頁面 |
二. 問題分析
1.知其然——問題很嚴重
更新生效但不及時: C級bug
更新新無效: C級+bug
頁面錯亂: B級bug
crash: A級bug
試想一下,如果我們自己的app出現這種bug,是何等的事故喲!但是這種事情就實實在在地發生了,而且還是發生在擁有10億級用戶的Android操作系統的官方api中!
2.知其所以然——Why!
讓我們來分析下這個問題的可能原因:
原因一、我們自己寫的adapter有問題。
原因二、這是bug。
排除法 搞起
case 原因二: 這個bug能發佈出來只有一種情況——Google發佈Android的api連冒煙測試都沒通過。可能性幾乎爲0 排除
case 原因一:是我們的代碼寫得不夠規範。可能性較大
那麼我們的代碼那裏寫的有問題呢?爲什麼按Google的api只重寫幾個PagerAdapter必要的抽象方法,在數據源更新的時候,調用notifyDateSetChanged()不能達到像ListView和RecyclerView那樣的效果呢?
這一切都要從PagerAdapter的刷新機制說起。
下圖是刷新單個Item的流程圖:
調用流程:
- 調用 PagerAdapter 的 notifyDataSetChanged() 方法
- 觸發 getItemPosition() 方法,該方法會爲每個 Item 返回狀態碼POSITION_NONE 或POSITION_UNCHANGED
- 如果是 POSITION_NONE,那麼該 Item 會被 destroyItem() 方法 remove 掉,然後instantiateItem()重新加載 顯示刷新後的頁面
- 如果是 POSITION_UNCHANGED,就不會重新加載,默認是 POSITION_UNCHANGED,無刷新效果
那麼問題來了,這個getItemPosition()是幹什麼的,我跟ta 認識,嗎?
我們回過頭來再來回顧我們寫的PagerAdapter都重寫了哪些方法
PagerAdapter:
重寫:getCount()、isViewFromObject()、instantiateItem()、destroyItem()
FragmentPagerAdapter:
重寫:getCount()、getItem()、
FragmentStateAdapter:
重寫:getCount()、getItem()
並沒有找到這個getItemPosition()啊,別急既然這方法是PagerAdapter的,我們去源碼裏找下。
public int getItemPosition(@NonNull Object object) {
return PagerAdapter.POSITION_UNCHANGED;//默認返回POSITION_UNCHANGED
}
...
哈哈,“真相只有一個!”就是因爲父類PagerAdapter的getItemPosition()直接返回POSITION_UNCHANGED,才導致這一連串的刷新問題。
那麼,我們是不是隻要在子類中重寫該方法返回POSITION_NONE就萬事大吉了呢?So,easy?媽媽再也不用擔心我的Viewpager不刷新?
老辦法,我們試一下子!
PagerAdapter | FragmentPagerAdapter | FragmentStateAdapter | |
---|---|---|---|
更新Item | OK | 不生效 | OK |
增加Item | OK | 不生效 | OK |
刪除Item | OK | 不生效 | OK |
清空Item | OK | OK | OK |
三. 解決方案
1.PagerAdapter 重寫該方法返回POSITION_NONE 可以解決刷新問題
2.FragmentPagerAdapter 效果並不理想 這跟它內部對Fragment的緩存有關
3.FragmentStateAdapter 重寫該方法返回POSITION_NONE 可以解決刷新問題
@Override
public int getItemPosition(@NonNull Object object) {
return POSITION_NONE;//返回POSITION_NONE 強制刷新
}
關於1和3 的【可以解決】這裏有幾點說明一下:
First…我們這裏所謂的刷新數據源包括:增加,刪除、更新、清空
Second…這裏的刷新雖然能達到效果,但是所有Item都會刷新,不能實現類似RecyclerView只更新單個tem的效果;
Third…如果只是更新數據源 不涉及動態增加、刪除、清空操作,那麼 可以這樣優化:
通過爲每個Item設置Tag,然後重建的時候根據Tag決定是否更新
注意 這種Tag方式只能保證更新操作不會出問題
關於2FragmentPagerAdapter的【效果並不理想】
嘗試的解決方案是這樣的,先講下思路:
在Adapter內維護一個所有fragment的集合fragments,注意這個集合需要在adapter內通過new創建,不能由外部指定,加載數據需要先clear 再addAll();
每次外部數據源list有刷新操作的時候:
然後通過fragmentManager將所有fragment移除
先將內部維護的fragemnt清空,
接着內部集合fragments 再addAll()
最後調用notifyDataSetChanged()
完整代碼如下:
package com.darcy.hellotest.ui.viewpager.adapter;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class FragmentsAdapter extends FragmentPagerAdapter {
private List<Fragment> fragments;//內維護一個所有fragment的集合fragments
private FragmentManager fragmentManager;
public FragmentsAdapter(FragmentManager fm, List<Fragment> list) {
super(fm);
this.fragmentManager = fm;
fragments = new ArrayList<>();//集合需要在adapter內通過new創建
setFragments(list);
}
/**
* 刷新 通過修改adapter數據源實現
* 每次更新刷新所有fragment
*
* @param fragments 新數據源
*/
public void setFragments(List<Fragment> fragments) {
if (this.fragments != null) {
FragmentTransaction transaction = fragmentManager.beginTransaction();
for (Fragment f : this.fragments) {//通過fragmentManager將所有fragment移除
transaction.remove(f);
}
transaction.commit();
fragmentManager.executePendingTransactions();
}
assert this.fragments != null;
this.fragments.clear();//清空
this.fragments.addAll(fragments);//addAll操作
notifyDataSetChanged();//調用notifyDataSetChanged()
}
@Override
public Fragment getItem(int i) {
return fragments.get(i);
}
@Override
public int getCount() {
return fragments.size();
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
return super.instantiateItem(container, position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
super.destroyItem(container, position, object);
}
@Override
public int getItemPosition(@NonNull Object object) {
return POSITION_NONE;
}
}
Activity中的刷新調用代碼如下:
//真更新 通過修改適配器數據源實現
private void refresh2() {
fragments.set(0, PagerFragment.newInstance("更新測試"));
mPagerAdapter.setFragments(fragments);
}
private void add() {
fragments.add(0, PagerFragment.newInstance("添加Item測試"));
mPagerAdapter.setFragments(fragments);
}
private void delete() {
fragments.remove(0);
mPagerAdapter.setFragments(fragments);
}
private void clean() {
fragments.clear();
mPagerAdapter.setFragments(fragments);
}
這個方法的實驗結果如下:
FragmentPagerAdapter | |
---|---|
更新Item | 有效 但是會導致下一個頁面首次加載時空白 |
增加Item | 有效 但是會導致下一個頁面首次加載時空白 |
刪除Item | 有效 但是會導致下一個頁面首次加載時空白 |
清空Item | OK |
很顯然 目前這個方案還不完善,哪位大佬知道,還望不吝賜教,這裏先行謝過了(教我,讓我做你的舔狗~~~)
所以我現在的做法是向FragmentStatePagerAdapter靠攏,既然FragmentPagerAdapter效果不理想,那就先拿FragmentStatePagerAdapter救急。
四. 總結
1.以上我們復現了ViewPager的刷新機制存在的問題——不能及時正確地更新頁面
2.接着我們分析了這種情況出現的原因——PagerAdapter的getItemPosition()直接返回POSITION_UNCHANGED 導致頁面不刷新
3.然後我們提出瞭解決方案:在子類中重寫該方法返回POSITION_NONE
這個方案針對PagerAdapter和FragmentStatePagerAdapter效果理想
對FragmentPagerAdapter由於其內部的緩存機制 效果並不理想
4.最後我們還嘗試針對FragmentPagerAdapter的緩存機制進行優化——強制清除緩存,但是效果也有瑕疵。期待大佬出來指點江山~~
此致:
我是漩渦小學生