前言
在做小视频相关业务的时候,踩了很多ViewPager的坑,之前也做过case study,但当时由于时间紧张,没有完全对ViewPager的原理进行解读,有一些坑也只知道如何避免,不知道为什么。
问题
先提出以下几个问题:
onPageSelect 和 onPageScrolled调用时序是怎样的?
setCurrentItem 会不会触发 onPageSelect、onPageScrolled、onPageStateChanged方法?
notifyDataSetChanged 会不会触发 onPageSelect、onPageScrolled、onPageStateChanged方法?
在onTouch过程中,通过notifyDataChanged触发了onPageScrolled方法后,结束时候会触发onPageScrolled方法么?
- 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方法。
- 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();
}
}
- 在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方法了。