FragmentViewPager中使用Fragment

Posted ITRenj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FragmentViewPager中使用Fragment相关的知识,希望对你有一定的参考价值。

Fragment(三)ViewPager中使用Fragment

博客对应的Demo地址:GitHubGitee

通过这篇博客,我们能知道以下问题:

  • Fragment 加载到 ViewPage上的过程和用户可见性判断
  • Fragment 加载到 ViewPage2上的过程和用户可见性判断

ViewPager 中使用 Fragment

1. Fragment 怎样加载到 ViewPage

使用 ViewPager需要一个 PagerAdapter,我们如果使用 Fragment 填充 ViewPager 的话,需要使用的是 FragmentPagerAdapter 或者 FragmentStatePagerAdapter ,当然这两个 Adapter 都是继承至 PagerAdapter 的,这两个 Adapter 有什么差别,在后面我们会说到。

对于 PagerAdapter 我们主要看三个方法 instantiateItem()destroyItem()setPrimaryItem()

  • instantiateItem():创建 ViewPager 页面时调用
  • destroyItem():移除 ViewPager 页面时调用
  • setPrimaryItem()ViewPager 页面改变显示位置时调用(比如调用 adapter.setCurrentItem() 方法时)

当然还有两个方法 startUpdate()finishUpdate(),分别表示将要开始改变位置和位置改变完成时调用,代码比较简单,直接看一下。就不多说了【FragmentPagerAdapterFragmentStatePagerAdapter 中都一样】

@Override
public void startUpdate(@NonNull ViewGroup container) 
    if (container.getId() == View.NO_ID) 
        throw new IllegalStateException("ViewPager with adapter " + this
                + " requires a view id");
    


@Override
public void finishUpdate(@NonNull ViewGroup container) 
	if (mCurTransaction != null) 
	    if (!mExecutingFinishUpdate) 
	        try 
	            mExecutingFinishUpdate = true;
	            mCurTransaction.commitNowAllowingStateLoss();
	         finally 
	            mExecutingFinishUpdate = false;
	        
	    
	    mCurTransaction = null;
	

ViewPager 改变位置时的方法调用顺序

当改变 ViewPager 的位置时,方法的调用顺序为: startUpdate() -> instantiateItem()【ViewPager 中的 addNewItem() 方法内部调用】 -> destroyItem() -> setPrimaryItem() -> finishUpdate()

注意:因为 ViewPager 的缓存和预加载机制,instantiateItem()destroyItem()方法会调用多次,并且顺序并非一个调用多次之后另一个在调用多次,而是交叉的调用。

instantiateItem() 方法解析

Fragment 加载到 ViewPage 上,主要在 instantiateItem() 方法中:

  • FragmentPagerAdapter

      // FragmentPagerAdapter 类
      public Object instantiateItem(@NonNull ViewGroup container, int position) 
          if (mCurTransaction == null) 
              mCurTransaction = mFragmentManager.beginTransaction();
          
    
          final long itemId = getItemId(position);
    
          String name = makeFragmentName(container.getId(), itemId);
      	// 当前需要显示的Fragment,是否已经存在
          Fragment fragment = mFragmentManager.findFragmentByTag(name);
          if (fragment != null) 
      		// 存在,调用 attach() 方法
              mCurTransaction.attach(fragment);
           else 
      		// 不存在,调用 add() 方法
              fragment = getItem(position);
              mCurTransaction.add(container.getId(), fragment,
                      makeFragmentName(container.getId(), itemId));
          
    
          return fragment;
      
    

    先通过 FragmentManager 判断将要显示的 Fragment 是否已经在容器中存在,如果存在调用 FragmentTransaction#attach() 方法,否则通过 getItem(position) 方法获取将要显示的 Fragment,然后调用 FragmentTransaction#add() 方法将 Fragment 添加到容器中。

  • FragmentStatePagerAdapter

      // FragmentStatePagerAdapter 类
      public Object instantiateItem(@NonNull ViewGroup container, int position) 
          if (mFragments.size() > position) 
      		// 从集合中获取将要显示的 Fragment
              Fragment f = mFragments.get(position);
              if (f != null) 
                  return f;
              
          
    
          if (mCurTransaction == null) 
              mCurTransaction = mFragmentManager.beginTransaction();
          
    
      	// 集合中没有获取到,就调用 getItem() 方法获取Fragment
          Fragment fragment = getItem(position);
      	// 状态处理
          if (mSavedState.size() > position) 
              Fragment.SavedState fss = mSavedState.get(position);
              if (fss != null) 
                  fragment.setInitialSavedState(fss);
              
          
      	// 将当前位置之前的所有位置,添加 null 元素进行占位,保证集合中的位置和 ViewPager 中的位置一一对应
          while (mFragments.size() <= position) 
              mFragments.add(null);
          
      	// 当前位置,使用 fragment 对象替换 null 元素
          mFragments.set(position, fragment);
      	// 调用 add() 方法
          mCurTransaction.add(container.getId(), fragment);
    
      	return fragment;
      
    

    首先从缓存集合中获取将要显示的 Fragmnet ,如果获取到了,就直接返回;没有获取到就通过 getItem(position) 方法获取将要显示的 Fragment,然后将其保存到缓存列表中,最后调用 FragmentTransaction#add() 方法将 Fragment 添加到容器中。

通过 instantiateItem() 这个方法,发现不管 FragmentPagerAdapter 还是 FragmentStatePagerAdapter,最终都是调用 FragmentTransaction#add() 方法将 Fragment 添加到容器中。

2. ViewPagerFragment 用户可见性判断

因为 ViewPager 的预加载机制,在 onResume() 监听是不准确的。

这时候,我们可以通过 setUserVisibleHint() 方法来监听,当方法传入值为true的时候,说明Fragment可见,为false的时候说明Fragment被切走了。但是需要注意的是,这个方法不属于生命周期方法,所以它可能在生命周期方法执行之前就执行了,也就是说,有可能执行这个方法的时候,Fragment 还没有被添加到容器中,所以需要进行判断一下。

androidX优化方案

在 AndroidX 当中,FragmentPagerAdapterFragmentStatePagerAdapter 的构造方法,添加一个 behavior 参数实现的。

// FragmentPagerAdapter
@Deprecated
public FragmentPagerAdapter(@NonNull FragmentManager fm) 
    this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);

public FragmentPagerAdapter(@NonNull FragmentManager fm,
        @Behavior int behavior) 
    mFragmentManager = fm;
    mBehavior = behavior;


// FragmentStatePagerAdapter
@Deprecated
public FragmentStatePagerAdapter(@NonNull FragmentManager fm) 
    this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);

public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
        @Behavior int behavior) 
    mFragmentManager = fm;
    mBehavior = behavior;

如果我们指定不同的 behavior,会有不同的表现:

  • BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTViewPager 中切换 Fragment,setUserVisibleHin()t 方法将不再被调用,他会确保 onResume() 的正确调用时机
  • BEHAVIOR_SET_USER_VISIBLE_HINT:跟之前的方式是一致的,我们可以通过 setUserVisibleHint() 结合 Fragment 的生命周期来监听。

具体实现

具体的实现这个功能在 instantiateItem()setPrimaryItem() 方法中,分别来看两个方法

  • instantiateItem() 方法

      // FragmentPagerAdapter#instantiateItem()
      public Object instantiateItem(@NonNull ViewGroup container, int position) 
          if (fragment != mCurrentPrimaryItem) 
              fragment.setMenuVisibility(false);
              if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) 
                  mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
               else 
                  fragment.setUserVisibleHint(false);
              
          
      
    
      // FragmentStatePagerAdapter#instantiateItem()
      public Object instantiateItem(@NonNull ViewGroup container, int position) 
          fragment.setMenuVisibility(false);
          if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) 
              fragment.setUserVisibleHint(false);
          
    
          if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) 
              mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
          
      
    

    FragmentPagerAdapterFragmentStatePagerAdapter中的 instantiateItem() 关于 behavior 的操作基本一样,如果是 mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT,调用 Fragment#setUserVisibleHint() 方法,否则调用 FragmentTransaction#setMaxLifecycle() 方法,将最大生命周期状态设置为 Lifecycle.State.STARTED ,也就是将 Fragment 的生命周期方法中兴到 onStart(),相关文章: 简书·《Fragment(二)状态改变与管理》CSDN·《Fragment(二)状态改变与管理》

  • setPrimaryItem() 方法

      // FragmentPagerAdapter/FragmentStatePagerAdapter#setPrimaryItem() 方法是一样的
      public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) 
          Fragment fragment = (Fragment)object;
          if (fragment != mCurrentPrimaryItem) 
              if (mCurrentPrimaryItem != null) 
      			 // 当前显示Fragment
                  mCurrentPrimaryItem.setMenuVisibility(false);
                  if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) 
      				// mBehavior 状态为 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
                      if (mCurTransaction == null) 
                          mCurTransaction = mFragmentManager.beginTransaction();
                      
      				// 最大生命周期设置为STARTED,生命周期回退到onPause()
                      mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
                   else 
      				// 可见性设置为false
                      mCurrentPrimaryItem.setUserVisibleHint(false);
                  
              
      		// 将要显示的Fragment
              fragment.setMenuVisibility(true);
              if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) 
      			// mBehavior 状态为 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
                  if (mCurTransaction == null) 
                      mCurTransaction = mFragmentManager.beginTransaction();
                  
      			// 最大生命周期设置为RESUMED,调用 onResume() 方法
                  mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
               else 
      			// 可见性设置为true
                  fragment.setUserVisibleHint(true);
              
    
              mCurrentPrimaryItem = fragment;
          
      
    

    FragmentPagerAdapterFragmentStatePagerAdaptersetPrimaryItem() 方法是一样的,具体的步骤在代码中已有注释,就不在重复。相关文章: 简书·《Fragment(二)状态改变与管理》CSDN·《Fragment(二)状态改变与管理》

结论

  • mBehavior 设置为 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 会通过 FragmentTransaction#setMaxLifecycle() 来修改当前Fragment和将要显示的Fragment的状态,使得只有正在显示的 Fragment 执行到 onResume() 方法,其他 Fragment 只会执行到 onStart() 方法,并且当 Fragment 切换到显示时执行 onResume() 方法,切换到不显示状态时触发 onPause() 方法。

  • mBehavior 设置为 BEHAVIOR_SET_USER_VISIBLE_HINT 时,当 Frament 可见性发生变化时调用 setUserVisibleHint(),但需注意Fragment是否已经加载到容器中。

3. FragmentPagerAdapterFragmentStatePagerAdapter 的差别

FragmentPagerAdapterFragmentStatePagerAdapter 的差别,可以从这两个类的 destroyItem() 方法中查看:

// FragmentPagerAdapter
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) 
    Fragment fragment = (Fragment) object;
	// 关键代码
    mCurTransaction.detach(fragment);
    if (fragment.equals(mCurrentPrimaryItem)) 
        mCurrentPrimaryItem = null;
    


// FragmentStatePagerAdapter
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) 
    Fragment fragment = (Fragment) object;
	// 关键代码
    mCurTransaction.remove(fragment);
    if (fragment.equals(mCurrentPrimaryItem)) 
        mCurrentPrimaryItem = null;
    

从上面的代码我们可以直接的看到区别:

  • FragmentPagerAdapter 调用 FragmentTransaction#detach() 方法,只是销毁了 Fragment 的视图,但是没有移除它。具体生命周期方法过程 => onCreateView() -> onDestroyView()
  • FragmentStatePagerAdapter 调用 FragmentTransaction#remove() 方法,移除了不用的 Fragment,具体生命周期方法过程 => onAttach() -> onDetach()

ViewPager2 中使用 Fragment

ViewPager2 是基于 RecyclerView 实现的

public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) 
    super(context, attrs, defStyleAttr);
    initialize(context, attrs);


 private void initialize(Context context, AttributeSet attrs) 
    mRecyclerView = new RecyclerViewImpl(context);
    attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());

使用 ViewPager2 加载 Fragment 也有自己特殊的 AdapterFragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder>,具体的使用也很简单:

    class Vp2FragmentAdapter(
        activity: FragmentActivity,
        private val fragments: List<Vp2Fragment>
    ) :
        FragmentStateAdapter(activity) 
        override fun getItemCount(): Int 
            return fragments.size
        

        override fun createFragment(position: Int): Fragment 
            return fragments[position]
        
    

重写对应方法,返回总的大小和每个 Fragment

Fragment 怎样加载到 ViewPage2

因为 FragmentStateAdapter 继承 RecyclerView.Adapter,所以我们主要看其中的 onCreateViewHolder()onBindViewHolder() 方法:

FragmentStateAdapteronCreateViewHolder() 方法

// FragmentStateAdapter#onCreateViewHolder()
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) 
    return FragmentViewHolder.create(parent);

返回一个 FragmentViewHolder 对象,创建过程如下:

public final class FragmentViewHolder extends ViewHolder 
    private FragmentViewHolder(@NonNull FrameLayout container) 
        super(container);
    

    @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) 
        FrameLayout container = new FrameLayout(parent.getContext());
        container.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT));
        container.setId(ViewCompat.generateViewId());
        container.setSaveEnabled(false);
        return new FragmentViewHolder(container);
    

    @NonNull FrameLayout getContainer() 
        return (FrameLayout) itemView;
    

继承 RecyclerView.ViewHolder ,内部创建一个 FrameLayout 作为每一个 item 的根布局,也就是我们最终的包含关系如下 RecyclerView -> FrameLayout -> Fragment。这个方法比较简单,我们接着看下一个方法(onBindViewHolder()

FragmentStateAdapteronBindViewHolder() 方法

public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) 
	// 1. 内部调用抽象方法 createFragment() 创建Fragment,需要子类实现
    ensureFragment(position);

	// 2. 获取Item的根布局 FrameLayout ,增加布局完成监听
    final FrameLayout container = holder.getContainer();
    if (ViewCompat.isAttachedToWindow(container)) 
        container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() 
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                    int oldLeft, int oldTop, int oldRight, int oldBottom) 
                if (container.getParent() != null) 
                    container.removeOnLayoutChangeListener(this);
					// 调用方法
                    placeFragmentInViewHolder(holder);
                
            
        );
    

	// 3. 需要从RecyclerView上移除的Fragment
    gcFragments();

  • 1.调用 FragmentStateAdapter#ensureFragment() 方法

      // FragmentStateAdapter#ensureFragment()
      private void ensureFragment(int position) 
          long itemId = getItemId(position);
          if (!mFragments.containsKey(itemId)) 
              Fragment newFragment = createFragment(position);
              newFragment.setInitialSavedState(mSavedStates.get(itemId));
              mFragments.put(itemId, newFragment);
          
      
    
      public abstract @NonNull Fragment createFragment(int position);
    
  • 2.获取Item的根布局 FrameLayout ,增加布局完成监听,调用 placeFragmentInViewHolder() 方法

      void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) 
          Fragment fragment = mFragments.get(holder.getItemId());
          FrameLayout container = holder.getContainer();
          View view = fragment.getView();
    
      	// Fragment已经add状态,直接将Fragment的View增加到Item上的FrameLayout上显示
          if (fragment.isAdded() && view.getParent() != null) 
              if (view.getParent() != container) 
                  addViewToContainer(view, container);
              
              return;
          
          if (fragment.isAdded()) 
              addViewToContainer(view, container);
              return;
          
    
      	// 如果Fragment不是 add 状态,那么就通过 FragmentManager 添加 Fragment,设置最大生命周期值为 STARTED
          if (!shouldDelayFragmentTransactions()) 
              scheduleViewAttach(fragment, container);
              mFragmentManager.beginTransaction()
                      .add(fragment, "f" + holder.getItemId())
                      .setMaxLifecycle(fragment, STARTED)
                      .commitNow();
              mFragmentMaxLifecycleEnforcer.updateFragmentMaxLifecycle(false);
           else 
      		// 延迟递归调用
              if (mFragmentManager.isDestroyed()) 
                  return; // nothing we can do
              
              mLifecycle.addObserver(new LifecycleEventObserver() 
                  @Override
                  public void onStateChanged(@NonNull LifecycleOwner source,
                          @NonNull Lifecycle.Event event) 
                      if (shouldDelayFragmentTransactions()) 
                          return;
                      
                      source.getLifecycle().removeObserver(this);
                      if (ViewCompat.isAttachedToWindow(holder.getContainer())) 
      					// 延迟递归调用
                          placeFragmentInViewHolder(holder);
                      
                  
              );
          
      
    

    通过以上两步,我们的Fragment就已经加载和显示到 ViewPager2 上了。

  • 3.需要从RecyclerView上移除的Fragment -> gcFragments()

      void gcFragments() 
      	// 查询需要移除的 `Fragment`
          Set<Long> toRemove = new ArraySet<>();
          for (int ix = 0; ix < mFragments.size(); ix++) 
              long itemId = mFragments.keyAt(ix);
              if (!containsItem(itemId)) 
                  toRemove.add(itemId);
                  mItemIdToViewHolder.remove(itemId); // in case they're still bound
              
          
          if (!mIsInGracePeriod) 
              mHasStaleFragments = false; // we've executed all GC checks
    
              for (int ix = 0; ix < mFragments.size(); ix++) 
                  long itemId = mFragments.keyAt(ix);
                  if (!isFragmentViewBound(itemId)) 
                      toRemove.add(itemId);
                  
              
          
    
      	// 遍历移除Fragment
          for (Long itemId : toRemove) 
      		// 调用方法,根据id移除对应的Fragment
              removeFragment(itemId);
          
      
    
      private void removeFragment(long itemId) 
          Fragment fragment = mFragments.get(itemId);
      	// 移除Fragment
          mFragmentManager.beginTransaction().remove(fragment).commitNow();
      	// 从缓存数据集中移除
          mFragments.remove(itemId);
      
    

FragmentViewPage2上用户可见性

我们查看源码,发现回调方法 onAttachedToRecyclerView() 中有相关的代码:

@CallSuper
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) 
    checkArgument(mFragmentMaxLifecycleEnforcer == null);
    mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
    mFragmentMaxLifecycleEnforcer.register(recyclerView);

接着看 FragmentMaxLifecycleEnforcer#register() 的实现:

void register(@NonNull RecyclerView recyclerView) 
        mViewPager = inferViewPager(recyclerView);
		// 页面改变监听,回调updateFragmentMaxLifecycle()方法
        mPageChangeCallback = new ViewPager2.OnPageChangeCallback() 
            @Override
            public void onPageScrollStateChanged(int state) 
                updateFragmentMaxLifecycle(false);
            

            @Override
            public void onPageSelected(int position) 
                updateFragmentMaxLifecycle(false);
            
        ;
        mViewPager.registerOnPageChangeCallback(mPageChangeCallback);
    

继续查看 updateFragmentMaxLifecycle() 方法:

void updateFragmentMaxLifecycle(boolean dataSetChanged) 

        mPrimaryItemId = currentItemId;
        FragmentTransaction transaction = mFragmentManager.beginTransaction();

        Fragment toResume = null;
		// 遍历所有缓存的Fragment
        for (int ix = 0; ix < mFragments.size(); ix++) 
            long itemId = mFragments.keyAt(ix);
            Fragment fragment = mFragments.valueAt(ix);
            if (!fragment.isAdded()) 
                continue;
            

			// 判断是否Fragment是滑出隐藏还是滑入显示
            if (itemId != mPrimaryItemId) 
				// 滑出的设置最大生命周期为 STARTED(由 RESUMED 变为 STARTED),也就是回调 onPause() 方法
                transaction.setMaxLifecycle(fragment, STARTED);
             else 
				// 需要显示的 Fragment
                toResume = fragment; // itemId map key, so only one can match the predicate
            

            fragment.setMenuVisibility(itemId == mPrimaryItemId);
        
		// 如果有需要显示的,将其生命周期状态设置为 RESUMED,调用 onResume() 方法
        if (toResume != null)  // in case the Fragment wasn't added yet
            transaction.setMaxLifecycle(toResume, RESUMED);
        

		// 提交修改
        if (!transaction.isEmpty()) 
            transaction.commitNow();
        
    

通过以上代码,我们知道了,在 ViewPager2 中使用 Fragment ,它的生命周期方法是正常的,只有正在显示的 Fragment 执行到 onResume() 方法,其他 Fragment 只会执行到 onStart() 方法,并且当 Fragment 切换到显示时执行 onResume() 方法,切换到不显示状态时触发 onPause() 方法。

以上是关于FragmentViewPager中使用Fragment的主要内容,如果未能解决你的问题,请参考以下文章

FragmentViewPager中使用Fragment

Fragment中TabLayout加FragmentViewPager

FragmentViewpager与Fragment实现懒加载

侦听DialogFragment从ViewPager Fragment中删除事件

Fragment

如何在两个不同的片段中使用存储库数据