ViewPager源码分析(发现刷新数据的正确使用姿势)

ViewPager源码分析(发现刷新数据的正确使用姿势)

ViewPager的使用场景在这不赘述。此篇分析关注的是核心部分,即item如何进行添加、删除,以及如何刷新数据。通过本篇文章,你会发现使用ViewPager出现的问题在这里都能找到原因所在。

以下源码分析的版本为support-v4版本27.1.1,版本不同可能有差异。

1.初始化

先来看下item是如何被添加的。先从setAdapter()方法来看:

public void setAdapter(@Nullable PagerAdapter adapter) {
    if (mAdapter != null) {
        mAdapter.setViewPagerObserver(null);
        mAdapter.startUpdate(this);
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        mAdapter.finishUpdate(this);
        mItems.clear();
        removeNonDecorViews();
        mCurItem = 0;
        scrollTo(0, 0);
    }

    final PagerAdapter oldAdapter = mAdapter;
    mAdapter = adapter;
    mExpectedAdapterCount = 0;

    if (mAdapter != null) {
        if (mObserver == null) {
            mObserver = new PagerObserver();
        }
        mAdapter.setViewPagerObserver(mObserver);
        mPopulatePending = false;
        final boolean wasFirstLayout = mFirstLayout;
        mFirstLayout = true;
        mExpectedAdapterCount = mAdapter.getCount();
        if (mRestoredCurItem >= 0) {
            mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
            setCurrentItemInternal(mRestoredCurItem, false, true);
            mRestoredCurItem = -1;
            mRestoredAdapterState = null;
            mRestoredClassLoader = null;
        } else if (!wasFirstLayout) {
            // 非首次布局时候会调用
            populate();
        } else {
            // 请求刷新布局
            requestLayout();
        }
    }
    ...
}

setAdapter()中并没有直接添加item的实现,我们关注两个地方:

1.如果在activity的onCreate()一开始就调用了setAdapter()来设置数据,此时会走requestLayout(),那我们就要去关注一下onMeasure()方法;

2.如果调用setAdapter()wasFirstLayout=flase,此时走populate()方法;

先贴一下onMeasure()源码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // For simple implementation, our internal size is always 0.
    // We depend on the container to specify the layout size of
    // our view.  We can't really know what it is since we will be
    // adding and removing different arbitrary views and do not
    // want the layout to change as this happens.
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
            getDefaultSize(0, heightMeasureSpec));

    final int measuredWidth = getMeasuredWidth();
    final int maxGutterSize = measuredWidth / 10;
    mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

    // Children are just made to fill our space.
    int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
    int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

    ...省略掉测量decor部分

    mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
    mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

    // Make sure we have created all fragments that we need to have shown.
    mInLayout = true;
    // 测量的时候,也会调用populate方法
    populate();
    mInLayout = false;

    // Page views next.
    size = getChildCount();
    for (int i = 0; i < size; ++i) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            ......
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp == null || !lp.isDecor) {
                final int widthSpec = MeasureSpec.makeMeasureSpec(
                        (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                child.measure(widthSpec, mChildHeightMeasureSpec);
            }
        }
    }
}

可以看到,onMeasure()中也会去调用populate(),所以大概可以猜到populate() 是添加item的地方。

接下来重点关注populate()的源码(addNewItem()的源码一并贴出),这个方法是ViewPager的核心:

void populate() {
    populate(mCurItem);
}

void populate(int newCurrentItem) {
    ItemInfo oldCurInfo = null;
    // 如果当前选中索引与即将选中索引不一致,则更新选中索引
    if (mCurItem != newCurrentItem) {
        oldCurInfo = infoForPosition(mCurItem);
        mCurItem = newCurrentItem;
    }

    ...

    // 通知adapter开始刷新
    mAdapter.startUpdate(this);

    // 缓存item个数
    final int pageLimit = mOffscreenPageLimit;
    // startPos为左边缓存item开始的索引
    final int startPos = Math.max(0, mCurItem - pageLimit);
    final int N = mAdapter.getCount();
    // endPos为右边缓存item结束的索引
    final int endPos = Math.min(N - 1, mCurItem + pageLimit);
    // 这边会检查修改adapter后是否调用notifyDataSetChanged进行刷新
    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());
    }

    // 这边几个关键变量
    // mItems表示item的数据集合
    // curIndex表示当前选中item在mItems数据中的索引
    // Locate the currently focused item or add it if needed.
    int curIndex = -1;
    ItemInfo curItem = null;
    for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
        final ItemInfo ii = mItems.get(curIndex);
        // 如果当前存在的item在目标的右侧,则结束循环;
        if (ii.position >= mCurItem) {
            // 当前目标item已存在,也结束循环
            if (ii.position == mCurItem)
                curItem = ii;
            break;
        }
    }

    // 如果当前选中item不存在,即创建。注意curIndex的值,表示目标item的索引
    if (curItem == null && N > 0) {
        curItem = addNewItem(mCurItem, curIndex);
    }

    // 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;
        // itemIndex表示在mItems中的索引,初始值为目标左侧索引
        int itemIndex = curIndex - 1;
        // 如果存在,先取目标item左边的第一个item
        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
        final int clientWidth = getClientWidth();
        // extraWidthLeft和leftWidthNeeded用于判断缓存范围,可见padding值会影响实际的缓存个数
        final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
        // 遍历目标item左边的item
        for (int pos = mCurItem - 1; pos >= 0; pos--) {
            // 判断如果ii超出了缓存范围
            if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                if (ii == null) {
                    // 左侧已无item。结束遍历
                    break;
                }
                // 如果ii.scrolling=false,就删除掉item
                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;
                }
                // ii在缓存范围中,且已存在,则继续左侧遍历
            } else if (ii != null && pos == ii.position) {
                extraWidthLeft += ii.widthFactor;
                itemIndex--;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            } else {
                // 创建缓存范围中的item(插入到itemIndex),注意itemIndex + 1的值
                ii = addNewItem(pos, itemIndex + 1);
                extraWidthLeft += ii.widthFactor;
                // 因为插入了缓存item,所以要移动当前目标所在索引
                curIndex++;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }

        // 遍历目标item右侧的item
        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;
                }
            }
        }

        // 根据设置的pageMargin设置页面间的间距,会影响layout的位置
        calculatePageOffsets(curItem, curIndex, oldCurInfo);
        // 通知当前确定的目标页面
        mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
    }

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

    // 通知adapter结束刷新
    mAdapter.finishUpdate(this);

    // Check width measurement of current pages and drawing sort order.
    // Update LayoutParams as needed.
    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;
            }
        }
    }

    // child绘制顺序重新排序,是否生效取决于是否调用setChildrenDrawingOrderEnabled(),该方法在setPageTransformer()有传入PageTransformer对象时会生效
    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(View.FOCUS_FORWARD)) {
                        break;
                    }
                }
            }
        }
    }
}

// item就是通过该方法添加的
ItemInfo addNewItem(int position, int index) {
    ItemInfo ii = new ItemInfo();
    ii.position = position;
    // 调用adapter的创建item方法,我们平时在使用的时候,如果item是View的话,会去调用container.addView方法;这就把item添加到ViewPager容器中了
    ii.object = mAdapter.instantiateItem(this, position);
    // 从adapter获取pageWidth赋值给widthFactor,默认为1f
    ii.widthFactor = mAdapter.getPageWidth(position);
    // 这边index表示的就是要添加的item在mItems中的索引,如果
    if (index < 0 || index >= mItems.size()) {
        mItems.add(ii);
    } else {
        mItems.add(index, ii);
    }
    return ii;
}

populate()的实现逻辑比较多,需要仔细阅读分析,才能更好的理解。item增删的核心的逻辑:

1.优先添加要选中的item,如果该item未创建,就创建并插入到合适的位置(取决于目标item在已存在数据的相对位置);

2.依次遍历目标item左边的索引,添加需要缓存的item(个数为mOffscreenPageLimit),同时超过缓存范围的item会去销毁(在ii.scrolling=true时,不会马上被销毁,会等下次populate()时且ii.scrolling=false再销毁,那scrolling什么时候会赋值为true?);

3.依次遍历目标item右边的索引,逻辑同左边;

总结下来就是:由中间开始,依次往左侧再依次往右侧,缓存范围内不存在就添加,超出范围已存在就删除

同时,还得到了一个结论,决定内存中item预加载(缓存)的数量不仅取决于mOffscreenPageLimit,还取决于ViewPager.mPaddingLeftAdapter.getPageWidth()(默认返回1f)返回的大小,即内存中存在的item数量最大值不止会达到 (mOffscreenPageLimit*2+1) * 2。这个结论在实际应用中,设置一屏同时显示多个item(广告banner等场景)的需求下就能得到验证了。

2.跳转到指定item

跳转到目标item分为直接跳转平滑跳转

看下跳转最终会调用的方法:

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;
    // 注意这里的判断,当目标页面超出当前缓存页面时,设置scrolling=true
    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去添加数据(或删除多余数据)
        populate(item);
        scrollToItem(item, smoothScroll, velocity, dispatchSelected);
    }
}

private void scrollToItem(int item, boolean smoothScroll, int velocity,
        boolean dispatchSelected) {
    final ItemInfo curInfo = infoForPosition(item);
    int destX = 0;
    if (curInfo != null) {
        final int width = getClientWidth();
        destX = (int) (width * Math.max(mFirstOffset,
                Math.min(curInfo.offset, mLastOffset)));
    }
    if (smoothScroll) {
        // 平滑滚动,内部在结束滚动时会调用completeScroll方法,completeScroll内部会把ItemInfo的scrolling标记为flase,并判断是否需要重新populate
        smoothScrollTo(destX, 0, velocity);
        if (dispatchSelected) {
            dispatchOnPageSelected(item);
        }
    } else {
        if (dispatchSelected) {
            dispatchOnPageSelected(item);
        }
        // 完成滚动,内部会把ItemInfo的scrolling标记为flase,并判断是否需要重新populate
        completeScroll(false);
        scrollTo(destX, 0);
        pageScrolled(destX);
    }
}

可知,直接跳转平滑跳转的差异仅仅在于是否通过Scroller去平滑滚动。同时上文留下的疑问也得到解答:ItemInfo的scrolling在目标item超出当前缓存范围时,就会被标记为true。

3.刷新数据

重新创建adaptersetAdapter也能作为刷新数据,但正常都不这么使用。我们这边所谓的刷新数据是指调用了adpaternotifyDataSetChanged()方法,该方法会回调到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;
    // 判断是否需要走populate流程
    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);
        // 通过adapter获取itemPosition
        final int newPos = mAdapter.getItemPosition(ii.object);

        // 默认返回POSITION_UNCHANGED,即跳过该item
        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }

        // 返回POSITION_NONE,则移除该item
        if (newPos == PagerAdapter.POSITION_NONE) {
            mItems.remove(i);
            i--;

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

            // 移除该item并标记需要走populate
            mAdapter.destroyItem(this, ii.position, ii.object);
            needPopulate = true;

            // 当前选中被移除,会判断mCurItem是否超出边界,超出则重新赋值有效索引
            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;
            }

            // 更新item的position,并标记需要走populate
            ii.position = newPos;
            needPopulate = true;
        }
    }

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

    // mItems根据position重新排序
    Collections.sort(mItems, COMPARATOR);

    if (needPopulate) {
        // 重置所有child的宽度,等待populate中重新测量
        // Reset our known page widths; populate will recompute them.
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (!lp.isDecor) {
                lp.widthFactor = 0.f;
            }
        }

        // 重新定位,并走测量布局绘制流程
        setCurrentItemInternal(newCurrItem, false, true);
        requestLayout();
    }
}

可知,getItemPosition()返回POSITION_UNCHANGED可以在每次调用刷新后,所有item都会被销毁后重新添加。这样做显然会造成一些不必要的开销,那有没有方式能做到按需创建呢?

1.Item按需创建和销毁

答案是肯定的。从源码中可以看到,getItemPosition()返回的newPos会被更新到已存在的ItemInfo.position中,这个值代表了ItemInfo对应的数据在Adapter中数据集合索引,在populate()的时候会被使用到。所以要求在PagerAdapter使用正确的姿势。来看个简单的例子:

private class Adapter extends PagerAdapter {

    @Override
    public int getCount() {
        return mData.size();
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        ViewHolder viewHolder = (ViewHolder) object;
        return viewHolder.mItemView == view;
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        Log.d(TAG, "instantiateItem() called with: position = [" + position + "]");
        View itemView = getLayoutInflater().inflate(R.layout.page_item, container, false);
        container.addView(itemView);
        String item = mData.get(position);
        bindData(item, itemView);
        // 返回自定义的对象,包含了itemView在创建时候对应的数据以及索引
        return new ViewHolder<>(itemView, item, position);
    }

    private void bindData(String item, View itemView) {
        TextView tv = itemView.findViewById(R.id.tv);
        tv.setText(item);
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Log.d(TAG, "destroyItem() called with: position = [" + position + "]");
        ViewHolder viewHolder = (ViewHolder) object;
        container.removeView(viewHolder.mItemView);
    }

    @Override
    public int getItemPosition(@NonNull Object object) {
        ViewHolder<String> viewHolder = (ViewHolder<String>) object;
        String item = viewHolder.mItem;
        // 当前内存中页面数据是否还存在于刷新后的数据集合中
        int newIndex = mData.indexOf(item);
        int itemPosition = newIndex == -1 ? POSITION_NONE : newIndex;
        Log.d(TAG, "getItemPosition: item=" + item + ",itemPosition=" + itemPosition);
        int oldPosition = viewHolder.mPosition;
        // 数据发生了位置改变
        if (itemPosition >= 0 && oldPosition != itemPosition) {
            Log.d(TAG, "getItemPosition: itemView position changed:"
                    + oldPosition + "->" + itemPosition);
            // 更新新的索引位置
            viewHolder.mPosition = itemPosition;
        }
        return itemPosition;
    }
}

// 封装了itemView以及item数据,建立映射关系
private class ViewHolder<Item> {
    private View mItemView;
    private Item mItem;
    private int mPosition;

    ViewHolder(View itemView, Item item, int position) {
        mItemView = itemView;
        mItem = item;
        mPosition = position;
    }
}

假设当前页面在索引为1的页面,此时交换索引0和2的数据,并调用adapter.notifyDataSetChanged(),此时会输出日志:

D/DataSetChangedActivity: getItemPosition: item=item:0,itemPosition=2
    getItemPosition: itemView position changed:0->2
D/DataSetChangedActivity: getItemPosition: item=item:1,itemPosition=1
D/DataSetChangedActivity: getItemPosition: item=item:2,itemPosition=0
    getItemPosition: itemView position changed:2->0

可以发现,并没有调用instantiateItem()重新创建视图以及destroyItem()销毁视图,并且滑动观察左右两个页面,发现也是被正确交换位置了。从ViewPager.dataSetChanged()源码可知,Adapter.getItemPosition()直接返回了新的索引位置,会在重新的populate()calculatePageOffsets()的时候更新了ItemInfo的offset,上文提到了,这会影响最终child的layout的位置,所以此时layout的顺序和对应的页面位置是:child-0在第2页,child-1在第1页,child-2在第0页。变成了从后往前layout子view,这样就达到了交换0和2页面的效果,且没有任何的创建和销毁视图。

按需创建和销毁的问题解决了,但是刚才的场景是数据内容不变,位置改变的情况,假设数据位置不变,内容改变了呢?刚才的写法对于位置不变但内容发生改变的数据对应的页面是没法反映出实际变化的。所以还需要有方式去做到视图的内容刷新。

2.Item视图刷新

改造下getItemPosition()方法:

@Override
public int getItemPosition(@NonNull Object object) {
    ViewHolder<String> viewHolder = (ViewHolder<String>) object;
    String item = viewHolder.mItem;
    // 当前内存中页面数据是否还存在于刷新后的数据集合中
    int newIndex = mData.indexOf(item);
    int itemPosition = newIndex == -1 ? POSITION_NONE : newIndex;
    Log.d(TAG, "getItemPosition: item=" + item + ",itemPosition=" + itemPosition);
    int oldPosition = viewHolder.mPosition;
    // 数据索引发生改变
    if (itemPosition >= 0) {
        if (oldPosition != itemPosition) {
            Log.d(TAG, "getItemPosition: itemView position changed:"
                    + oldPosition + "->" + itemPosition);
            // 更新新的索引位置
            viewHolder.mPosition = itemPosition;
        }
        // 重新绑定数据以便于刷新视图内容
        bindData(item, viewHolder.mItemView);
    }
    return itemPosition;
}

当数据还存在时,把内存中已存在的对应页面视图重新绑定数据。这样的做法显然不是最优解,有没有办法只刷新内容改变的视图?

需要解决两个问题:

  1. 如何判断:更新了索引的视图是否即将被销毁?如果是,那可以忽略不用刷新
  2. 如何判断:同一个数据对象在刷新前后时,要展示到视图上的数据内容是否发生改变?

针对第一个问题,要能够判断出视图新的索引位置是否超出了缓存范围。首先,在dataSetChanged()循环调用getItemPosition()的过程中,最终要定位显示的item索引值是可能发生改变的;其次,上文中“缓存的视图个数不仅仅取决于mOffscreenPageLimit参数“的结论都将影响判断。

针对第二个问题,需要数据层面的设计能够判断出刷新前后同个数据对象内容是否变化,且该变化是否已经得到视图的响应。

综上,把内存中已存在的对应页面视图重新绑定数据会是个比较有效的做法,尽管有些绑定是多余的。

4.应用

根据上面的源码分析,我们可以对PagerAdapter进行封装,提供支持按需创建和销毁的支持,以及item视图刷新逻辑。

/**
 * Created by wrs on 2018/8/2.
 * <br/>
 * 对PagerAdapter进行封装,通过{@link #getItemPosition(Object)}返回正确的值,达到支持ViewPager数据刷新时,
 * 视图按需创建、删除以及刷新的目的;
 *
 * @param <Item> Item的数据类型
 */
@SuppressWarnings("unchecked")
public abstract class GracePagerAdapter<Item> extends PagerAdapter {

    private static final String TAG = "GraceViewPager";

    private List<Item> mItems;

    public GracePagerAdapter(@NonNull List<Item> items) {
        mItems = items;
    }

    @Override
    public int getCount() {
        return mItems.size();
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        ViewItemHolder viewItemHolder = (ViewItemHolder) object;
        return viewItemHolder.mItemView == view;
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        Log.d(TAG, "instantiateItem() called with: position = [" + position + "]");
        Item item = mItems.get(position);
        View itemView = instantiateItemView(container, item, position);
        bindItemView(itemView, item, position, true);
        container.addView(itemView);
        return new ViewItemHolder(item, itemView, position);
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Log.d(TAG, "destroyItem() called with: position = [" + position + "]");
        ViewItemHolder viewItemHolder = (ViewItemHolder) object;
        container.removeView(viewItemHolder.mItemView);
    }

    @Override
    public int getItemPosition(@NonNull Object object) {
        ViewItemHolder viewItemHolder = (ViewItemHolder) object;
        Item item = viewItemHolder.mItem;
        // 判断当前内存中页面数据是否还存在于刷新后的数据集合中,不存在返回POSITION_NONE进行移除
        int newPos = mItems.indexOf(item);
        int itemPosition = newPos == -1 ? POSITION_NONE : newPos;
        int oldPos = viewItemHolder.mPosition;
        Log.d(TAG, "getItemPosition: oldPos=" + oldPos + ",newPos=" + newPos);
        if (itemPosition >= 0) {
            // 数据索引发生改变
            if (oldPos != itemPosition) {
                // 更新索引位置
                viewItemHolder.mPosition = itemPosition;
            }
            // 当前页面重新绑定数据,以便于刷新视图内容
            bindItemView(viewItemHolder.mItemView, item, itemPosition, false);
        }
        return itemPosition;
    }

    // 负责持有视图、数据的对应关系
    private class ViewItemHolder {
        private Item mItem;
        private View mItemView;
        private int mPosition;

        ViewItemHolder(Item item, View itemView, int position) {
            mItem = item;
            mItemView = itemView;
            mPosition = position;
        }
    }

    /**
     * 创建视图
     *
     * @param container 容器,即ViewPager
     * @param item      数据
     * @param position  索引
     * @return View
     */
    @NonNull
    protected abstract View instantiateItemView(@NonNull ViewGroup container, Item item, int position);

    /**
     * 给ItemView绑定数据(创建视图以及数据刷新时都会调用)
     *
     * @param itemView 视图
     * @param item     数据
     * @param position 索引
     * @param first    是否为首次绑定调用,视图创建后首次绑定该值为true;数据刷新时调用为false
     */
    protected abstract void bindItemView(@NonNull View itemView, Item item, int position, boolean first);

}

5.拓展:一屏显示多页+切换动画

在实际需求中,ViewPager被广泛用于类似首页Banner、广告Banner等场景,同时还要求实现滑动页面的动画。有经验的同学就知道,可以采用paddingLeft、paddingRightclipToPadding=false实现一屏显示多页,采用PagerTransformer实现动画。但是,这将伴随着许多问题,数据刷新、ViewPager大小改变、padding动态改变都会导致显示异常。由于篇幅原因,将会另起一篇文章解决ViewPager动画异常(数据刷新、padding、pageMargin)来分析产生原因和解决方案。

猜你喜欢

转载自blog.csdn.net/wurensen/article/details/81390641
今日推荐