美文网首页程序员
正确实现ViewPager+多Fragment组合方式展示UI

正确实现ViewPager+多Fragment组合方式展示UI

作者: 海_3efc | 来源:发表于2020-04-27 12:12 被阅读0次

1.简介

在项目中有时会使用ViewPager展示页面,在页面较少时很简单直接使用FragmentPagerAdapter即可满足(非本文重点略过)。但是如果有很多几十甚至几百时,如果还像原来那么使用,就会出现界面卡,内存占用高,甚至会出现OOM问题(不信的话自己可以尝试下)。那么要如何才能高效、低耗内存的实现呢?下面就重点来讲讲。

2.正确使用

2.1 准备

重写系统FragmentStatePagerAdapter类,为什么要重新呢?因为系统源码中缓存采用的是ArrayList进行数据缓存,当Frgment数量过大时会出现错误,所以这里进行了重写,采用LongSparseArray对Fragment和SaveState进行缓存,代码如下:

package com.zhh.commonview.adapter;

import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.util.LongSparseArray;
import android.support.v4.view.PagerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by zhanghai on 2020/3/9.
 *
 * function:
 * ......
 * 由于篇幅注释忽略,参考系统的FragmentStatePagerAdapter注释
 */
public abstract class BaseFragmentStatePagerAdapter extends PagerAdapter {
    private static final String TAG = "FragmentPagerAdapter";

    @NonNull
    private final FragmentManager mFragmentManager;
    @Nullable
    private FragmentTransaction mCurTransaction = null;
    @Nullable
    private Fragment mCurrentPrimaryItem = null;

    @NonNull
    private final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
    @NonNull
    private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>();

    public BaseFragmentStatePagerAdapter(@NonNull FragmentManager fm) {
        mFragmentManager = fm;
    }

    /**
     * Clear cache data.
     */
    public void clear(){
        if(mFragments != null && mFragments.size() > 0){
            mFragments.clear();
        }
        if(mSavedStates != null && mSavedStates.size() > 0){
            mSavedStates.clear();
        }
    }

    /**
     * Return the Fragment associated with a specified position.
     */
    public abstract Fragment getItem(int position);

    /**
     * 获取指定position的fragment
     * @return
     */
    public Fragment getCurrentPrimaryItem(int position){
        long tag = getItemId(position);
        return mFragments.get(tag);
    }

    @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
    @NonNull
    public Object instantiateItem(ViewGroup container, int position) {
        long tag = getItemId(position);
        Fragment fragment = mFragments.get(tag);
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (fragment != null) {
            return fragment;
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        fragment = getItem(position);
        // restore state
        final Fragment.SavedState savedState = mSavedStates.get(tag);
        if (savedState != null) {
            fragment.setInitialSavedState(savedState);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.put(tag, fragment);
        mCurTransaction.add(container.getId(), fragment, "f" + tag);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;
        int currentPosition = getItemPosition(fragment);

        int index = mFragments.indexOfValue(fragment);
        long fragmentKey = -1;
        if (index != -1) {
            fragmentKey = mFragments.keyAt(index);
            mFragments.removeAt(index);
        }

        //item hasn't been removed
        if (fragment.isAdded() && currentPosition != POSITION_NONE) {
            mSavedStates.put(fragmentKey, mFragmentManager.saveFragmentInstanceState(fragment));
        } else {
            mSavedStates.remove(fragmentKey);
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        mCurTransaction.remove(fragment);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, @Nullable 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;
        }
    }

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

    @Override
    public Parcelable saveState() {
        Bundle state = null;
        if (mSavedStates.size() > 0) {
            // save Fragment states
            state = new Bundle();
            long[] stateIds = new long[mSavedStates.size()];
            for (int i = 0; i < mSavedStates.size(); i++) {
                Fragment.SavedState entry = mSavedStates.valueAt(i);
                stateIds[i] = mSavedStates.keyAt(i);
                state.putParcelable(Long.toString(stateIds[i]), entry);
            }
            state.putLongArray("states", stateIds);
        }
        for (int i = 0; i < mFragments.size(); i++) {
            Fragment f = mFragments.valueAt(i);
            if (f != null && f.isAdded()) {
                if (state == null) {
                    state = new Bundle();
                }
                String key = "f" + mFragments.keyAt(i);
                mFragmentManager.putFragment(state, key, f);
            }
        }
        return state;
    }

    @Override
    public void restoreState(@Nullable Parcelable state, ClassLoader loader) {
        if (state != null) {
            Bundle bundle = (Bundle) state;
            bundle.setClassLoader(loader);
            long[] fss = bundle.getLongArray("states");
            mSavedStates.clear();
            mFragments.clear();
            if (fss != null) {
                for (long fs : fss) {
                    mSavedStates.put(fs, (Fragment.SavedState) bundle.getParcelable(Long.toString(fs)));
                }
            }
            Iterable<String> keys = bundle.keySet();
            for (String key : keys) {
                if (key.startsWith("f")) {
                    Fragment f = mFragmentManager.getFragment(bundle, key);
                    if (f != null) {
                        f.setMenuVisibility(false);
                        mFragments.put(Long.parseLong(key.substring(1)), f);
                    } else {
                        Log.w(TAG, "Bad fragment at key " + key);
                    }
                }
            }
        }
    }

    /**
     * Return a unique identifier for the item at the given position.
     * <p>
     * <p>The default implementation returns the given position.
     * Subclasses should override this method if the positions of items can change.</p>
     *
     * @param position Position within this adapter
     * @return Unique identifier for the item at position
     */
    public long getItemId(int position) {
        return position;
    }
}

逻辑基本跟系统的FragmentStatePagerAdapter逻辑一致。新建一个adapter类继承该Adapter,并重写下方法:

@Override
    public int getCount() {
        return mItems == null ? 0 : mItems.size();
    }

    @Override
    public long getItemId(int position) {
        return mItems == null || mItems.size() <=0 ? super.getItemId(position) : mItems.get(position).hashCode();
    }

    @Override
    public int getItemPosition(Object object) {
        if(object instanceof BaseMoreFragmentAdv){
            //TODO:这里是重点
            BaseMoreFragmentAdv item = (BaseMoreFragmentAdv) object;
            int itemValue = item.getFragmentIdentifier();
            for (int i = 0; i < mItems.size(); i++) {
                if (mItems.get(i).hashCode() == itemValue) {
                    return i;
                }
            }
        }
        return POSITION_NONE;
    } 

其中关键点是getItemPosition方法,这里通过判断标记唯一性去检查是否新new一个fragment。item.getFragmentIdentifier()是在Fragment中实现的一个方法,该方法是返回Fragment的唯一标识,主要可通过传入的数据进行绑定,我这里是通过重写类的hashCode方法与之绑定,确保唯一性。

2.2 Adapter使用

通过数据去创建Fragment,并且不自己进行维护一个List去存储Fragment。

private void initPagers() {
        final List<ObjectBean> list = new ArrayList<>();
        for (int i = 0;i < 200;i++){
            list.add(new ObjectBean(String.format(Locale.CHINESE,"fragment_%d",i)));
        }
        CommonViewPagerFragmentStateAdapter adapter = new CommonViewPagerFragmentStateAdapter<ObjectBean>(getSupportFragmentManager(),list) {
            @Override
            public Fragment getItem(final int position) {
                ObjectBean bean = list.get(position);
                TestFragment f = new TestFragment();
                Bundle bundle = new Bundle();
                bundle.putInt("identifier",bean.hashCode());
                bundle.putString("index",bean.index);
                f.setArguments(bundle);
                return f;
            }
        };

        vp_pager.setAdapter(adapter);
    }

代码很简单,很容易理解。
针对Fragment多的情况下:
请勿先创建Fragment保存到List中,然后把该list传给Adapter。
请勿先创建Fragment保存到List中,然后把该list传给Adapter。
请勿先创建Fragment保存到List中,然后把该list传给Adapter。
错误例子:

List<TestFragment> fragments = new ArrayList<>();
        for (int i = 0;i < 200;i++){
            TestFragment f = new TestFragment();
            String index = String.format(Locale.CHINESE,"fragment_%d",i);
            Bundle bundle = new Bundle();
            bundle.putInt("identifier",index.hashCode());
            bundle.putString("index",index);
            f.setArguments(bundle);
            fragments.add(f);
        }
CommonViewPagerFragmentStateAdapter adapter = new CommonViewPagerFragmentStateAdapter<ObjectBean>(getSupportFragmentManager(),fragments);

至于原因,我想源码中注释的一句话最能说明。如下:

* <p>This version of the pager is more useful when there are a large number
 * of pages, working more like a list view.  When pages are not visible to
 * the user, their entire fragment may be destroyed, only keeping the saved
 * state of that fragment.  This allows the pager to hold on to much less
 * memory associated with each visited page as compared to
 * {@link FragmentPagerAdapter} at the cost of potentially more overhead when
 * switching between pages.

大致意思是说,当有大量页数时它会跟有用,类似列表。当页面对用户不可见时,整个fragment会被销毁,仅保留Fragment对应的SaveState数据,这样就能保证占用内存低。

所以如果你自己存储了一份Fragment list,那么就起不到低占内存的作用。

3 扩展

其实看到上面就已经能满足ViewPager+Fragment使用了。
那么为什么还有个扩展呢?其实这里主要是想讲讲我在使用的过程碰到的问题。

1.TransactionTooLargeException异常

这个问题主要是由于使用Bundle传递到Fragment中造成的,Bundle数据累计超过Android设定的上限。具体可参考https://blog.csdn.net/self_study/article/details/60136277,这里对该异常进行了详细描述。
下面我就说一说我的解决方案:
Bundle传的数据大,想要不抛异常,只能降低Bundle传的数据,因此我是通过修改传索引的方式,而非直接穿序列化的实体,然后在Fragment中通过索引从列表中获取数据。例子如下,一看就明白。
Activity中:

mPagerAdapter = new CommonViewPagerFragmentStateAdapter<MistakeBean>(getSupportFragmentManager(), mistakeBeanList){
                @Override
                public Fragment getItem(int position) {
                    Bundle bundle = new Bundle();
                    ...
                      //原来的使用方式
//                    bundle.putSerializable("bean", mistakeBeanList.get(position));
                    //修改后的使用方式
                    bundle.putInt("position",position);
                    ...
                    RedoMistakeFragment_ fragment_ = new RedoMistakeFragment_();
                    fragment_.setArguments(bundle);
                    return fragment_;
                }
            };

在Fragment中通过索引获取对应的数据:

......
private int mFragmentPosition;//当前fragment对应的索引
private void init() {
      Bundle bundle = getArguments();
      if (bundle != null) {
          mFragmentPosition = bundle.getInt("position");
          //通过索引获取缓存列表中的实体数据
          mBean = (MistakeBean) TaskCacheManager.getInstance().getQuestion(mFragmentPosition);
      }
}
2.同界面UI刷新问题

在当前的界面中刷新UI,即重新执行ViewPager初始化时:
1.如果数据列表数量发生变化,需要清除adapter中的数据,可调用clear()方法,否则会出现越界问题;
2.如果数据数量未发生变化,可不清除原adapter中的缓存数据,当然这里建议要清除下,减少内存使用;

4.源码例子

https://github.com/zhang-hai/zhhcommonview

相关文章

网友评论

    本文标题:正确实现ViewPager+多Fragment组合方式展示UI

    本文链接:https://www.haomeiwen.com/subject/uprjwhtx.html