Fragment 与 FragmentPagerAdapter (二)

一、前言

上一篇文,Fragment 与 FragmentPagerAdapter (一) 描述了

  • Fragment的系统回收以及初始化
  • FragmentPagerAdapterFragmentStatePagerAdapter的区别

这一篇主要还是来说说PagerAdapter的基础分析,以及关于无限循环的ViewPager的实战。

虽然这是第二篇,其实这篇反而偏基础点,多是讲PagerAdapter中各个接口的作用。

二、PagerAdapter 简单分析

用多了RecycleView的人都知道,是有个BaseAdapterRecycleView绑定。

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

ViewPager.setAdapter()的时候,向PagerAdapter注册一个观察者,即调用: 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的观察者,而不是通过注册观察者的方式。当数据更新---notifyDataSetChanged()直接调用观察者的方法mViewPagerObserver.onChanged();

至于原因,我们可以猜测下:

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

    来自ViewPager的观察者的执行优先于其它观察者

另外谈下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!

1. instantiateItem()

    /**
     * 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!

@param container The containing View in which the page will be shown
将会在改页面展示的View,也就是这个View会与Fragment绑定。

它有个返回值,会与position绑定,如FragmentPagerAdapter返回生成的Fragment对象。
也就是 position -- fragment -- container就绑定一起了。

2. destroyItem()

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

3. setPrimaryItem()

/**
     * 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是否正在被展示从而执行某些骚操作

4. isViewFromObject()

/**
     * 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 object
     * @param object Object to check for association with view
     * @return true if view is associated with the key object object
     */
    public abstract boolean isViewFromObject(@NonNull View view, @NonNull Object object);

isViewFromObject()
上述几个方法中,都需要传入一个Object参数,其实就是instantiateItem()所返回的Fragment。

例如,假设instantiateItem()返回的不是Fragment,那么在执行setPrimaryItem()的时候,我们就无从下手去处理这个位置对应的Fragment,无法执行(Fragment)object.setUserVisibleHint(true)。因为Fragment是缓存在FragmentManager,而PagerAdaper本身是不做缓存的。除非我们自己缓存了每个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;
    }

5. getItem(int position)

还有个在FragmentPagerAdapter和FragmentStatePagerAdapter中定义的
public abstract Fragment getItem(int position);
,用于真正生成Fragment实例的方法,然后被instantiateItem()调用。毕竟在instantiateItem()中还涉及各种Fragment的缓存,不一定需要调用到getItem()。

三、无限循环实战

大部分上,使用FragmentPagerAdapter和FragmentStatePagerAdapter,可以分别满足我们对于少量或者较多页面的左右滑动切换的需求。

但有时候,需求又是多变的,比如上下滑动,无限左右滑动。
比如这个(或许可以理解成 小说阅读时的左右翻页):

  1. 有很多个不同的数据,通过左右滑动页面,展示不同的数据。
  2. 每个数据的类型格式相同

Talk is cheap, let me talk more!

思路一

ViewGroup添加3个全屏的View,编号1、2、3,使用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的实现其实是考虑要很全面,坑可能不少。或许可以考虑用在上下滑的无限循环。当然现在是有了支持上下滑的ViewPager库的,另外androidx的Viewpager2库也可以直接支持。
所以本文接下来说说思路2.

实现思路二

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

其实如果想一下,如果们这里不是使用Fragment来写,单纯的使用View来处理的话,由于每个View都是相同的,我们是必然要循环利用这些View的,想一下常用的RecycleView,没错了,加个缓存吧,使用List保存回收的view,然后再用个ViewHolder优化下代码,大概就想下面这样了

class MyViewPagerAdapter extends PagerAdapter {
        private LinkedList mViewCache = null;

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

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            holder = null;
            View convertView = null;
            if (mViewCache.size() == 0) {
                convertView = View.inflate(ExperimentActivity.this, R.layout.item_exper_viewpager, null);
                holder = new ViewHolder();
                holder.ivPic = (ImageView) convertView.findViewById(R.id.iv_title_pic);
                holder.tvName = (TextView) convertView.findViewById(R.id.tv_exper_name);
                convertView.setTag(holder);
            } else {
                convertView = mViewCache.removeFirst();
                holder = (ViewHolder) convertView.getTag();
            }
            holder.tvName.setText("string");
            container.addView(convertView);
            return convertView;
        }


        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
            mViewCache.add((View) object);
        }
        public static class ViewHolder {
            public TextView tvName;
            public ImageView ivPic;
        }
    }

同理,使用Fragment的形式,我们可以设置一个FragmentPool来动态初始化fragment以及缓存fragment。

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

    //Fragment缓存池,每个被remove的Fragment的添加队列尾部,等待被重新利用
    private val cacheFragments = ArrayDeque()
      
    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        val tag = "DataFragment$position"
        var fragment = mFragmentManager.findFragmentByTag(tag)
        //ViewPager本身缓存的页面 或系统回收后恢复的缓存页面
        if (fragment != null) {
              return fragment
        }
    
        fragment = getItem(position)

        fragment.setMenuVisibility(false)
        fragment.userVisibleHint = false
        mFragmentManager.beginTransaction().add(container.id, fragment, tag).commitNow()
        return fragment
    }

    override fun destroyItem(container: ViewGroup, position: Int, fragment: Any) {
        //从FragmentManager中移除
        mFragmentManager.beginTransaction().remove(fragment as Fragment)
            .commitNowAllowingStateLoss()
        if (fragment is DataFragment) {
            cacheFragments.add(fragment)
        }
    }

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

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

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

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

你可能感兴趣的:(Fragment 与 FragmentPagerAdapter (二))