场景
存在一种需求,当用户系统中,属于某一组织的用户登录之后(或者账户切换),要求主页面显示不同的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重复添加问题解决方法》,原理基本相同。
以上是一般常见的问题,至于其他问题,可以留言。