ViewPager 和 Fragment 使用注意的问题


参考地址:http://blog.csdn.net/wangjinyu501/article/details/8169924


一:使用简介

ViewPager

ViewPager 如其名所述,是负责翻页的一个 View。准确说是一个  ViewGroup,包含多个 View 页,在手指横向滑动屏幕时,其负责对 View 进行切换。为了生成这些 View 页,需要提供一个  PagerAdapter 来进行和数据绑定以及生成最终的 View 页。
  • setAdapter()
    • ViewPager 通过 setAdapter() 来建立与 PagerAdapter 的联系。这个联系是双向的,一方面,ViewPager 会拥有 PagerAdapter 对象,从而可以在需要时调用 PagerAdapter 的方法;另一方面,ViewPager 会在 setAdapter() 中调用 PagerAdapter 的 registerDataSetObserver() 方法,注册一个自己生成的 PagerObserver 对象,从而在 PagerAdapter 有所需要时(如 notifyDataSetChanged() 或 notifyDataSetInvalidated() 时),可以调用 Observer 的 onChanged() 或 onInvalidated() 方法,从而实现 PagerAdapter 向 ViewPager 方向发送信息。
  • dataSetChanged()
    • 在 PagerObserver.onChanged(),以及 PagerObserver.onInvalide() 中被调用。因此当 PagerAdapter.notifyDataSetChanged() 被触发时,ViewPager.dataSetChanged() 也可以被触发。该函数将使用 getItemPosition() 的返回值来进行判断,如果为 POSITION_UNCHANGED,则什么都不做;如果为 POSITION_NONE,则调用 PagerAdapter.destroyItem() 来去掉该对象,并设置为需要刷新 (needPopulate = true) 以便触发 PagerAdapter.instantiateItem() 来生成新的对象。

PagerAdapter

PageAdapter 是 ViewPager 的支持者,ViewPager 将调用它来取得所需显示的页,而 PageAdapter 也会在数据变化时,通知 ViewPager。这个类也是FragmentPagerAdapter 以及 FragmentStatePagerAdapter 的基类。如果继承自该类,至少需要实现 instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。

  • getItemPosition()
    • 该函数用以返回给定对象的位置,给定对象是由 instantiateItem() 的返回值。
    • 在 ViewPager.dataSetChanged() 中将对该函数的返回值进行判断,以决定是否最终触发 PagerAdapter.instantiateItem() 函数。
    • 在 PagerAdapter 中的实现是直接传回 POSITION_UNCHANGED。如果该函数不被重载,则会一直返回 POSITION_UNCHANGED,从而导致 ViewPager.dataSetChanged() 被调用时,认为不必触发 PagerAdapter.instantiateItem()。很多人因为没有重载该函数,而导致调用
       PagerAdapter.notifyDataSetChanged() 后,什么都没有发生。
  • instantiateItem(View container, int position)
    • 在每次 ViewPager 需要一个用以显示的 Object 的时候,该函数都会被 ViewPager.addNewItem() 调用。
  • notifyDataSetChanged()
    • 在数据集发生变化的时候,一般 Activity 会调用 PagerAdapter.notifyDataSetChanged(),以通知 PagerAdapter,而 PagerAdapter 则会通知在自己这里注册过的所有 DataSetObserver。其中之一就是在 ViewPager.setAdapter() 中注册过的 PageObserver。PageObserver 则进而调用 ViewPager.dataSetChanged(),从而导致 ViewPager 开始触发更新其内含 View 的操作。

FragmentPagerAdapter

FragmentPagerAdapter 继承自 PagerAdapter。相比通用的 PagerAdapter,该类更专注于每一页均为 Fragment 的情况。如文档所述, 该类内的每一个生成的 Fragment 都将保存在内存之中,因此适用于那些相对静态的页,数量也比较少的那种;如果需要处理有很多页,并且数据动态性较大、占用内存较多的情况,应该使用 FragmentStatePagerAdapter。FragmentPagerAdapter 重载实现了几个必须的函数,因此来自 PagerAdapter 的函数,我们只需要实现 getCount(),即可。且,由于 FragmentPagerAdapter.instantiateItem() 的实现中,调用了一个新增的虚函数 getItem(),因此,我们还至少需要实现一个 getItem()。因此,总体上来说,相对于继承自 PagerAdapter,更方便一些。
  • getItem()
    • 该类中新增的一个虚函数。函数的目的为生成新的 Fragment 对象。重载该函数时需要注意这一点。在需要时,该函数将被 instantiateItem() 所调用。
    • 如果需要向 Fragment 对象传递相对静态的数据时,我们一般通过 Fragment.setArguments() 来进行,这部分代码应当放到 getItem()。它们只会在新生成 Fragment 对象时执行一遍。
    • 如果需要在生成 Fragment 对象后,将数据集里面一些动态的数据传递给该 Fragment,那么,这部分代码不适合放到 getItem() 中。因为当数据集发生变化时,往往对应的 Fragment 已经生成,如果传递数据部分代码放到了 getItem() 中,这部分代码将不会被调用。这也是为什么很多人发现调用 PagerAdapter.notifyDataSetChanged() 后,getItem() 没有被调用的一个原因。
  • instantiateItem()
    • 函数中判断一下要生成的 Fragment 是否已经生成过了,如果生成过了,就使用旧的,旧的将被 Fragment.attach();如果没有,就调用 getItem() 生成一个新的,新的对象将被 FragmentTransation.add()。
    • FragmentPagerAdapter 会将所有生成的 Fragment 对象通过 FragmentManager 保存起来备用,以后需要该 Fragment 时,都会从 FragmentManager 读取,而不会再次调用 getItem() 方法
    • 如果需要在生成 Fragment 对象后,将数据集中的一些数据传递给该 Fragment,这部分代码应该放到这个函数的重载里。在我们继承的子类中,重载该函数,并调用 FragmentPagerAdapter.instantiateItem() 取得该函数返回 Fragment 对象,然后,我们该 Fragment 对象中对应的方法,将数据传递过去,然后返回该对象。
    • 否则,如果将这部分传递数据的代码放到 getItem()中,在 PagerAdapter.notifyDataSetChanged() 后,这部分数据设置代码将不会被调用。
  • destroyItem()
    • 该函数被调用后,会对 Fragment 进行 FragmentTransaction.detach()。这里不是 remove(),只是 detach(),因此 Fragment 还在 FragmentManager 管理中,Fragment 所占用的资源不会被释放。
ViewPager中的Fragment保存:
package com.toutouunion.adapter;

import java.util.List;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.view.ViewGroup;

/**
 * @author XianFeng
 * @createdTime 2015年4月17日 下午3:00:39
 * @usage 找达人适配器
 *
 */
public class TalentFragmentAdapter extends FragmentPagerAdapter {
	
	private FragmentManager fManager;
	private List fragmentsList;

	public TalentFragmentAdapter(FragmentManager fm, List fragments) {
		super(fm);
		this.fManager = fm;
		this.fragmentsList = fragments;
	}

	@Override
	public Fragment getItem(int arg0) {
		return fragmentsList.get(arg0);
	}

	@Override
	public int getCount() {
		return fragmentsList.size();
	}
	
	@Override
	public Object instantiateItem(ViewGroup container, int position) {
		Fragment fragment = (Fragment) super.instantiateItem(container, position);
		this.fManager.beginTransaction().show(fragment).commit();
		return fragment;
	}
	
	@Override
	public void destroyItem(ViewGroup container, int position, Object object) {
		Fragment fragment = fragmentsList.get(position);
		fManager.beginTransaction().hide(fragment).commit();
	}

}



FragmentStatePagerAdapter

FragmentStatePagerAdapter 和前面的 FragmentPagerAdapter 一样,是继承子 PagerAdapter。但是,和 FragmentPagerAdapter 不一样的是,正如其类名中的 'State' 所表明的含义一样,该 PagerAdapter 的实现将只保留当前页面,当页面离开视线后,就会被消除,释放其资源;而在页面需要显示时,生成新的页面(就像 ListView 的实现一样)。这么实现的好处就是当拥有大量的页面时,不必在内存中占用大量的内存。
  • getItem()
    • 一个该类中新增的虚函数。
    • 函数的目的为生成新的 Fragment 对象。
    • Fragment.setArguments() 这种只会在新建 Fragment 时执行一次的参数传递代码,可以放在这里。
    • 由于 FragmentStatePagerAdapter.instantiateItem() 在大多数情况下,都将调用 getItem() 来生成新的对象,因此如果在该函数中放置与数据集相关的 setter 代码,基本上都可以在 instantiateItem() 被调用时执行,但这和设计意图不符。毕竟还有部分可能是不会调用 getItem() 的。因此这部分代码应该放到 instantiateItem() 中。
  • instantiateItem()
    • 除非碰到 FragmentManager 刚好从 SavedState 中恢复了对应的 Fragment 的情况外,该函数将会调用 getItem() 函数,生成新的 Fragment 对象。新的对象将被 FragmentTransaction.add()。
    • FragmentStatePagerAdapter 就是通过这种方式,每次都创建一个新的 Fragment,而在不用后就立刻释放其资源,来达到节省内存占用的目的的。
  • destroyItem()
    • 将 Fragment 移除,即调用 FragmentTransaction.remove(),并释放其资源。

二:setOffscreenPageLimit(int limit)

ViewPager定义了一个:

private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;默认值 是1,这表示你的预告加载的页面数量是1,假设当前有四个Fragment的tab,显示一个,预先加载下一个.这样你在移动前就已经加载了下一个界面,移动时就可以看到已经加载的界面了。

打印日志可以看到onActivityCreated等方法在初始化第一个Fragment完成后就会初始化下一个Fragment. 

提供的公共方法源码如下:

public void setOffscreenPageLimit(int limit) {
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
                    DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES;
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }


从这个方法来看,不管你设置什么值,至少会预先加载下一个Fragment,你想预先加载几个就可以传入相应的参数.
这种情况如音乐播放时,如果有自动加载歌词就可以使用了.

如果你的界面需要加载一些大量的数据,但你不想预先加载下一个界面(需要网络或耗时的操作),使用ViewPager却很无耐.特别是下一个界面有可能你一段很长时间不会使用到,如我开发的微博,在显示主页后我不想立即加载下一个界面,因为都有ListView,如果我不访问它,就不必加载无用的资源.

可以通过修改这个值,但有,修改后就会有一个麻烦的地方,因为移动时不会预先加载下一个界面的关系,所以会看到一片黑色的背景.

如果不介意黑色背景,可以覆盖这个类,然后定义默认的加载数量为0
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES=0;就是不预先加载下一个界面.

如果想预加载,可以使用原来的ViewPager,或这里直接改为mOffscreenPageLimit=你要加载的数量。

由于sdk的更新问题,不是所有v4 support都适合的,v17时就换了


ViewPager切换动画教程:

http://blog.csdn.net/lmj623565791/article/details/40411921

http://blog.csdn.net/lmj623565791/article/details/38026503



三:getChildFragmentManager() 与 getSupportFragmentManager()


官方解释:

The definition of getChildFragmentManager() is:

Return a private FragmentManager for placing and managing Fragments inside of this Fragment.

Meanwhile the definition of getFragmentManager() (or in this case getSupportFragmentManager()) is:

Return the FragmentManager for interacting with fragments associated with this fragment's activity.

Basically, the difference is that Fragment's now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.

In this case, what I'm guessing is you've added the Fragments to the Activity's FragmentManager. You get the child FragmentManager which doesn't contain what you are looking for. Thus you get the exception because it can't find the Fragment with the given ID because it's in a different FragmentManager.

所以,在Fragment中嵌套Fragment时,一定要使用 getChildFragmentManager();

否则,会在ViewPager中出现fragment不会加载的情况,即fragment出现空白页的情况!

可以参看附件:尝试将HomeFragment中的72行中的getChildFragmentManager()改为getFragmentManager(),然后运行,点击其他底部tab之后,在返回第一个tab,可以看到fragment不加载的情况!

参考附件:http://download.csdn.net/detail/wenbitianxiafeng/7858269



四:Fragment生命周期部分简介


      1.Fragment虽然有onResume和onPause的,但是这两个方法是Activity的方法,调用时机也是与Activity相同。


      2.通过阅读ViewPager和PageAdapter相关的代码,切换Fragment实际上就是通过设置setUserVisibleHint和

setMenuVisibility来实现的,调用这个方法时并不会释放掉Fragment(即不会执行onDestoryView)。


      3.由于setUserVisibleHint优于onCreate调用,所以当onCreate调用完毕setUserVisibleHint就不会触发,这时需

要在首个显示的fragment(或者Activity中切换显示Fragment的相应地方)调用setUserVisibleHint方法,然后在对应

Fragment中重写(Override)setUserVisibleHint方法即可。


五:PagerTabStrip的使用

简单使用示例如下,代码中包含注释:

布局文件



    

    

        
    

适配器

/**
	 * 实现一个PagerAdapter内部类
	 * 
	 * @author Administrator
	 * 
	 */
	private class MyPageAdapter extends PagerAdapter {
		String[] titles;
		List views;

		public MyPageAdapter(String[] titles, List views) {
			this.titles = titles;
			this.views = views;
		}

		/**
		 * 要显示的页面的个数
		 */
		@Override
		public int getCount() {
			return views == null ? 0 : views.size();
		}

		/**
		 * 获取一个指定页面的title描述 如果返回null意味着这个页面没有标题,默认的实现就是返回null
		 * 
		 * 如果要显示页面上的title则此方法必须实现
		 */
		@Override
		public CharSequence getPageTitle(int position) {
			return titles[position];
		}

		/**
		 * 创建指定position的页面。这个适配器会将页面加到容器container中。
		 * 
		 * @param container
		 *            创建出的实例放到container中,这里的container就是viewPager
		 * @return 返回一个能表示该页面的对象,不一定要是view,可以其他容器或者页面。
		 */
		@Override
		public Object instantiateItem(ViewGroup container, int position) {
			// 必须要把创建的对象先添加到容器中,再返回。一般简单的adapter直接就返回显示的界面就完事了。
			container.addView(views.get(position));
			return views.get(position);
		}

		/**
		 * 此方法会将容器中指定页面给移除 该方法中的参数container和position跟instantiateItem方法中的内容一致
		 * 
		 * @param object
		 *            这个object 就是 instantiateItem方法中返回的那个Object
		 */
		@Override
		public void destroyItem(ViewGroup container, int position, Object object) {
			container.removeView(views.get(position));
		}

		/**
		 * 这个方法就是比较一下容器中页面和instantiateItem方法返回的Object是不是同一个
		 * 
		 * @param arg0
		 *            ViewPager中的一个页面
		 * @param arg1
		 *            instantiateItem方法返回的对象
		 */
		@Override
		public boolean isViewFromObject(View arg0, Object arg1) {
			return arg0 == arg1;
		}

	}
效果如

ViewPager 和 Fragment 使用注意的问题_第1张图片

pagerTabStrip.setTabIndicatorColor(int);可以改变导航条的颜色
pagerTabStrip.setTextColor(color);可以改变导航文字的颜色,字体大小类似TextView的修改。

但是暂时实现不了如下效果图:控制文字和indicator的位置,常使用library——Android ViewPagerIndicator




注意事项:

1.FragmentTransaction.commitAllowingStateLoss()问题

官方文档介绍:

Like commit() but allows the commit to be executed after an activity's state is saved. This is dangerous 

because the commit can be lost if the activity needs to later be restored from its state, so this should 

only be used for cases where it is okay for the UI state to change unexpectedly on the user.

类似于commit()方法,但是commit在Activity的onSaveInstanceState(Bundle outState)方法之后调用会出错:

IllegalStateException: Can not perform this action after onSaveInstanceState

解决上述错误办法:将commit()方法替换成commitAllowingStateLoss()方法即可!


参考博客:http://blog.csdn.net/harvic880925/article/details/38521865


六:PagerAdapter的notifyDataSetChanged失效问题

解决方案两种:

第一种:

重写PagerAdapter的getItemPosition方法,如下:

public int getItemPosition(Object object) {
    return POSITION_NONE;
}

这样,在调用notifyDataSetChanged方法的时候,ViewPager会remove掉所有的view,然后重新加载,在视图内容比较复杂的情况下,这种解决方案效率低下。


第二种:

在PagerAdapter的instantiateItem()方法中,使用setTag()为需要改变的view添加tag,当需要改变ViewPager内容时,只需要通过findViewByTag()方法找到对应的view,修改即可。

相比第一种方案,此方案更加灵活和高效。


详情参考Stack OverFlow问题:

http://stackoverflow.com/questions/7263291/viewpager-pageradapter-not-updating-the-view/8024557#





你可能感兴趣的:(【Android,应用开发】)