专栏首页lzj_learn_note自定义无限循环ViewPager(三)――ViewPager方法改造实现无限循环

自定义无限循环ViewPager(三)――ViewPager方法改造实现无限循环

自定义无限循环ViewPager分成了以下三篇文章进行讲解:

  1. ViewPager初始化源码解析
  2. ViewPager滑动原理解析
  3. ViewPager方法改造实现无限循环

在前面两篇文章中,已经对ViewPager的实现原理有了分析,相信大家对于ViewPager的页面切换也有了一定的了解,接下来就是在ViewPager的基础上对其进行改造,达到无限循环的目的。

在改造之前,我们先想想为什么当ViewPager滑动到第一页的时候,再向右滑动的时候,ViewPager无法滑动了?同理,当ViewPager滑动到最后一页的时候,再向左滑动的时候,ViewPager也无法滑动了?

在第一篇文章中,有分析过ViewPager的populate()的方法,我们知道此方法主要作用是更新缓存列表以及计算缓存页面的偏移量。此方法中,在针对创建几个缓存页面的时候,存在着限制,这便是导致ViewPager在滑动到第一页和最后一页时,无法再滑动的原因。

    //添加左边缓存页面
    for (int pos = mCurItem - 1; pos >= 0; pos--) {
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) ii.object));
                        }
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }


         //添加右边缓存页面
         for (int pos = mCurItem + 1; pos < N; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                        + " view: " + ((View) ii.object));
                            }
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }

从上面的代码中,添加左边缓存页面,pos必须大于等于0,即当mCurItem=0显示第一个页面时,它是没有左边的缓存页面的。而我们所希望的是,如果当mCurItem=0,那么缓存列表中缓存页面的position的应该是[-1,0,1],而不是[0,1]。同理,添加右边缓存页面,pos必须小于N,即当mCurItem=N-1最后一个页面的时,缓存列表应该是[N-2,N-1,N],而不是[N-2,N-1],其中N=mAdapter.getCount()。

显示第一个页面的情况

显示最后一个页面的情况

所以要达到当显示第一个页面时,左边也存在缓存页面以及当显示最后一个页面,右边也存在缓存页面的目的,就必须放开添加缓存页面的for,而且position也不再代表页面在adapter数据中的位置, 如果要得到在adapter数据中的位置,需通过下面的公式得到。

Math.abs(N+ position%N) % N

如: position=-1时,adapterIndex = N-1(最后一个页面position); position=-2时,adapterIndex = N-2(倒数第二个页面position); position=N时,adapterIndex = 0(第一个页面position); position=N+1时,adapterIndex =1(第二个页面position);

populate()

所以populate()方法改造如下

void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        int focusDirection = View.FOCUS_FORWARD;
        if (mCurItem != newCurrentItem) {
            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        if (getWindowToken() == null) {
            return;
        }

        mAdapter.startUpdate(this);

        final int pageLimit = mOffscreenPageLimit;
        final int N = mAdapter.getCount();
        //改动
        //放开缓存页面的起始和结束的限制
        int startPos = mCurItem - pageLimit;
        int endPos =  mCurItem + pageLimit;

        if (N != mExpectedAdapterCount) {
            String resName;
            try {
                resName = getResources().getResourceName(getId());
            } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
            }
            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
                    " contents without calling PagerAdapter#notifyDataSetChanged!" +
                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
                    " Pager id: " + resName +
                    " Pager class: " + getClass() +
                    " Problematic adapter: " + mAdapter.getClass());
        }

        int curIndex = -1;
        ItemInfo curItem = null;
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }

        if (curItem != null) {
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            final float leftWidthNeeded = clientWidth <=0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            //改动
            //因为极端情况下,缓存的页面数为adpter数据的长度,所以需要遍历mExpectedAdapterCount-1次,
            for (int pos = mCurItem -1; pos>=mCurItem-mExpectedAdapterCount; pos--) {
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, ii.position, ii.object);
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >=0 ? mItems.get(itemIndex) : null;
                } else {
                    ii = addNewItem(pos, itemIndex+1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >=0 ? mItems.get(itemIndex) : null;
                }
            }

            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                final float rightWidthNeeded = clientWidth <=0 ? 0 :
                        (float) getPaddingRight() / (float) clientWidth + 2.f;
                 //改动
                //同添加左边缓存页面原理一样
                for (int pos = mCurItem +1 ; pos <= mCurItem+mExpectedAdapterCount ; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, ii.position, ii.object);
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }

            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

        if (DEBUG) {
            Log.i(TAG, "Current page list:");
            for (int i=0; i<mItems.size(); i++) {
                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
            }
        }

        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);

        mAdapter.finishUpdate(this);

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            lp.childIndex = i;
            if (!lp.isDecor && lp.widthFactor == 0.f) {
                // 0 means requery the adapter for this, it doesn't have a valid width.
                final ItemInfo ii = infoForChild(child);
                if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }
        sortChildDrawingOrder();

        if (hasFocus()) {
            View currentFocused = findFocus();
            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
            if (ii == null || ii.position != mCurItem) {
                for (int i=0; i<getChildCount(); i++) {
                    View child = getChildAt(i);
                    ii = infoForChild(child);
                    if (ii != null && ii.position == mCurItem) {
                        if (child.requestFocus(focusDirection)) {
                            break;
                        }
                    }
                }
            }
        }
    }

如果position不再代表页面在adapter数据中的位置,那么添加创建页面的addNewItem(int position, int index)也需要进行改动,而且计算页面偏移量calculatePageOffsets()方法中的mFirstOffsetmLastOffset赋值也需要做下改动。

addNewItem(int position, int index)

ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        //改动,获取在adapter数据中的位置
        int tempPos = Math.abs(mExpectedAdapterCount + position%mExpectedAdapterCount) % mExpectedAdapterCount;
        ii.position = position;
        //根据tempPos 创建页面
        ii.object = mAdapter.instantiateItem(this, tempPos);
        ii.widthFactor = mAdapter.getPageWidth(tempPos);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }

calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)

 private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
        final int N = mAdapter.getCount();
        final int width = getClientWidth();
        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
        // Fix up offsets for later layout.
        if (oldCurInfo != null) {
            final int oldCurPosition = oldCurInfo.position;
            // Base offsets off of oldCurInfo.
            if (oldCurPosition < curItem.position) {
                int itemIndex = 0;
                ItemInfo ii = null;
                float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                for (int pos = oldCurPosition + 1;
                     pos <= curItem.position && itemIndex < mItems.size(); pos++) {
                    ii = mItems.get(itemIndex);
                    while (pos > ii.position && itemIndex < mItems.size() - 1) {
                        itemIndex++;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos < ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset += mAdapter.getPageWidth(pos) + marginOffset;
                        pos++;
                    }
                    ii.offset = offset;
                    offset += ii.widthFactor + marginOffset;
                }
            } else if (oldCurPosition > curItem.position) {
                int itemIndex = mItems.size() - 1;
                ItemInfo ii = null;
                float offset = oldCurInfo.offset;
                for (int pos = oldCurPosition - 1;
                     pos >= curItem.position && itemIndex >= 0; pos--) {
                    ii = mItems.get(itemIndex);
                    while (pos < ii.position && itemIndex > 0) {
                        itemIndex--;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos > ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset -= mAdapter.getPageWidth(pos) + marginOffset;
                        pos--;
                    }
                    offset -= ii.widthFactor + marginOffset;
                    ii.offset = offset;
                }
            }
        }

        // Base all offsets off of curItem.
        final int itemCount = mItems.size();
        float offset = curItem.offset;
        int pos = curItem.position - 1;
        //改动
        mFirstOffset =  -Float.MAX_VALUE;
        mLastOffset =  Float.MAX_VALUE;

        // Previous pages
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
            final ItemInfo ii = mItems.get(i);
            while (pos > ii.position) {
                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
            }
            offset -= ii.widthFactor + marginOffset;
            ii.offset = offset;
        }
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        pos = curItem.position + 1;

        // Next pages
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
            final ItemInfo ii = mItems.get(i);
            while (pos < ii.position) {
                offset += mAdapter.getPageWidth(pos++) + marginOffset;
            }
            if (ii.position == N - 1) {
                mLastOffset = offset + ii.widthFactor - 1;
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }
        mNeedCalculatePageOffsets = false;
    }

在第二篇文章中有分析到ViewPager的页面的滑动和切换存在两种方式,一种是拖拽通过调用performDrag()方法得到需要滑动的距离,然后调用scrollTo()方法完成页面的滑动,而在performDrag()方法中会根据mFirstOffsetmLastOffset的值进行边界判断限制,但这两个值在calculatePageOffsets()进行了最小和最大值的赋值更改,所以根据页面的偏移和滑动距离得到的最终滑动距离不会造成边界超出,因而此方式所涉及的方法不需要改造。

而第二种方式需要在手指抬起后,根据显示页面的偏移量得到目标页面,然后调用 setCurrentItemInternal()滑动到目标页面。由于经过改造后,页面的偏移量存在负数,所以导致原来的determineTargetPage()确定目标页面会存在问题,需要加以改造。

determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX)

   private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
       int targetPage;
       if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
           targetPage = velocity > 0 ? currentPage : currentPage + 1;
       } else {
           //改动
           if (currentPage >= mCurItem) {
               targetPage = pageOffset > 0.6f ? currentPage + 1 : currentPage;
           } else {
               targetPage = pageOffset > 0.4f ? mCurItem : currentPage;
           }
       }

       if (mItems.size() > 0) {
           final ItemInfo firstItem = mItems.get(0);
           final ItemInfo lastItem = mItems.get(mItems.size() - 1);

           // Only let the user target pages we have items for
           targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
       }

       return targetPage;
   }

setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity)

   void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        if (mAdapter == null || mAdapter.getCount() <= 0) {
            setScrollingCacheEnabled(false);
            return;
        }
        if (!always && mCurItem == item && mItems.size() != 0) {
            setScrollingCacheEnabled(false);
            return;
        }

          //改动,放开限制
//        if (item < 0) {
//            item = 0;
//        } else if (item >= mAdapter.getCount()) {
//            item = mAdapter.getCount() - 1;
//        }

        final int pageLimit = mOffscreenPageLimit;
        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
            // We are doing a jump by more than one page.  To avoid
            // glitches, we want to keep all current pages in the view
            // until the scroll ends.
            for (int i = 0; i < mItems.size(); i++) {
                mItems.get(i).scrolling = true;
            }
        }
        final boolean dispatchSelected = mCurItem != item;

        if (mFirstLayout) {
            // We don't have any idea how big we are yet and shouldn't have any pages either.
            // Just set things up and let the pending layout handle things.
            mCurItem = item;
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            requestLayout();
        } else {
            populate(item);
            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
        }
    }
总结

将Viewpager拷贝一份到自己的目录中去,将本文讲到需要改造的方法复制替换掉ViewPager原有的方法即可,这样就可以达到无限循环的目的了。不过值得一提的是,因为position不再代表页面在adapter数据中的位置,所以在各个接口回调中的position值,需要通过公式Math.abs(N+ position%N) % N才能得到在adapter数据中真正的位置。

最后

关于改造ViewPager变为无限循环的第三部分所有内容就已经介绍完了,总的来说只要对ViewPager的相关原理有了一定的了解后,关于它的改造还是比较简单的。如果大家觉得本篇文章对各位有些帮助,希望能点个喜欢,谢谢!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android TextView实现查看全部和收起功能

    在工作遇到上图所示的一个小需求,将“查看全部”的提示连在原文的后面,使用一个textview显示。实现该功能大致步骤:

    用户3106371
  • Android RecyclerView之粘性头部+点击事件

    实现上图列表的粘性头部功能一般通过在布局页面额外写粘性头部View,然后通过监听列表的滑动来控制显示隐藏粘性头部View。而如果列表使用RecyclerView...

    用户3106371
  • 自定义无限循环ViewPager(一)――ViewPager初始化源码解析

    大部分app首页一般都会有个无限循环的广告轮播位,通常都是采用ViewPager来实现的,对此大家肯定不会感到陌生。而关于无限循环的ViewPager的实现,一...

    用户3106371
  • 51Nod-1645-中位数变换

    ACM模版 描述 ? 题解 这个题很明显是找规律的问题,直接暴力肯定会超时……虽然我也是暴力也两发才反应过来……平时做题总是抱着侥幸心理,比赛时却总是胆小如鼠…...

    f_zyj
  • HUST 1354 - Rubiks (DP)

    1354 - Rubiks 时间限制:1秒 内存限制:64兆 452 次提交 102 次通过 题目描述 Isun is a genius. Not on...

    ShenduCC
  • F2. Animal Observation (hard version)

    time limit per test:3 seconds memory limit per test:512 megabytes inputstandard ...

    某些人
  • HDU 1074 Doing Homework(状压DP)

    Doing Homework Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32...

    ShenduCC
  • PAT 甲级 1068 Find More Coins(0,1背包)

    1068. Find More Coins (30) 时间限制 150 ms 内存限制 65536 kB 代码长度限制 16000 B ...

    ShenduCC
  • HOJ 2317 Pimp My Ride(状态压缩DP)

    Pimp My Ride My Tags (Edit) Source : TUD 2005 Time limit : 3 sec Me...

    ShenduCC
  • 洛谷P4717 【模板】快速沃尔什变换(FWT)

    attack

扫码关注云+社区

领取腾讯云代金券