解決ViewPager和PagerAdapter中調用notifyDataSetChanged失效問題(從notifyDataSetChanged方法的源碼入手,超詳細)

從PagerAdapter的notifyDataSetChanged方法源碼入手解決ViewPager和PagerAdapter中調用notifyDataSetChanged失效的解決辦法

1:問題描述

週末了,總結一下這周在項目中通過ViewPagerPhotoView做一個照片查看器的效果,調用notifyDataSetChanged方法無法更新界面的問題。如下圖:
在這裏插入圖片描述
問題是當我點擊右上角的刪除按鈕時數據更新更新後調用了 PagerAdapternotifyDataSetChanged的,但是界面沒更新**。即在ViewPager中當數據改變時,通過notifyDataSetChanged方法失效,無法刷新界面
附上點擊刪除按鈕的代碼:

 public void onClickRight() {
        //刪除當前位置的數據
        mList.remove(mCurrentPosition);
        //更新界面        
        mPhotoPreviewAdapter.notifyDataSetChanged();//問題就是調用了notifyDataSetChanged界面沒更新
        mTvTitle.setText("圖片產看"+(mCurrentPosition+1)+"/"+mList.size());
        if (mList.size()==0) {
            finish();
        }
    }

2:解決辦法

**思路:**既然PagerAdapternotifyDataSetChanged方法失效,那就得看它的源碼到底做了些什麼,這裏簡單描述一下notifyDataSetChanged方法的工作原理,着重講解決辦法,(注意:如果你想要更加深入的瞭解,解決此問題的原理請看文章的第3部分notifyDataSetChanged源碼分析)

  1. notifyDataSetChanged方法通過一個觀察者模式(observer.onChange()),將處理邏輯交給了PagerObserverdataSetChanged()方法
  2. dataSetChanged方法中通過getItemPosition(Object object)方法查詢一遍所有child view進行遍歷,注意child view由當前正在展示的item和預加載的item組成。
  3. getItemPosition(Object object)等於PagerAdapter.POSITION_UNCHANGED(默認值)表示當前頁面不需要更新,不用銷燬;
  4. getItemPosition(Object object)等於PagerAdapter.POSITION_NONE時需要更新,那麼該child view所有item 都會被destroyItem(ViewGroup container, int position, Object object)方法remove掉,然後重新加載新的item。
  5. 總結一下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();
    }
}

在代碼中注視的很詳細這裏就不多解釋了,祝大家週末愉快,有問題歡迎留言。

推薦我的另一篇文章安卓性能優化從ViewPager的源碼層理解實現fragment 的懶加載(仿微信頭條)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章