ViewPager踩坑记录

前言
在做小视频相关业务的时候,踩了很多ViewPager的坑,之前也做过case study,但当时由于时间紧张,没有完全对ViewPager的原理进行解读,有一些坑也只知道如何避免,不知道为什么。

问题
先提出以下几个问题:

onPageSelect 和 onPageScrolled调用时序是怎样的?
setCurrentItem 会不会触发 onPageSelect、onPageScrolled、onPageStateChanged方法?
notifyDataSetChanged 会不会触发 onPageSelect、onPageScrolled、onPageStateChanged方法?
在onTouch过程中,通过notifyDataChanged触发了onPageScrolled方法后,结束时候会触发onPageScrolled方法么?

  1. onPageSelect 和 onPageScrolled调用时序是怎样的?
@Override
public boolean onTouchEvent(MotionEvent ev) {
    ```
    switch (action & MotionEventCompat.ACTION_MASK) {
        ```
        case MotionEvent.ACTION_MOVE:
            ```
            setScrollState(SCROLL_STATE_DRAGGING);
            ```
            if (mIsBeingDragged) {
                ```
                needsInvalidate |= performDrag(x);
            }
            break;

        case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) {
                ```
                setCurrentItemInternal(nextPage, true, true, initialVelocity);
                needsInvalidate = resetTouch();
                ```
            }
            break;
        ```
    }
    if (needsInvalidate) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
    return true;
}

从上面ViewPager#onTouch代码看出,正常一次滑动,在有item的情况下,
应当是在ACTION_MOVE的过程中,
先调用:onPageScrollStateChanged
再调用:onPageScrolled
最后在 ACTION_UP的过程中,
调用onPageSelected。
最后在needsInvalidate为true的情况下,触发ViewCompat.postInvalidateOnAnimation(this),其中会在重绘过程中调用computeScroll,最后再调用一次onPageScrolled方法。

  1. setCurrentItem 会不会触发 onPageSelect、onPageScrolled、onPageStateChanged方法?
    public void setCurrentItem(int item) {
        mPopulatePending = false;
        setCurrentItemInternal(item, !mFirstLayout, false);
    }

这个方法给的注释是:如果ViewPager已经通过了它当前的Adapter第一次布局,则缓慢通过动画划过去。

 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
    setCurrentItemInternal(item, smoothScroll, always, 0);
 }

 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        ```
        final boolean dispatchSelected = mCurItem != item;
        if (mFirstLayout) {
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            requestLayout();
        } else {
            populate(item);
            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
        }
    }

这就解释了为什么Activity启动情况下有时会调用onPageSelect,而有时不会,在FirstLayout的情况下。目标item与当前item不等时才触发onPageSelected。而requestLayout

子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ```
        if (mFirstLayout) {
            scrollToItem(mCurItem, false, 0, false);
        }
        mFirstLayout = false;
    }

其中onLayout过程会触发scrollToItem

    private void scrollToItem(int item, boolean smoothScroll, int velocity,
            boolean dispatchSelected) {
        ```
        if (smoothScroll) {
            ```
        } else {
            ```
            completeScroll(false);
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }

在onLayout过程中,completeScroll(false),scrollTo(destX, 0),pageScrolled(destX)都会触发我们的onPageScroll,其中

private void completeScroll(boolean postEvents) {
        boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
        if (needPopulate) {
            if (postEvents) {
                ViewCompat.postOnAnimation(this, mEndScrollRunnable);
            } else {
                mEndScrollRunnable.run();
            }
        }
    }
private final Runnable mEndScrollRunnable = new Runnable() {
        public void run() {
            setScrollState(SCROLL_STATE_IDLE);
            populate();
        }
    };

completeScroll方法还会触发onPageStateChanged方法。所以,依靠这三个方法来判断页面是否滑动过页面是不靠谱的。

3.PagerAdapter.notifyDataSetChanged 会不会触发 onPageSelect、onPageScrolled、onPageStateChanged方法?

    public void notifyDataSetChanged() {
        synchronized (this) {
            if (mViewPagerObserver != null) {
                mViewPagerObserver.onChanged();
            }
        }
        mObservable.notifyChanged();
    }

首先 notifyDataSetChanged基于观察者模式,在ViewPager在setAdapter的时候,会将一个观察者交给PagerAdapter。

   public void setAdapter(PagerAdapter adapter) {
        ```
        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            mAdapter.setViewPagerObserver(mObserver);
            ```
        }
        ```
    }

在PagerAdapter调用onChanged的时候,ViewPager会调用自身的dataSetChanged方法

private class PagerObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

从dataSetChanged代码中,我们可以看出,是否触发setCurrentItemInternal和requestLayout主要是由PagerAdapter的ItemPosition来控制。有一些不需要更改的页面就用POSITION_UNCHANGED来控制,就很大程度避免了ViewPager中布局的重绘。

    void dataSetChanged() {
        ```
        for (int i = 0; i < mItems.size(); i++) {
            ```
            final int newPos = mAdapter.getItemPosition(ii.object);
            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }
            if (newPos == PagerAdapter.POSITION_NONE) {
                ```
                needPopulate = true;
                ```
                continue;
            }
            if (ii.position != newPos) {
                ```
                needPopulate = true;
            }
        }
        ```
        if (needPopulate) {
            ```
            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
        }
    }
  1. 在onTouch过程中,通过notifyDataChanged触发了onPageScrolled方法后,结束时候会触发onPageScrolled方法么?
    刚才第一个问题中,我们发现在onTouch的ACTION_UP的过程中,我们会根据resetTouch的返回值来确定当前触摸事件是否触发ViewCompat.postInvalidateOnAnimation(this)
    private boolean resetTouch() {
        boolean needsInvalidate;
        mActivePointerId = INVALID_POINTER;
        endDrag();
        needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
        return needsInvalidate;
    }

mLeftEdge和mRightEdge都是EdgeEffectCompat,这是Android边缘效应的相关类,在ViewPager的体现就是左右两道半透明颜色阴影。onRelease代表当前被释放了。在这个case里,needsInvalidate为true。
至于ViewCompat.postInvalidateOnAnimation(this) 最后仍然会触发invalidate,通过层层调用调用到computeScroll方法

 @Override
    public void computeScroll() {
        mIsScrollStarted = true;
        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            if (oldX != x || oldY != y) {
                scrollTo(x, y);
                if (!pageScrolled(x)) {
                    mScroller.abortAnimation();
                    scrollTo(0, y);
                }
            }
            // Keep on drawing until the animation has finished.
            ViewCompat.postInvalidateOnAnimation(this);
            return;
        }
        // Done with scroll, clean up state.
        completeScroll(true);
    }

因为在notifyDataChanged过程中,已经将页面重置了,在此时 oldX == x,oldY == y,所以就不调用pageScrolled方法了。

你可能感兴趣的:(ViewPager踩坑记录)