01
前言
在移动应用开发中,滚动效果是提升用户体验的重要手段之一。然而,设计师的酷炫效果往往给开发者带来不小的挑战。本文将通过一个广告效果的实现,深入探讨ViewPager2的滚动机制,并结合源码分析如何优化自定义滚动效果。
02
ViewPager2源码分析
为了更好地理解ViewPager2的滚动机制,我们首先从源码入手,分析其内部实现。
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来管理页面的布局。
ViewPager2支持水平和垂直两种滑动方向,这是通过设置LinearLayoutManager的orientation
属性来实现的:
public void setOrientation(@Orientation int orientation) {
mLayoutManager.setOrientation(orientation);
mAccessibilityProvider.onSetOrientation();
}
在LinearLayoutManager
中,orientation
决定了RecyclerView的滑动方向:
LinearLayoutManager.HORIZONTAL
:水平滑动;LinearLayoutManager.VERTICAL
:垂直滑动。ViewPager2的PageTransformer
机制是通过RecyclerView的ItemDecoration
和OnScrollListener
来实现的。当用户滑动页面时,ViewPager2会调用PageTransformer
的transformPage
方法,对每个页面进行变换。
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);
}
}
}
ViewPager2的滑动事件处理依赖于RecyclerView的OnScrollListener
和OnFlingListener
。当用户滑动页面时,RecyclerView会触发onScrolled
和onScrollStateChanged
事件,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();
}
};
ViewPager2的页面切换动画是通过RecyclerView的ItemAnimator
和PageTransformer
共同实现的。默认情况下,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.7f, 1 - Math.abs(position));
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setAlpha(1 - Math.abs(position));
} else { // 页面完全不可见
page.setAlpha(0);
}
}
由于ViewPager2基于RecyclerView实现,因此它的性能优化策略与RecyclerView类似:
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的实现
要实现广告中的滚动效果,关键在于自定义PageTransformer
。PageTransformer
的核心方法是transformPage(View page, float position)
,其中position
表示当前页面的位置。
通过position
的变化,我们可以控制页面的平移、缩放、透明度等属性,从而实现复杂的滚动效果。
在这种情况下,我们手指向上划。就变成了下图的样子。这时候page1
变成了可见视图。因为他位于position0
到position1
之间。原来的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);
}
});
}
首先我们定义mOffset
为负的下边三个卡片的高度,定义pageHeight
为当前卡片的高度;
当position<1
的时候即可见视图向上移动 ,如果没有任何操作的话,那么向上移动的距离为整个屏幕高度,然而实际上移动的距离却是上边大卡片的高度,那么我们就需要反方向移动距离为 整个屏幕-第一张卡片的高度 = |mOffset| 即代码 view.setTranslationY(mOffset * (position))
;
相反,当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
主标题渐变效果
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
方法将图片向上移动一部分,然后在展开的时候再移动回来,但是想法很美好,但是要考虑很多问题,首先,图片不可能完全适配手机的宽高比,所以移动的高度不可能是我们想要的,那么我们接下来看一下适配宽高的过程:
宽度被拉伸了a倍 那高度拉伸之后就应该是 pic_h*a
;
上下被裁切的总高就是 pic_h*a - show_h
;
由于上下裁切高度是一样的 所以 上边裁切的高度就是( pic_h*a - show_h)/ 2
;
由于图片被拉伸了 所以下发的位移高度 h也需要乘以系数a 结果为 h*a
;
算出来结果过就是位移的高度为 下发的高度减去上边被拉伸的高度 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);
}
}
});
}
首先当position>1的时候图片完全处于不可见状态,那么需要把我们的图片直接移动到相应的位置,也就是picOffset
的位置;
当第一张小卡片向上移动的时候即0<=position<1
的时候那么我们需要平滑移动到完整状态,乘以position
代表平滑滚动。
07
结合源码的优化建议
LayoutManager
,例如实现堆叠效果或3D旋转效果;PageTransformer
中避免频繁的UI操作,例如避免在transformPage
中频繁调用setTranslationX
或setAlpha
,以减少性能开销;08
总结
通过分析ViewPager2的源码,我们可以更好地理解其内部实现机制,并能够根据需求进行定制和优化。ViewPager2的强大功能为我们提供了丰富的扩展性,开发者可以结合RecyclerView的特性,实现各种复杂的滚动效果。
希望本文的源码分析和优化建议能够为您的文章增加技术深度,帮助读者更好地理解ViewPager2的工作原理。