ViewPager2的滑动回弹动画

844 阅读2分钟

ViewPager2的滑动回弹动画

首先分析回弹动画的效果,滑动小于一定阈值时,松手后触发,页面回弹到原位置。

切入点

第一反应就是onTouchEvent,发现ViewPager2没有重写,并且它是一个ViewGroup,我们看它初始化的时候设置了什么,调用了 initialize,看到最终嵌套了一个自定义的RecyclerView,发现没有重写onTouchEvent,那么只有可能通过addOnScrollListener来拦截触摸。

继续分析initialize,我们可以把目标锁定在ScrollEventAdapterPagerSnapHelperImpl,通过查看类描述,我们可以基本确定是在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,计算出距离中心的移动量,最后移动过去。