美文网首页
FragmentStatePagerAdapter支持动态更新数

FragmentStatePagerAdapter支持动态更新数

作者: 蛋西 | 来源:发表于2020-05-08 15:46 被阅读0次

在Android开发中,我们应该使用到很高频率的一个控件就是ViewPager。但是在使用ViewPager的过程中,我们会发现有两个问题,一是不能关闭预加载;二是更新ViewPager的Adapter不生效。所以我在这里以FragmentStatePagerAdapter为例,探讨一下为什么更新adapter无法生效,并且提出解决方案。

为什么Adapter更新不生效

更新不生效其实很简单,我们看一下源码的调用过程
PagerAdapter.java

 //  PagerAdapter.java
  /**
     * 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();
    }

ViewPager.java

// ViewPager.java
    private class PagerObserver extends DataSetObserver {
        PagerObserver() {
        }

        @Override
        public void onChanged() {
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            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);
            // 调用adapter的getItemPosition方法,获取新位置
            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();
        }
    }

PageAdapter.getItemPosition(ii.object)

    public int getItemPosition(@NonNull Object object) {
        return POSITION_UNCHANGED;
    }

默认是返回POSITION_UNCHANGED,所以结合上面的逻辑

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

这里跳出循环,所以其实什么事情都没有做,也没办法因为数据集的变更而变更UI。

如何解决

其实ViewPager.dataSetChanged()方法已经为我们预留了更新的逻辑。

......

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;
           }
           // 这里的newPos就是上面从PagerAdapter.getItemPosition()获取到的位置信息
           if (ii.position != newPos) {
               if (ii.position == mCurItem) {
                   // Our current item changed position. Follow it.
                   newCurrItem = newPos;
               }

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

......

所以我们要跳过newPos == PagerAdapter.POSITION_UNCHANGEDnewPos == PagerAdapter. POSITION_NONE 的逻辑,需要对PagerAdapter.getItemPosition()进行改造。

复制FragmentStatePagerAdapter源码

由于改造getItemPosition()方法需要对源码进行操作,所以我们首先需要复制一份源码,暂时叫DynamicFragmentStatePagerAdapter

改造getItemPosition

@Override
    public int getItemPosition(@NonNull Object object) {
        int index = indexOfFragments(object);
        return index != -1 ? index : super.getItemPosition(object);
    }

    private int indexOfFragments(Object object) {
        if (object instanceof Fragment) {
            return mFragments.indexOf(object);
        }
        return -1;
    }

getItemPosition方法的注释我们了解一下,
/**
* Called when the host view is attempting to determine if an item's position
* has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
* item has not changed or {@link #POSITION_NONE} if the item is no longer present
* in the adapter.
*
* <p>The default implementation assumes that items will never
* change position and always returns {@link #POSITION_UNCHANGED}.
*
* @param object Object representing an item, previously returned by a call to
* {@link #instantiateItem(View, int)}.
* @return object's new position index from [0, {@link #getCount()}),
* {@link #POSITION_UNCHANGED} if the object's position has not changed,
* or {@link #POSITION_NONE} if the item is no longer present.
*/
简单来说就是就是根据这个方法,来判断当前item的位置是否发生了改变,返回值包括POSITION_UNCHANGED、POSITION_NONE、以及[0, {@link #getCount()}。所以当我们数据集发生变化的时候,其实fragment的位置,也要相应的发生变化否则就会在错误的位置获取到错误的页面,会发生页面显示错乱。
mFragments对象是用来缓存当前adapter维护的在内存中的fragment缓存对象。是一个数组结构,数组会随着数据集的增大而增大,数组的内容是fragment对象,会根据当前viewpager位置,保存的ViewPager.setOffscreenPageLimit(size)中size的大小的实例,ViewPager进行滑动时,超出page limit的页面会被执行destroyItem方法,并且预加载的fragment会执行instantiateItem方法,确保adapter中只保留相应数量的fragment实例。这里不展开说,可以详细看下FragmentStatePagerAdapter.instantiateItem(@NonNull ViewGroup container, int position)的方法源码。
mSavedState对象用来缓存adapter中所有fragment执行onSaveInstanceState()之后的数据,在destroyItem方法中调用被销毁的fragment的onSaveInstanceState方法,并把数据放在mSavedState对应的位置。随后在执行instantiateItem的时候,首先从mFragments找相应位置的fragment,并且找到mSavedState相应位置的state数据,进行页面的恢复。

有了上面的这两个知识点,我们就知道接下来要怎么做,首先我们在给fragment确定位置的时候,也就是在getItemPosition方法中,我们根据当前的fragment对象在mFragments确定新的位置。如果位置发生变化,则会重新刷新。代码如下:

    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 (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);
        // 这里会做刷新操作,设置新的current item位置
        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();
        }
    }

所以一切的根源,就是在数据集发生变化时,需要通知对应位置的** mFragmentsmSavedState**对象,插入null对象。

举例

如果我们要在数据集的头部插入数据,我们可以这么做,在copy源码的DynamicFragmentStatePagerAdapter类中加入我们的自定义方法,例如:

    public void insertEmptyHeaderFragment() {
        mFragments.add(0, null);
        mSavedState.add(0, null);
    }

在数据集插入单个数据的同时,也在该方法中插入对应的null,这样位置和值才能对的上,以至于恢复页面的时候,不会找错save state而在对的位置产生错误的页面。举例

    fun insertData(data: XXX) {
         dataSet?.add(0, data)
         insertEmptyHeaderFragment()
    }

插入其他位置,我相信对同学们应该也不难了。记得数据集更新后要调用adapter.notifyDataSetChanged()方法。这样就会去刷新数据集了。

总结

重点是改变getItemPosition()的位置计算,并且在更新数据集的时候,更新mFragments和mSavedState的位置。本文是根据使用ViewPager+FragmentStatePagerAdapter来举例,如果是其他adapter类,相信你经过上面的介绍之后,应该不是什么难事。最后就是,遇到困难不用怕,分析,跟踪源码,不能通过继承实现的,就copy源码来改,只要思想不滑坡,方法总比困难多。欢迎有不清楚的同学,可以线下持续交流。

相关文章

网友评论

      本文标题:FragmentStatePagerAdapter支持动态更新数

      本文链接:https://www.haomeiwen.com/subject/uhyfnhtx.html