ViewPager2的滑动回弹动画
首先分析回弹动画的效果,滑动小于一定阈值时,松手后触发,页面回弹到原位置。
切入点
第一反应就是onTouchEvent,发现ViewPager2没有重写,并且它是一个ViewGroup,我们看它初始化的时候设置了什么,调用了 initialize,看到最终嵌套了一个自定义的RecyclerView,发现没有重写onTouchEvent,那么只有可能通过addOnScrollListener来拦截触摸。
继续分析initialize,我们可以把目标锁定在ScrollEventAdapter和PagerSnapHelperImpl,通过查看类描述,我们可以基本确定是在PagerSnapHelperImpl中实现的。
源码分析
我们首要目标是追踪addOnScrollListener
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
...
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
...
}
}
private void setupCallbacks() throws IllegalStateException {
...
mRecyclerView.addOnScrollListener(mScrollListener);
...
}
mScrollListener:
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// 滚动停止后
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 处理visible change的情况,具体看方法注释
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
snapToTargetExistingView:
void snapToTargetExistingView() {
...
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
这段源码一眼看去,就是找了个什么snapView,然后计算出了一个snapDistance,然后滚动到该位置,八九不离十了,核心就在这两个方法中。
我们继续分析下:
-
snapView是指什么? -
snapDistance是指什么?
findSnapView
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
}
...
return null;
}
findCenterView:
@Nullable
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
// 拿到rv当前可见的几个子View
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
// 计算出rv的中点
final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
int absClosest = Integer.MAX_VALUE;
// 取距离rv中心点最近的子View
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
// 计算出子View的中点
int childCenter = helper.getDecoratedStart(child)
+ (helper.getDecoratedMeasurement(child) / 2);
int absDistance = Math.abs(childCenter - center);
/* if child center is closer than previous closest, set it as closest */
// 记录最接近的子View和距离
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
可知snapView就是最接近RecyclerView中间点的子View
calculateDistanceToFinalSnap
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView) {
int[] out = new int[2];
...
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(targetView,
getVerticalHelper(layoutManager));
}
...
return out;
}
distanceToCenter:
private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
// 跟findCenterView一样
// 计算出targetView中心点距离rv中心点的距离
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
return childCenter - containerCenter;
}
可知snapDistance就是snapView的中间点距离RecyclerView的中间点的距离
结论
到此源码就分析完了,滑动回弹的实现方式是:滑动停止后,遍历当前可见的子View,找到最接近中心点的View,计算出距离中心的移动量,最后移动过去。