美文网首页
ViewPager踩坑记录

ViewPager踩坑记录

作者: 桥寻 | 来源:发表于2018-02-08 20:36 被阅读323次

    前言
    在做小视频相关业务的时候,踩了很多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踩坑记录

          本文链接:https://www.haomeiwen.com/subject/xqhxtftx.html