Android ViewPager使用记录

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/dahaohan/article/details/51019240

Android ViewPager

ViewPager是android提供的可以方便实现左右滑动切换以页为单位的视图控件,可以快速实现导航页/菜单,横向滑动浏览图片等功能,常用来与Fragment配合使用。

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v4.view.PagerTabStrip
            android:id="@+id/pagertab"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            ></android.support.v4.view.PagerTabStrip>
        <!--
        <android.support.v4.view.PagerTitleStrip
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_gravity="bottom"></android.support.v4.view.PagerTitleStrip>
        -->
    </android.support.v4.view.ViewPager>

PagerTabStrip/PagerTitleStrip为扩展的用于上一页/当前页/下一页的提示交互视图,实际使用效果并不理想,可参考:http://www.tuicool.com/articles/UNRnay
与ListView类似的ViewPager采用的也是Adapter适配器模式,故而PagerAdapter自然是使用ViewPager的核心了。

PagerAdapter核心函数

官方API文档:http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html
PagerAdapter is more general than the adapters used for AdapterViews. Instead of providing a View recycling mechanism directly ViewPager uses callbacks to indicate the steps taken during an update. A PagerAdapter may implement a form of View recycling if desired or use a more sophisticated method of managing page Views such as Fragment transactions where each page is represented by its own Fragment.
从上述描述可以了解到PagerAdapter比一般的AdapterViews更为”简陋“,因为ViewPager不像ListView等提供视图回收机制,而是使用一系列的回调来完成视图的更新,需要自己根据这一系列回调规则去维护视图的回收利用。

When you implement a PagerAdapter, you must override the following methods at minimum:

  • instantiateItem(ViewGroup, int)
  • destroyItem(ViewGroup, int, Object)
  • getCount()
  • isViewFromObject(View, Object)

ViewPager associates each page with a key Object instead of working with Views directly. This key is used to track and uniquely identify a given page independent of its position in the adapter. A call to the PagerAdapter method startUpdate(ViewGroup) indicates that the contents of the ViewPager are about to change. One or more calls to instantiateItem(ViewGroup, int) and/or destroyItem(ViewGroup, int, Object) will follow, and the end of an update will be signaled by a call to finishUpdate(ViewGroup). By the time finishUpdate returns the views associated with the key objects returned by instantiateItem should be added to the parent ViewGroup passed to these methods and the views associated with the keys passed to destroyItem should be removed. The method isViewFromObject(View, Object) identifies whether a page View is associated with a given key object.

ViewPager的核心是将每一个页面跟一个唯一的Object的key关联在一起。

上述四个函数是PagerAdapter的核心:
instantiateItem(ViewGroup container, int position)负责创建postion位置的页面实例,并且将该页面加入给定的container(container即为ViewPager本身)内;其返回值即为代表该page的唯一的key Object.
destroyItem(ViewGroup container, int position, Object object)负责将postion位置的page从container内删除,object即为该页面的唯一key值。
isViewFromObject(View view, Object object)判断view是否与key object是相互关联在一起的。
getCount()即为获取page的数量

ViewPager具体工作原理分析

通过ViewPager的源码去详细了解几个函数的作用,特别是isViewFromObject(View view, Object object)以及getItemPosition(Object object)这两个函数。

ViewPager源码有如下结构和函数:

    static class ItemInfo {
        Object object;
        int position;
        boolean scrolling;
        float widthFactor;
        float offset;
    }

    ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        ii.position = position;
        ii.object = mAdapter.instantiateItem(this, position);
        ii.widthFactor = mAdapter.getPageWidth(position);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }

可见ViewPager内每个页面对应一个ItemInfo类,保存了页面的key object,position两个关键信息。在viewPager更新内容或者页面滑动到新的位置时会触发执行populate(int newCurrentItem)函数:

        // Fill 3x the available width or up to the number of offscreen
        // pages requested to either side, whichever is larger.
        // If we have no current item we have no work to do.
        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;
            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;
                }
            }

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

            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

可见ViewPager每次会动态的增减当前持有的pages,仅仅只加载维持包括currenItem以及其左右一共3个page的视图。

isViewFromObject函数

如下源码,当ViewPager进行layout或者处理当前哪个page获取focus时,会遍历所有的mItems通过infoForChild函数比对child view和key object是否匹配来获得正确的page的itemInfo的信息。

简单点说就是ViewPager在处理child View时,仅仅只能能拿到child view即page,又需要通过该page view拿到对应的key,postion(ItemIfon)信息,而isViewFromObject函数就需要提供判断child view与key的关系是否匹配的功能

        ItemInfo infoForChild(View child) {
            for (int i=0; i<mItems.size(); i++) {
                ItemInfo ii = mItems.get(i);
                if (mAdapter.isViewFromObject(child, ii.object)) {
                    return ii;
                }
            }
            return null;
        }

        // Page views. Do this once we have the right padding offsets from above.
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                ItemInfo ii;
                if (!lp.isDecor && (ii = infoForChild(child)) != null) {
                    int loff = (int) (childWidth * ii.offset);
                    int childLeft = paddingLeft + loff;
                    int childTop = paddingTop;
                    if (lp.needsMeasure) {
                        // This was added during layout and needs measurement.
                        // Do it now that we know what we're working with.
                        lp.needsMeasure = false;
                        final int widthSpec = MeasureSpec.makeMeasureSpec(
                                (int) (childWidth * lp.widthFactor),
                                MeasureSpec.EXACTLY);
                        final int heightSpec = MeasureSpec.makeMeasureSpec(
                                (int) (height - paddingTop - paddingBottom),
                                MeasureSpec.EXACTLY);
                        child.measure(widthSpec, heightSpec);
                    }
                    if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
                            + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
                            + "x" + child.getMeasuredHeight());
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                }
            }
        }

getItemPosition 函数

该函数主要决定当dataSet变化时,当前显示的页面是否需要更新,返回值如下:
PagerAdapter.POSITION_UNCHANGED代表当前已经加载的页面数据没有变化,不更新已加载的page数据
PagerAdapter.POSITION_NONE代表当前页面已经不存在于ViewPager内,重新加载当前几页数据

例如:当前有三页pages显示都为花的图片,设置一个按钮去更新三页pages的数据使之变为鸟的图片,然后调用notifyDataSetChanged();如果getItemPosition函数返回值POSITION_UNCHANGED,当前显示的三页将不会有变化依然显示花的图案(若滑动离开当前页,造成左右页面的动态删减重新加载才会更新显示为花的图案)。
若返回值POSITION_NONE则在notifyDataSetChanged()触发之后,页面立即更新。

查看ViewPager源码dataSetChanged()函数:

    void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter is non-null.
        final int adapterCount = mAdapter.getCount();
        mExpectedAdapterCount = adapterCount;
        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
                mItems.size() < adapterCount;
        int newCurrItem = mCurItem;

        boolean isUpdating = false;
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            final int newPos = mAdapter.getItemPosition(ii.object);

            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }
            //若页面不存在则删除该页面,后续重新加载
            if (newPos == PagerAdapter.POSITION_NONE) {
                mItems.remove(i);
                i--;

                if (!isUpdating) {
                    mAdapter.startUpdate(this);
                    isUpdating = true;
                }

                mAdapter.destroyItem(this, ii.position, ii.object);
                needPopulate = true;

                if (mCurItem == ii.position) {
                    // Keep the current item in the valid range
                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                    needPopulate = true;
                }
                continue;
            }

            if (ii.position != newPos) {
                if (ii.position == mCurItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem = newPos;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }

        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }

PageTransformer实现ViewPager切换动画

ViewPager内定义了接口PageTransformer用于实现前后page的过场动画,根据函数说明
public void transformPage(View page, float position);
page代表当前的页面,postion的取值为[-1,1],
前面分析了ViewPager最多会维持三个页面,-1则代表page当前处于最左边页面的初始位置;0代表页面已经到达并处于中间页面位置;1则代表页面处在最右边的页面位置。
根据postion的变化可以在page上加上各种动画过场。
实现起来比较简单,可参考文章:
http://blog.csdn.net/lmj623565791/article/details/40411921/
http://gqdy365.iteye.com/blog/2114968

    /**
     * A PageTransformer is invoked whenever a visible/attached page is scrolled.
     * This offers an opportunity for the application to apply a custom transformation
     * to the page views using animation properties.
     *
     * <p>As property animation is only supported as of Android 3.0 and forward,
     * setting a PageTransformer on a ViewPager on earlier platform versions will
     * be ignored.</p>
     */
    public interface PageTransformer {
        /**
         * Apply a property transformation to the given page.
         *
         * @param page Apply the transformation to this page
         * @param position Position of page relative to the current front-and-center
         *                 position of the pager. 0 is front and center. 1 is one full
         *                 page position to the right, and -1 is one page position to the left.
         */
        public void transformPage(View page, float position);
    }

猜你喜欢

转载自blog.csdn.net/dahaohan/article/details/51019240