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 <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()所返回的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>保存回收的view,然后再用个ViewHolder优化下代码,大概就想下面这样了

class MyViewPagerAdapter extends PagerAdapter {
        private LinkedList<View> 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<TodayDataFragment>()
      
    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。当然缓存池的结构也要相应的变一变。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 化名 小红他从小没有见过他的父母,大人都说是他的爷爷一手把小红带大的,在村里没有人愿意和她玩,我也是偷着和他玩,直...
    创业如歌666简书凤芹阅读 209评论 0 1
  • 我喜欢了他很久,可惜不会有结果。 我不是一见钟情,也不算日久生情,就是简单的一个梦。梦里,他对我很好,我感...
    浅薰mintcream阅读 343评论 0 1
  • 大麦网全部商品 全部票品 北京站订票 上海站订票 深圳站订票 广州站订票 武汉站订票 重庆站订票 杭州站订票 天津...
    Dorann阅读 116评论 0 0
  • 以前曾有次机缘巧合在寺庙里听禅,记得当时禅师说每个人六道轮回来处不同,有的人前世其实是小兽,比如说小兔子什么的,...
    小兔林阅读 272评论 0 0
  • 在那次受伤以后,竹川清水就消失了…… 她被仆人带走的,校方并没有联系到她的父母,也并没有看到她...
    画屏香dxt阅读 365评论 0 2