美文网首页Android开发经验谈Android
Fragment 与 FragmentPagerAdapter

Fragment 与 FragmentPagerAdapter

作者: 真心czx | 来源:发表于2019-08-17 23:18 被阅读19次

上一篇文,描述了FragmentPagerAdapter的一些基础使用。
这一篇主要还是来说说PagerAdapter的基础原理,以及关于无限循环的ViewPager的实战。

PagerAdapter 原理

用多了RecycleView的人都知道,也是有个BaseAdapterRecycleView绑定。
ViewPager是同样的原理,ViewPager是一个ViewGroup,缓存有多个子View(页面),而适配器类PagerAdapter为每个页面来提供数据,通过几个简单的接口,即可实现复杂功能而无须接触ViewPager里面繁而又繁的复杂逻辑计算代码。

ViewPager.setAdapter()的时候,向Adapter注册一个观察者,即调用: mAdapter.setViewPagerObserver(mObserver);
当数据发生更新时,也就是调用 adapter.notifyDataSetChange()ViewPager就会接受到通知,从而刷新界面使adapter重新生成数据(或者说提供一个新的界面)。

class ViewPager   
   public void setAdapter(@Nullable PagerAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.setViewPagerObserver(null);
            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);
            ...
        }

        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;

        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            mAdapter.setViewPagerObserver(mObserver);
            ...
        }
      ...
 }

class PagerAdapter {
  private final DataSetObservable mObservable = new DataSetObservable();
 private DataSetObserver mViewPagerObserver;
  void setViewPagerObserver(DataSetObserver observer) {
        synchronized (this) {
            mViewPagerObserver = observer;
        }
    }
  
    public void notifyDataSetChanged() {
        synchronized (this) {
            if (mViewPagerObserver != null) {
                //ViewPager专用的观察者
                mViewPagerObserver.onChanged();
            }
        }
        mObservable.notifyChanged();
    }
}

其实通过源码我们可以发现,PagerAdapter虽然提供了注册观察者的模式,直接保留了ViewPager的观察者,而不是通过注册观察者的方式。当数据更新直接调用观察者的方法mViewPagerObserver.onChanged();

至于原因,我们可以猜测下:
首先是有使用synchronized关键字,那么就可以避免多线程的影响。
另外,我们又可以随时的使用 setAdapter()来切换,当切换时会执行mAdapter.setViewPagerObserver(null); 那么当又有其他地方调用mAdapter. notifyDataSetChanged()那么就会引起???
=但是这两个方法都涉及了UI绘制,也就是只能在主线程调用。所以应该是不会有多线程的困扰的,所以猜不出原因。。
也许原因只是,代码上比较直观,便于区分吧。

另外这里谈下adapter.notifyDataSetChange()无法刷新界面数据的问题。
这里就是mViewPagerObserver.onChanged()的不作为了,看下源码就很清晰了

void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter is non-null
        ...
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            final int newPos = mAdapter.getItemPosition(ii.object);
          //当getItemPosition()返回POSITION_UNCHANGED时,是不会做更新的!而默认返回POSITION_UNCHANGED,所以需要重写该方法返回POSITION_NONE,
            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

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

而且即便,重写了该方法,也要注意使用FragmentPagerAdapter时候,由于Fragment的缓存,无法重新初始化Fragment对象,使得某些参数没有得到更改也是原因之一

PagerAdapter中需要掌握的几个方法

先通过观察FragmentPagerAdapter的源码,我们会发现实现非常简单。就是继承PagerAdapter之后,实现了几个方法用于生成以及缓存Fragment!

    /**
     * Create the page for the given position.  The adapter is responsible
     * for adding the view to the container given here, although it only
     * must ensure this is done by the time it returns from
     * {@link #finishUpdate(ViewGroup)}.
     *
     * @param container The containing View in which the page will be shown.
     * @param position The page position to be instantiated.
     * @return Returns an Object representing the new page.  This does not
     * need to be a View, but can be some other container of the page.
     */
    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        return instantiateItem((View) container, position);
    }

instantiateItem()顾名思义就是在ViewPagerposition页面生成一个用于展示的界面,当然你也可以什么都不做,无非就是展示空白页罢了。
注意,这里有个参数ViewGroup container 这个container,多研读下注释,可能还有部分人不理解这个是指代哪个View!
它有个返回值,会与position绑定,如FragmentPagerAdapter返回生成的Fragment对象。
也就是 position -- fragment -- container就绑定一起了。

/**
     * Remove a page for the given position.  The adapter is responsible
     * for removing the view from its container, although it only must ensure
     * this is done by the time it returns from {@link #finishUpdate(ViewGroup)}.
     *
     * @param container The containing View from which the page will be removed.
     * @param position The page position to be removed.
     * @param object The same object that was returned by
     * {@link #instantiateItem(View, int)}.
     */
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        destroyItem((View) container, position, object);
    }

destroyItem()这个就更简单了,销毁指定的位置的页面,当某个位置不在缓存范围内时,就会被移除。
FragmentPagerAdapter在此执行fragment.detach(),没有销毁Fragment对象。

/**
     * Called to inform the adapter of which item is currently considered to
     * be the "primary", that is the one show to the user as the current page.
     * This method will not be invoked when the adapter contains no items.
     *
     * @param container The containing View from which the page will be removed.
     * @param position The page position that is now the primary.
     * @param object The same object that was returned by
     * {@link #instantiateItem(View, int)}.
     */
    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        setPrimaryItem((View) container, position, object);
    }

setPrimaryItem()当滑动至某个页面时,该方法会被调用。一指定当前页面的方式。
FragmentPagerAdapter中重写该方法使之执行了 fragment.setUserVisibleHint(true),让我们可以通过getUserVisibleHint()来判断该Fragment是否正在被展示从而执行某些骚操作

/**
     * Determines whether a page View is associated with a specific key object
     * as returned by {@link #instantiateItem(ViewGroup, int)}. This method is
     * required for a PagerAdapter to function properly.
     *
     * @param view Page View to check for association with <code>object</code>
     * @param object Object to check for association with <code>view</code>
     * @return true if <code>view</code> is associated with the key object <code>object</code>
     */
    public abstract boolean isViewFromObject(@NonNull View view, @NonNull Object object);

isViewFromObject()
上述几个方法中,都有个Object参数,其实就是instantiateItem()所返回的对象。例如,假设instantiateItem()返回的不是Fragment,那么在执行setPrimaryItem()的时候,我们就无从下手去处理这个位置对应的Fragment,无法执行执行(Fragment)object.setUserVisibleHint(true),除非我们自己缓存了每个position对应的Fragment,然后执行fragemntList.get(position).setUserVisibleHint(true).

说回这个isViewFromObject(),对于ViewPager来说,是这样使用的:

class ViewPager {
  ItemInfo infoForChild(View child) {
        for (int i = 0; i < mItems.size(); i++) {
            ItemInfo ii = mItems.get(i);
            if (mAdapter.isViewFromObject(child, ii.object)) {
                return ii;
            }
        }
        return null;
    }
}

所以这个方法是用于遍历寻找到,某个页面(View)匹配哪一个Object,从而找到对应的ItemInfo(ViewPager里面储存每个子页面所使用的数据结构)

所以若是直接instantiateItem()返回一个View的话,那么:

  public boolean isViewFromObject(View view, Object object) {
      return view == object;
  }

所以若是直接instantiateItem()返回一个Fragment的话,那么:

    public boolean isViewFromObject(View view, Object object) {
        return ((Fragment)object).getView() == view;
    }

还有个FragmentPagerAdapter中定义的
public abstract Fragment getItem(int position);
这个就没啥好说的了,真正用于生成Fragment实例的方法,然后被instantiateItem()调用。

无限循环实战

Talk is cheap, show me the code!

需求:
1.有很多个不同的数据,通过左右滑动页面,展示不同的数据。

  1. 每个数据的类型格式相同
    或许可以理解成 小说阅读时的左右翻页。
思路一

ViewGroup添加3个全屏的View,编号123,使用View.setTranslationX()方式将其中两个View(1和3),放在2两边,然后拦截滑动事件,当滑动时同时设置这三个view在X轴的偏移量,当滑动结束时,比如向左滑动,那么现在显示的为3,且3的x轴偏移量变为0,这时候,重新设置1和2的偏移量使之在3的两边

思路二

继承PagerAdapter,将数据大小,即 adapter.getCount() 返回 Int.MAX_VALUE, viewPager.currentItem 初始化为 Int.MAX_VALUE/2

思路1的实现其实是考虑要很全面,坑可能不少。或许可以考虑用在上下滑的无限循环。
所以本文接下来用的是思路2.

1.首先为什么不直接使用FragmentPagerAdapterFragmentStatePagerAdapter

FragmentPagerAdapter会根据缓存所有位置的Fragment,那么这数量级就点大了!
FragmentStatePagerAdapter在上一篇已经有提到了,初始化的时候,直接就OOM了。

其实如果想一下,如果们这里不是使用Fragment来写,单纯的使用View来处理的话,由于每个View都是相同的,我们是必然要循环利用这些View的,直接使用ArrayList<View>保存view,那么在instantiateItem()中 return viewList(pos),其中要注意若view已经被addToParent,那么需要先ViewGroup.removeView(),再执行ViewGroup.addView().

public Object instantiateItem(ViewGroup container, int position) {
             //对ViewPager页号求模取出View列表中要显示的项
             position %= viewlist.size();
             if (position<0){
                 position = viewlist.size()+position;
             }
             ImageView view = viewlist.get(position);
             //如果View已经在之前添加到了一个父组件,则必须先remove,否则会抛出IllegalStateException。
             ViewParent vp =view.getParent();
             if (vp!=null){
                 ViewGroup parent = (ViewGroup)vp;
                 parent.removeView(view);
             }
             container.addView(view);  
             //add listeners here if necessary
             return view;  
         }  

同理,使用Fragment的形式,我们也可以使用ArrayList<Fragment>直接初始化三个Fragment的形式进行循环利用。当然这里同样有 fragment.isAdd()的问题,所以建议设置一个FragmentPool来动态初始化fragment以及缓存fragment。

另外要注意的是,由于Fragment的事务处理都是异步的,所以记得使用commitNowAllowingStateLoss(),因为如果不是立即提交,很可能滑动时出现空白页的情况。。

使用由于我们也有必要设置一个FragmentPool
其中不同的是,由于Fragment的事务处理都是异步的,那么我们就不可以直接采用

  //Fragment缓存池,每个被remove的Fragment的添加队列尾部,等待被重新利用
  private val cacheFragments = ArrayDeque<TodayDataFragment>()
  override fun instantiateItem(container: ViewGroup, position: Int): Any {

        val fragment = getItem(position)

        fragment.setMenuVisibility(false)
        fragment.userVisibleHint = false

        Log.i(TAG, "Fragment添加, tag =${fragment.arguments?.getString(TodayDataFragment.PARAM_DATE)}")
        mFragmentManager.beginTransaction().add(container.id, fragment).commitNow()
        return fragment
    }

    override fun destroyItem(container: ViewGroup, position: Int, fragment: Any) {
        //从FragmentManager中移除
        mFragmentManager.beginTransaction().remove(fragment as Fragment).commitNowAllowingStateLoss()
        mCurTransaction = null
        if (fragment is TodayDataFragment) {
            Log.i(TAG, "Fragment移除,并加入缓存池 tag =${fragment.arguments?.getString(TodayDataFragment.PARAM_DATE)}")
            cacheFragments.add(fragment)
        }
    }

    private fun getItem(position: Int): Fragment {
        var fragment = cacheFragments.poll()
        if(fragment == null || fragment.isAdded || fragment.isStateSaved) {
            Log.i(TAG, "新建fragment")
            if(fragment!=null) {
                cacheFragments.add(fragment)
            }
            fragment = TodayDataFragment()
        }
        getItemBundle(position)?.let {
            fragment.arguments = it
        }
        return fragment
    }

至于其他的几个方法,照搬FragmentPagerAdapter就可以了!

另外,可以参照RecycleView的写法,我们还可以扩展出一种MutiType的形式,而不是仅支持一种Fragment。当然缓存池的结构也要相应的变一变。

写篇文章真的难。。三个星期前就写下这篇的三分之二了,今天才动手完结它。
我真的太难了,上辈子一定是数学高考最后一道答题。

相关文章

网友评论

    本文标题:Fragment 与 FragmentPagerAdapter

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