美文网首页Android开发Android开发经验谈Android基础知识
从源码解析ViewPager动态更改Fragment的实现

从源码解析ViewPager动态更改Fragment的实现

作者: 云淡风轻扬 | 来源:发表于2019-07-31 14:39 被阅读5次

    从源码解析ViewPager动态更改Fragment的实现

    需求背景(What)

    项目中有个需求的实现,详情页中有两个Tab(概览、数据详情),概览页根据业务类型不同,显示不同的UI。并且详情可修该业务类型,并且动态更换掉概览页面。抽象出来就是ViewPager中包含AFragment、BFragment,当业务类型在ViewPager显示时被更改,需要把AFragment替换成CFragment。

    问题(Why)

    原本可供ViewPager使用的Adapter有FragmentPagerAdapter与FragmentStatePagerAdapter,前者和后者的区别是前者类内的每一个生成的 Fragment 都将保存在内存之中,后者只保留当前页面,当页面离开视线后,就会被消除,释放其资源。而我们系统在销毁前,会把Fragment的Bundle在我们的onSaveInstanceState(Bundle)保存下来;而在页面需要显示时,Fragment就会根据我们的savedInstanceState生成新的页面。

    所以跟据以上分析,大部分场景FragmentPagerAdapter已经可以满足我们的需求了,满心欢喜的使用该Adapter,在需要动态改变的地方调用notificationDataChange()方法,期待结果就像ListView、RecyclerView一样,刷新出来新的UI。

    但是......依然显示AFragment,纹丝不动。

    解决方法(how)

    没办法找资料,看FragmentPagerAdapter源码

    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }
        //获取itemId 默认是fragment的position
        long itemId = this.getItemId(position);
        String name = makeFragmentName(container.getId(), itemId);
        //通过tag获取Fragment
        Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            this.mCurTransaction.attach(fragment);
        } else {
            fragment = this.getItem(position);
            //添加Fragment,并标记tag = "android:switcher:" + viewId + ":" + id
            this.mCurTransaction.add(container.getId(), fragment, 
                                     makeFragmentName(container.getId(), itemId));
        }
    
        if (fragment != this.mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }
    
        return fragment;
    }
    //省略部分代码......
    public long getItemId(int position) {
        return (long)position;
    }
    
    private static String makeFragmentName(int viewId, long id) {
        return "android:switcher:" + viewId + ":" + id;
    }
    

    通过阅读这部分源码可以知道,FragmentPagerAdapter默认提供的getItemId是position,因此就算调用notificationDataChange(),第一个Fragment改成CFragment但itemId依旧是0,然后通过tag获取到的Fragment依旧还是AFragment,所以我们在自己实现FragmentPagerAdapter时需要实现getItemId方法,保证每个Fragment的itemid不重复,我用的是:

    @override
    public long getItemId(int position) {
        return mFragmentList.get(position).hashCode();
    }
    

    改变此处应该可以了吧,跑了一遍,依然纹丝不动......慌了,这怎么回事

    定定神,接着看源码。instantiateItem是什么时候调用的呢?它是抽象类PagerAdapter 中定义的,PagerAdapter文件头注视中有段话:

    PagerAdapter supports data set changes. Data set changes must occur on the
    main thread and must end with a call to {@link #notifyDataSetChanged()} similar
    to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data
    set change may involve pages being added, removed, or changing position. The
    ViewPager will keep the current page active provided the adapter implements the method {@link #getItemPosition(Object)}.
    

    再看看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(android.view.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.
        */
        public int getItemPosition(Object object) {
            return POSITION_UNCHANGED;
        }
    

    从注释看,原来系统默认使用POSITION_UNCHANGED表示item的位置不变化;返回object新的index值用来更新位置,这个可以用来动态增减item用;返回POSITION_NONE则表示这个item不再出现,所以对于我们目前固定两个Fragment,只替换第一个位置的场景需要使用POSITION_NONE。如此信心大增,只要在自己实现FragmentPagerAdapter时实现getItemPosition方法。

    最终我的Adapter实现是:

    mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
        @Override
        public Fragment getItem(int position) {
            return mFragments.get(position);
        }
    
        @Override
        public int getCount() {
            return mFragments.size();
        }
    
        @Override
        public int getItemPosition(@NonNull Object object) {
            return POSITION_NONE;
        }
    
        @Override
        public long getItemId(int position) {
            return mFragments.get(position).hashCode();
        }
    });
    

    如此终于,需求实现了。但是,依然没有说明上面提到instantiateItem是什么时候调用的。

    PagerAdapter类中,第一行代码就是

     private DataSetObservable mObservable = new DataSetObservable();
    

    一看就知道使用了观察者模式,这是被观察对象,也就是Adapter对应的数据集,下面找到了观察者的注册方法

     //省略其他代码......
     /**
      * This method should be called by the application if the data backing this adapter has changed and associated views should update.
      */
       public void notifyDataSetChanged() {
           mObservable.notifyChanged();
       }
    
     /**
       * Register an observer to receive callbacks related to the adapter's data      changing.
       *
       * @param observer The {@link android.database.DataSetObserver} which will receive callbacks.
       */
       public void registerDataSetObserver(DataSetObserver observer) {
           mObservable.registerObserver(observer);
       }
    
       /**
         * Unregister an observer from callbacks related to the adapter's data changing.
         *
         * @param observer The {@link android.database.DataSetObserver} which will be unregistered.
         */
        public void unregisterDataSetObserver(DataSetObserver observer) {
            mObservable.unregisterObserver(observer);
        }
    //省略其他代码......
    

    PagerAdapter的实现类是供ViewPager调用的,所以再看ViewPager代码,果然在其中是有PagerAdapter,以下是ViewPager部分代码,觉得太过冗长可以跳过看结论。

    //省略其他代码......
    private PagerAdapter mAdapter;
    
    //省略其他代码......
    /**
     * Set a PagerAdapter that will supply views for this pager as needed.
     *
     * @param adapter Adapter to use
     */
    public void setAdapter(PagerAdapter adapter) {
        if (mAdapter != null) {
            //解除之前的监听
            mAdapter.unregisterDataSetObserver(mObserver);
            mAdapter.startUpdate(this);
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
            mAdapter.finishUpdate(this);
            mItems.clear();
            removeNonDecorViews();
            mCurItem = 0;
            scrollTo(0, 0);
        }
    
        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;
    
        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            //重新注册监听
            mAdapter.registerDataSetObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                populate();
            } else {
                requestLayout();
            }
        }
    
        if (mAdapterChangeListener != null && oldAdapter != adapter) {
            mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
        }
    }
    //省略其他代码......
    //内部类
    private class PagerObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            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);
            //获取当前新数据item的position
            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;
            }
        }
       //省略其他代码......
    }
     
    

    综上,当我们调用FragmentPagerAdapter的notifyDataSetChanged方法时,PagerAdapter的notifyDataSetChanged也会执行,由于观察者模式,它也会通知观察者ViewPager中的PagerObserver,继续调用ViewPager的dataSetChanged方法。这个方法中,当mAdapter.getItemPosition(object)拿到的position是POSITION_UNCHANGED时,什么都没变化;是POSITION_NONE才会移除当前位置旧的item对象,换上新的;其它不等于position的(增加、删除item)也会处理。

    总结

    我们通过源码FragmentPagerAdapter、PagerAdapter、ViewPager终于弄清楚了更新ViewPager的方法,以及为什么之前我们不能成功更新页面的原因。当我们需要实现这样ViewPager中Fragment数量不变,改变实体对象的时候,我们需要自己重新覆盖FragmentPagerAdapter的getItemPosition和getItemId两个方法。

    相关文章

      网友评论

        本文标题:从源码解析ViewPager动态更改Fragment的实现

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