android可以无限循环滑动的ViewPager

前言:最近有需求需要某个页面是可以无限滑动的,这个页面是用ViewPager实现的,但是ViewPager本身并不能无限滑动,所以想在android现有ViewPager的基础之上,实现这个功能,本文提供基于PagerAdapter和FragmentPagerAdapter的可以复用view和fragment的一种实现,github地址:https://github.com/ChenSWD/InfiniteViewPager

分析:
ViewPager滑动边界是怎么判断的?
ViewPager能不能滑动依赖于: mAdapter.getCount()和mCurItem变量,即:当前位置和item总个数。那我们直接让mAdapter.getCount()返回一个很大的值(Integer.MAX_VALUE)就可以了,即有无限个item。
基于以上,这样操作对性能上有多大的影响?其实ViewPager是有缓存机制的,默认缓存当前位置和其两边的各一个item,所以在内存上的问题是不存在的,但是我还是想自己缓存一些可复用的view,向ListView那样,因为一般在新建view时会执行LayoutInflater.inflate(),会解析xml格式的布局文件,会重复创建销毁view,把view缓存起来可以解决是个不错的选择。

基于PagerAdapter普通自定义布局的分析:

因为要修改adapter和ViewPager的源码,所以把相关文件拷贝一份:ViewPager(ViewPagerCopy),PagerAdapter(PagerAdapterCopy),FragmentPagerAdapter(InfiniteFragmentPagerAdapter)。
我们假设ViewPager需要循环的有3个item,position分别是0 1 2,把这个叫做item实际位置,在无限循环的ViewPager里面,0的位置实际上对应的是Integer.MAX_VALUE/21-Integer.MAX_VALUE/2+12-Integer.MAX_VALUE/2+2,把映射后的位置称为item的扩展位置。

1、新建InfinitePagerAdapter继承PagerAdapterCopy。在PagerAdapterCopy里面新增方法 public abstract int getRealCount();,该方法返回实际的item个数;新增方法:public int getRealPosition(int position){},该方法是给ViewPagerCopy里OnPageChangeListener使用的,根据扩展的位置返回实际的位置;新增方法:public abstract void onPageSelected(int extendPosition, int realPosition),该方法在每一次切换页面时调用。
2、所有需要循环的adapter继承自InfinitePagerAdapter,需要说明的是getExtendItem()方法,该方法在设置ViewPager初始的时候显示第几页时使用,根据实际的位置,返回扩展后的位置,所有destroyItem()instantiateItem()方法的position参数全部是扩展的位置:

public abstract class InfinitePagerAdapter extends PagerAdapterCopy {
    public static final int INFINITE_COUNT = Integer.MAX_VALUE;

    @Override
    public int getCount() {
        //返回无限个
        return INFINITE_COUNT;
    }

    /**
     * 在调用setCurrentItem()时使用该方法返回扩展后的位置
     * 该方法需要在getRealCount()有返回值的时候使用,
     * ViewPager的setCurrentItem()方法要在设置数据之后调用,因为不知道 RealCount,就不能算出扩展后的位置
     */
    public int getExtendItem(int item) {
        int real = getRealCount();
        int total = getCount();
        if (real <= 0) {
            return item;
        }
        //找到大概中间位置的组号
        int index = total / real / 2;
        //返回中间组的第item个位置
        return index * real + item;
    }
}

3、下面给出具体使用的PagerActivity和PagerAdapter的代码,相关注释在代码中都有,为什么缓存view的个数是limit*2+2,在代码中特别做了解释:

public class PagerActivity extends AppCompatActivity {
    private ViewPagerCopy mViewPager;
    private PagerAdapter mAdapter;
    List<Integer> text = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pager);
        for (int i = 0; i < 3; i++) {
            text.add(0);
        }
        //最多缓存两个(当前view一边两个,总共5个)
        int limit = 2;
        mViewPager = findViewById(R.id.view_pager);

        //缓存view的个数为limit*2 + 2
        //为什么是limit*2 + 2:正常情况下应该缓存 limit*2 + 1 个view,因为左右两边各缓存limit个,
        //在从左向右滑动换页时是没有问题的,因为会先执行destroyItem()方法再执行instantiateItem()方法,
        //这样instantiateItem时就可以复用destroyItem时remove的view
        //但是ViewPager在从右向左滑动的时候,会先执行instantiateItem()方法再执行destroyItem()方法,
        // 这样就必须多缓存一个,即:limit*2 + 2个
        mAdapter = new PagerAdapter(limit * 2 + 2);
        mAdapter.setTextList(text);
        mViewPager.setAdapter(mAdapter);
        //设置缓存的限制,默认会缓存1个
        mViewPager.setOffscreenPageLimit(limit);
        //数据的设置(setTextList())要在setCurrentItem()之前,
        mViewPager.setCurrentItem(mAdapter.getExtendItem(1));
        mAdapter.notifyDataSetChanged();
    }
}

public class PagerAdapter extends InfinitePagerAdapter {
    View[] viewList;
    List<Integer> textList = new ArrayList<>();

    public PagerAdapter(int size) {
        viewList = new View[size];
    }

    public void setTextList(List<Integer> textList) {
        this.textList = textList;
    }

    @Override
    public Object instantiateItem(ViewGroup container, final int position) {
        final ViewHolder holder;
        //这里position是扩展后的位置,所以要根据位置,计算出当前要复用的item的下标位置
        //循环复用viewList里的view
        int viewPos = position % viewList.length;
        if (viewList[viewPos] == null) {
            View view = LayoutInflater.from(container.getContext()).inflate(R.layout.pager_adapter_item, container, false);
            holder = new ViewHolder(view);
            view.setTag(holder);
            viewList[viewPos] = view;
        } else {
            holder = (ViewHolder) viewList[viewPos].getTag();
        }
        final int realPos = position % textList.size();
        holder.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                textList.set(realPos, textList.get(realPos) + 1);
                holder.textView.setText("实际位置 " + realPos
                        + "\n是第" + position + "个扩展的位置"
                        + "\ncount = " + textList.get(realPos));
            }
        });
        bindViewHolder(position);
        container.addView(viewList[viewPos]);
        return viewList[viewPos];
    }

    //每一次切换页面的时候刷新一下状态
    public void bindViewHolder(int extendPos) {
        int viewPos = extendPos % viewList.length;
        if (viewList[viewPos] != null) {
            final ViewHolder holder = (ViewHolder) viewList[viewPos].getTag();
            final int realPos = extendPos % textList.size();
            holder.textView.setText("实际位置 " + realPos
                    + "\n是第" + extendPos + "个扩展的位置"
                    + "\ncount = " + textList.get(realPos));
        }
    }

    @Override
    public int getRealCount() {
        return textList.size();
    }

    @Override
    public void onPageSelected(int extendPosition, int realPosition) {
        bindViewHolder(extendPosition);
    }

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

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }

    private static class ViewHolder {
        private TextView textView;

        public ViewHolder(View view) {
            textView = view.findViewById(R.id.text);
        }
    }
}

基于FragmentPagerAdapter的Fragment无限循环的分析:

分析:因为ViewPager配合Fragment使用,很多时候Fragment都不相同,不能简单的复用,一般来说实际Fragment是比较多的,直接在instantiateItem()方法中,把扩展position转换成实际position就行(position%getRealCount)。
但是当实际fragment个数小于limit*2+2的时候,会出现fragment缓存不够用的情况,例如真实fragment:0,1,2,需要缓存的个数limit=2,那么就需要6个真实的fragment才能完成循环复用,比如当前位置为2,缓存的fragment需要是0,1,2,3,4。3的位置是不能复用0位置的,因为同一个fragment不能被attach两次,这时候fragment是不够用的,怎么办呢?我是在3的位置new了一个新的和0位置一样的fragment,4的位置new了一个和1一样的fragment,然后添加到fragment栈中的。
我是这样做的(感觉还可以完善很多东西),定义一个Map:Map<Integer, List<AbstractFragment>> fragmentMap;,key是fragment的实际位置,value是一个list,用于装入在key这个位置重复的fragment,比如上面0和3都会被装入到key为0的list中。
以下都是fragment很少,需要重复添加的情况下做的分析:
1、在AbstractFragment 中增加一个字段表示当前fragment是不是能被复用(没有被attach),因为用isAdded()和isDetached()方法似乎都不能标识:

public abstract class AbstractFragment<T> extends Fragment {
    //是否可被添加,初始的true
    private boolean isUsable = true;

    public boolean isUsable() {
        return isUsable;
    }

    public void setUsable(boolean isUsable) {
        this.isUsable = isUsable;
    }

    //用来刷新fragment的状态
    public abstract void refreshData(T data);
}

2、修改InfinitePagerAdapter的instantiateItem()destroyItem(),这两个方法主要给fragment设置可用的标识(调用setUsable()方法),新增方法 protected abstract String findUsableFragmentTag(int pos)是自定义FragmentAdapter需要实现的,这个方法是用来根据扩展的位置,返回可用的fragment的Tag,改变了原先Tag获取的方式:

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

        final long itemId = getItemId(position);
        //先查找有没有可以复用的fragment,没有的话要调用getItem()新建
        String name = findUsableFragmentTag(position);
        if (name == null) {
            // Do we already have this fragment?
            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);
        }
        if (fragment instanceof AbstractFragment) {
            ((AbstractFragment) fragment).setUsable(false);
        }
        return fragment;
    }

    protected abstract String findUsableFragmentTag(int pos);

    @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);
        if (object instanceof AbstractFragment) {
            ((AbstractFragment) object).setUsable(true);
        }
    }

3、自定义的FragmentPagerAdapter,主要是getItem()方法,该方法根据扩展位置,返回一个可用的实际位置的fragment,做法是根据实际位置去查找Map,找到list里面第一个可用的fragment返回,如果list中无当前位置可用的fragment,就去新建一个:

public class FragmentPagerAdapter extends InfiniteFragmentPagerAdapter {
    private int realCount;
    private Map<Integer, List<AbstractFragment>> fragmentMap = new HashMap<>();
    private FragmentAdapterCallBack callBack;
    private List<DataEntity> entityList;

    public FragmentPagerAdapter(FragmentManager fm, List<AbstractFragment> fragments, FragmentAdapterCallBack callBack, List<DataEntity> entityList) {
        super(fm);
        realCount = fragments.size();
        for (int i = 0; i < fragments.size(); i++) {
            List<AbstractFragment> list = new ArrayList<>();
            list.add(fragments.get(i));
            fragmentMap.put(i, list);
        }
        this.callBack = callBack;
        this.entityList = entityList;
    }

    @Override
    public Fragment getItem(int position) {
        int realPos = position % getRealCount();
        List<AbstractFragment> fragments = fragmentMap.get(realPos);
        //根据实际位置去查找Map,找到list里面第一个可用的fragment返回
        for (AbstractFragment fragment : fragments) {
            if (fragment.isUsable()) {
                return fragment;
            }
        }
        AbstractFragment fragment = null;
        //如果list中无当前位置可用的fragment,就去新建一个
        if (callBack != null) {
            fragment = callBack.generateFragmentByPosition(realPos);
            fragments.add(fragment);
        }
        if (fragment == null)
            throw new NullPointerException("新生成的fragment不能为空");
        return fragment;
    }

    @Override
    protected String findUsableFragmentTag(int position) {
        int realPos = position % getRealCount();
        //根据实际位置去查找Map,找到可用的就返回它的Tag
        List<AbstractFragment> fragments = fragmentMap.get(realPos);
        for (AbstractFragment fragment : fragments) {
            if (fragment.isUsable()) {
                return fragment.getTag();
            }
        }
        return null;
    }

    @Override
    public int getRealCount() {
        return realCount;
    }

    @Override
    public void onPageSelected(int extendPosition, int realPosition) {
        //在换页的时候,判断是不是要刷新扩展后的fragment的状态
        refreshFragmentsIfNeed(realPosition, entityList.get(realPosition));
    }

    //当前fragment是不是扩展添加的,用来判断是否刷新Fragment的状态
    public void refreshFragmentsIfNeed(int pos, DataEntity entity) {
        //在当前位置有扩展的fragment的情况下,且当前位置的数据被更改过,查找当前已经attach的fragment,刷新它们的状态
        if (entity != null && entity.isRefresh()) {
            List<AbstractFragment> fragments = fragmentMap.get(pos);
            if (fragments != null && fragments.size() > 1) {
                for (AbstractFragment fragment : fragments) {
                    if (!fragment.isUsable()) {
                        fragment.refreshData(entity);
                    }
                }
            }
            entity.setRefresh(false);
        }
    }
}

总结:以上基本完成了自定义PagerAdapter和FragmentPagerAdapter实现无限滑动的功能,这里只是提供一种解决方式,在具体使用中可能还要具体修改一些逻辑。在后续个人使用中,会完善一些代码,如果发现一些bug也会及时修复,如果有好的思路也请指教。说了这么多可能都不如看一下代码:https://github.com/ChenSWD/InfiniteViewPager

猜你喜欢

转载自blog.csdn.net/weixin_38062353/article/details/82564016