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方法了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容