FragmentPagerAdapter 页面类型、数量、内容更新问题

场景

存在一种需求,当用户系统中,属于某一组织的用户登录之后(或者账户切换),要求主页面显示不同的ViewPager + Fragment组合,并且要求app无需退出就能刷新组合以及组合中的页面。

此外,为了保证Fragment和Fragment中View不必要的inflate和渲染,要求尽可能重用已存在的Fragment和View。显然FragmentPagerAdapter是首选。但是存在三个问题:

1、FragmentPagerAdapter默认无法更新,需要重写getItemPosition,返回值为PagerAdapter.POSITION_NONE才可以更新

2、重用的Fragment设置参数无法重新初始化

3、重用的Fragment类型和新的Fragment类型存在不匹配问题,如旧的UserFragment页面,但是新的要求是ListFragment,所以类型存在问题。

 

解决方案

我们需要重写FragmentPagerAdapter,但问题是存在各种不方便的因素,因此,我们需要自定义FragmentPagerAdapter。

public abstract class CustomFragmentPagerAdapter extends PagerAdapter {
    private static final String TAG = "FragmentPagerAdapter";
    private static final boolean DEBUG = false;

    private final FragmentManager mFragmentManager;
    private FragmentTransaction mCurTransaction = null;
    private Fragment mCurrentPrimaryItem = null;

    private final LongSparseArray<String> fragmentViewTypeManager = new LongSparseArray<String>();

    public CustomFragmentPagerAdapter(FragmentManager fm) {
        mFragmentManager = fm;
    }

  

    @Override
    public void startUpdate(ViewGroup container) {
        if (container.getId() == View.NO_ID) {  //viewPager必须赋值ID,否则无法添加fragment
            throw new IllegalStateException("ViewPager with adapter " + this
                    + " requires a view id");
        }
    }

    @SuppressWarnings("ReferenceEquality")
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        mCurTransaction = beginTransaction();
        final long itemId = getItemId(position);

         final int count = this.getFragmentTypeCount();
        int fragmentType = 0;
        if(count>0){
            fragmentType = getItemFragmentType(position);
        }
        if(fragmentType>0 && fragmentType>=count){
            throw  new IllegalArgumentException("{fragmentType's number >= fragmentTypeCount's number} is illegal");
        }

        // 生成tag,用于保存和标记每个位置的fragment
        final String name = makeFragmentName(container.getId(), itemId); //生成tag
        final String oldFragmentName = fragmentViewTypeManager.get(fragmentType);

        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            final String fragmentClassName = fragment.getClass().getName();
            if(!fragmentClassName.equals(oldFragmentName)) {  
                //如果发现新旧类型不一致,移除旧类型
                if (DEBUG) Log.v(TAG, "Removeing item #" + itemId + ": f=" + fragment);
                mCurTransaction.remove(fragment);
                //获取新类型
                fragment = getItem(null,position);
                if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
                mCurTransaction.add(container.getId(), fragment,name);
            }else {

                Fragment newFragment = getItem(fragment,position);  
               //获取newFragment ,如果2次fragment不一致,移除旧的fragment
                if(newFragment!=fragment){
                    if (DEBUG) Log.v(TAG, "Removeing item #" + itemId + ": f=" + fragment);
                    mCurTransaction.remove(fragment);
                    fragment = newFragment;
                    if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
                    mCurTransaction.add(container.getId(), fragment,name);
                }else {
                   //如果获取到fragment与原来的是同一个,attach即可
                    if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
                    mCurTransaction.attach(fragment);
                }
            }
        } else {
            fragment = getItem(fragment,position);
            if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
            mCurTransaction.add(container.getId(), fragment,name);
        }

         if(fragment!=null){  
           //保存类型,用来校验缓存的正确性
            fragmentViewTypeManager.put(fragmentType,fragment.getClass().getName());
        }

        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {

        mCurTransaction = beginTransaction();
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        mCurTransaction.detach((Fragment)object);  //dattach fragment
    }

    @SuppressWarnings("ReferenceEquality")
    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
                fragment.setUserVisibleHint(true);
            }
            mCurrentPrimaryItem = fragment;  //设置当前的fragment
        }
    }

    @Override
    public void finishUpdate(ViewGroup container) {
        if (mCurTransaction != null) {
            mCurTransaction.commitNowAllowingStateLoss();
          //提交,注意该方法将任务加入到mainLooper中,可能产生延迟
            mCurTransaction = null;
        }
    }

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

    @Override
    public Parcelable saveState() {
        return null;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
    }

    /**
     *
     *  获取每个fragment的id,注意保证唯一性
     */
    public long getItemId(int position) {
        return position;
    }
   //生成tag
    public static String makeFragmentName(int viewId, long id) {
        return "android:switcher:" + viewId + ":" + id;
    }

    public  FragmentTransaction beginTransaction(){
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        return  mCurTransaction;
    }
    public FragmentManager getFragmentManager(){
        return mFragmentManager;
    }

  /**
     * 获取当前位置的fragment
     */
    public abstract Fragment getItem(Fragment contentFragment,int position);
   /**
    *  获取当前位置的type  FragmentType
    */
    public abstract int getItemFragmentType(int position);
   /**
    *  获取当前类型的数量 FragmentCount
    */
    public abstract int getFragmentTypeCount();
   /**
    *  在ViewPager中调用,告诉ViewPager该位置的Fragment是可以被替换和更新的
    * 这里人可以继续优化,由于ViewPager.LayoutParams中的position是非public的,因此要优化可以在该类的基类中完成
    */
   @Override
    public int getItemPosition(Object object) {
        if(!(object instanceOf Fragment))
        {
         return PagerAdapter.POSITION_NONE;
       }       
         return PagerAdapter.POSITION_UNCHANGED;
     
    }

    public  boolean isEmpty(){
        return  getCount()==0;
    }
}

到这里,我们便可以实现他的子类

 

static  class MyPagerAdapter extends CustomFragmentPagerAdapter{

        private ArrayList<FragmentTabEntity> dataEntities;
        private final  String TAG_NAME = "MyPagerAdapter ";

        public MyPagerAdapter(FragmentManager fm,List<FragmentTabEntity> dataEntities) {
            super(fm);
            this.dataEntities = new ArrayList<>();
            this.dataEntities.addAll(dataEntities);
        }
        @SuppressWarnings("unchecked")
        public void updateDataEntities(List<FragmentTabEntity> dataEntities) {
            this.dataEntities.clear();
            if(dataEntities!=null && dataEntities.size()>0){
                this.dataEntities.addAll(dataEntities);
            }
            this.notifyDataSetChanged();
        }


        @Override
        public CharSequence getPageTitle(int position) {
            final FragmentTabEntity entity = dataEntities.get(position);
            return entity.getTitle();
        }


        @Override
        public int getItemFragmentType(int position) {
            final FragmentTabEntity  dataEntity = dataEntities.get(position);
            return dataEntity.getType(position);  //获取类型,注意,最大值不能大于getFragmentTypeCount()
        }

        @Override
        public int getFragmentTypeCount() {
           
            return FragmentTabEntity.getTotalTypeCount(); 
          //获取所有fragment的类型数量,一般是固定值,主要看程序实现方式了
        }

        @Override
        public Fragment getItem(Fragment contentFragment,int position) {

            final int fragmentType = getItemFragmentType(position);
            final FragmentTabEntity dataEntity = dataEntities.get(position);

            BaseFragment fragment = null;
            if(contentFragment==null) {
                if (fragmentType == 0) {
                    fragment = new IndexFragment();
                  } else  if(fragmentType ==1){
                     fragment = new UserFragment();
                 } else  if(fragmentType ==2){
                      fragment = new WebFragment();
                 }else if(fragmentType ==3){
                    fragment = new ListFragment();
                 }
            }else{
                fragment = (BaseFragment) contentFragment;
            }
            fragment.setPosition(position);

            if(fragment!=null) {
                Bundle fb = new Bundle();
                fb.putString(BaseFragment.KEY_TYPE, dataEntity.getType());
                fb.putString(BaseFragment.KEY_TITLE, dataEntity.getTitle());
                fragment.setNoneStateArguments(fb); //使用非状态参数传递方法
            }
            return fragment;
        }

        @Override
        public int getCount() {
            return dataEntities.size();
        }

    }

使用方法

private MyPagerAdapter  mTabPagerAdapter;


public  void updatePager(List<FragmentTabEntity> data)
{

 if((pager.getAdapter() instanceof MyPagerAdapter)){
      mTabPagerAdapter = (MyPagerAdapter) pager.getAdapter();
 }
 if(mTabPagerAdapter==null){
       mTabPagerAdapter = new MyPagerAdapter(getChildFragmentManager(),data);
       pager.setAdapter(mTabPagerAdapter);

 }else{
       mTabPagerAdapter.updateDataEntities(data);
 }
}

 

Fragment ViewCache问题 & 生命周期问题

到这一步事实上我们的自定义FragmentPagerAdapter已经完成了,但是这里还存在不完美的问题,那就是Fragment中添加了View Cache的情况,此外,对于生命周期的控制,可能或多或少出现旧页面向新页面过渡时闪烁问题。

 

1、View Cache 问题

先来看看这种Fragment的定义方式

public class BaseFragment extends Fragment{

  
private SoftReference<View> mRootViewCache = null; //实现viewcache
private boolean isFinishedInflated = false;
private Bundle mNoneStateArguments;
private int position = -1;

public void setPosition(int position){
  this.position = position;
}
public int getPosition(){
   return  this.position;
}

  @Nullable
    @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        View root = null;
        if(!cacheIsEmpty()){
            root = mRootViewCache.get();
        }
        if(root==null){
            root = inflater.inflate(R.layout.base_view_layout, container, false);
            root.findViewById(R.id.toolbar).setVisibility(View.GONE);
            mRootViewCache = new SoftReference<View>(root);
        }
        return root;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        try {
            isFinishedInflated = true;
            renderFragmentView(view);

        } catch (Exception e) {
            e.printStackTrace();
           
        }
    }

private boolean cacheIsEmpty(){
        return mRootViewCache==null || mRootViewCache.get()==null;
    }

@Override
public void onResume() {
        super.onResume();
        if(getUserVisibleHint() ){
            onFragmetShow();
        }
    }

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if(!isFinishedInflated) return;
        if( getUserVisibleHint()){
            onFragmetShow();
        }else if(isResumed()){
            onFragmetHide();
        }
}

    @Override
public void onStop() {
        super.onStop();
        if(getUserVisibleHint()){
            onFragmetHide();
        }
    }

   public void  onFragmetShow(){

      if(getView()==null) return ;
     getView().post(new Runnable(){  
         public void run(){
            //这里可以用来获取Fragment的参数,然后更新
        }
     });
      
   }

   public void  onFragmetHide(){


   }
 
   public void setNoneStateArguments(Bundle bundle){  //解决已初始化状态的参数刷新问题
       this.mNoneStateArguments = bundle;
   }

  public void getNoneStateArguments(){

   return  this.mNoneStateArguments!=null? this.mNoneStateArguments:new Bundle();
 }

}

 

对于原生页面,新旧页面闪烁并不是很明显,但是对于Webview页面,这种闪烁很明显,导致该问题的原因是View Cache,因此,我们需要在Fragment中添加clearView方法来清空一下Cache

 public void clearView() {
        
        if(mRootViewCache!=null){
            mRootViewCache.clear();
        }
    }

 

在MyPagerAdapter的getItem方法中,我们有必要植入一个flag


        @Override
        public Fragment getItem(Fragment contentFragment,int position) {

            final int fragmentType = getItemFragmentType(position);
            final FragmentTabEntity dataEntity = dataEntities.get(position);

            BaseFragment fragment = null;
            if(contentFragment==null) {
                if (viewType == 0) {
                    fragment = new IndexFragment();
                  } else  if(fragmentType ==1){
                     fragment = new UserFragment();
                 } else  if(fragmentType ==2){
                      fragment = new WebFragment();
                 }else if(fragmentType ==3){

                    fragment = new ListFragment();
                }
            }else{
                fragment = (BaseFragment) contentFragment;
            }

            final Bundle fa = fragment.getArguments();
            if(fa!=null) {
                final String oldTag = fa.getString("md5", "");
                if (!TextUtils.isEmpty(oldTag) && !oldUrl.oldTag(dataEntity.getMd5())) {
                    fragment.clearView();  //如果tag不一致,清空一下view cache
                }
            }

            if(fragment!=null) {
                Bundle fb = new Bundle();
                  
                fb.putString("md5",dataEntity.getMd5());   //植入新的md5 tag    
         
                fb.putString(BaseFragment.KEY_TYPE, dataEntity.getType());
                fb.putString(BaseFragment.KEY_TITLE, dataEntity.getTitle());
                fragment.setArguments(fb);
            }
            return fragment;
        }

 

2、生命周期问题

关于onFragmentShow与onFragmentHide的生命周期用法,请参考《Fragment页面切换》,这里我们主要说一下mainLooper问题

  public void  onFragmetShow(){

      if(getView()==null) return ;
     getView().post(new Runnable(){  
         public void run(){
            //这里可以用来获取Fragment的参数,然后更新
        }
     });
      
   }

如果要更新UI,我们建议这里使用post将消息发送到mainLooper,为什么要这样呢?

主要原因是FragmentPagerAdapter的finishUpdate中使用了commit方法,这个方法是将任务发送到mainLooper的队列中,而不是立即执行。

    @Override
    public void finishUpdate(ViewGroup container) {
        if (mCurTransaction != null) {
            mCurTransaction.commitNowAllowingStateLoss();
            mCurTransaction = null;
        }
    }

基于队列的先进先出,FragmentTransaction将更新消息加入到Fragment add/attach消息之后,我们如果直接获取argument可能出现数据不一致的问题,因此我们需要将我们的方法作为任务同样放入到mainLooper中。如果不这么做,可能导致获取到的argument是旧的,导致我们更新时使用了旧的参数。当然,可以参考《Android Fragment重复添加问题解决方法》,原理基本相同。

 

以上是一般常见的问题,至于其他问题,可以留言。

    原文作者:移动开发
    原文地址: https://my.oschina.net/ososchina/blog/3004180
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞