從PagerAdapter的notifyDataSetChanged方法源碼入手解決ViewPager和PagerAdapter中調用notifyDataSetChanged失效的解決辦法
1:問題描述
週末了,總結一下這周在項目中通過ViewPager
和PhotoView
做一個照片查看器的效果,調用notifyDataSetChanged
方法無法更新界面的問題。如下圖:
問題是當我點擊右上角的刪除按鈕時數據更新更新後調用了 PagerAdapter
的notifyDataSetChanged
的,但是界面沒更新**。即在ViewPager
中當數據改變時,通過notifyDataSetChanged
方法失效,無法刷新界面。
附上點擊刪除按鈕的代碼:
public void onClickRight() {
//刪除當前位置的數據
mList.remove(mCurrentPosition);
//更新界面
mPhotoPreviewAdapter.notifyDataSetChanged();//問題就是調用了notifyDataSetChanged界面沒更新
mTvTitle.setText("圖片產看"+(mCurrentPosition+1)+"/"+mList.size());
if (mList.size()==0) {
finish();
}
}
2:解決辦法
**思路:**既然PagerAdapter
的notifyDataSetChanged
方法失效,那就得看它的源碼到底做了些什麼,這裏簡單描述一下notifyDataSetChanged
方法的工作原理,着重講解決辦法,(注意:如果你想要更加深入的瞭解,解決此問題的原理請看文章的第3部分notifyDataSetChanged
源碼分析)
notifyDataSetChanged
方法通過一個觀察者模式(observer.onChange()
),將處理邏輯交給了PagerObserver
的dataSetChanged()方法
。- 在
dataSetChanged
方法中通過getItemPosition(Object object)
方法查詢一遍所有child view進行遍歷,注意child view由當前正在展示的item和預加載的item組成。 - 當
getItemPosition(Object object)
等於PagerAdapter.POSITION_UNCHANGED(默認值)
表示當前頁面不需要更新,不用銷燬; - 當
getItemPosition(Object object)
等於PagerAdapter.POSITION_NONE
時需要更新,那麼該child view 中所有item 都會被destroyItem(ViewGroup container, int position, Object object)
方法remove掉,然後重新加載新的item。 - 總結一下
notifyDataSetChanged
方法通過getItemPosition(Object object)
方法的返回值判斷是否需要銷燬當前view創建新view。所以解決方案就是複寫getItemPosition方法的返回值
方法1:複寫adapter的getItemPosition方法
@Override
public int getItemPosition(@NonNull Object object) {
//返回值爲 POSITION_NONE 即可實現刷新界面效果
return POSITION_NONE;
}
複寫getItemPosition
方法將返回值置爲POSITION_NONE
即可實現界面更新,但是存在資源浪費問題,看下圖效果和日誌(從adapter的destroyItem方法入手):如上圖:已基本實現了更新界面的效果,但是從日誌中看出這種方案的缺陷:資源浪費,當要刪除第2張圖片時,預加載的item 圖片1和圖片3也被銷燬了,造成了item不必要的銷燬和創建工作。如果你的圖片查看器中圖片少,這種方案足以滿足。但是如果你對代碼性能有要求的話,請看方案二
方法2: 方案1的升級版,提升性能
即:刪除指定的item時,預加載的條目的item不會被銷燬和重新創建
這次先看實現效果如下圖:
看圖中studio的日誌和效果:當刪除指定位置的圖片時,直銷燬當前位置的item,預加載的item
沒有銷燬,減少了不必要的銷燬創建動作,提升了性能。圖上需要注意的是:日誌中的position指的是下標,而模擬器中的圖片title的數字是圖片的個數,等於position+1
,這倆不要弄混了。
接下來分析實現方案:
實現原理很簡單:就是將getItemPosition()
方法的返回值POSITION_NONE
,做一下處理,即界面上要刪除的item的postion和Adapter中銷燬的item的position相等時返回POSITION_NONE
, 不等時返回POSITION_UNCHANGED
(這句話比較繞口),大白話講就是比較兩個位置是否相等。
此時解決問題的關鍵就是在adapter的getItemPosition(@NonNull Object object)
中如何獲取兩個position進行比較。
step1:獲取activity中要刪除的item的position
在自定義的Adapter中定義一個成員變量標誌表示activity界面中要刪除item的位置mPosition
,並提供一個setPosition
方法, 在ViewPager的addOnPageChangeListener
監聽中實時更新這個position
。
step2:通過在創建item時,即instantiateItem(@NonNull ViewGroup container, int position)
方法中獲Adapter中要銷燬item的position**
自定義的Adapter中instantiateItem
方法中設置tag,在getItemPosition(@NonNull Object object)
方法中通過getTag
獲取position
。
過程如下圖:
在ViewPager的addOnPageChangeListener
監聽中,實時更新這個界面要刪除數據的position
到此ViewPager和PagerAdapter中調用notifyDataSetChanged失效的問題已完美解決。
3:notifyDataSetChanged的源碼分析
從第二部分中我們知道,notifyDataSetChanged
方法最終通過getItemPosition(Object object)
方法,遍歷viewpager
中的所有的child item,這裏就看一下最終掉的dataSetChanged
方法,以後有時間專門會開一篇ViewPager的源碼分析的文章。
這裏推薦我的另一篇文章安卓性能優化從ViewPager的源碼層理解實現fragment 的懶加載(仿微信頭條)
void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null.
final int adapterCount = mAdapter.getCount();
mExpectedAdapterCount = adapterCount;
// 判斷是否需要走populate流程
boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
&& mItems.size() < adapterCount;
int newCurrItem = mCurItem;
boolean isUpdating = false;
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
// 通過adapter獲取itemPosition
final int newPos = mAdapter.getItemPosition(ii.object);
// 默認返回POSITION_UNCHANGED,即跳過該item
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
// 返回POSITION_NONE,則移除該item
if (newPos == PagerAdapter.POSITION_NONE) {
mItems.remove(i);
i--;
if (!isUpdating) {
mAdapter.startUpdate(this);
isUpdating = true;
}
// 移除該item並標記需要走populate
mAdapter.destroyItem(this, ii.position, ii.object);
needPopulate = true;
// 當前選中被移除,會判斷mCurItem是否超出邊界,超出則重新賦值有效索引
if (mCurItem == ii.position) {
// Keep the current item in the valid range
newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
needPopulate = true;
}
continue;
}
// 如果數據索引發生改變
if (ii.position != newPos) {
// 如果是選中位置索引變更,則更新選中索引
if (ii.position == mCurItem) {
// Our current item changed position. Follow it.
newCurrItem = newPos;
}
// 更新item的position,並標記需要走populate
ii.position = newPos;
needPopulate = true;
}
}
if (isUpdating) {
mAdapter.finishUpdate(this);
}
// mItems根據position重新排序
Collections.sort(mItems, COMPARATOR);
if (needPopulate) {
// 重置所有child的寬度,等待populate中重新測量
// Reset our known page widths; populate will recompute them.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isDecor) {
lp.widthFactor = 0.f;
}
}
// 重新定位,並走測量佈局繪製流程
setCurrentItemInternal(newCurrItem, false, true);
requestLayout();
}
}
在代碼中注視的很詳細這裏就不多解釋了,祝大家週末愉快,有問題歡迎留言。