今天来聊一聊ViewPager2的原理,相比用开发android的小伙伴对ViewPager都很熟悉,但是ViewPager2估计用到的应该在少数,今天来了解一下ViewPager2的实现原理。
ViewPager2的用法和ViewPage类似,所以才起了一个重复的名字吧,ViewPager2和ViewPager最根本的区别就是可以把ViewPager2看作是RecycleView,不错它的内部实现就是RecycleView,现在进入它的源码瞧瞧,眼见为实吗。
public final class ViewPager2 extends ViewGroup
ViewPager2 继承ViewGroup
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
ViewPager2的构造方法里面有个initialize方法所有的子控件添加都是放在这个方法里面
private void initialize(Context context, AttributeSet attrs) {
mAccessibilityProvider = sFeatureEnhancedA11yEnabled
? new PageAwareAccessibilityProvider()
: new BasicAccessibilityProvider();
mRecyclerView = new RecyclerViewImpl(context);
mRecyclerView.setId(ViewCompat.generateViewId());
mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
mLayoutManager = new LinearLayoutManagerImpl(context);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
setOrientation(context, attrs);
mRecyclerView.setLayoutParams(
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());
// Create ScrollEventAdapter before attaching PagerSnapHelper to RecyclerView, because the
// attach process calls PagerSnapHelperImpl.findSnapView, which uses the mScrollEventAdapter
mScrollEventAdapter = new ScrollEventAdapter(this);
// Create FakeDrag before attaching PagerSnapHelper, same reason as above
mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
mPagerSnapHelper = new PagerSnapHelperImpl();
mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
// Add mScrollEventAdapter after attaching mPagerSnapHelper to mRecyclerView, because we
// don't want to respond on the events sent out during the attach process
mRecyclerView.addOnScrollListener(mScrollEventAdapter);
mPageChangeEventDispatcher = new CompositeOnPageChangeCallback(3);
mScrollEventAdapter.setOnPageChangeCallback(mPageChangeEventDispatcher);
// Callback that updates mCurrentItem after swipes. Also triggered in other cases, but in
// all those cases mCurrentItem will only be overwritten with the same value.
final OnPageChangeCallback currentItemUpdater = new OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
if (mCurrentItem != position) {
mCurrentItem = position;
mAccessibilityProvider.onSetNewCurrentItem();
}
}
@Override
public void onPageScrollStateChanged(int newState) {
if (newState == SCROLL_STATE_IDLE) {
updateCurrentItem();
}
}
};
// Prevents focus from remaining on a no-longer visible page
final OnPageChangeCallback focusClearer = new OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
clearFocus();
if (hasFocus()) { // if clear focus did not succeed
mRecyclerView.requestFocus(View.FOCUS_FORWARD);
}
}
};
// Add currentItemUpdater before mExternalPageChangeCallbacks, because we need to update
// internal state first
mPageChangeEventDispatcher.addOnPageChangeCallback(currentItemUpdater);
mPageChangeEventDispatcher.addOnPageChangeCallback(focusClearer);
// Allow a11y to register its listeners after currentItemUpdater (so it has the
// right data). TODO: replace ordering comments with a test.
mAccessibilityProvider.onInitialize(mPageChangeEventDispatcher, mRecyclerView);
mPageChangeEventDispatcher.addOnPageChangeCallback(mExternalPageChangeCallbacks);
// Add mPageTransformerAdapter after mExternalPageChangeCallbacks, because page transform
// events must be fired after scroll events
mPageTransformerAdapter = new PageTransformerAdapter(mLayoutManager);
mPageChangeEventDispatcher.addOnPageChangeCallback(mPageTransformerAdapter);
attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
}
可以看到这里面做的一切操作都是初始化RecycleView将Recyclew当作子控件添加的ViewPager2容器里面
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = mRecyclerView.getMeasuredWidth();
int height = mRecyclerView.getMeasuredHeight();
// TODO(b/70666626): consider delegating padding handling to the RecyclerView to avoid
// an unnatural page transition effect: http://shortn/_Vnug3yZpQT
mTmpContainerRect.left = getPaddingLeft();
mTmpContainerRect.right = r - l - getPaddingRight();
mTmpContainerRect.top = getPaddingTop();
mTmpContainerRect.bottom = b - t - getPaddingBottom();
Gravity.apply(Gravity.TOP | Gravity.START, width, height, mTmpContainerRect, mTmpChildRect);
mRecyclerView.layout(mTmpChildRect.left, mTmpChildRect.top, mTmpChildRect.right,
mTmpChildRect.bottom);
if (mCurrentItemDirty) {
updateCurrentItem();
}
}
而onlayout方法只要负责RecyclewView放在什么位置就好。这么看来好像ViewPager2介绍就完毕了再介绍就是RecyclewView的实现原理了。接下来来看一下ViewPager2的特效扩展的实现原理。
ViewPager2.PageTransformer
如果你想实现ViewPager2切换界面时的反转效果的一些特效比如缩小放大等等,就需要继承上面的类。实现transformPage的方法,在里面依据position的变化确定RecyclewView子条目的效果。
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -2 is two pages to the left.
* Minimum / maximum observed values depend on how many pages we keep
* attached, which depends on offscreenPageLimit.
*
* @see #setOffscreenPageLimit(int)
*/
void transformPage(@NonNull View page, float position);
}
如果是咱们自己写RecyclewView话,想要实现子条目在滑动时实现一些效果应该怎么做,当然一般会想到监听滑动的回调方法取出所有显示的子View根据据屏幕中间位置来给每个View添加效果,这么想的话ViewPager2是不是也是这么实现的。来看一下源码:
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
mScrollHappened = true;
updateScrollEventValues();
if (mDispatchSelected) {
// Drag started settling, need to calculate target page and dispatch onPageSelected now
mDispatchSelected = false;
boolean scrollingForward = dy > 0 || (dy == 0 && dx < 0 == mViewPager.isRtl());
// "&& values.mOffsetPx != 0": filters special case where we're scrolling forward and
// the first scroll event after settling already got us at the target
mTarget = scrollingForward && mScrollValues.mOffsetPx != 0
? mScrollValues.mPosition + 1 : mScrollValues.mPosition;
if (mDragStartPosition != mTarget) {
dispatchSelected(mTarget);
}
} else if (mAdapterState == STATE_IDLE) {
// onScrolled while IDLE means RV has just been populated after an adapter has been set.
// Contract requires us to fire onPageSelected as well.
int position = mScrollValues.mPosition;
// Contract forbids us to send position = -1 though
dispatchSelected(position == NO_POSITION ? 0 : position);
}
// If position = -1, there are no items. Contract says to send position = 0 instead.
dispatchScrolled(mScrollValues.mPosition == NO_POSITION ? 0 : mScrollValues.mPosition,
mScrollValues.mOffset, mScrollValues.mOffsetPx);
// Dispatch idle in onScrolled instead of in onScrollStateChanged because RecyclerView
// doesn't send IDLE event when using setCurrentItem(x, false)
if ((mScrollValues.mPosition == mTarget || mTarget == NO_POSITION)
&& mScrollValues.mOffsetPx == 0 && !(mScrollState == SCROLL_STATE_DRAGGING)) {
// When the target page is reached and the user is not dragging anymore, we're settled,
// so go to idle.
// Special case and a bit of a hack when mTarget == NO_POSITION: RecyclerView is being
// initialized and fires a single scroll event. This flags mScrollHappened, so we need
// to reset our state. However, we don't want to dispatch idle. But that won't happen;
// because we were already idle.
dispatchStateChanged(SCROLL_STATE_IDLE);
resetState();
}
}
其中dispatchSelected和dispatchStateChanged,一个对应回调
private void dispatchSelected(int target) {
if (mCallback != null) {
mCallback.onPageSelected(target);
}
}
一个对应回调
private void dispatchScrolled(int position, float offset, int offsetPx) {
if (mCallback != null) {
mCallback.onPageScrolled(position, offset, offsetPx);
}
}
而最终的回调onPageScrolled方法会回调到transformPage。如下所示:
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (mPageTransformer == null) {
return;
}
float transformOffset = -positionOffset;
for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
View view = mLayoutManager.getChildAt(i);
if (view == null) {
throw new IllegalStateException(String.format(Locale.US,
"LayoutManager returned a null child at pos %d/%d while transforming pages",
i, mLayoutManager.getChildCount()));
}
int currPos = mLayoutManager.getPosition(view);
float viewOffset = transformOffset + (currPos - position);
mPageTransformer.transformPage(view, viewOffset);
}
}
果然和咱们的猜想一样。接下来再来看一下ViewPager2是正面保持和ViewPager一样缓存固定条目的子View的,RecyclewView大家都知道它是内部源码根据屏幕大小计算显示几个子View的,怎么让RecyclewView固定显示两个呢,如果一屏可以容纳5个子界面的情况下。是不是可以重写LayoutManager在布局子条目的时候加一些逻辑,答案是可以实现的。如下ViewPager2的实现如下:
private class LinearLayoutManagerImpl extends LinearLayoutManager {
LinearLayoutManagerImpl(Context context) {
super(context);
}
@Override
public boolean performAccessibilityAction(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, int action, @Nullable Bundle args) {
if (mAccessibilityProvider.handlesLmPerformAccessibilityAction(action)) {
return mAccessibilityProvider.onLmPerformAccessibilityAction(action);
}
return super.performAccessibilityAction(recycler, state, action, args);
}
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(recycler, state, info);
mAccessibilityProvider.onLmInitializeAccessibilityNodeInfo(info);
}
@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
@NonNull int[] extraLayoutSpace) {
int pageLimit = getOffscreenPageLimit();
if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
// Only do custom prefetching of offscreen pages if requested
super.calculateExtraLayoutSpace(state, extraLayoutSpace);
return;
}
final int offscreenSpace = getPageSize() * pageLimit;
extraLayoutSpace[0] = offscreenSpace;
extraLayoutSpace[1] = offscreenSpace;
}
@Override
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,
@NonNull View child, @NonNull Rect rect, boolean immediate,
boolean focusedChildVisible) {
return false; // users should use setCurrentItem instead
}
}
其中calculateExtraLayoutSpace方法就是用来计算剩余空间的,感兴趣的同学可以了解一下RecyclewView的实现原理和细节,其中pageLimit就是咱们想让他最多一下展示几个控件分方setOffscreenPageLimit方法设置的。ViewPager还有一个效果就是在手指离开屏幕时怎么保证当前那个position居中,RecyclewView默认是不带这个功能的,除非你用PagerSnapHelper,ViewPager2里面的方法是实现的了。如下所示:
mPagerSnapHelper = new PagerSnapHelperImpl();
mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
ViewPager2比ViewPager强大的一点就是可以快速划过好几屏界面,不像ViewPager只能一屏一屏的滑动。
好了,这就是ViewPager2的实现原理了。