Fragment 页面切换状态监控

原创
2018/03/22 17:49
阅读数 8.8K

由于使用不通的事务方法,场景也是不通的,这里我们重点讨论show/hide与attach/dettach两类问题。当然,我们绕不开的是add/remove和replace。

一、replace事务

replace相对简单,对应的是Fragment最简单的生命周期,因此页面的切换在onResume中即可。

二、add事务

实际上add和remove虽然是【添加】和【移除】,但是实际上这俩个事务很少同时使用。常见的使用情况反而是attach/dettach+add和show/hide+add事务的组合相对常见。本质上,add+remove的事务组合和replace类似,因此也没有必要去remove。

单独的add事务无法实现页面切换,这里我们主要说明attach/detach+add和show/hide+add。

2.1 attach/detach+add事务组合

参考android.support.v4.app.FragmentPagerAdapter源码:

public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
            + " v=" + ((Fragment)object).getView());
    mCurTransaction.detach((Fragment)object);
}

@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;
    }
}

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

这种场景下,使用add只是简单的添加,attach和dettach负责页面的切换。Fragment在ViewPager中一般是提前重建的,因此,传统的Fragment生命周期已经不适合,这里我们看到setUserVisibleHint被调用,因此,我们可以使用,setUserVisibleHint机制。



@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
    if(getUserVisibleHint()) {
        onVisible();
     } else {
        onInvisible();
     }
}

protected void onVisible(){
      
}
protected void onInvisible(){

}

但是这里有个问题,在初始化方法instantiateItem中,我们如果在没有判断的情况下,强行setUserVisibleHint,可能在onCreate之前执行,造成生命周期混乱。

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

实际上在setPrimaryItem比较合理,因为提前在ViewPager提前创建了Fragment并且调用了onAttach->onCreate。对于当前的问题,我们的解决方法通过flag去控制。

protected boolean isCreated = false;
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    isCreated = true;
}

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
         if(!isCreated) return;
        if(getUserVisibleHint()) {
            onShow();
        } else {
            onHide();
        }
}

protected void onShow(){
      
}
protected void onHide(){

}
 

 

此外,如果不仅仅对tab实现控制,还要实现打开/退出新的activity时UI的监控,我们在上述代码的基础上实现如下方法即可

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

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

2.1 show/hide+add事务组合

这类主要运用于FragmentManager自行管理的页面

   public Fragment showFragment(FragmentManager fragmentManager,int position, Bundle bundle) {
       
            FragmentTransaction mCurTransaction = fragmentManager.beginTransaction();
        

        try {

            String name = makeFragmentName(mViewContainer.getId(), position);
            Fragment fragment = mFragmentManager.findFragmentByTag(name);
            if (fragment == null) {
                fragment = instantiateItem(position);
                fragment.setUserVisibleHint(false);
            }

            if (mCurrentPrimaryItem != fragment) {
                if (mCurrentPrimaryItem != null) {
                    mCurTransaction.hide(mCurrentPrimaryItem);
                    mCurrentPrimaryItem.setUserVisibleHint(false);
                }
                if (fragment.isAdded()) {
                    mCurTransaction.show(fragment);
                } else if(fragment.getFragmentManager()!=null){
                    mCurTransaction.show(fragment);
                }else {
                    mCurTransaction.add(viewId, fragment, makeFragmentName(mViewContainer.getId(), position));
                }
                mCurrentPrimaryItem = fragment;
            }

            if(bundle!=null)
                mCurrentPrimaryItem.setArguments(bundle);

            if (!mCurrentPrimaryItem.getUserVisibleHint()) {
                mCurrentPrimaryItem.setUserVisibleHint(true);
            }

            mCurTransaction.commit();
            mCurTransaction = null;
            return fragment;

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

同样,对于这类生命周期,我们也可以参考attach/detach+add的处理方式,但是我们这里存在更好的方法,而且配合onResume一起调用,那就是onHiddenChanged,他的优点是在add时不会调用,在hide/show时才会调用。因此,在fragment之间切换时会调用onHiddenChanged,在Activity之间切换时调用onResume,这种更新更好解决问题,而且可以肯定的是,onHiddenChanged在执行过一次onResume之后才会被调用

@override
public void onResume(){
   if( getUserVisibleHint()){ 
//onResume在Fragment不可见时也会调用,为了防止此情况发生,需要做判断
       onShow();
   }

}
@override
public void onStop(){  //onstop被调用,Fragment页面必然隐藏
   onHide();
}
@override
public void onHiddenChanged(boolean hidd) {
  if (hidd) {
      //隐藏时所作的事情
      onHide();
   } else {
      //显示时所作的事情
      onShow();
   }

}
protected void onShow(){
      
}
protected void onHide(){

}

三、FragmentTabHost事务

FragmentTabHost相对来说没有就没有那么简单了,内部通过了attach/detach+add的事务模式,对于这种更新,我们只能通过手动方式来实现了。

当然,我们最好定义一个接口,这里不再赘述了。

 

四、常见问题

@Fragment已经被加载的状态异常,在一个FragmentManager中,同一个Fragment的实例只允许被add一次。那么。这个问题是怎么产生的呢?

java.lang.IllegalStateException: Fragment already added: Fragment

答:FragmentManager添加Fragment的过程中使用了事务,而在commit的时候讲add任务放到了队列中,由于任何队列具有阻塞和延时问题,因此可能导致消息执行过程虽然同步,但也产生了延时。

BackStackRecord.java源码如下

    int commitInternal(boolean allowStateLoss) {
        if (mCommitted) throw new IllegalStateException("commit already called");
        if (FragmentManagerImpl.DEBUG) {
            Log.v(TAG, "Commit: " + this);
            LogWriter logw = new LogWriter(TAG);
            PrintWriter pw = new PrintWriter(logw);
            dump("  ", null, pw, null);
        }
        mCommitted = true;
        if (mAddToBackStack) {
            mIndex = mManager.allocBackStackIndex(this);
        } else {
            mIndex = -1;
        }
        mManager.enqueueAction(this, allowStateLoss);
        return mIndex;
    }

FragmentManager.java源码

    public void enqueueAction(Runnable action, boolean allowStateLoss) {
        if (!allowStateLoss) {
            checkStateLoss();
        }
        synchronized (this) {
            if (mDestroyed || mHost == null) {
                throw new IllegalStateException("Activity has been destroyed");
            }
            if (mPendingActions == null) {
                mPendingActions = new ArrayList<Runnable>();
            }
            mPendingActions.add(action);
            if (mPendingActions.size() == 1) {
                mHost.getHandler().removeCallbacks(mExecCommit);
                mHost.getHandler().post(mExecCommit);
            }
        }
    }

解决方案:我们不能通过Fragment.isAdd()或者FragmentManager.findFragmentByTag(tag)去判断是否已经add,因此,我们可以利用FragmentTransaction,当add之后,Fragment会自动引用到FragmentManager或者tag,因此,我们add之前也可以通过 Fragment.getTag()来判断是否已经处于add状态了。

②FragmentTabHost+ViewPager组合是否合理

答:此组合是误用,FragmentTabHost并不推荐ViewPager一起使用。首先是FragmentTabHost可以自动添加Fragment到布局上,并且有自己管理Fragment生命周期的能力,但是ViewPager只能通过PagerAdapter添加页面,某种意义上,FragmentTabHost的作用被削弱了,同时做了无用功,因此非常消耗性能。

正确方案:

<1>. FragmentTabHost + 【TabWidget,可选,如果不添加时会自动内部生产】 +Fragment + 【Layout,可选,如果不添加时会自动内部生产】

<2>. TabWidget + ViewPager +Fragment + Layout

@FragmentId和布局containerId的区别

答案:大多数情况下Fragmentid是两者只相同

 

 

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部