ViewPager2 使用分析

3,905 阅读4分钟

前言

ViewPager2 出来有段时间了,也在项目中体验了一把(真香)。

首先我们先要知道的是,ViewPager2AndroidX 的组件,所以我们需要升级到AndroidX 才能使用。

整体上来说ViewPager2ViewPager的重构升级版,拥有更好的性能与功能。

ViewPager2 版本更新

主要功能

  • 对之前的ViewPager实现的改进:
    • RTL(从右向左)布局支持
    • 垂直方向支持
    • 可靠的 Fragment 支持(包括处理底层 Fragment 集合的更改)
    • 数据集更改动画(包括 DiffUtil 支持)
  • 从之前的 ViewPager 实现中轻松迁移(API 尽可能一致)。请参阅迁移指南示例应用

请参阅指南,了解如何使用 ViewPager2 在 Fragment 之间滑动。

RTL(从右向左)布局支持

之前做过阿语的国际化项目,适配ViewPager我们需要将数据进行反转,还要将ViewPager 当前默认页面设置到最后一页,这样做有个后遗症触发多个页面的创建性能并不是很好。

相较于ViewPager对RTL的不友好,ViewPager2内部已经支持RTL,让我们有跟多的精力在业务上。

垂直方向支持

因为ViewPager2内部分页是通过RecyclerView以及LinearLayoutManager来实现的,所以我们通过在XML中或者代码设置我们的方向。

@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(SOURCE)
@IntDef({ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL})
public @interface Orientation {
}

private void setOrientation(Context context, AttributeSet attrs) {
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPager2);
    if (Build.VERSION.SDK_INT >= 29) {
        saveAttributeDataForStyleable(context, R.styleable.ViewPager2, attrs, a, 0, 0);
    }
    try {
        setOrientation(
                a.getInt(R.styleable.ViewPager2_android_orientation, ORIENTATION_HORIZONTAL));
    } finally {
        a.recycle();
    }
}

/**
 * Sets the orientation of the ViewPager2.
 *
 * @param orientation {@link #ORIENTATION_HORIZONTAL} or {@link #ORIENTATION_VERTICAL}
 */
public void setOrientation(@Orientation int orientation) {
    mLayoutManager.setOrientation(orientation);
    mAccessibilityProvider.onSetOrientation();
}

可靠的 Fragment 支持(包括处理底层 Fragment 集合的更改)

ViewPager2是通过ReycyclerView实现的,所以我们看下适配器相关的代码

StatefulAdapter

用于ViewPager2将所有已添加到FragmentManager或者已经数据持久化的Fragment数据 进行保存,方便后续数据的恢复,FragmentStateAdapter中有具体实现方法

public interface StatefulAdapter {
    /** Saves adapter state */
    @NonNull Parcelable saveState();

    /** Restores adapter state */
    void restoreState(@NonNull Parcelable savedState);
}
FragmentViewHolder

FragmentStateAdapter中使用,用于保存Fragmentcontainer,而不是Fragment。因为我们的RecyclerView最强大的功能是View的复用,如果Fragment复用话那不就乱套了

public final class FragmentViewHolder extends ViewHolder {
    private FragmentViewHolder(@NonNull FrameLayout container) {
        super(container);
    }

    @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
        FrameLayout container = new FrameLayout(parent.getContext());
        container.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT));
        container.setId(ViewCompat.generateViewId());
        container.setSaveEnabled(false);
        return new FragmentViewHolder(container);
    }

    @NonNull FrameLayout getContainer() {
        return (FrameLayout) itemView;
    }
}
FragmentStateAdapter
  • FragmentStateAdapter继承自RecyclerView.Adapter,所以ViewPager2拥有刷新数据的能力,就像普通的Adapter使用;
  • FragmentStateAdapter同样拥有控制所以Fragments数据持久化,生命周期的能力;
    • mSavedStates保存需要效持久化的Fragment 数据(Bundle)。这些Fragment持久化数据都是之前已经添加到FragmentManager,现在暂时移出,减少内存消耗。等再次使用的时候再恢复数据。
    • mFragments保存添加到FragmentManager的mFragments
    • mFragmentMaxLifecycleEnforcer这个类的功能是负责Fragment的生命周期控制,通过调用 FragmentTransaction#setMaxLifecycle控制已添加到FragmentManagerFragment。如果是当前要显示的Fragment,则生命周期可以执行到RESUME 否则 STAERT。用于懒加载

ViewPager2 分析

成员
  • CompositeOnPageChangeCallback用page事件的分发,里面保存了所有监听ViewPager2.OnPageChangeCallback观察者
  • ScrollEventAdapter的作用是将RecyclerView.OnScrollListener事件转变为ViewPager2.OnPageChangeCallback事件,给CompositeOnPageChangeCallback分发
  • PageTransformerAdapter的作用是将页面的滑动事件转变为PageTransformer可以接受的事件,有关 PageTransformer 的示例和视频,请参见缩小页面转换器深度页面转换器部分。
  • FakeDrag 的作用是用来实现模拟拖动的效果;
  • PagerSnapHelper用于页面的手势滑动工具,实现页面切换
核心方法
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());
}

Viewpager2 最重要的初始化方法,通过上面的代码我们可以看出:通过添加RecyclerView当子View,让RecyclerView来管理每个页面数据,Viewpager2只充当容器

总结

其实ViewPager2本身的源码是非常简单的,它的核心点就在各个组件当中,所以本文就不对ViewPager2的内部源码进行分析。到此为止,我们对ViewPager2的源码分析完毕。