PagerAdapter閃屏坑的修復

weixin_33816300發表於2017-05-31

背景

最近在填前同事的一個坑時,不小心遇到另外一個坑。 在一個禮物皮膚,原實現是gridView + ViewPager實現的(有幾頁禮物),在送使用者免費禮物時,重新整理ViewPager裡面的item時,出現了閃屏。

其實很多童鞋知道,PagerAdapter在呼叫notifyDataSetChanged(), 如果使用預設的會不起作用

點進notifyDataSetChanged()

/**
     * This method should be called by the application if the data backing this adapter has changed
     * and associated views should update.
     */
    public void notifyDataSetChanged() {
        synchronized (this) {
            if (mViewPagerObserver != null) {
                mViewPagerObserver.onChanged();
            }
        }
        mObservable.notifyChanged();
    }

可以看到

  1. mViewPagerObserver 是怎麼傳進來的呢?該類實際實現類是啥?

搜尋全類只有一處賦值

  void setViewPagerObserver(DataSetObserver observer) {
        synchronized (this) {
            mViewPagerObserver = observer;
        }
    }
1019687-6b700d2ea0a97f8c.png
image.png

可以看出是PagerObserver類,有ViewPager類初始化setAdapter(PagerAdapter adapter)的時候傳過來。
回到剛才的 mViewPagerObserver.onChanged();PagerObserver的實現如下

 @Override
        public void onChanged() {
            dataSetChanged();
        }

恩,所以這裡dataSetChanged()才是真正的實現:

void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter is non-null.

        final int adapterCount = mAdapter.getCount();
        mExpectedAdapterCount = adapterCount;
        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);
            final int newPos = mAdapter.getItemPosition(ii.object);

            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if (newPos == PagerAdapter.POSITION_NONE) {
                mItems.remove(i);
                i--;

                if (!isUpdating) {
                    mAdapter.startUpdate(this);
                    isUpdating = true;
                }

                mAdapter.destroyItem(this, ii.position, ii.object);
                needPopulate = true;

                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;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }

        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }

        Collections.sort(mItems, COMPARATOR);

        if (needPopulate) {
            // 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();
        }
    }

這裡的程式碼:

       if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if (newPos == PagerAdapter.POSITION_NONE) {
         }

恩,明顯是根據PagerAdapter.POSITION_NONE、PagerAdapter.POSITION_UNCHANGED來判斷是否進行更新操作。 PagerAdapter.POSITION_UNCHANGED是什麼時候打上標籤的呢?

1019687-c54a24be866f0ee9.png
image.png

哎呀,getItemPosition方法返回的,於是有了解決方法1.

  1. mObservable.notifyChanged();
 /**
     * Invokes {@link DataSetObserver#onChanged} on each observer.
     * Called when the contents of the data set have changed.  The recipient
     * will obtain the new contents the next time it queries the data set.
     */
    public void notifyChanged() {
        synchronized(mObservers) {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

好吧這裡是逐個通知Observer呼叫onChanged();

解決方案如下:

   public int getItemPosition(Object object) {
           return PagerAdapter.POSITION_NONE;
   }

game over了麼?當然沒有。

上述解決方法只是解決了一個問題,注意測試的話,就會發覺引入了本文標題中提到的閃屏問題~~
到底是哪裡出現的問題呢?前面的我們原始碼都讀的沒有問題,唯一沒注意的就是最後更新的邏輯了。我們再次仔細看看:

1019687-f3f9ddc725029497.png
image.png

注意標箭頭的地方,原來這裡是把整個item remove掉了,難怪會出現閃屏。 事實上我們也可以通過斷點或打log的方式,看本文提到的gridView重新整理時是否複用。
知道了這裡,本文的解決方法如下,使用一個SparseArray來儲存,然後手動重新整理。

class MyPagerAdapter extends PagerAdapter {

        private MyGridViewAdapter mGridAdapter;
        private SparseArray<GridView> mViews = new SparseArray<>();

        @Override
        public int getCount() {
            if (mInnerAdapter == null || mMaxRows == 0 || mColumns == 0) {
                return 0;
            }
            return (int) Math.ceil(mInnerAdapter.getCount() / (double) (mMaxRows * mColumns));
        }

        // Remove a page for the given position.
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
            mViews.remove(position);
        }

        // Determines whether a page View is associated with a specific key object as returned by instantiateItem(ViewGroup, int).
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        /**
         * PagerAdapter.POSITION_NONE 會導致呼叫notifyDataSetChanged
         * 呼叫 destroyItem 導致重新新增item,閃屏的出現
         * 但是這裡系統的實現bug, 見ViewPager$PagerObserver
         * 預設是POSITION_UNCHANGED 即不重新整理, 呼叫notifyDataSetChanged無反應,
         * 這裡使用手動重新整理
         *
         * @param object
         * @return
         */
        @Override
        public int getItemPosition(Object object) {
            int index = -1;
            if (mViews != null) {
                index = mViews.indexOfValue((GridView) object);
            }
            return index != -1 ? index : PagerAdapter.POSITION_NONE;
        }

        @Override
        public void notifyDataSetChanged() {
            GridView view;
            int size = mViews.size();
            for (int index = 0; index < size; index++) {
                view = mViews.valueAt(index);
                if (view != null) {
                    ((MyGridViewAdapter) view.getAdapter()).notifyDataSetChanged();
                }
            }
            super.notifyDataSetChanged();
        }

        // Create the page for the given position.
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            GridView mGridView = new GridView(mContext);
            ....
            mGridAdapter = new MyGridViewAdapter(mInnerAdapter, position);
            mGridView.setAdapter(mGridAdapter);

            container.addView(mGridView);
            mViews.put(position, mGridView);
            return mGridView;
        }
    }

相關文章