cover_image

ViewPager2的滚动机制与优化

杨福 搜狐技术产品
2025年04月16日 23:31

01

前言

在移动应用开发中,滚动效果是提升用户体验的重要手段之一。然而,设计师的酷炫效果往往给开发者带来不小的挑战。本文将通过一个广告效果的实现,深入探讨ViewPager2的滚动机制,并结合源码分析如何优化自定义滚动效果。

02

ViewPager2源码分析

为了更好地理解ViewPager2的滚动机制,我们首先从源码入手,分析其内部实现。

2.1 ViewPager2的核心类结构

ViewPager2的核心类包括:

  • ViewPager2:继承自ViewGroup,负责管理页面滑动和布局;
  • RecyclerView:ViewPager2内部使用RecyclerView来实现页面的滑动和复用;
  • LinearLayoutManager:ViewPager2默认使用LinearLayoutManager来管理页面的布局;
  • FragmentStateAdapter:用于管理Fragment页面的Adapter;
  • PageTransformer:用于自定义页面滑动时的动画效果。
2.2 ViewPager2的初始化

在ViewPager2的构造函数中,它会初始化RecyclerView和LinearLayoutManager,并设置一些默认参数:

public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    // 初始化RecyclerView
    mRecyclerView = new RecyclerView(context);
    mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    // 初始化LinearLayoutManager
    mLayoutManager = new LinearLayoutManager(context);
    mRecyclerView.setLayoutManager(mLayoutManager);
    // 设置滑动方向
    setOrientation(ORIENTATION_HORIZONTAL);
    // 添加RecyclerView到ViewPager2中
    attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
}

从源码中可以看出,ViewPager2的核心是一个RecyclerView,并且默认使用LinearLayoutManager来管理页面的布局。

2.3 滑动方向的设置

ViewPager2支持水平和垂直两种滑动方向,这是通过设置LinearLayoutManager的orientation属性来实现的:

public void setOrientation(@Orientation int orientation) {
    mLayoutManager.setOrientation(orientation);
    mAccessibilityProvider.onSetOrientation();
}

LinearLayoutManager中,orientation决定了RecyclerView的滑动方向:

  • LinearLayoutManager.HORIZONTAL:水平滑动;
  • LinearLayoutManager.VERTICAL:垂直滑动。
2.4 PageTransformer的实现

ViewPager2的PageTransformer机制是通过RecyclerView的ItemDecorationOnScrollListener来实现的。当用户滑动页面时,ViewPager2会调用PageTransformertransformPage方法,对每个页面进行变换。

public void setPageTransformer(@Nullable PageTransformer transformer) {
    if (transformer != null) {
        if (mPageTransformerAdapter == null) {
            mPageTransformerAdapter = new PageTransformerAdapter();
        }
        mPageTransformerAdapter.setPageTransformer(transformer);
    } else {
        mPageTransformerAdapter = null;
    }
    mRecyclerView.setItemAnimator(null); // 禁用ItemAnimator,避免与PageTransformer冲突
}

PageTransformerAdapter中,ViewPager2会监听RecyclerView的滑动事件,并根据滑动位置调用transformPage方法:

@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
    if (mPageTransformer != null) {
        for (int i = 0; i < recyclerView.getChildCount(); i++) {
            View child = recyclerView.getChildAt(i);
            float position = getPosition(child);
            mPageTransformer.transformPage(child, position);
        }
    }
}
2.5 滑动事件的处理

ViewPager2的滑动事件处理依赖于RecyclerView的OnScrollListenerOnFlingListener。当用户滑动页面时,RecyclerView会触发onScrolledonScrollStateChanged事件,ViewPager2会通过这些事件来更新当前页面的状态。

private final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            updateCurrentItem();
        }
    }

    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        dispatchOnPageScrolled();
    }
};
2.6 页面切换的动画

ViewPager2的页面切换动画是通过RecyclerView的ItemAnimatorPageTransformer共同实现的。默认情况下,ViewPager2会禁用RecyclerView的ItemAnimator,以避免与PageTransformer冲突。

mRecyclerView.setItemAnimator(null); // 禁用ItemAnimator

如果需要自定义页面切换动画,可以通过PageTransformer来实现。例如,实现一个缩放和透明度变化的动画:

public void transformPage(View page, float position) {
    if (position < -1) { // 页面完全不可见
        page.setAlpha(0);
    } else if (position <= 1) { // 页面部分可见
        float scaleFactor = Math.max(0.7f1 - Math.abs(position));
        page.setScaleX(scaleFactor);
        page.setScaleY(scaleFactor);
        page.setAlpha(1 - Math.abs(position));
    } else { // 页面完全不可见
        page.setAlpha(0);
    }
}
2.7 ViewPager2的性能优化

由于ViewPager2基于RecyclerView实现,因此它的性能优化策略与RecyclerView类似:

  • ViewHolder复用:通过RecyclerView的ViewHolder机制,ViewPager2可以高效地复用页面,减少内存开销;
  • DiffUtil:ViewPager2支持使用DiffUtil来优化数据更新的性能,避免不必要的页面重绘;
  • 预加载:通过setOffscreenPageLimit方法,可以设置预加载的页面数量,提升滑动流畅度。
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
    if (limit < 1) {
        throw new IllegalArgumentException("Offscreen page limit must be >= 1");
    }
    mOffscreenPageLimit = limit;
    mRecyclerView.requestLayout();
}

03

从一个广告效果引出ViewPager2的滚动机制

在开发过程中,我们经常会遇到各种各样的滚动效果。例如,下图展示了一个竖向的ViewPager2滑动效果,但它与传统的ViewPager效果有所不同:

图片

这个效果的特点是:

  • 竖向滑动;
  • 页面之间有重叠效果;
  • 页面切换时有复杂的动画效果。

为了实现这个效果,我们可以通过自定义PageTransformer来控制页面的滑动行为。接下来,我们将结合源码分析,详细讲解如何实现这个效果。

04

自定义PageTransformer的实现

要实现广告中的滚动效果,关键在于自定义PageTransformerPageTransformer的核心方法是transformPage(View page, float position),其中position表示当前页面的位置。

position的含义
  • position = 0:当前页面完全可见;
  • position > 0:当前页面正在向右或向下滑动;
  • position < 0:当前页面正在向左或向上滑动。

通过position的变化,我们可以控制页面的平移、缩放、透明度等属性,从而实现复杂的滚动效果。

图片

在这种情况下,我们手指向上划。就变成了下图的样子。这时候page1变成了可见视图。因为他位于position0position1之间。原来的page0变成了将要可见视图, 将要可见视图其实是看不见的。但有机会(上划或者下划)成为可见视图。

图片

以此类推就很容易理解position的含义了,但是需要及注意点position值得是视图左边的位置,看图直接带入即可。

代码实现:
public void customPagerTransform() {
    final float mOffset = -3 * CardAdapter.BOTTOM_CARD_HEIGHT;
    mViewPager.setPageTransformer((view, position) -> {
        int pageHeight = view.getHeight();
        if (position < 1) {
            view.setTranslationY(mOffset * position);
            if (position >= 0) {
                RelativeLayout layoutLiveContent = view.findViewById(R.id.title_content);
                layoutLiveContent.setTranslationY((pageHeight - CardAdapter.BOTTOM_CARD_HEIGHT + mOffset) * (1 - position));
            }
        } else {
            float tansY = -pageHeight * (position - 1) + (mOffset + (position - 1) * CardAdapter.BOTTOM_CARD_HEIGHT);
            view.setTranslationY(tansY);
            RelativeLayout layoutLiveContent = view.findViewById(R.id.title_content);
            layoutLiveContent.setTranslationY(0);
        }
    });
}
  1. 首先我们定义mOffset 为负的下边三个卡片的高度,定义pageHeight 为当前卡片的高度;

  2. position<1的时候即可见视图向上移动 ,如果没有任何操作的话,那么向上移动的距离为整个屏幕高度,然而实际上移动的距离却是上边大卡片的高度,那么我们就需要反方向移动距离为 整个屏幕-第一张卡片的高度 = |mOffset| 即代码 view.setTranslationY(mOffset * (position))

  3. 相反,当position>=1的时候也就是即将可见和完全不可见视图我们需要怎么计算呢?我们接下来看一张图片。

图片

我们以C卡片为例,如果不操作的话那么我们的卡片应该处于右边C位置,然而实际上我们的卡片确却是在左边B位置,这样我们需要向下移动的卡片的距离为2*小卡片高度+ 大卡片高度,以此类推,D卡片需要多滑动一个小卡片的距离,以此类推,然而为了平滑移动需要乘以position - 1,即滑动的距离为:

float tansY = -pageHeight * (position - 1) + (mOffset + (position - 1) * (CardAdapter.BOTTOM_CARD_HEIGHT));
view.setTranslationY(tansY);

接下来我们继续看一下引导文案副标题的滑动,我们还是用图片为大家呈现:

图片

可以看到当position<1的时候,引导文案由引导文案区域1挪到了引导文案区域2,那么我们需要向下移动的高度为大卡片高度-小卡片高度,然而引导文案的父控件即大卡片也在移动且移动的高度之前计算过为mOffset 所以要减去这个高度,由于这个值为负值,所以计算结果为:

(pageHeight - CardAdapter.BOTTOM_CARD_HEIGHT + mOffset) * (1 - position)

乘以1 - position也是为了平滑滚动。

那么当position>=1这种情况下引导文案本来就是在最上边所以不需要滚动,所以 layoutLiveContent.setTranslationY(0);

05

主标题渐变效果

主标题的渐变效果可以通过在ViewPager2上方覆盖两个TextView,并根据滑动位置动态改变透明度和文案来实现。

public void customPagerTransform() {
    mViewPager.setPageTransformer((view, position) -> {
        if (position < 1) {
            if (position >= 0) {
                int tag = (int) view.getTag();
                int pos1 = (tag - 1) % mCardBeanList.size();
                int pos2 = (tag) % mCardBeanList.size();
                CardBean adCard1 = mCardBeanList.get(pos1);
                mTitle2.setText(adCard1.getGuideTitle1());
                mTitle2.setAlpha(position);
                CardBean adCard2 = mCardBeanList.get(pos2);
                mTitle1.setText(adCard2.getGuideTitle1());
                mTitle1.setAlpha(1 - position);
            }
        } else {
            // 其他逻辑
        }
    });
}

效果如下:

图片

06

图片位移效果

图片位移效果可以通过在小卡片状态下调用setTranslationY方法将图片向上移动一部分,然后在展开时再移动回来。

public void customPagerTransform() {
    final float mOffset = -3 * CardAdapter.BOTTOM_CARD_HEIGHT;
    mViewPager.setPageTransformer((view, position) -> {
        ImageView ivPicture = view.findViewById(R.id.iv_picture);
        if (position < 1) {
            if (position >= 0) {
                if (ivPicture.getTag() != null) {
                    int picOffset = (int) ivPicture.getTag();
                    ivPicture.setTranslationY(-picOffset * position);
                }
            }
        } else {
            ivPicture.setVisibility(View.VISIBLE);
            if (ivPicture.getTag() != null) {
                int picOffset = (int) ivPicture.getTag();
                ivPicture.setTranslationY(-picOffset);
            }
        }
    });
}
图片
图片可以看到第一张图片小卡片的人头部分没有显示出来,但是移动到第一张卡片的时候却显示的整张完整的图片,这样做的原因是为了在小卡片的时候显示我们想要显示的区域,这样效果会更好,那么这个效果我们需要怎么做呢?

思考:我们可以在小卡片的时候调用setTranslationY方法将图片向上移动一部分,然后在展开的时候再移动回来,但是想法很美好,但是要考虑很多问题,首先,图片不可能完全适配手机的宽高比,所以移动的高度不可能是我们想要的,那么我们接下来看一下适配宽高的过程:

图片
  1. 宽度被拉伸了a倍 那高度拉伸之后就应该是 pic_h*a

  2. 上下被裁切的总高就是 pic_h*a - show_h

  3. 由于上下裁切高度是一样的 所以 上边裁切的高度就是( pic_h*a - show_h)/ 2

  4. 由于图片被拉伸了 所以下发的位移高度 h也需要乘以系数a 结果为 h*a

  5. 算出来结果过就是位移的高度为 下发的高度减去上边被拉伸的高度 h*a - (pic_h*a - show_h) /2

有了算法那我们写起代码就会得心应手,首先我们看一下实现代码:

int picTag = 0;
holder.iv_picture.setTranslationY(0);
Bitmap bitmap = cardBean.getPicSource();
if (bitmap != null && bitmap.getWidth() != 0) {
    float bias = Float.parseFloat(ScreenUtils.getScreenWidth(mContext) + "") / Float.parseFloat(bitmap.getWidth() + "");
    int height = bitmap.getHeight();
    float scaleSize = (height * bias - hei) / 2;
    float tranY = cardBean.getPicFirstHeight() * bias - scaleSize;
    if (tranY <= 0 || tranY >= hei - BOTTOM_CARD_HEIGHT) {
        picTag = 0;
    } else {
        picTag = (int) tranY;
    }
}
holder.iv_picture.setTag(picTag);

if (tranY <= 0 || tranY >= hei - BOTTOM_CARD_HEIGHT) 这个操作是为了做容错处理,移动距离如果为负数或者大于小卡片的高度那么可能会出现留白的情况。

计算完成之后我们可以给图片设置一个tag标记当前图片位移的距离,接下来我们可以再transform中根据滑动平滑改变图片位移,因为从小卡片变回大卡片过程中我们需要将图片的位移还原,我们看一下实现代码:

public void customPagerTransform() {
    final float mOffset = -3 * CardAdapter.BOTTOM_CARD_HEIGHT;
    mViewPager.setPageTransformer((view, position) -> {
        ...
        ImageView ivPicture = view.findViewById(R.id.iv_picture);
        if (position < 1) {
            if (position >= 0) {
                ...
                if (ivPicture.getTag() != null) {
                    int picOffset = (int) ivPicture.getTag();
                    ivPicture.setTranslationY(-picOffset * position);
                }
            }
        } else {
            ...
            ivPicture.setVisibility(View.VISIBLE);
            if (ivPicture.getTag() != null) {
                int picOffset = (int) ivPicture.getTag();
                ivPicture.setTranslationY(-picOffset);
            }
        }
    });
}
  1. 首先当position>1的时候图片完全处于不可见状态,那么需要把我们的图片直接移动到相应的位置,也就是picOffset的位置;

  2. 当第一张小卡片向上移动的时候即0<=position<1的时候那么我们需要平滑移动到完整状态,乘以position代表平滑滚动。

07

结合源码的优化建议

    1. 自定义LayoutManager:如果需要实现更复杂的布局效果,可以自定义LayoutManager,例如实现堆叠效果或3D旋转效果;
    2. 优化PageTransformer:在PageTransformer中避免频繁的UI操作,例如避免在transformPage中频繁调用setTranslationXsetAlpha,以减少性能开销;
    3. 使用DiffUtil:在数据更新时,使用DiffUtil来优化页面刷新,避免不必要的重绘。

    08

    总结

    通过分析ViewPager2的源码,我们可以更好地理解其内部实现机制,并能够根据需求进行定制和优化。ViewPager2的强大功能为我们提供了丰富的扩展性,开发者可以结合RecyclerView的特性,实现各种复杂的滚动效果。

    希望本文的源码分析和优化建议能够为您的文章增加技术深度,帮助读者更好地理解ViewPager2的工作原理。



    继续滑动看下一个
    搜狐技术产品
    向上滑动看下一个